You now know that functions can accept and return other functions. A decorator is the practical application of that idea: a function that takes a function as input, wraps it in some extra behaviour, and returns the wrapped version. The caller uses the original name but gets the enhanced behaviour for free.
Decorators solve cross-cutting concerns — logging, timing, access control, caching — without changing the function they wrap. Once you recognise the pattern, you will spot @login_required, @app.route, and @property everywhere in Python code. Writing your own decorators is a milestone that separates intermediate programmers from beginners.
flowchart LR A["original greet()"] --> D["decorator(wrapper)"] D --> B["wrapper runs pre-code"] B --> C["calls original greet()"] C --> E["wrapper runs post-code"] E --> F["returns result"] style D fill:#3776ab,color:#fff
Manual decoration
Before learning the @ shortcut, it helps to see what happens by hand. A decorator is just a higher-order function. It receives the original function, defines an inner wrapper, and returns that wrapper. The wrapper calls the original function inside itself, sandwiched by whatever extra work the decorator needs to do.
In the example below, announce is the decorator. It prints a message before and after the function it wraps runs. greet = announce(greet) replaces the name greet with the wrapper, so every future call goes through the decorator. This explicit rebinding makes it clear that the original function still exists; it is simply being hidden behind a new name that points to the wrapper.
def announce(fn):
def wrapper(name):
print('About to run...')
result = fn(name)
print('Done running.')
return result
return wrapper
def greet(name):
return f'Hello, {name}!'
greet = announce(greet)
print(greet('Ada'))
The @ syntax
Writing greet = announce(greet) after every function definition is repetitive and easy to forget. Python provides @decorator_name as syntactic sugar that does exactly the same thing, placed immediately above the def.
@announce before def greet(...) means define greet, then immediately pass it through announce and rebind the name. There is no magic — it is purely shorthand for the manual assignment you saw above. The decorated function is still a normal callable; it just happens to be the wrapper returned by the decorator. Understanding this equivalence is essential when you start stacking multiple decorators or debugging decorated code.
def announce(fn):
def wrapper(name):
print('About to run...')
result = fn(name)
print('Done running.')
return result
return wrapper
@announce
def greet(name):
return f'Hello, {name}!'
print(greet('Ada'))
flowchart TD A["@announce"] --> B["def greet(): ..."] B --> C["greet = announce(greet)"] style A fill:#3776ab,color:#fff
A decorator that modifies the return value
Decorators are not limited to side effects like printing. They can inspect, modify, or replace the return value entirely. The wrapper calls the original function, receives its result, and then decides what to hand back to the caller.
Below, uppercase is a decorator that converts whatever the wrapped function returns into an upper-case string. It does not care what the original function does; it only cares about the value coming back. This is a common pattern for normalisation, validation, and format conversion.
def uppercase(fn):
def wrapper(*args):
result = fn(*args)
return str(result).upper()
return wrapper
@uppercase
def greet(name):
return f'Hello, {name}!'
@uppercase
def add(a, b):
return a + b
print(greet('world'))
print(add(2, 3))
Preserving identity with functools.wraps
When you decorate a function, the original name gets rebound to the wrapper. That means greet.__name__ becomes 'wrapper' instead of 'greet', and the docstring disappears. For introspection tools, debuggers, and documentation generators, that is a problem.
The standard fix is functools.wraps, a decorator for your decorator. It copies metadata — name, docstring, module — from the original function onto the wrapper. It is not required for correctness, but omitting it is considered unpolished. Whenever you write a decorator that might be used by other developers, include @wraps(fn) above your wrapper definition.
Stacking decorators
You can apply more than one decorator to the same function. They nest from bottom to top: the decorator closest to the def runs first, and each subsequent decorator wraps the result of the one before it.
@bold
@italic
def title():
return 'Hello'
This is equivalent to title = bold(italic(title)). The inner italic wraps the original title, then bold wraps the result. When reading stacked decorators, work from the inside out to understand the final behaviour.
flowchart TD A["original fn"] --> B["@italic"] B --> C["@bold"] C --> D["final wrapped fn"] style B fill:#3776ab,color:#fff style C fill:#3776ab,color:#fff
Write a decorator double_result that doubles whatever the decorated function returns. The wrapper should use *args to forward arguments. Apply it manually (no @ syntax) in the tests by doing fn = double_result(fn).
def double_result(fn):
# your wrapper here
pass
Define an inner wrapper that calls fn(*args) and multiplies by 2.
Return the wrapper, not the result of calling it.
def double_result(fn):
def wrapper(*args):
return fn(*args) * 2
return wrapper
What does this print? Trace the order: wrapper runs first, then the original function, then the wrapper finishes.
def tag(fn):
def wrapper(text):
return '<p>' + fn(text) + '</p>'
return wrapper
@tag
def echo(text):
return text
print(echo('hi'))
echo is rebound to the wrapper returned by tag.
wrapper calls echo('hi') and wraps the result in tags.
Write a decorator count_calls that counts how many times the decorated function is called. Use a closure variable calls. The wrapper should increment calls each invocation and then call the original function normally. Return calls from the wrapper as the result (ignore the original function's return value for this exercise).
def count_calls(fn):
# use a closure variable
pass
Use a nonlocal calls variable inside the wrapper.
Increment before or after calling fn, then return calls.
def count_calls(fn):
calls = 0
def wrapper(*args):
nonlocal calls
calls += 1
fn(*args)
return calls
return wrapper
This decorator is meant to return the original function's result doubled, but it forgets to return anything. Fix the wrapper so the decorated function's return value is preserved and doubled.
def double_result(fn):
def wrapper(*args):
fn(*args) * 2
return wrapper
The wrapper must return the doubled result.
Without a return statement, the wrapper gives back None.
def double_result(fn):
def wrapper(*args):
return fn(*args) * 2
return wrapper
Write a decorator require_positive that checks every positional argument passed to the decorated function. If any argument is less than or equal to zero, return the string 'all args must be positive' instead of calling the function. Otherwise call the function normally and return its result. Use *args.
def require_positive(fn):
# your wrapper here
pass
Check args with any(arg <= 0 for arg in args).
Return the error string early if the check fails.
def require_positive(fn):
def wrapper(*args):
if any(arg <= 0 for arg in args):
return 'all args must be positive'
return fn(*args)
return wrapper
Recap
- A decorator is a higher-order function that wraps another function to add behaviour.
@decoratorabove adefis shorthand for rebinding the name through the decorator.- The wrapper should accept
*args(and**kwargs) so it works with any signature. - Use
functools.wrapsto preserve the original function's metadata. - Decorators are closures: the wrapper captures the original function in its enclosing scope.
- Multiple decorators stack from the inside out, each wrapping the result of the one below it.
Next you will learn context managers, a pattern for running setup and teardown code reliably around a block — the with statement.