Python Functions and Decorators
- Description:
def, default arguments, positional/keyword-only parameters,*args/**kwargs, argument unpacking,lambda, closures, decorators (with and without arguments),functools.wraps, and useful built-in decorators - My Notion Note ID: K2A-D1-8
- Created: 2022-07-28
- Updated: 2026-05-11
- License: Reuse is very welcome. Please credit Yu Zhang and link back to the original on yuzhang.io
Table of Contents
- 1.
def - 2.
*argsand**kwargs - 3. Argument Unpacking at the Call Site
- 4.
lambda - 5. Closures
- 6. Decorators
- 7. Type Hints
1. def
def greet(name: str, greeting: str = "Hello") -> str:
return f"{greeting}, {name}!"
greet("Yu") # "Hello, Yu!"
greet("Yu", greeting="Hi") # "Hi, Yu!"
- Functions are first-class, store in variables, pass around, return
- No separate function-pointer syntax
1.1 Default Arguments
- Defaults evaluated once at
deftime - Safe for immutable values (
int,str,tuple) - For mutable values (
list,dict) it's the classic mutable-default bug, useNonesentinel and create inside
1.2 Keyword-Only and Positional-Only Parameters
def write(data, *, encoding="utf-8", errors="strict"):
# ^ everything after * is keyword-only
...
write(data, encoding="ascii") # OK
write(data, "ascii") # TypeError, would be positional
def pow(x, n, /, mod=None):
# ^ everything before / is positional-only (3.8+)
...
- Keyword-only → forces self-documenting call sites
- Positional-only → library authors keep parameter-name freedom
2. *args and **kwargs
*args→ extra positional args collected into a tuple**kwargs→ extra keyword args collected into a dict
def log(level, *args, **kwargs):
msg = " ".join(str(a) for a in args)
print(f"[{level}] {msg}", **kwargs)
log("INFO", "user", user_id, file=sys.stderr)
- Closest C++ analog: variadic templates, but
*args/**kwargsare runtime and untyped
3. Argument Unpacking at the Call Site
*and**also work in reverse at a call site, spread a sequence into positionals, a mapping into keywords
def add(a, b, c): return a + b + c
xs = [1, 2, 3]
add(*xs) # add(1, 2, 3)
opts = {"a": 1, "b": 2, "c": 3}
add(**opts) # add(a=1, b=2, c=3)
- Standard pattern to forward all arguments:
wrapped(*args, **kwargs)
4. lambda
- Anonymous, single-expression function
square = lambda x: x * x
sorted(words, key=lambda w: w.lower())
- Limits: one expression only; no statements, no annotations
- C++ lambdas (arbitrary body) are closer to a
defplus closure than tolambda
5. Closures
- Nested functions capture names from the enclosing scope by reference
- Reading captured names is free; rebinding requires
nonlocal
def make_adder(n):
def add(x):
return x + n # n captured from enclosing frame
return add
add5 = make_adder(5)
add5(10) # 15
Late-binding gotcha, closures in a loop see the final value of the loop variable:
fns = [lambda: i for i in range(3)]
[f() for f in fns] # [2, 2, 2], not [0, 1, 2]
# Fix: bind via a default argument
fns = [lambda i=i: i for i in range(3)]
[f() for f in fns] # [0, 1, 2]
6. Decorators
- A callable that takes a function (or class) and returns a replacement
@decoratoris sugar forf = decorator(f)
import time
def timed(fn):
def wrapper(*args, **kwargs):
t0 = time.perf_counter()
result = fn(*args, **kwargs)
print(f"{fn.__name__} took {time.perf_counter() - t0:.3f}s")
return result
return wrapper
@timed
def slow():
time.sleep(0.1)
slow() # "slow took 0.103s"
- No native C++ equivalent, closest is wrapping with a function template or attaching attributes
6.1 functools.wraps
- Without
wraps, the wrapper loses__name__,__doc__, and signature - Tools (introspection, debuggers,
help()) rely on the metadata, always apply on the inner wrapper
from functools import wraps
def timed(fn):
@wraps(fn)
def wrapper(*args, **kwargs):
...
return wrapper
6.2 Decorators With Arguments
- A decorator factory returns a decorator, three layers (factory → decorator → wrapper)
def retry(times):
def decorator(fn):
@wraps(fn)
def wrapper(*args, **kwargs):
for attempt in range(times):
try:
return fn(*args, **kwargs)
except Exception:
if attempt == times - 1:
raise
return wrapper
return decorator
@retry(times=3)
def fetch(url): ...
6.3 Useful Built-Ins: lru_cache, cached_property, singledispatch
from functools import lru_cache, cached_property, singledispatch
@lru_cache(maxsize=None)
def fib(n):
return n if n < 2 else fib(n - 1) + fib(n - 2)
class Document:
@cached_property
def word_count(self):
return len(self.text.split()) # computed once per instance
@singledispatch
def serialize(obj): raise TypeError
@serialize.register
def _(obj: int): return str(obj)
@serialize.register
def _(obj: list): return "[" + ",".join(serialize(x) for x in obj) + "]"
- Other standard decorators:
@staticmethod,@classmethod,@property,@dataclass,@abstractmethod
7. Type Hints
def parse(text: str, *, strict: bool = False) -> dict[str, int]:
...
- Annotations stored on the function object (
fn.__annotations__) - Used by static checkers; no runtime effect by default