Functions, Args & Scope
Functions bundle reusable logic with flexible parameter binding. Python's LEGB scope rules, variadic *args/**kwargs, and keyword-only parameters let you design APIs that are both expressive and safe.
Recipe
def greet(name: str, *, excited: bool = False) -> str:
msg = f"Hello, {name}"
return msg + "!" if excited else msg
def connect(host: str, port: int = 443, **options: object) -> dict:
return {"host": host, "port": port, **options}
def total(*amounts: float, tax: float = 0.0) -> float:
return sum(amounts) * (1 + tax)When to reach for this:
- Designing library APIs with optional configuration
- Wrapping callables with decorators (see iterators section)
- Implementing closures that remember state
- Avoiding global mutable state in modules
Working Example
from collections.abc import Callable
def make_multiplier(factor: int) -> Callable[[int], int]:
def multiply(value: int) -> int:
return value * factor
return multiply
def append_item(item: object, bucket: list | None = None) -> list:
if bucket is None:
bucket = []
bucket.append(item)
return bucket
def call_with_retry(fn: Callable[[], object], retries: int = 3) -> object:
last_error: Exception | None = None
for attempt in range(retries):
try:
return fn()
except Exception as exc:
last_error = exc
raise RuntimeError("failed") from last_error
counter = 0
def increment_global() -> int:
global counter
counter += 1
return counter
def make_counter() -> Callable[[], int]:
count = 0
def inc() -> int:
nonlocal count
count += 1
return count
return inc
if __name__ == "__main__":
double = make_multiplier(2)
print(double(21))
print(append_item("a"))
print(append_item("b")) # fresh list - no shared default
inc = make_counter()
print(inc(), inc())What this demonstrates:
- Closure captures
factorfrom enclosing scope Nonesentinel avoids mutable default list bugnonlocalupdatescountinside nested functionglobalmodifies module-levelcounterexplicitly
Deep Dive
How It Works
- LEGB - Local (function), Enclosing (nested), Global (module), Built-in (
len,print). - Parameter kinds - Positional, keyword, positional-only (
/), keyword-only (*). *args- Tuple of extra positional arguments.**kwargs- Dict of extra keyword arguments.- Annotations - Stored in
__annotations__; evaluated at function definition time.
Parameter Ordering
def f(pos_only, /, standard, *, kw_only, **kwargs): ...| Segment | Binding |
|---|---|
Before / | Positional-only |
After / | Positional or keyword |
After * | Keyword-only |
Python Notes
# Unpacking at call site
args = (1, 2)
kwargs = {"excited": True}
greet("Ada", **kwargs)
# functools.partial defers arg binding
from functools import partial
double = partial(make_multiplier, 2)Gotchas
- Mutable defaults -
def f(x=[])shares one list across calls. Fix:Nonedefault and assign inside. - Late-binding closures -
funcs = [lambda: i for i in range(3)]captures variable, not value. Fix: Default arglambda i=i: i. - Shadowing builtins -
def len(x):breakslen(). Fix: Rename parameters. - global for module state - Makes testing harder. Fix: Prefer classes or explicit config objects.
- **Too many *args/kwargs - Hides API surface. Fix: Name important parameters explicitly.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
dataclass | Stateful bundles of fields | Single pure transform |
functools.partial | Partial application | Need keyword-only enforcement |
Callable Protocol | Structural typing for callbacks | Simple one-off lambda |
Class with __call__ | Stateful callable objects | Stateless functions |
FAQs
What is LEGB?
Name lookup order: Local, Enclosing (nested outer functions), Global (module), Built-in namespace.
When do I use * in the parameter list?
To mark following parameters as keyword-only: def f(a, *, b=1) forces f(1, b=2).
What does / mean in Python 3.8+?
Separates positional-only parameters: def f(a, b, /, c) - a and b cannot be passed by keyword.
How do *args and **kwargs work together?
*args collects extra positionals as tuple; **kwargs collects extra keywords as dict. Order in signature matters.
Are default values evaluated once?
Yes at function definition time - why mutable defaults are shared. Immutable defaults (int, str, tuple) are safe.
When should I use nonlocal?
When a nested function must rebind a variable in an enclosing function - counters, accumulators in closures.
Can I annotate *args?
Yes: def f(*args: int) or *args: int with TypedDict patterns in advanced typing - see type hints section.
What is a lambda limitation?
Single expression only - no statements. Use def for anything non-trivial.
How do I forward all arguments?
def wrapper(*args, **kwargs): return inner(*args, **kwargs) - common in decorators.
Does return type None need annotation?
-> None documents no useful return. Omit only when obvious; mypy benefits from explicit None.
Related
- Closures & Scope Capture - free variables and factories
- Writing Decorators - wrapping functions
- functools - partial and wraps
- Type Hints Basics - annotating signatures
Stack versions: This page was written for Python 3.14.0 (stable 3.14, maintenance 3.13), FastAPI 0.115+, Django 5.2, Flask 3.1, Pydantic 2, PyTorch 2.6+, pandas 2.2+, Polars 1.x, ruff 0.9+, and uv 0.6+.