A Python decorator is a function that takes another function and returns a new, usually enhanced, version of it. You apply it with the @ symbol on the line above a function definition. Decorators let you add behaviour such as logging, timing, caching or permission checks to a function without touching its code.
The idea looks magical until you see the three small ideas behind it. This guide builds up to decorators step by step, with real output at each stage.
In this guide
The short version
@decoratorabovedef fis just shorthand forf = decorator(f).- A decorator takes a function, defines an inner
wrapper, and returns the wrapper. - Use
*args, **kwargsso the wrapper accepts any call, and alwaysreturnthe result. - Add
@functools.wraps(func)so the wrapped function keeps its name and docstring.
Two ideas you need first
1. Functions are objects
In Python you can pass a function to another function, like any value (see Python Functions):
def shout(text):
return text.upper() + "!"
def apply(func, value):
return func(value)
print(apply(shout, "hello"))
Output
HELLO!
2. Functions can be created inside functions
An inner function remembers the variables of the function that created it. This is called a closure. Here the inner multiply remembers n long after make_multiplier has returned:
def make_multiplier(n):
def multiply(x):
return x * n
return multiply
double = make_multiplier(2)
print(double(5))
Output
10
A decorator is just these two ideas together: a function that receives a function, and builds and returns a new one.
Writing your first decorator
Here is loud. It takes a function, defines a wrapper that does something before and after calling it, and returns the wrapper. Done by hand, you replace the original function with the wrapped one:
def loud(func):
def wrapper():
print("before")
func()
print("after")
return wrapper
def hello():
print("hello")
hello = loud(hello)
hello()
Output
before
hello
after
The line hello = loud(hello) is the whole trick. Python gives it a shorthand: put @loud above the function, and it does exactly the same thing:
def loud(func):
def wrapper():
print("before")
func()
print("after")
return wrapper
@loud
def hello():
print("hello")
hello()
Output
before
hello
after
Handling arguments and return values
The wrapper above only works for functions with no arguments and no result. A general-purpose decorator accepts anything using *args, **kwargs (see Python *args and **kwargs Explained), passes it on, and returns whatever the original returned:
def logged(func):
def wrapper(*args, **kwargs):
print("calling", func.__name__, args, kwargs)
result = func(*args, **kwargs)
print("returned", result)
return result
return wrapper
@logged
def add(a, b):
return a + b
total = add(2, b=3)
print(total)
Output
calling add (2,) {'b': 3}
returned 5
5
This is the standard shape of a decorator. Learn it once and you can write most of them.
Keeping the function’s name with functools.wraps
A decorated function is really the wrapper, so it loses its own identity. Its name and docstring now belong to wrapper, which confuses debugging tools and documentation:
def deco(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@deco
def greet():
"""Say hello."""
print(greet.__name__, greet.__doc__)
Output
wrapper None
The fix is a decorator from the standard library. Add @wraps(func) above the wrapper and it copies the name, docstring and other details across:
from functools import wraps
def deco(func):
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@deco
def greet():
"""Say hello."""
print(greet.__name__, greet.__doc__)
Output
greet Say hello.
Make it a habit: every decorator you write should use @wraps.
Decorators that take arguments
To write @repeat(3), you need one more layer. repeat(3) is called first and must return the real decorator, which then receives the function:
from functools import wraps
def repeat(times):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
result = None
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(3)
def ping():
print("ping")
ping()
Output
ping
ping
ping
Read it from the outside in: repeat(times) returns decorator, which takes func and returns wrapper. Three levels, one job each.
Practical examples
Counting calls
Functions are objects, so the wrapper can carry data of its own. This decorator counts how often the function is called:
from functools import wraps
def count_calls(func):
@wraps(func)
def wrapper(*args, **kwargs):
wrapper.calls += 1
return func(*args, **kwargs)
wrapper.calls = 0
return wrapper
@count_calls
def hi():
return "hi"
hi()
hi()
hi()
print(hi.calls)
Output
3
Caching results
The standard library ships a ready-made decorator for caching: functools.lru_cache. It remembers results for arguments it has already seen. Recursive Fibonacci normally repeats a huge amount of work. With the cache, each value from 0 to 30 is computed exactly once, so the function body ran only 31 times:
from functools import lru_cache
calls = 0
@lru_cache(maxsize=None)
def fib(n):
global calls
calls += 1
return n if n < 2 else fib(n - 1) + fib(n - 2)
print(fib(30))
print(calls)
print(fib.cache_info().hits > 0)
Output
832040
31
True
Other common uses: timing how long a function takes, retrying a flaky network call, checking permissions before running, and logging.
Stacking decorators
You can apply more than one. They are applied from the bottom up: the one closest to the function wraps it first, and the next wraps the result:
def bold(f):
def w():
return "<b>" + f() + "</b>"
return w
def italic(f):
def w():
return "<i>" + f() + "</i>"
return w
@bold
@italic
def text():
return "hi"
print(text())
Output
<b><i>hi</i></b>
@italic ran first, giving <i>hi</i>, and then @bold wrapped that.
Decorators you already use
Python has several built in. @property turns a method into an attribute you read without brackets, @classmethod receives the class instead of an instance, and @staticmethod is a plain function that lives in a class:
class Circle:
def __init__(self, r):
self._r = r
@property
def area(self):
return round(3.14159 * self._r ** 2, 2)
@classmethod
def unit(cls):
return cls(1)
@staticmethod
def describe():
return "a round shape"
c = Circle.unit()
print(c.area)
print(Circle.describe())
Output
3.14
a round shape
These belong to classes and objects, and decorators are also how many libraries add features, such as web frameworks registering routes.
Common mistakes
Mistake 1: forgetting to return the result
If the wrapper calls the function but does not return its value, every decorated function suddenly returns None:
def bad(func):
def wrapper(*args, **kwargs):
func(*args, **kwargs)
return wrapper
@bad
def add(a, b):
return a + b
print(add(1, 2))
Output
None
Mistake 2: forgetting to return the wrapper
If the decorator does not return wrapper, the decorated name becomes None and calling it fails:
def deco(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
@deco
def hi():
return "hi"
try:
hi()
except TypeError as e:
print(type(e).__name__)
print(hi)
Output
TypeError
None
Mistake 3: leaving out the brackets on a decorator that takes arguments
@repeat and @repeat(3) are different things. Without the brackets, the function is passed in as times, and the call breaks later:
def repeat(times):
def decorator(func):
def wrapper(*args, **kwargs):
for _ in range(times):
func(*args, **kwargs)
return wrapper
return decorator
@repeat
def ping():
print("ping")
try:
ping()
except TypeError as e:
print(type(e).__name__)
Output
TypeError
Mistake 4: forgetting @wraps
Everything still runs, but the function’s name and docstring are lost, which makes debugging harder.
Try it yourself
Work out each answer first, then open the solution.
1. Write a decorator shout that converts the returned string to upper case.
Show solution
def shout(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs).upper()
return wrapper
@shout
def greet(name):
return "hello " + name
print(greet("asha"))
Output
HELLO ASHAThe wrapper calls the function, changes the result, and returns it.
2. Write a decorator only_positive that raises ValueError when any argument is negative.
Show solution
def only_positive(func):
def wrapper(*args):
if any(a < 0 for a in args):
raise ValueError("negative argument")
return func(*args)
return wrapper
@only_positive
def add(a, b):
return a + b
print(add(2, 3))
try:
add(2, -3)
except ValueError as e:
print("Error:", e)
Output
5
Error: negative argument3. Write a decorator tag(name) that wraps the returned text in HTML tags.
Show solution
def tag(name):
def decorator(func):
def wrapper():
return f"<{name}>" + func() + f"</{name}>"
return wrapper
return decorator
@tag("p")
def hello():
return "hi"
print(hello())
Output
<p>hi</p>A decorator that takes an argument needs the extra outer function.
Run these in our free Python compiler.
Frequently asked questions
What is a decorator in Python?
A decorator is a function that takes another function, adds some behaviour around it, and returns the new version. You apply it with @name above the function definition.
What does the @ symbol do in Python?
It applies a decorator. Writing @deco above def f() is the same as writing f = deco(f) after the definition.
Why should I use functools.wraps?
Without it, the decorated function takes on the wrapper’s name and docstring. @wraps(func) copies the original’s details onto the wrapper so debugging and documentation still work.
How do I write a decorator that takes arguments?
Add an outer function that takes the arguments and returns the actual decorator: three nested functions in total, as in the repeat(times) example.
What are some common built-in decorators?
@property, @classmethod, @staticmethod, functools.lru_cache, functools.wraps and contextlib.contextmanager.
Can a class be used as a decorator?
Yes. A class that defines __call__ can wrap a function, which is handy when the decorator needs to keep state. A function-based decorator is usually simpler.
Related reading
- Python Functions: Arguments, Return and Scope – functions as objects and closures.
- Python *args and **kwargs Explained – how the wrapper accepts any call.
- Python lambda, map, filter and reduce Explained – more ways to pass functions around.
- Python Context Managers and the with Statement – another way to wrap behaviour.
Run this code in your browser
The free Upskly compiler runs Python with nothing to install. Paste the example, change it and see what happens.
Stuck on a traceback?
AI Assist works inside the Python notebook, so you can ask about an error or a concept without leaving the cell.
Test yourself
Timed questions on Decorators and Functional Tools, with an explanation for every answer.