Handbooks / Python / Chapter 2

Functions and Closures

7 pages · ~5 min✓ Reviewed

Builds on Types and Data Structures. Next up: OOP in Python.

PythonPage 1

Functions as Objects: Mental Model

1. Functions Are Objects

  • def builds a function object and binds it to a name
  • Assign it, or store it in a list or dict
  • Pass it as an argument, or return it
python
def f(x): return x * 2
g = f
ops = {'dbl': f}
del f
print(g(2), ops['dbl'](3))

del f removes one name, not the object

output
4 6

2. Higher-Order Functions

CallFunction argument
sorted(words, key=len)len computes the sort key
map(str.upper, xs)str.upper applied to each item
filter(None, xs)None keeps only truthy items
def make(): return freturns a function

3. Names and Callables

  • g = f: two names, one object
  • callable(obj) is True for functions, classes, objects with __call__

4. Interview Point

  • Function as dict value? It is an ordinary object
  • What makes it callable? A __call__ method
  • f vs f()? f is the object, f() runs it
PythonPage 2

Arguments: Positional, Keyword, *args, **kwargs

1. Signature order

  1. Positionala
  2. Defaultb=1
  3. *argstuple
  4. Keyword-onlyc, d=2
  5. **kwargsdict
  • *args collects extra positional args into a tuple
  • **kwargs collects extra keyword args into a dict
python
def show(a, b=1, *args, c, d=2, **kw):
    print(a, b, args, c, d, kw)
show(1, 2, 3, 4, c=5, x=6)
output
1 2 (3, 4) 5 2 {'x': 6}

2. Keyword-only, positional-only

MarkerRuleExample
* or *argsafter it, name requireddef f(a, *, key); f(1, 2) is a TypeError
/before it, no namesdef f(a, b, /, c)

3. Call site and binding

  • f(*[1,2], **{'k': 3}) expands a sequence and a mapping
  • * in def vs call? def collects, call expands
  • Why keyword-only? call sites stay readable: f(x, key=1)
  • Binding order? positional, then keyword, then defaults
Duplicate value

f(1, a=2) binds a twice: TypeError, multiple values for argument 'a'.

PythonPage 3

Default Argument Pitfalls

1. Evaluated Once

  • Defaults are built once, at def time
  • Stored in f.__defaults__, reused on every call
  • Safe: int, str, tuple, None
  • Danger: a default that is mutated or reused

2. The Trap and the Fix

  • add(1) then add(2) share one list, giving [1, 2]
python
def add(x, acc=[]):
    acc.append(x)
    return acc
add(1); add(2)
print(add.__defaults__)

the default list itself changed

output
([1, 2],)
  • Fix: acc=None, then acc = [] if acc is None else acc
  • time.time() or datetime.now() as a default freezes at def time
  • Compute it inside the body

3. Interview Point

  • Why does the list persist? One list is made at def time and every call reuses it
  • How to inspect a default? Read f.__defaults__
  • When is it intentional? A memo cache, e.g. def fib(n, memo={})
PythonPage 4

Scope (LEGB) and Closures

1. LEGB and Rebinding

  1. Localinside def
  2. Enclosingouter def
  3. Globalmodule
  4. Built-inlen, print
  • First match wins; no match raises NameError
  • Assigning anywhere in a def makes the name local for the whole function
  • Reading it before the assignment raises UnboundLocalError
  • global x / nonlocal x only to rebind; reading or mutating needs neither

2. Closures and Late Binding

  • Closure = inner function keeping outer variables after the outer returns
  • Captured cells show in f.__closure__
python
def counter():
    n = 0
    def inc():
        nonlocal n
        n += 1
        return n
    return inc
c = counter(); c(); print(c(), c.__closure__[0].cell_contents)
output
2 2
Late-binding trap

Closures capture the variable, not its value. [lambda: i for i in range(3)] all return 2. Fix: lambda i=i: i

3. Interview Point

  • What is LEGB? Local, Enclosing, Global, Built-in lookup order
  • nonlocal vs global? Rebinds an outer-function name vs a module name
  • Loop lambdas all return 2? They share i; bind it with i=i
PythonPage 5

Lambdas and Decorators

1. Lambda

  • lambda args: expr = one expression, anonymous
  • No statements, no annotations
  • Best as a key=: sorted(users, key=lambda u: u.age)

2. Decorators

  • @dec above def f is exactly f = dec(f)
  • Wrapper takes *args, **kwargs: fits any signature
  • Add @functools.wraps(fn) or __name__ and __doc__ are lost
python
import functools
def log(fn):
    @functools.wraps(fn)
    def wrapper(*args, **kwargs):
        return fn(*args, **kwargs)
    return wrapper

standard template

3. Decorator with Arguments

  1. retry(3)returns decorator
  2. decorator(fn)returns wrapper
  3. wrapper(*a, **kw)calls fn

4. Interview Point

  • Timing decorator? time.perf_counter() before and after fn(...), then return the result
  • Why wraps? Keeps __name__ and __doc__
  • @retry(3)? Expands to f = retry(3)(f)
  • Stacking? Applied bottom-up; the top decorator is outermost
Three layers

Arguments add one outer function: factory → decorator → wrapper.

PythonPage 6

Cheat Sheet: Functions, Scope and Decorators

1. Arguments and defaults

python
def f(a, b=1, *args, kw, kw2=2, **kwargs): ...
def g(a, /, b, *, c): ...

order: positional, default, *args, keyword-only, **kwargs

  • / ends positional-only; * or *args starts keyword-only
  • Mutable default like acc=[] is shared by every call
  • Fix: acc=None, then create acc = [] inside the body

2. Scope and closures

  1. Local
  2. Enclosing
  3. Global
  4. Built-in
  • nonlocal or global only to rebind; reading needs neither
  • Closure = inner function + captured variables
  • Loop capture: lambda: i sees the last i; fix with lambda i=i: i

3. Decorators

python
def deco(fn):
    @functools.wraps(fn)
    def wrapper(*a, **kw):
        return fn(*a, **kw)
    return wrapper
  • Stacked decorators apply bottom-up: the one nearest def wraps first
Keep the identity

Skip @functools.wraps and the function loses its __name__ and __doc__.

PythonPage 7

Check yourself

What does the last line print?
  • [1, 2]
  • The default list is built once at def time, so both calls share it
  • Fix: acc=None, then acc = [] if acc is None else acc
def add(x, acc=[]):
    acc.append(x)
    return acc
add(1)
print(add(2))
What is printed, and how do you get [0, 1, 2]?
  • [2, 2, 2]
  • Each lambda looks up i when called, after the loop has ended
  • Bind it early: lambda i=i: i
fs = [lambda: i for i in range(3)]
print([f() for f in fs])
Why does inc() raise an error, and what fixes it?
  • UnboundLocalError
  • n += 1 assigns to n, so n is local for the whole function
  • Declare global n to rebind the module-level name
n = 0
def inc():
    n += 1
inc()
What does f.__name__ give, and why?
  • 'wrapper'
  • @timed means f = timed(f), so f is now the wrapper
  • Fix: put @functools.wraps(fn) on the wrapper
def timed(fn):
    def wrapper(*a, **kw):
        return fn(*a, **kw)
    return wrapper

@timed
def f(): pass