Functions as objects, lambdas and closures

Python · Practical · 12 min

Sorting by key, callbacks and decorators all rely on functions being ordinary values.

In Python, functions are first-class objects: you can assign them to variables, store them in dicts, pass them as arguments and return them from other functions. sorted(people, key=get_age) passes the function get_age itself — note there are no parentheses.

A lambda is a tiny anonymous function limited to a single expression: lambda p: p["age"]. It's ideal for short key= arguments. If you'd want to name it or it grows beyond one simple expression, write a def instead; assigning a lambda to a name gains nothing. The operator module provides ready-made ones like itemgetter("age") and attrgetter("name").

Python resolves names using the LEGB rule: Local, Enclosing function, Global (module), then Built-in. A function defined inside another can read the enclosing function's variables, and it keeps them alive after the outer function returns. That combination — a function plus the variables it captured — is a closure:

def make_counter():
    count = 0
    def increment():
        nonlocal count
        count += 1
        return count
    return increment

Assigning to a name makes it local by default, so to rebind a captured variable you declare it nonlocal (or global for a module variable).

A classic surprise: closures capture variables, not values. Lambdas created in a loop all see the loop variable's final value. Bind the current value with a default argument (lambda x=x: ...) or use functools.partial.

Example

from functools import partial
from operator import itemgetter

people = [{"name": "Ana", "age": 31}, {"name": "Ben", "age": 27}]
print([p["name"] for p in sorted(people, key=lambda p: p["age"])])
# ['Ben', 'Ana']
print(max(people, key=itemgetter("age"))["name"])   # Ana

ops = {"add": lambda a, b: a + b, "mul": lambda a, b: a * b}
print(ops["mul"](6, 7))                # 42

def make_multiplier(factor):
    def multiply(x):
        return x * factor              # captures factor
    return multiply

print(make_multiplier(3)(5))           # 15
late = [lambda: i for i in range(3)]
print([f() for f in late])             # [2, 2, 2]
bound = [lambda i=i: i for i in range(3)]
print([f() for f in bound])            # [0, 1, 2]

int_base2 = partial(int, base=2)
print(int_base2("1010"))               # 10

Remember

  • Functions are values: pass them without calling them
  • Use lambda only for short single expressions like sort keys
  • Closures capture variables, not values; use nonlocal to rebind them

Watch out for

  • Writing key=get_age() (calling it) instead of key=get_age.
  • Expecting loop-created lambdas to remember each iteration's value.

Check yourself

  1. Given `funcs = [lambda: n for n in range(3)]`, what is `funcs[0]()`?
  2. Inside a nested function, what does `nonlocal total` allow?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.python.org/3/tutorial/controlflow.html#lambda-expressions