Python · 09/10 5 min
Scope, closures and function arguments
LEGB lookup, UnboundLocalError, nonlocal, keyword-only args, *args/**kwargs and decorators.
LEGB
Python looks names up in Local → Enclosing → Global → Built-in scope.
Assigning makes a name local
count = 0
def hit():
count += 1 # UnboundLocalError: count is local (it's assigned) but not set yet
def hit():
global count # module-level counter
count += 1
def make_counter():
n = 0
def inc():
nonlocal n # closure variable
n += 1
return n
return inc
Prefer returning values or using an object over global.
Shadowing built-ins
list = [1, 2] # now list(...) is broken for the rest of the module
id, type, input, max, sum, dict # same story
Arguments
def search(q, *, limit=10, offset=0): # after * : keyword-only
...
search("x", 20) # TypeError: forces callers to say limit=20
def log(msg, *args, **kwargs): ... # args is a tuple, kwargs a dict
f(*items) # unpack a list into positional args
Defaults are evaluated once, at definition time. That's why def f(x=[]) shares one list, and def f(at=time.time()) freezes the timestamp. Use None and compute inside.
Decorators
import functools
def retry(times):
def deco(fn):
@functools.wraps(fn) # keep fn's name and docstring
def wrapper(*args, **kwargs):
for attempt in range(times):
try:
return fn(*args, **kwargs) # don't forget the return
except TimeoutError:
if attempt == times - 1:
raise
return wrapper
return deco
Forgetting return inside the wrapper is the classic decorator bug: every decorated function suddenly returns None.
lru_cache
@functools.lru_cache(maxsize=None) memoises a function. Arguments must be hashable (no lists or dicts), and the cache lives for the whole process: clear it between tests with fn.cache_clear().
Practise it now
Next in Python
asyncio essentials