Scope & Lambdas

The Course
•
January 8, 2026
•
6 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)")

Before you write your own

Predict. Two ideas from this lesson: where a name lives, and what key= does.

What will this print? Python
count = 10

def add_one():
    count = 1
    return count + 1

print(add_one(), count)

words = ["kiwi", "fig", "banana"]
print(sorted(words, key=len))
print(min(words, key=lambda w: w[-1]))
Check your prediction
2 10
['fig', 'kiwi', 'banana']
banana

The count = 1 inside the function is a new local name, so the global count is untouched. key=len sorts by length. And the lambda gives each word’s last letter — i, g, a — so the smallest key belongs to banana.

Fix. This crashes with the UnboundLocalError from above. Fix it the way the warning recommends — pass the running total in and return the new one — without using global. It should print 5.

Changing a global Python
total = 0

def add(amount):
    total += amount

add(5)
print(total)
Check your fix
def add(total, amount):
    return total + amount

total = 0
total = add(total, 5)
print(total)
5

Now the function only works with what it’s given and hands back its answer; the caller decides where the answer goes. A function like that can be tested with a single line — which a function that edits globals can’t.

Exercise

Sort a leaderboard

Sort the players by score, highest first; where scores tie, sort by name alphabetically. Print each with its position. Your output should be:

1. Ada — 95
2. Alan — 88
3. Grace — 88
players = [
    {"name": "Grace", "score": 88},
    {"name": "Ada", "score": 95},
    {"name": "Alan", "score": 88},
]

# Your code here
Hint 1: the idea

A key can return a tuple, and Python compares tuples item by item: the first items decide, and only a tie looks at the second. So one key can say “by score, then by name”. The catch: scores should run highest first while names run A to Z, and a single reverse=True would flip both.

Hint 2: a first step

Sort by score alone first — sorted(players, key=lambda p: p["score"], reverse=True) — and print the result. Then ask: how could you make a bigger score sort earlier without reverse? Think about what a minus sign does to the order of numbers. enumerate(ranked, start=1) gives you the positions.

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.