How Python resolves names and how functions capture their environment
Functions are first-class citizens in Python — they can be passed as arguments, returned from other functions, and stored in variables. This makes closures and higher-order functions natural, but also introduces scope bugs that are hard to debug if you don't know the rules.
The late-binding closure trap is a well-known gotcha — it looks wrong to anyone who doesn't understand how Python looks up free variables at call time rather than definition time.
When Python sees a variable name, it searches four scopes in order: Local → Enclosing → Global → Built-in (LEGB). It finds the first match and stops. This lookup happens at runtime, not when the function is defined.
A closure remembers the enclosing scope it was created in — but it remembers the variable (the label), not the value at creation time. If the enclosing variable later changes, the closure sees the new value.
When Python sees a variable name, it searches four scopes in order: Local → Enclosing → Global → Built-in. The first match wins — this lookup happens at runtime, not at definition time.
*args and **kwargs let a function accept any number of positional and keyword arguments — essential for wrappers and forwarding calls without listing every parameter.
*args collects extra positional arguments as a tuple; **kwargs collects keyword arguments as a dict. Use *iterable and **mapping at the call site to unpack them back out.
Closures capture the variable reference, not its value at creation time. In loops, this causes a classic bug where all closures read the final loop value instead of the one at the time they were created.
Closures capture the variable, not its value. The default-argument trick captures the current value at definition time because default arguments are evaluated immediately.