cs.thefarshad
medium

Functions, Scope & Closures

def, positional/keyword/default arguments, *args and **kwargs, lambdas, closures, and the LEGB scope rule.

A function packages a reusable piece of behavior. In Python, functions are first-class objects: you can pass them as arguments, return them from other functions, and store them in data structures. That flexibility is what makes closures, decorators, and functional-style code possible.

def greet(name):
    """Return a greeting (this line is the docstring)."""
    return f"Hello, {name}!"

print(greet("Ada"))     # Hello, Ada!
f = greet               # the function object itself, not a call
print(f("Alan"))        # Hello, Alan!

Arguments: positional, keyword, default

Arguments can be passed by position or by keyword, and parameters can declare defaults. Keyword arguments make calls self-documenting and let you skip middle defaults.

def connect(host, port=5432, *, timeout=30):
    return f"{host}:{port} (t={timeout})"

connect("db")                       # db:5432 (t=30)
connect("db", 6543)                 # positional port
connect("db", timeout=5)            # skip port, set timeout by keyword

The bare * marks every following parameter as keyword-only, so callers must write timeout=5. One sharp edge: never use a mutable default like def f(items=[]). That list is created once and shared across calls. Use None as the sentinel and build the real default inside:

def add(item, items=None):
    if items is None:
        items = []
    items.append(item)
    return items

*args and **kwargs

To accept any number of extra arguments, use *args (a tuple of leftover positionals) and **kwargs (a dict of leftover keywords). The same * and ** also unpack a sequence or mapping at the call site.

def report(*args, **kwargs):
    print("positional:", args)      # a tuple
    print("keyword:", kwargs)       # a dict

report(1, 2, mode="fast")
# positional: (1, 2)
# keyword: {'mode': 'fast'}

nums = [3, 1, 2]
print(max(*nums))                   # unpack -> max(3, 1, 2) -> 3

Lambdas

A lambda is a small anonymous function limited to a single expression. They shine as throwaway arguments to functions like sorted, map, and filter:

pairs = [("ada", 36), ("alan", 41), ("grace", 85)]
oldest = sorted(pairs, key=lambda p: p[1], reverse=True)
# [('grace', 85), ('alan', 41), ('ada', 36)]

If a lambda grows beyond one clear expression, write a named def instead.

Scope and the LEGB rule

When you use a name, Python searches four scopes in order — Local, Enclosing, Global, Built-in — and uses the first binding it finds. The visualizer traces a nested call: as functions are called their frames are pushed on the stack, and each name lookup walks the LEGB ladder until it hits a match.

call stack (top = current)
global()running
base=10outer=<fn>
LEGB lookup
LLocal
EEnclosing
GGlobal
BBuilt-in
1/11
module loads — base and outer live in the Global scope

By default, assigning to a name inside a function creates a new local. To rebind a name in an outer scope you must say so explicitly with global or nonlocal:

count = 0
def bump():
    global count        # without this, count = ... would make a local
    count += 1

Closures

A closure is a function that remembers variables from the enclosing scope where it was defined, even after that scope has returned. The inner function “closes over” those names. This is how you build function factories and stateful callbacks without classes:

def multiplier(factor):
    def multiply(x):
        return x * factor    # factor is captured from the enclosing scope
    return multiply

triple = multiplier(3)
print(triple(10))            # 30 — `factor` (3) lives on inside `triple`

Closures are the foundation of decorators and much of Python’s functional toolkit.

Takeaways

  • Functions are first-class objects — pass, return, and store them freely.
  • Mix positional, keyword, and default args; never use a mutable default value.
  • *args/**kwargs collect extra arguments; */** unpack them at call sites.
  • Name lookup follows LEGB; use global/nonlocal to rebind outer names.
  • A closure captures enclosing variables, letting inner functions carry state.

References