Scope & Lambdas

The Course
January 8, 2026
4 min read

Every name in Python lives somewhere. Knowing where saves you from a whole category of confusing bugs.

Local and global

Names created inside a function are local to it: they exist while the call runs and then vanish.

Locals do not escape Python
def compute():
    secret = 42          # local to this call
    return secret

print(compute())
print(secret)            # NameError - it never existed out here

Functions can read names from the surrounding module:

Reading a global Python
tax_rate = 0.2           # module level - a global

def with_tax(amount):
    return amount * (1 + tax_rate)     # reading is fine

print(with_tax(100))

But assigning to that name inside the function creates a new local instead of changing the global:

Assignment makes it local Python
counter = 0

def increment_broken():
    counter = counter + 1     # UnboundLocalError
    return counter

print(increment_broken())

Python decides at compile time that counter is local (because the function assigns to it), then finds it has no value yet. The global keyword overrides that:

global, and why to avoid it Python
counter = 0

def increment():
    global counter
    counter += 1
    return counter

print(increment(), increment(), increment())
global is almost always the wrong answer

A function that changes module state can break any other part of the program, and makes tests order-dependent. Pass values in, return values out. If several functions need shared state, that is usually a signal to write a class — which is Day 6.

The LEGB rule

When Python meets a name it looks in four places, in order: Local, then any Enclosing function, then Global (module), then Built-in.

All four levels Python
name = "global"

def outer():
    name = "enclosing"

    def inner():
        name = "local"
        print("inner sees:", name)

    inner()
    print("outer sees:", name)

outer()
print("module sees:", name)
print("built-in example:", len("abc"))    # len comes from the builtins layer
Do not shadow the built-ins

Naming a variable list, sum, type or id hides the built-in for the rest of the scope. list = [1, 2] then list("abc") fails with a baffling message.

Lambdas

A lambda is a function written as a single expression, with no name. It is the same idea as def, minus the ceremony.

def and lambda side by side Python
def double(x):
    return x * 2

double_lambda = lambda x: x * 2

print(double(5), double_lambda(5))

# Lambdas take several arguments too:
area = lambda width, height: width * height
print(area(3, 4))

Assigning a lambda to a name, as above, is pointless — def is clearer. The real use is passing a small function to another function.

Sorting with key=

This is where lambdas earn their existence.

Sorting by whatever you like Python
people = [
    ("Ada", 1815, "Mathematician"),
    ("Grace", 1906, "Admiral"),
    ("Alan", 1912, "Logician"),
]

print(sorted(people, key=lambda person: person[1]))          # by year
print(sorted(people, key=lambda person: person[0]))          # by name
print(sorted(people, key=lambda person: len(person[2])))     # by role length

words = ["banana", "Fig", "cherry", "apple"]
print(sorted(words))                                # capitals sort first
print(sorted(words, key=str.lower))                 # case-insensitive

The key function is called once per item, and Python sorts by whatever it returns. The same idea works for max, min and sorted alike:

key on max and min Python
books = [
    {"title": "Dune", "pages": 412},
    {"title": "Piranesi", "pages": 245},
    {"title": "Ubik", "pages": 224},
]

longest = max(books, key=lambda book: book["pages"])
print("Longest:", longest["title"])

by_title = sorted(books, key=lambda book: book["title"])
for book in by_title:
    print(f"  {book['title']} ({book['pages']}p)")
Exercise

Sort a leaderboard

Sort the players by score, highest first; where scores tie, sort by name alphabetically. Print each as 1. Ada — 95.

Hint: a key can return a tuple, and Python compares tuples item by item.

players = [
    {"name": "Grace", "score": 88},
    {"name": "Ada", "score": 95},
    {"name": "Alan", "score": 88},
]

# Your code here
Show one solution
players = [
    {"name": "Grace", "score": 88},
    {"name": "Ada", "score": 95},
    {"name": "Alan", "score": 88},
]

ranked = sorted(players, key=lambda p: (-p["score"], p["name"]))

for position, player in enumerate(ranked, start=1):
    print(f"{position}. {player['name']}{player['score']}")

The trick is the tuple key (-score, name): negating the score sorts it descending while the name still sorts ascending, in a single pass.

What you learned

  • Names assigned in a function are local and disappear when it returns.
  • Functions can read globals but assigning creates a local — global overrides this, and is rarely right.
  • Python resolves names Local → Enclosing → Global → Built-in.
  • lambda args: expression is an unnamed one-expression function.
  • key= lets sorted, max and min sort by anything, including a tuple.
Last updated on January 8, 2026

Was this article helpful?

Your response is saved on this device.