By the end of this chapter, you should understand:
- What a decorator is.
- Why decorators exist.
- How decorator syntax translates to ordinary assignment.
- Why functions can wrap other functions.
- How closures make decorators possible.
- How to write a basic function decorator.
- Why wrappers need
*argsand**kwargs. - Why
functools.wrapsmatters. - How to write decorators that return values correctly.
- How to write decorators with arguments.
- How multiple decorators stack.
- How decorators work on methods.
- How class decorators work.
- How decorators compare with context managers.
- How decorators compare with inheritance and composition.
- How common standard-library decorators fit the pattern.
- When decorators improve design.
- When decorators hide too much.
Chapter 60 studied context managers.
Context managers wrap a block of execution:
with timer():
do_work()Decorators wrap functions, methods, or classes:
@timer
def do_work():
...Both abstractions remove repeated structure.
Context managers say:
run this setup and cleanup around this block
Decorators say:
transform this definition when it is created
Decorators are everywhere in Python:
@property
@classmethod
@staticmethod
@dataclass
@contextmanager
@functools.cache
@app.route("/users")
@pytest.mark.slowYou have already used decorators many times.
Now we study how they work.
At the simplest level, a decorator is a callable that takes an object and returns an object.
Most commonly:
function in -> function out
Example:
def decorator(function):
return functionUse:
@decorator
def greet():
return "hello"This decorator does nothing.
It receives the function object and returns the same function object.
The decorated function still works:
greet()returns:
"hello"Even this no-op example proves the core idea:
decorator syntax passes the defined object through another callable
This:
@decorator
def greet():
return "hello"means roughly:
def greet():
return "hello"
greet = decorator(greet)The name greet is rebound to whatever decorator(greet) returns.
That is why decorators are powerful.
They can replace the original function with:
- the same function
- a wrapper function
- a callable object
- a descriptor-like object
- something else callable
Most function decorators return a wrapper function.
But the mechanism is simply:
define object
call decorator with object
bind name to result
This happens when the function definition is executed.
Usually that means import time for module-level functions.
Decorators rely on a fact you already know:
functions are objects
You can assign a function to another name:
def greet():
return "hello"
say_hello = greetYou can pass a function to another function:
def call(function):
return function()
call(greet)You can return a function from a function:
def make_greeter():
def greet():
return "hello"
return greetDecorators use all of this.
A decorator receives a function object.
It often creates a new function that calls the original.
Then it returns the new function.
Let us write a decorator that prints before and after a function call.
def announce(function):
def wrapper():
print("before")
result = function()
print("after")
return result
return wrapperUse:
@announce
def greet():
print("hello")Call:
greet()Output:
before
hello
after
What happened?
The original greet function was passed to announce.
announce returned wrapper.
The name greet now refers to wrapper.
When you call greet(), you are calling the wrapper.
The wrapper calls the original function inside it.
How does wrapper remember function?
Closure.
In:
def announce(function):
def wrapper():
print("before")
result = function()
print("after")
return result
return wrapperwrapper uses function from the enclosing scope.
Even after announce returns, wrapper keeps a reference to function.
That is a closure.
Closures from Volume I now become practical.
The pattern is:
outer function receives original function
inner function remembers original function
outer function returns inner function
This is the heart of function decorators.
The first decorator only works for functions with no arguments.
This fails:
@announce
def greet(name):
print(f"hello {name}")
greet("Maya")because wrapper() does not accept name.
Fix with *args and **kwargs:
def announce(function):
def wrapper(*args, **kwargs):
print("before")
result = function(*args, **kwargs)
print("after")
return result
return wrapperNow the wrapper accepts any positional and keyword arguments and passes them through.
Use:
@announce
def greet(name, punctuation="!"):
print(f"hello {name}{punctuation}")Call:
greet("Maya", punctuation=".")Output:
before
hello Maya.
after
Most general-purpose decorators should preserve the wrapped function's call shape by using:
*args, **kwargsA wrapper must return the original function's result unless it intentionally changes behavior.
Bug:
def announce(function):
def wrapper(*args, **kwargs):
print("before")
function(*args, **kwargs)
print("after")
return wrapperUse:
@announce
def add(a, b):
return a + bNow:
add(2, 3)returns:
Nonebecause the wrapper did not return the result.
Correct:
def announce(function):
def wrapper(*args, **kwargs):
print("before")
result = function(*args, **kwargs)
print("after")
return result
return wrapperDecorators wrap behavior.
They should not accidentally throw away return values.
Consider:
def announce(function):
def wrapper(*args, **kwargs):
print("before")
result = function(*args, **kwargs)
print("after")
return result
return wrapperIf the wrapped function raises, "after" will not print:
@announce
def fail():
raise ValueError("bad")Call:
fail()Output:
before
Then ValueError propagates.
If you need cleanup-like behavior, use try/finally:
def announce(function):
def wrapper(*args, **kwargs):
print("before")
try:
return function(*args, **kwargs)
finally:
print("after")
return wrapperThis is similar in spirit to context managers.
The wrapper controls what happens around a function call.
Our decorator has a problem.
@announce
def greet():
"""Return a greeting."""
return "hello"Check:
print(greet.__name__)
print(greet.__doc__)You may see:
wrapper
NoneWhy?
Because greet now refers to the wrapper function.
The wrapper has its own metadata.
Use functools.wraps:
from functools import wraps
def announce(function):
@wraps(function)
def wrapper(*args, **kwargs):
print("before")
result = function(*args, **kwargs)
print("after")
return result
return wrapperNow metadata is preserved:
greet.__name__
greet.__doc__wraps also sets __wrapped__, which helps introspection tools find the original function.
Professional decorators should almost always use functools.wraps.
Timing is a common decorator example.
from functools import wraps
from time import perf_counter
def timed(function):
@wraps(function)
def wrapper(*args, **kwargs):
start = perf_counter()
try:
return function(*args, **kwargs)
finally:
elapsed = perf_counter() - start
print(f"{function.__name__} took {elapsed:.3f}s")
return wrapperUse:
@timed
def build_index(items):
return sorted(items)Call:
build_index([3, 1, 2])This returns the sorted list and prints timing.
Notice:
try:
return function(*args, **kwargs)
finally:
...The timing prints even if the function raises.
The exception still propagates.
This is usually the right behavior for timing.
Example:
from functools import wraps
def log_calls(function):
@wraps(function)
def wrapper(*args, **kwargs):
print(f"calling {function.__name__}")
result = function(*args, **kwargs)
print(f"{function.__name__} returned {result!r}")
return result
return wrapperUse:
@log_calls
def add(a, b):
return a + bCall:
add(2, 3)Output:
calling add
add returned 5
This is useful for teaching.
In production, use the logging module instead of print.
Also be careful logging arguments or return values.
They may contain secrets or large data.
Decorators make cross-cutting behavior easy.
That does not remove responsibility.
Decorators are applied when the function is defined.
For a module-level function, that usually means import time.
Example:
def decorate(function):
print(f"decorating {function.__name__}")
return function
@decorate
def greet():
return "hello"When Python executes the definition, it prints:
decorating greet
Calling greet() later does not apply the decorator again.
The wrapper may run on every call, but the decorator function itself ran when the definition was created.
This distinction matters for decorators that register routes, tests, plugins, or commands.
Registration often happens at import time.
Sometimes a decorator needs configuration.
We want:
@repeat(3)
def greet():
print("hello")This requires three layers:
from functools import wraps
def repeat(times):
def decorator(function):
@wraps(function)
def wrapper(*args, **kwargs):
result = None
for _ in range(times):
result = function(*args, **kwargs)
return result
return wrapper
return decoratorWhy three layers?
repeat(times) receives decorator arguments
decorator(function) receives the function being decorated
wrapper(*args, **kwargs) receives call arguments
Use:
@repeat(3)
def greet():
print("hello")This is equivalent to:
def greet():
print("hello")
greet = repeat(3)(greet)First repeat(3) returns a decorator.
Then that decorator receives greet.
Retries are a practical decorator-with-arguments example.
from functools import wraps
def retry(times, exceptions=(Exception,)):
def decorator(function):
@wraps(function)
def wrapper(*args, **kwargs):
last_error = None
for _ in range(times):
try:
return function(*args, **kwargs)
except exceptions as error:
last_error = error
raise last_error
return wrapper
return decoratorUse:
@retry(3, exceptions=(TimeoutError,))
def fetch_data():
...This retries only TimeoutError.
Design concerns:
- Should there be delay between retries?
- Should delay grow over time?
- Which exceptions are safe to retry?
- Is the function idempotent?
- Should failures be logged?
- Should final error preserve full context?
The decorator shape is simple.
The policy is not.
Decorators can make hard behavior look too easy, so design carefully.
You can apply multiple decorators:
@decorator_a
@decorator_b
def function():
...This means:
def function():
...
function = decorator_a(decorator_b(function))The decorator closest to the function applies first.
Then the one above it applies to the result.
Call order depends on wrappers.
Example:
def outer(function):
@wraps(function)
def wrapper(*args, **kwargs):
print("outer before")
result = function(*args, **kwargs)
print("outer after")
return result
return wrapper
def inner(function):
@wraps(function)
def wrapper(*args, **kwargs):
print("inner before")
result = function(*args, **kwargs)
print("inner after")
return result
return wrapperUse:
@outer
@inner
def greet():
print("hello")Call output:
outer before
inner before
hello
inner after
outer after
Stacking order matters.
Decorators can wrap methods too.
Example:
def log_calls(function):
@wraps(function)
def wrapper(*args, **kwargs):
print(f"calling {function.__name__}")
return function(*args, **kwargs)
return wrapperUse:
class User:
def __init__(self, name):
self.name = name
@log_calls
def greet(self):
return f"hello {self.name}"When:
user.greet()is called, self is passed as the first positional argument to wrapper.
Then wrapper passes it to the original method:
function(*args, **kwargs)This is why general decorators use *args, **kwargs.
They work for functions and methods.
Decorator order matters with method-transforming decorators.
Example:
class User:
@classmethod
@log_calls
def create(cls):
return cls()This first applies log_calls to the function, then classmethod wraps the result.
That is usually what you want.
This order may behave differently:
class User:
@log_calls
@classmethod
def create(cls):
return cls()Now log_calls receives a classmethod object, not a plain function.
That may not work with a decorator expecting a normal callable.
General rule:
method-shaping decorators such as classmethod, staticmethod, and property usually belong closest to the function or in a carefully understood order
When stacking decorators, read from bottom to top for application.
Then think from outside to inside for call behavior.
Decorators can also decorate classes.
Example:
def add_table_name(cls):
cls.table_name = cls.__name__.lower()
return clsUse:
@add_table_name
class User:
passNow:
User.table_nameis:
"user"This is equivalent to:
class User:
pass
User = add_table_name(User)Class decorators are often simpler than metaclasses when you want to transform one class after creation.
Dataclasses use this idea:
@dataclass
class Point:
x: int
y: intThe class object is passed through the dataclass decorator.
The decorator returns the processed class.
Class decorators can also take arguments.
Example:
def table(name):
def decorator(cls):
cls.table_name = name
return cls
return decoratorUse:
@table("users")
class User:
passEquivalent:
class User:
pass
User = table("users")(User)This is often easier than a metaclass:
class User(metaclass=ModelMeta, table="users"):
...Use class decorators when the transformation is local and explicit.
Use metaclasses only when class creation itself needs deeper control or inherited behavior.
A decorator can be a callable object.
Example:
from functools import wraps
class CountCalls:
def __init__(self, function):
self.function = function
self.count = 0
wraps(function)(self)
def __call__(self, *args, **kwargs):
self.count += 1
return self.function(*args, **kwargs)Use:
@CountCalls
def greet():
return "hello"Now:
greet()
greet()
print(greet.count)prints:
2This works because the decorator returns an object that is callable.
Function-based decorators are more common.
Callable class decorators are useful when the decorator needs persistent state.
You already know several decorators.
property:
class Circle:
@property
def area(self):
return 3.14159 * self.radius ** 2classmethod:
class User:
@classmethod
def anonymous(cls):
return cls("anonymous")staticmethod:
class Email:
@staticmethod
def normalize(value):
return value.strip().lower()dataclass:
@dataclass
class Point:
x: int
y: intcontextmanager:
@contextmanager
def managed():
yieldfunctools.cache or lru_cache:
@lru_cache(maxsize=128)
def fib(n):
...These look different in purpose, but they share one mechanism:
take an object, return a replacement object
Caching is a common decorator use.
Example:
from functools import lru_cache
@lru_cache(maxsize=128)
def fibonacci(n):
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)The decorator stores previous results.
Repeated calls with the same arguments can return cached values.
Design concerns:
- Are arguments hashable?
- Can results become stale?
- How much memory can the cache use?
- Should the cache be cleared?
- Is the function pure enough for caching?
Caching decorators are powerful because they change performance behavior without changing call sites.
They can also hide memory and freshness issues.
Use them deliberately.
Frameworks often use decorators to register functions.
Example:
routes = {}
def route(path):
def decorator(function):
routes[path] = function
return function
return decoratorUse:
@route("/users")
def users():
return "users"The decorator runs when the function is defined.
It stores the function in routes.
It returns the original function.
This kind of decorator does not necessarily wrap behavior.
It records metadata or registers the function.
That is common in:
- web frameworks
- CLI frameworks
- task queues
- plugin systems
- test frameworks
Decorator does not always mean wrapper.
It means transformation or registration at definition time.
Decorators can validate inputs.
Example:
from functools import wraps
def require_positive(function):
@wraps(function)
def wrapper(value, *args, **kwargs):
if value <= 0:
raise ValueError("value must be positive")
return function(value, *args, **kwargs)
return wrapperUse:
@require_positive
def square_root(value):
return value ** 0.5This works.
But validation decorators can become awkward when function signatures differ.
This decorator assumes the first argument is the value to validate.
For broad validation, explicit checks inside the function or a dedicated validation library may be clearer.
Decorators are best when the validation rule is truly cross-cutting and the call shape is consistent.
Web frameworks often use decorators for authorization:
def require_admin(function):
@wraps(function)
def wrapper(request, *args, **kwargs):
if not request.user.is_admin:
raise PermissionError("admin required")
return function(request, *args, **kwargs)
return wrapperUse:
@require_admin
def delete_user(request, user_id):
...This can be clear because authorization is a cross-cutting concern.
But too many decorators can hide the route's behavior.
Example:
@route("/users/{id}")
@require_admin
@rate_limit("10/minute")
@audit_log
@transactional
def delete_user(request, id):
...This may be appropriate in a framework.
It may also become hard to reason about.
Decorator stacks need discipline.
Decorators and context managers both wrap behavior.
Decorator:
@timed
def work():
...Context manager:
with timer():
work()Use a decorator when the behavior should apply every time the function is called.
Use a context manager when the behavior should apply to a specific block.
Example:
@timed
def build_index():
...means every call is timed.
with timer():
build_index()
save_index()means this block is timed.
The scope differs.
Decorators attach behavior to definitions.
Context managers attach behavior to execution blocks.
Decorating a method changes that method object on the class.
Subclasses inherit the decorated method unless they override it.
Example:
class Base:
@log_calls
def save(self):
...
class Child(Base):
passChild().save() uses the decorated method.
If the subclass overrides:
class Child(Base):
def save(self):
...the override is not decorated unless you decorate it too or call super().save().
This matters when decorators enforce behavior such as permissions, transactions, or logging.
If the behavior must apply to all subclasses, an explicit base-class method pattern may be safer.
Decorators are local to the object they decorate.
Inheritance can bypass them through overriding.
Tools often inspect functions.
They may read:
__name____doc__- annotations
- signatures
__wrapped__
Without functools.wraps, decorators can break introspection.
Example:
def bad_decorator(function):
def wrapper(*args, **kwargs):
return function(*args, **kwargs)
return wrapperNow tools see:
wrapperinstead of the original function name.
With:
@wraps(function)metadata is copied and __wrapped__ points to the original.
This helps:
- documentation tools
- test tools
- web frameworks
- type inspection
- debugging
- decorator stacking
Use wraps.
It is small and important.
Decorators can confuse static type checkers if they change call signatures.
Simple wrapper:
def log_calls(function):
@wraps(function)
def wrapper(*args, **kwargs):
return function(*args, **kwargs)
return wrapperAt runtime this works.
But type checkers may need help preserving the original signature.
Modern Python typing provides tools such as ParamSpec and TypeVar for well-typed decorators.
That belongs in the static typing chapter.
For now, remember:
a decorator can preserve runtime behavior while obscuring static type information
The more a decorator changes arguments or return values, the more carefully it must be typed and documented.
Some decorators intentionally change return values.
Example:
def as_json(function):
@wraps(function)
def wrapper(*args, **kwargs):
result = function(*args, **kwargs)
return json.dumps(result)
return wrapperUse:
@as_json
def user_data():
return {"name": "Maya"}Now:
user_data()returns a string, not a dictionary.
This can be useful.
It can also surprise callers.
If a decorator changes return type, make that clear in naming and documentation.
Invisible behavior changes make APIs harder to trust.
Some decorators inject or modify arguments.
Example:
def with_database(function):
@wraps(function)
def wrapper(*args, **kwargs):
database = connect()
try:
return function(database, *args, **kwargs)
finally:
database.close()
return wrapperUse:
@with_database
def load_users(database):
return database.query("select * from users")This works but changes how the function is called and understood.
Frameworks often do this.
Application code should be cautious.
Dependency injection through explicit arguments is often clearer.
Decorators that modify arguments should be obvious and well-tested.
Decorators can hold state in closures.
Example:
def count_calls(function):
count = 0
@wraps(function)
def wrapper(*args, **kwargs):
nonlocal count
count += 1
print(f"{function.__name__} called {count} times")
return function(*args, **kwargs)
return wrapperUse:
@count_calls
def greet():
return "hello"Each decorated function gets its own count variable.
Stateful decorators can be useful.
But think about:
- thread safety
- test isolation
- reset behavior
- memory use
- whether state belongs somewhere more explicit
Hidden state inside decorators can make debugging difficult.
Chapter 57 taught metaclasses.
Class decorators are often simpler.
Class decorator:
def register(cls):
registry[cls.__name__] = cls
return clsUse:
@register
class JsonPlugin:
passMetaclass:
class RegistryMeta(type):
def __new__(mcls, name, bases, namespace):
cls = super().__new__(mcls, name, bases, namespace)
registry[name] = cls
return clsThe decorator is explicit.
The metaclass is inherited.
Use a class decorator when:
- one class opts into behavior
- transformation happens after class creation
- no custom namespace is needed
- inheritance-wide behavior is unnecessary
Use a metaclass only for deeper class-creation control.
Decorator syntax is optional.
This:
@timed
def work():
...is equivalent to:
def work():
...
work = timed(work)Manual decoration can be useful when decoration is conditional:
def work():
...
if debug:
work = log_calls(work)Be cautious.
Conditional decoration can make behavior environment-dependent.
That may be useful for debugging.
It may also be confusing.
The @ syntax is usually preferred when decoration is part of the function's definition.
Bug:
def decorator(function):
def wrapper(*args, **kwargs):
return function(*args, **kwargs)There is no:
return wrapperUse:
@decorator
def greet():
return "hello"Now greet becomes None, because the decorator returned None.
Correct:
def decorator(function):
def wrapper(*args, **kwargs):
return function(*args, **kwargs)
return wrapperThe decorator must return the replacement object.
Bug:
def decorator(function):
return function()This calls the function at decoration time.
That is usually wrong.
Correct wrapper:
def decorator(function):
def wrapper(*args, **kwargs):
return function(*args, **kwargs)
return wrapperDecorators usually should return something callable for later.
They should not execute the decorated function immediately unless that is very intentionally the design.
Bug:
def decorator(function):
def wrapper():
return function()
return wrapperThis breaks decorated functions that need arguments.
General wrapper:
def decorator(function):
@wraps(function)
def wrapper(*args, **kwargs):
return function(*args, **kwargs)
return wrapperUse this shape unless the decorator is intentionally limited to a specific signature.
Bug:
def decorator(function):
def wrapper(*args, **kwargs):
return function(*args, **kwargs)
return wrapperThis works but damages metadata.
Better:
from functools import wraps
def decorator(function):
@wraps(function)
def wrapper(*args, **kwargs):
return function(*args, **kwargs)
return wrapperMake this muscle memory.
Professional decorators should preserve the original function's identity as much as possible.
Bug:
def safe(function):
@wraps(function)
def wrapper(*args, **kwargs):
try:
return function(*args, **kwargs)
except Exception:
return None
return wrapperThis hides all errors.
Maybe the function failed because of bad input.
Maybe there is a bug.
Maybe the database is down.
Now callers only see None.
Catch narrow exceptions.
Handle only what you can handle.
Let unexpected errors propagate.
Decorators that hide failures are dangerous.
This can become hard to read:
@route("/orders/{id}")
@require_login
@require_permission("orders:read")
@rate_limit("60/minute")
@cache_response(30)
@trace
@transactional
def get_order(request, id):
...Maybe every decorator is justified.
But the function's behavior is now spread across many wrappers.
Ask:
- Is the order obvious?
- Are errors easy to trace?
- Does each decorator preserve metadata?
- Are side effects documented?
- Would middleware, composition, or explicit code be clearer?
Decorators are wonderful until they become fog.
Use them to clarify repeated structure, not to bury behavior.
This:
@decorator
def function():
...passes function to decorator.
This:
@decorator()
def function():
...calls decorator() first.
The result must itself be a decorator.
Example:
@repeat(3)
def greet():
...means:
greet = repeat(3)(greet)If a decorator takes no configuration, use:
@decoratorIf a decorator factory takes configuration, use:
@decorator(...)Some advanced decorators support both forms, but that adds complexity.
Start with one clear form.
Registration decorators run when definitions execute.
Example:
@route("/users")
def users():
...The route registration usually happens at import time.
This is normal in many frameworks.
But import-time side effects can surprise people.
Avoid decorators that perform expensive work at import time:
- network calls
- database connections
- large file scans
- irreversible global changes
Registration is usually okay.
Heavy execution should usually wait until runtime.
Before writing a decorator, ask:
Is this behavior truly cross-cutting?
If no, put it in the function body.
Ask:
Should this apply to every call?
If no, use a context manager around selected calls.
Ask:
Will the decorator preserve arguments, return values, metadata, and exceptions?
If yes, use *args, **kwargs, return, and wraps.
Ask:
Does the decorator change the function's contract?
If yes, name and document it clearly.
Ask:
Does order matter when stacked?
If yes, keep stacks short and clear.
Ask:
Does decoration do work at import time?
If yes, keep it lightweight.
Ask:
Would a function, context manager, class, descriptor, or middleware be clearer?
Decorators are one tool, not the only tool.
Write a decorator that prints before and after a function call.
Solution:
from functools import wraps
def announce(function):
@wraps(function)
def wrapper(*args, **kwargs):
print("before")
try:
return function(*args, **kwargs)
finally:
print("after")
return wrapperUse:
@announce
def greet(name):
print(f"hello {name}")Call:
greet("Maya")Write a timing decorator.
Solution:
from functools import wraps
from time import perf_counter
def timed(function):
@wraps(function)
def wrapper(*args, **kwargs):
start = perf_counter()
try:
return function(*args, **kwargs)
finally:
elapsed = perf_counter() - start
print(f"{function.__name__}: {elapsed:.3f}s")
return wrapperTest:
@timed
def add(a, b):
return a + b
assert add(2, 3) == 5The decorator should not change the return value.
Write a decorator that repeats a function call.
Solution:
from functools import wraps
def repeat(times):
def decorator(function):
@wraps(function)
def wrapper(*args, **kwargs):
result = None
for _ in range(times):
result = function(*args, **kwargs)
return result
return wrapper
return decoratorUse:
@repeat(3)
def greet():
print("hello")This prints "hello" three times.
Write a command registry.
Solution:
commands = {}
def command(name):
def decorator(function):
commands[name] = function
return function
return decoratorUse:
@command("hello")
def hello():
return "hello"Test:
assert commands["hello"] is helloThis decorator registers the function but does not wrap it.
Write a class decorator that adds a slug attribute.
Solution:
def add_slug(cls):
cls.slug = cls.__name__.lower()
return clsUse:
@add_slug
class UserProfile:
passTest:
assert UserProfile.slug == "userprofile"Class decorators receive and return class objects.
Given:
@a
@b
@c
def f():
...What is the equivalent assignment?
Answer:
def f():
...
f = a(b(c(f)))Decorators apply from bottom to top.
The call behavior depends on what each decorator returns.
What is wrong?
def log(function):
def wrapper(*args, **kwargs):
print("calling")
function(*args, **kwargs)
return wrapperAnswer:
The wrapper does not return the wrapped function's result.
It also does not use functools.wraps.
Better:
from functools import wraps
def log(function):
@wraps(function)
def wrapper(*args, **kwargs):
print("calling")
return function(*args, **kwargs)
return wrapperDecorators transform functions, methods, or classes at definition time.
Decorator syntax:
@decorator
def function():
...is roughly:
function = decorator(function)A function decorator usually returns a wrapper function.
Closures let the wrapper remember the original function.
General wrappers should accept *args and **kwargs, pass them through, and return the original result.
functools.wraps preserves metadata and should be used for most wrappers.
Decorators with arguments are decorator factories.
They add one more layer:
factory arguments -> decorator -> wrapper
Multiple decorators apply from bottom to top.
Decorators can wrap methods, but order matters with classmethod, staticmethod, and property.
Class decorators transform class objects after creation.
Decorators can also register functions or classes without wrapping them.
Decorators are useful for logging, timing, caching, validation, authorization, registration, retries, framework routes, test markers, and class transformations.
They should be used carefully because they can hide behavior, change contracts, obscure metadata, swallow exceptions, or create import-time side effects.
The design principle is:
use decorators when repeated definition-level behavior becomes clearer as a reusable wrapper or transformation
If behavior should apply only to a block, use a context manager.
If behavior belongs inside the function's core logic, keep it explicit.
If behavior changes the function's contract, make that change visible.
Chapter 61 completes Volume II Part III: Pythonic Abstractions.
We studied:
- iterators
- generators
- context managers
- decorators
These abstractions are Pythonic because they make common control-flow patterns reusable:
iterators -> consume values one at a time
generators -> produce values lazily
context managers -> manage scoped setup and cleanup
decorators -> transform definitions
Next we begin Part IV: Robust Programs and I/O.
Chapter 62 studies exceptions.
We have already seen exceptions in many places:
StopIterationends iterators.__exit__receives exception information.- decorators can log, transform, suppress, or propagate errors.
- validation raises
ValueErrororTypeError.
Now we study exceptions directly.
Chapter 62 will cover:
- what exceptions are
- how
try,except,else, andfinallywork - exception hierarchy
- raising exceptions
- custom exception classes
- chaining exceptions
- exception groups
- when to catch and when to let errors propagate
- robust error-handling design
The transition is:
Pythonic abstractions control flow
exceptions handle abnormal flow
Robust Python begins when errors are treated as part of the design, not as afterthoughts.