If you've ever written @staticmethod above a method, or @app.route("/") in a Flask app, you've already used a decorator — you just might not have written one yourself yet. A decorator is a function that wraps another function to add behavior around it — logging, timing, access checks, retries — without touching the original function's code at all.
Understanding how they work under the hood demystifies a lot of the "magic" you see in Python frameworks.
Functions as First-Class Values
Decorators only make sense once you know that in Python, functions are values just like numbers or strings — you can pass them as arguments, return them from other functions, and assign them to variables:
def greet():
return "Hello!"
say_hi = greet # assign the function itself, not call it
print(say_hi())
Output:
Hello!
Building a Decorator from Scratch
A decorator is just a function that takes a function as input and returns a new function — usually a "wrapper" that calls the original somewhere inside it:
def shout(func):
def wrapper():
result = func()
return result.upper()
return wrapper
def greet():
return "hello!"
greet = shout(greet) # manually "decorate" greet
print(greet())
Output:
HELLO!
shout took the original greet function, built a new wrapper function around it, and returned that wrapper. Calling the "decorated" greet now actually runs wrapper, which calls the original function and modifies its result.
The @ Syntax
Writing greet = shout(greet) by hand every time gets tedious, so Python gives you the @ syntax as shorthand for exactly that pattern:
@shout
def greet():
return "hello!"
print(greet())
Output:
HELLO!
This produces the exact same result as the manual version above — @shout above a function definition is pure syntax sugar for greet = shout(greet). Nothing more mysterious is happening.
Decorating Functions with Arguments
The wrapper above only works for functions that take no arguments. Real decorators need to accept any arguments using *args and **kwargs, and should use functools.wraps to preserve the original function's name and docstring. A complete, practical example — a decorator that times how long a function takes to run — is built step by step in our Python decorator program example.
When to Reach for a Decorator
Decorators shine when you have behavior you want applied consistently across many different functions — logging every call, checking a user is authenticated before a route handler runs, caching expensive results, or retrying a flaky network call. Rather than copy-pasting that logic into every function, you write it once as a decorator and apply it with a single @ line wherever it's needed.
