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.