DocsHub
Functions

Decorators

Learn what decorators are, how they work, and how to write and use them in Python.

Decorators

A decorator is a function that wraps another function to add extra behaviour — without changing the original function's code.

That sounds abstract. Let's build up to it from scratch.


First — functions are objects

In Python, functions are just objects like anything else. You can store them in variables, pass them to other functions, and return them from functions.

def greet():
    print("Hello!")

# store a function in a variable
say_hello = greet

say_hello()   # Hello!

say_hello and greet now point to the same function. We did not call it — we just stored it.

You can also pass a function as an argument to another function:

def greet():
    print("Hello!")

def run(func):
    func()   # call whatever function was passed in

run(greet)   # Hello!

This is the foundation. Keep it in mind.


Second — functions inside functions

You can define a function inside another function:

def outer():
    print("I am outer.")

    def inner():
        print("I am inner.")

    inner()

outer()

Output:

I am outer.
I am inner.

inner only exists inside outer. You cannot call it from outside.


Third — returning a function

A function can return another function:

def outer():
    def inner():
        print("Hello from inner!")
    return inner   # return the function itself, not its result

func = outer()
func()   # Hello from inner!

outer() returns inner — not the result of calling inner, but the function object itself. Then we call it with func().


Now — what is a decorator?

A decorator is a function that:

  1. Takes a function as input
  2. Defines a new wrapper function around it
  3. Returns the wrapper
def my_decorator(func):
    def wrapper():
        print("Before the function runs.")
        func()
        print("After the function runs.")
    return wrapper

To use it, pass your function through the decorator:

def greet():
    print("Hello!")

greet = my_decorator(greet)

greet()

Output:

Before the function runs.
Hello!
After the function runs.

What happened? my_decorator took greet, wrapped it inside wrapper, and returned wrapper. Now greet points to wrapper. When you call greet(), you are actually calling wrapper() — which runs the extra code before and after the original greet().


The @ syntax — cleaner way to decorate

Writing greet = my_decorator(greet) every time is clunky. Python gives you a cleaner syntax using @:

def my_decorator(func):
    def wrapper():
        print("Before the function runs.")
        func()
        print("After the function runs.")
    return wrapper

@my_decorator
def greet():
    print("Hello!")

greet()

Output:

Before the function runs.
Hello!
After the function runs.

@my_decorator above the function is exactly the same as writing greet = my_decorator(greet). It is just cleaner.

You call greet wrapper runs Before code runs Original greet runs After code runs Done

Decorating functions with arguments

The simple wrapper above only works for functions that take no arguments. To make it work for any function, use *args and **kwargs in the wrapper:

def my_decorator(func):
    def wrapper(*args, **kwargs):
        print("Before.")
        result = func(*args, **kwargs)
        print("After.")
        return result
    return wrapper

@my_decorator
def add(a, b):
    return a + b

print(add(3, 4))

Output:

Before.
After.
7

*args and **kwargs collect whatever arguments were passed to add and forward them to the original function. Now your decorator works on any function regardless of its arguments.


Real example 1 — a timer decorator

Measure how long any function takes to run:

import time

def timer(func):
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        print(f"{func.__name__} took {end - start:.4f} seconds.")
        return result
    return wrapper

@timer
def calculate_sum(n):
    return sum(range(n))

print(calculate_sum(1_000_000))

Output:

calculate_sum took 0.0412 seconds.
499999500000

You did not touch calculate_sum at all. The timer behaviour was added on top by the decorator.


Real example 2 — a login required decorator

A very common pattern in web development — check if a user is logged in before running a function:

def login_required(func):
    def wrapper(user, *args, **kwargs):
        if not user.get("is_logged_in"):
            print("Access denied. Please log in first.")
            return
        return func(user, *args, **kwargs)
    return wrapper

@login_required
def view_dashboard(user):
    print(f"Welcome to your dashboard, {user['name']}!")

@login_required
def view_settings(user):
    print(f"Opening settings for {user['name']}.")

# logged in user
user_ali = {"name": "Ali", "is_logged_in": True}

# not logged in
user_sara = {"name": "Sara", "is_logged_in": False}

view_dashboard(user_ali)    # Welcome to your dashboard, Ali!
view_dashboard(user_sara)   # Access denied. Please log in first.
view_settings(user_ali)     # Opening settings for Ali.
view_settings(user_sara)    # Access denied. Please log in first.

One decorator — applied to two functions. Both get the login check without repeating the logic inside each function.


Real example 3 — a repeat decorator

Run a function multiple times:

def repeat(times):
    def decorator(func):
        def wrapper(*args, **kwargs):
            for _ in range(times):
                func(*args, **kwargs)
        return wrapper
    return decorator

@repeat(3)
def say_hello(name):
    print(f"Hello, {name}!")

say_hello("Ali")

Output:

Hello, Ali!
Hello, Ali!
Hello, Ali!

This is a decorator with arguments — notice the extra outer function repeat(times) that receives the argument and returns the actual decorator. The structure is one level deeper but the idea is the same.

When a decorator needs its own arguments like @repeat(3), you need three levels:

  • Outer function takes the decorator's argument
  • Middle function takes the original function
  • Inner wrapper calls the original function

When a decorator needs no arguments like @timer, you only need two levels.


Stacking decorators

You can apply multiple decorators to one function. They are applied from bottom to top:

def bold(func):
    def wrapper():
        return f"<b>{func()}</b>"
    return wrapper

def italic(func):
    def wrapper():
        return f"<i>{func()}</i>"
    return wrapper

@bold
@italic
def greet():
    return "Hello"

print(greet())   # <b><i>Hello</i></b>

@italic is applied first (closest to the function), then @bold wraps around it.


functools.wraps — keeping the original function's identity

When you decorate a function, the wrapper replaces it — including its name and docstring. This can cause confusion when debugging:

def my_decorator(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@my_decorator
def greet():
    """Says hello."""
    print("Hello!")

print(greet.__name__)   # wrapper — not greet!

Fix this with functools.wraps:

import functools

def my_decorator(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@my_decorator
def greet():
    """Says hello."""
    print("Hello!")

print(greet.__name__)   # greet
print(greet.__doc__)    # Says hello.

Always use @functools.wraps(func) inside your decorators. It is one line and it keeps the original function's name and docstring intact. It is considered best practice.


Summary

ConceptExample
Basic decoratordef decorator(func):
Apply a decorator@decorator
Handle any argumentsdef wrapper(*args, **kwargs):
Decorator with argumentsThree nested functions
Stack decoratorsMultiple @ lines above a function
Preserve function identity@functools.wraps(func)

On this page