Python Decorators — A Quick Guide
What is a decorator?
A decorator is a function that wraps another function to add extra behavior without changing the original function's code. Think of it like gift wrapping: the gift (your function) stays the same, but the wrapping (the decorator) adds something extra around it — logging, timing, access checks, caching, etc.
Under the hood, a decorator just takes a function as input and returns a new
function. The @decorator_name syntax above a function definition is
"syntactic sugar" for:
say_hello = my_decorator(say_hello)
That's it. Once you see decorators as "a function that takes a function and returns a function," the rest is just patterns.
A simple, fully-commented example
import time
from functools import wraps
def timer(func):
"""
This is our decorator. It takes a function (func) and returns
a new function (wrapper) that adds timing behavior around it.
"""
@wraps(func) # Preserves func's name/docstring (best practice, see below)
def wrapper(*args, **kwargs):
# --- Code here runs BEFORE the original function ---
start = time.perf_counter()
# Call the original function and store its result
result = func(*args, **kwargs)
# --- Code here runs AFTER the original function ---
end = time.perf_counter()
print(f"{func.__name__} took {end - start:.4f} seconds")
# Always return the original result so behavior isn't broken
return result
return wrapper
@timer
def slow_add(a, b):
"""Adds two numbers, slowly (for demonstration)."""
time.sleep(1)
return a + b
# Because of @timer, calling slow_add() actually calls wrapper(),
# which calls the real slow_add() inside it.
result = slow_add(3, 4)
print(result)
# Output:
# slow_add took 1.0002 seconds
# 7
Why *args, **kwargs?
The wrapper function needs to accept any arguments, because it might be
wrapping a function with a completely different signature. *args catches
positional arguments, **kwargs catches keyword arguments, and both get
passed straight through to the original function.
Why @wraps(func)?
Without it, slow_add.__name__ would become "wrapper" and its docstring
would be lost — because wrapper is technically what got returned.
@wraps(func) (from functools) copies over the original function's name,
docstring, and other metadata, which matters a lot for debugging, tooling,
and documentation.
Decorators with their own arguments
Sometimes you want the decorator itself to be configurable, e.g.
@repeat(3). That needs one more layer of nesting:
def repeat(times):
"""Outer function: takes the decorator's own arguments."""
def decorator(func):
"""Middle function: takes the function being decorated."""
@wraps(func)
def wrapper(*args, **kwargs):
"""Inner function: actually runs each time the function is called."""
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(3)
def greet(name):
print(f"Hello, {name}!")
greet("Sam")
# Output:
# Hello, Sam!
# Hello, Sam!
# Hello, Sam!
Three layers, three jobs:
repeat(times)— captures the decorator's own argumentdecorator(func)— captures the function being wrappedwrapper(*args, **kwargs)— runs on every actual call
Common built-in decorators you'll see immediately
@staticmethod— method that doesn't takeself@classmethod— method that takesclsinstead ofself@property— lets a method be accessed like an attribute@functools.lru_cache— caches return values for given inputs@functools.wraps— used inside your own decorators (see above)
Mental model / rules of thumb
- A decorator is just a function that returns a function.
@my_decoratorabove a function is shorthand forfunc = my_decorator(func).- Always use
*args, **kwargsin your wrapper unless you know exactly what arguments you're intercepting. - Always use
@functools.wraps(func)inside your decorator so introspection (__name__,__doc__, help(), debuggers) still works correctly. - Decorators run once, at definition/import time — that's when
func = my_decorator(func)happens. Thewrappercode inside runs every time the decorated function is actually called. This trips people up constantly — keep it straight. - You can stack multiple decorators — they apply bottom-up:
@decorator_a @decorator_b def f(): ... # equivalent to: f = decorator_a(decorator_b(f))
Further reading
- Real Python — Primer on Python Decorators (excellent, thorough, widely recommended as a first deep-dive)
- Official docs —
functoolsmodule (seewraps,lru_cache,singledispatch,partial) - PEP 318 — Decorators for Functions and Methods (the original proposal, useful for historical/design context)
- Python docs glossary — decorator (short, precise definition)
Good next exercises for practice
- Write a
@debugdecorator that prints the arguments and return value of every call. - Write a
@retry(n)decorator that retries a function up tontimes if it raises an exception. - Look at how Flask/FastAPI use decorators (
@app.route(...)) to register functions — a great real-world example of decorators with arguments.
Python Dunder Methods Cheat Sheet
Object Lifecycle
| Method | Purpose |
|---|---|
| __new__(cls, ...) | Creates and returns the bare instance. Runs before __init__. Rarely overridden. |
| __init__(self, ...) | Initializes the instance after creation. The one everyone uses. |
| __del__(self) | Called when the object is garbage collected. Rarely needed. |
Representation
| Method | Purpose | Triggered by |
|---|---|---|
| __repr__(self) | Unambiguous, developer-facing string | repr(obj), console echo, debugger |
| __str__(self) | Readable, user-facing string | str(obj), print(obj) |
| __format__(self, spec) | Custom string formatting | f"{obj:spec}", format(obj) |
| __bytes__(self) | Byte-string representation | bytes(obj) |
Callable & Container Behavior
| Method | Purpose | Triggered by |
|---|---|---|
| __call__(self, ...) | Makes the instance callable like a function | obj() |
| __len__(self) | Length | len(obj) |
| __getitem__(self, key) | Index/key access | obj[key] |
| __setitem__(self, key, val) | Index/key assignment | obj[key] = val |
| __delitem__(self, key) | Index/key deletion | del obj[key] |
| __contains__(self, item) | Membership test | item in obj |
Iteration
| Method | Purpose | Triggered by |
|---|---|---|
| __iter__(self) | Returns an iterator | for x in obj, [x for x in obj], iter(obj) |
| __next__(self) | Returns next value, raises StopIteration when done | next(obj) |
| __reversed__(self) | Reverse iteration | reversed(obj) |
Comparison
| Method | Operator |
|---|---|
| __eq__(self, other) | == |
| __ne__(self, other) | != |
| __lt__(self, other) | < |
| __le__(self, other) | <= |
| __gt__(self, other) | > |
| __ge__(self, other) | >= |
| __hash__(self) | hash(obj) — needed if the object should be usable as a dict key / in a set |
| __bool__(self) | bool(obj), truthiness in if obj: |
Arithmetic
| Method | Operator |
|---|---|
| __add__(self, other) | + |
| __sub__(self, other) | - |
| __mul__(self, other) | * |
| __truediv__(self, other) | / |
| __floordiv__(self, other) | // |
| __mod__(self, other) | % |
| __pow__(self, other) | ** |
| __neg__(self) | unary -obj |
| __abs__(self) | abs(obj) |
| __iadd__(self, other) | += (in-place; falls back to __add__ if absent) |
| __radd__(self, other) | right-side + (e.g. 5 + obj when obj is on the right) |
Attribute Access
| Method | Purpose | Triggered by |
|---|---|---|
| __getattr__(self, name) | Called only when normal lookup fails | obj.missing_attr |
| __getattribute__(self, name) | Called for every attribute access (rarely overridden — easy to break things) | obj.any_attr |
| __setattr__(self, name, val) | Intercepts attribute assignment | obj.attr = val |
| __delattr__(self, name) | Intercepts attribute deletion | del obj.attr |
Context Managers
| Method | Purpose | Triggered by |
|---|---|---|
| __enter__(self) | Setup, returns value bound by as | with obj as x: |
| __exit__(self, exc_type, exc_val, tb) | Cleanup, can suppress exceptions | end of with block |
Class-Level / Metaprogramming
| Method | Purpose |
|---|---|
| __class__ | The instance's class (attribute, not a method) |
| __subclasshook__(cls, subclass) | Customize issubclass() checks |
| __init_subclass__(cls) | Hook that runs when a subclass is defined |
| __set_name__(self, owner, name) | Runs when a descriptor is assigned a name in a class body |
Quick Reference: Your Bacon / DataRange Examples
class Bacon:
def __init__(self, name): # init on creation
self.name = name
def __call__(self): # make instance callable
print(self.name)
class DataRange:
def __iter__(self): # enable `for` and list comprehensions
return iter(self.data)
def __len__(self): # enable len()
return len(self.data)
def __repr__(self): # enable clean printing
return f"DataRange({self.data})"
Rule of thumb: you rarely need more than a handful of these in any one class. __init__, __repr__, __eq__, and __iter__/__len__ cover the vast majority of real-world use.
Contents
- Python Decorators — A Quick Guide
- What is a decorator?
- A simple, fully-commented example
- Why `*args, **kwargs`?
- Why `@wraps(func)`?
- Decorators with their own arguments
- Common built-in decorators you'll see immediately
- Mental model / rules of thumb
- Further reading
- Good next exercises for practice
- Python Dunder Methods Cheat Sheet
- Object Lifecycle
- Representation
- Callable & Container Behavior
- Iteration
- Comparison
- Arithmetic
- Attribute Access
- Context Managers
- Class-Level / Metaprogramming
- Quick Reference: Your `Bacon` / `DataRange` Examples
Pages Here
No sub-pages yet.
Page Info
Wiki: Program Training
Created on Sep 17, 2026 by Stuart Anderson
Maintainers
| Editor | Last Activity |
|---|---|
| Stuart Anderson creator | Sep 17, 2026 |