Decorators
Decorators give you a clean syntax for wrapping a function with extra behavior: logging, timing, caching, access checks, route registration. Before any of that makes sense, it helps to remember that functions in Python are values like any other.
Functions Are Values
A function in Python is an object. You can assign it to a variable, pass it as an argument, return it from another function, or stash it in a list.
def shout(text: str) -> str:
return text.upper() + "!"
yell = shout
print(yell("ship it"))
# SHIP IT!
yell and shout now point at the same function object. That ability to pass functions around is what makes decorators possible.
A Simple Decorator
A decorator is a function that takes a function and returns a function, usually a wrapped version that adds behavior before or after the original.
def log_call(func):
def wrapper(*args, **kwargs):
print(f"calling {func.__name__}")
return func(*args, **kwargs)
return wrapper
@log_call
def normalize_title(title: str) -> str:
return title.strip().title()
print(normalize_title(" ship docs "))
# calling normalize_title
# Ship Docs
The @log_call line above the function is shorthand. Python sees it and rewrites the definition as:
def normalize_title(title: str) -> str:
return title.strip().title()
normalize_title = log_call(normalize_title)
The original normalize_title gets passed into log_call, and the name normalize_title is rebound to the wrapper that came back. Every later call to normalize_title actually runs wrapper, which prints the log line and then forwards the arguments along.
functools.wraps
The wrapper above has a problem. The decorated function answers to the wrapper's identity, not the original's:
print(normalize_title.__name__)
# wrapper
That breaks debugging output, breaks tools that read docstrings, and breaks anything that introspects the function. functools.wraps copies the original's name, docstring, and a few other attributes onto the wrapper:
from functools import wraps
def log_call(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"calling {func.__name__}")
return func(*args, **kwargs)
return wrapper
@log_call
def normalize_title(title: str) -> str:
"""Strip whitespace and title-case a task title."""
return title.strip().title()
print(normalize_title.__name__)
# normalize_title
print(normalize_title.__doc__)
# Strip whitespace and title-case a task title.
Use @wraps(func) on the inner wrapper whenever you write a decorator. It's a one-line habit that keeps the wrapped function honest about who it is.
Decorators You'll Use
You don't have to write decorators to use them. Most decorators you'll meet in this workshop come from libraries:
@contextmanagerturns a generator into a context manager.@app.get("/projects")will register a route handler with FastAPI.@field_validator("name")will attach custom validation to a Pydantic field.
Some are applied directly (@contextmanager). Others are called first with arguments (@app.get("/projects")), and the return value is what gets applied. Either shape wraps the function below it with extra behavior. Your code keeps calling that function by name.