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.
Search across all documentation pages
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.
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:
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:
factor from enclosing scopeNone sentinel avoids mutable default list bugnonlocal updates count inside nested functionglobal modifies module-level counter explicitlylen, print)./), keyword-only (*).*args - Tuple of extra positional arguments.**kwargs - Dict of extra keyword arguments.__annotations__; evaluated at function definition time.def f(pos_only, /, standard, *, kw_only, **kwargs): ...| Segment | Binding |
|---|---|
Before / | Positional-only |
After / | Positional or keyword |
After * | Keyword-only |
# 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)def f(x=[]) shares one list across calls. Fix: None default and assign inside.funcs = [lambda: i for i in range(3)] captures variable, not value. Fix: Default arg lambda i=i: i.def len(x): breaks len(). Fix: Rename parameters.| 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 |
Name lookup order: Local, Enclosing (nested outer functions), Global (module), Built-in namespace.
To mark following parameters as keyword-only: def f(a, *, b=1) forces f(1, b=2).
Separates positional-only parameters: def f(a, b, /, c) - a and b cannot be passed by keyword.
*args collects extra positionals as tuple; **kwargs collects extra keywords as dict. Order in signature matters.
Yes at function definition time - why mutable defaults are shared. Immutable defaults (int, str, tuple) are safe.
When a nested function must rebind a variable in an enclosing function - counters, accumulators in closures.
Yes: def f(*args: int) or *args: int with TypedDict patterns in advanced typing - see type hints section.
Single expression only - no statements. Use def for anything non-trivial.
def wrapper(*args, **kwargs): return inner(*args, **kwargs) - common in decorators.
-> None documents no useful return. Omit only when obvious; mypy benefits from explicit None.
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+.
Reviewed by Chris St. John·Last updated Jul 16, 2026