A decorator is just a callable that takes a function and returns a new one, often a wrapper, letting us extend behavior while keeping functions clean and following the Open–Closed Principle.
What Is the @decorator Syntax?
The @decorator syntax is syntactic sugar for applying a decorator to a function or class.
These two snippets are exactly equivalent:
1 |
|
and
1 | def f(): |
In other words, the @decorator syntax:
Passes the function
ftodecoratorReassigns
fto the result of that call
So when Python executes the @decorator line, it effectively runs:
1 | f = decorator(f) |
How Decorators Work: The Core Mechanism
At its core, a decorator is a callable (usually a function) that:
Takes another function as input
Returns a new function (often a modified or enhanced version of the original)
Basic Pattern
1 | def my_decorator(func): # The decorator |
Execution Flow
Decoration phase
When Python sees@my_decorator, it calls:1
say_hello = my_decorator(say_hello)
my_decoratorreturnswrapper, so nowsay_helloactually refers towrapper.Replacement
The originalsay_hellofunction object is replaced bywrapper.Execution
When we callsay_hello(), we are really callingwrapper(), which:Does “before” work
Calls the original
say_helloDoes “after” work
Why Use Decorators Instead of Direct Modification?
A natural question is: why use this extra indirection instead of writing the logic directly inside our functions?
We can compare:
| Aspect | Direct Modification | Using Decorators |
|---|---|---|
| Core principle | Invasive: we modify function internals | Non-invasive: we wrap behavior from outside |
| Code reusability | Poor: logic is duplicated in many functions | High: one decorator can be reused everywhere |
| Maintainability | Low: many places to change | High: change decorator once, all decorated functions update |
| Responsibility separation | Mixed: core logic + “extra stuff” together | Clear: core logic vs cross-cutting concerns |
| Flexibility | Rigid: hard to add/remove behavior cleanly | Flexible: easy to add/remove a decorator |
Decorators give us a clean way to add cross-cutting concerns (logging, authentication, timing, etc.) without polluting each function’s core business logic.
Practical Example: Authentication
Consider adding authentication to a function.
Direct Modification
1 | def delete_item(user_id, item_id): |
Here, the authentication logic is tightly coupled with the business logic.
Decorator Approach
1 | def login_required(func): |
Advantages of the decorator approach:
The function
delete_itemfocuses only on its core responsibility.We can reuse
@login_requiredon many other functions.If our authentication logic changes, we update
login_requiredin one place.
Advanced Decorator Patterns
Decorating Functions with Parameters
Real-world functions almost always take arguments. To support this, we typically use *args and **kwargs in the wrapper:
1 | def log_decorator(func): |
Now @log_decorator can be applied to any function, regardless of its signature.
Decorators with Their Own Arguments
Sometimes we want the decorator itself to accept parameters (e.g., repeat count, log level, etc.). In this case, we use a decorator factory: a function that returns a decorator.
1 | def repeat(num_times): # Outer function: takes decorator arguments |
Conceptually, @repeat(num_times=3) is equivalent to:
1 | say_hello = repeat(3)(say_hello) |
So the call chain is:
repeat(3)returnsdecorator_repeatdecorator_repeat(say_hello)returnswrappersay_hellois replaced bywrapper
Preserving Function Metadata with functools.wraps
A common side effect of decorators is that the wrapper function hides the original function’s metadata such as __name__, __doc__, etc. To preserve these, we use functools.wraps.
1 | from functools import wraps |
Without @wraps, example.__name__ would be 'wrapper', and its docstring would be the wrapper’s docstring.
Design Philosophy Behind Decorators
Decorators are a Pythonic way to implement the Open–Closed Principle:
Software entities should be open for extension but closed for modification.
Instead of modifying existing function bodies (which can be risky and break encapsulation), we extend their behavior externally by wrapping them with decorators.
A useful analogy is a camera:
The camera body is the core function.
Decorators are like lenses and filters: we attach them externally to change behavior without opening up the camera and modifying its internals.
This design leads to code that is more:
Modular
Reusable
Easier to test and reason about
Common Real-World Use Cases
In practice, decorators appear everywhere in modern Python code. Typical use cases include:
Logging
Automatically logging function calls, arguments, and return values.Timing / Profiling
Measuring how long a function takes to run.Authentication / Authorization
Checking user permissions, especially in web frameworks (@login_required,@permission_required, etc.).Caching / Memoization
Storing results of expensive computations, for example@functools.lru_cache.Validation / Pre-conditions
Validating function arguments before running the function body.Retry Logic
Automatically retrying operations that may fail transiently (e.g., network calls).
Summary
The
@decoratorsyntax is just syntactic sugar forfunc = decorator(func).
A decorator is a callable that takes a function and returns a new function, often wrapping extra behavior around it.
Decorators shine when we deal with cross-cutting concerns such as logging, authentication, timing, caching, and validation.
Using decorators, we keep our core business logic clean and focused, while centralized decorators handle reusable concerns.
This approach aligns closely with good software engineering principles, especially the Open–Closed Principle.
As our Python projects grow in complexity, decorators evolve from “extra syntax” into an indispensable tool for writing clean, maintainable, and extensible code.