BugForge
All lessons
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().

Next in Python
asyncio essentials