Imagine you want to time how long several functions take, or log every call, or check a user is logged in before a function runs. You could paste the same lines into the top and bottom of every function, but that's repetitive and clutters the code that actually matters.
A decorator solves this neatly. It's a function that takes another function and returns an improved version of it. You apply it by writing @decorator_name on the line above a function definition, and the extra behaviour is added without touching the function's own code.
You've probably already seen decorators such as @property or @app.route("/") in web frameworks. In this lesson you'll learn the two ideas decorators are built on, write your own step by step, make them work with any function, give them arguments, and use the helpful decorators that come with Python.
Functions are objects
In Python, a function is a value like any other. You can store it in a variable, pass it to another function and return it from a function:
def shout(text):
return text.upper() + "!"
say = shout # no brackets: the function itself
print(say("hello")) # HELLO!A function can also be defined inside another function. The inner function can use the outer function's variables, even after the outer function has finished. This is called a closure, and it's what lets a decorator remember the function it wraps.
Writing a decorator
def announce(func):
def wrapper():
print("Starting...")
func()
print("Done.")
return wrapper
@announce
def backup():
print("Backing up files")@announce is shorthand for backup = announce(backup). The name backup now refers to wrapper, which prints a message, calls the original function, then prints another. Calling backup() prints all three lines.
Wrapping any function
Real decorators must work with functions that take arguments and return values. Use *args and **kwargs to pass everything through, and return the result:
import functools
def log_calls(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}{args}")
result = func(*args, **kwargs)
return result
return wrapper@functools.wraps(func) copies the original function's name and docstring onto the wrapper. Without it, every decorated function would be called "wrapper" in error messages and help text.
Writing a decorator that takes arguments
Here is the three-layer pattern in full. The outer function receives the settings, the middle one receives the function, and the inner wrapper does the work on every call:
import functools
def repeat(times):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
result = None
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(times=3)
def cheer(name):
print(f"Go {name}!")
cheer("Ada")This prints Go Ada! three times. @repeat(times=3) first calls repeat(3), which returns decorator; Python then applies that to cheer. Stacked decorators are applied from the bottom up: the one closest to the def wraps the function first.
Decorators with arguments, and built-ins
To give a decorator settings, such as @retry(times=3), add one more layer: a function that takes the settings and returns the decorator. It's three nested functions, which looks complex at first but always follows the same pattern.
Python includes several useful decorators:
@functools.lru_cacheremembers results, so repeat calls with the same arguments return instantly.@propertylets a method be read like an attribute, as inaccount.balance.@staticmethodand@classmethodchange how methods in a class receive their first argument.@dataclasswrites a class's boilerplate for you, as you'll see in the dataclasses lesson.
Python Decorators Syntax
Decorator template
import functools
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
# before
result = func(*args, **kwargs)
# after
return result
return wrapper
@decorator
def my_function():
...Python Decorators Summary
- Functions are objects: they can be passed to and returned from other functions.
- A decorator takes a function and returns a new function that adds behaviour.
@decoratorabove a definition meansfunc = decorator(func).- Use
*args, **kwargsand return the result so the wrapper works for any function; add@functools.wraps. - Built-in decorators include
lru_cache,property,staticmethod,classmethodanddataclass.
Python Decorators Examples
Each example shows the code, the output it produces and what each part does.
A timing decorator
import functools
import time
def timed(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
seconds = time.perf_counter() - start
print(f"{func.__name__} took {seconds:.2f}s")
return result
return wrapper
@timed
def slow_add(a, b):
time.sleep(0.5)
return a + b
print(slow_add(2, 3))slow_add took 0.50s
5Line-by-line explanation
- Line 7Record the time before calling the original function.
- Line 8Pass every argument through unchanged.
- Line 11Return the original result, so slow_add still returns 5.
- Line 14The decorator is applied once, when the function is defined. The exact time may differ slightly.
A decorator with an argument
import functools
def repeat(times):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for _ in range(times):
func(*args, **kwargs)
return wrapper
return decorator
@repeat(times=3)
def cheer(name):
print(f"Go {name}!")
cheer("team")Go team!
Go team!
Go team!Line-by-line explanation
- Line 3The outer function receives the setting.
- Line 4It returns the real decorator, which receives the function.
- Line 12repeat(times=3) runs first and returns decorator, which then wraps cheer.
Caching with lru_cache
from functools import lru_cache
@lru_cache
def fib(n):
if n < 2:
return n
return fib(n - 1) + fib(n - 2)
print(fib(80))
print(fib.cache_info().hits)23416728348467685
78Line-by-line explanation
- Line 3The cache stores every result.
- Line 7Without the cache, fib(80) would make billions of calls. With it, each value is calculated once.
- Line 10hits counts how many calls were answered from the cache.
Common Python Decorators Mistakes
Calling the function in the decorator line
@timed()
def report():
...@timed
def report():
...timed doesn't take settings, so it shouldn't have brackets. Brackets are only for decorators like repeat(times=3).
Forgetting to return the result
def wrapper(*args, **kwargs):
func(*args, **kwargs) # result is lostdef wrapper(*args, **kwargs):
return func(*args, **kwargs)Losing the function's name
def log_calls(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
# report.__name__ is now 'wrapper'def log_calls(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapperPython Decorators in Real Projects
Decorators in frameworks
Flask and FastAPI use decorators to connect web addresses to functions, as in @app.get("/users"). Testing tools use them to mark tests and supply data. Django uses @login_required to protect pages, and many libraries offer @retry decorators that try a network call again when it fails. Once you understand the pattern, all of these become much less mysterious.
Python Decorators FAQ
What is a decorator in Python, in simple terms?
A decorator is a function that wraps another function to add behaviour before or after it runs, without editing the original function's code. Writing @timer above def build(): is shorthand for build = timer(build).
What is a closure and why do decorators need one?
A closure is an inner function that remembers variables from the function that created it, even after that outer function has returned. The wrapper inside a decorator is a closure: it remembers func, the original function, so it can call it later.
Why should I use functools.wraps?
Without it, the decorated function takes on the wrapper's name and docstring, so help(), error messages, logging and testing tools all show wrapper. @functools.wraps(func) copies the original name, docstring and other metadata onto the wrapper.
Can I use more than one decorator on a function?
Yes. Stack them on separate lines. They are applied from the bottom up, so in @a then @b above a function, b wraps the function first and a wraps the result. Order matters when, for example, one decorator caches results and another checks permissions.
Can decorators be used on classes and methods?
Yes. Decorators work on methods in exactly the same way, as long as the wrapper accepts *args, which includes self. A decorator can also be applied to a whole class, which is how @dataclass adds methods to the class it decorates.
Python Decorators Exercises
Answer each one and check it instantly. If you get stuck, open the hint before the answer.
Complete the line that applies the timed decorator to the function.
timed
def build_report():
...
What is @shout above def hello(): shorthand for?
What does this print?
def twice(func):
def wrapper():
func()
func()
return wrapper
@twice
def hi():
print("hi")
hi()
Why does a wrapper use *args, **kwargs?
What does @functools.wraps(func) do?
Python Decorators Quiz
5 questions to check what you learned.
What is a decorator?
- A function that takes a function and returns a modified version of it
- A comment that styles code
- A type of class
- A loop
What is a closure?
- A function with no return
- An inner function that remembers variables from the function it was defined in
- The end of a file
- A closed file object
What does
@lru_cachedo?- Stores results so repeated calls with the same arguments return instantly
- Deletes old functions
- Logs every call
- Limits recursion
How many nested functions does a decorator with arguments normally have?
- One
- Two
- Three
- Four
When does a decorator run?
- Every time the program starts a loop
- Once, when the decorated function is defined; the wrapper runs on each call
- Only when an error happens
- Never, it's just a label