Python 2026-05-14

Python Decorators: A Comprehensive Guide

This post is based on the helper project available on GitHub:

zacniewski/python-decorators-intro

Python decorators are one of the most powerful and elegant features of the language. They allow you to modify or extend the behavior of functions or classes without changing their source code. If you've ever used @app.route in Flask or @app.task in Celery, you've used decorators.

In this guide, we'll demystify decorators from the ground up, starting with the fundamental concept that makes them possible.

1. The Foundation: Functions are Objects

In Python, functions are first-class objects. This means you can assign them to variables, pass them as arguments to other functions, and even return them from functions.

def shout(word="yes"):
    return word.capitalize() + "!"

# Assign function to a variable
scream = shout

print(scream())
# Output: Yes!
2. Handcrafting Your First Decorator

A decorator is simply a function that takes another function as an argument and returns a new function (a wrapper). The wrapper "wraps" the original function, allowing you to execute code before and after it runs.

def my_decorator(func):
    def wrapper():
        print("Something is happening before the function is called.")
        func()
        print("Something is happening after the function is called.")
    return wrapper

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

# Manual decoration
say_hello = my_decorator(say_hello)

say_hello()
# Output:
# Something is happening before the function is called.
# Hello!
# Something is happening after the function is called.
3. Syntactic Sugar: The @ Symbol

Python provides a cleaner way to apply decorators using the @ symbol. This is just "syntactic sugar" for the manual assignment we did above.

@my_decorator
def say_hi():
    print("Hi!")

# This is exactly the same as say_hi = my_decorator(say_hi)
4. Stacking Decorators

You can apply multiple decorators to a single function. They are applied from the bottom up (the one closest to the function runs first).

def bread(func):
    def wrapper():
        print("</''''''\>")
        func()
        print("<\______/>")
    return wrapper

def ingredients(func):
    def wrapper():
        print("#tomatoes#")
        func()
        print("~salad~")
    return wrapper

@bread
@ingredients
def sandwich():
    print("--ham--")

sandwich()
# Output:
# </''''''\>
# #tomatoes#
# --ham--
# ~salad~
# <\______/>
5. Passing Arguments to Decorated Functions

To decorate functions that take arguments, your wrapper needs to accept those arguments and pass them along. Using *args and **kwargs ensures your decorator works with any number of arguments.

def logger(func):
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__} with {args}")
        return func(*args, **kwargs)
    return wrapper

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

print(add(5, 10))
6. Decorators with Arguments

Sometimes you want to pass arguments to the decorator itself. This requires an extra level of nesting: a "decorator maker" that returns the actual decorator.

def repeat(num_times):
    def decorator_repeat(func):
        def wrapper(*args, **kwargs):
            for _ in range(num_times):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator_repeat

@repeat(num_times=3)
def greet(name):
    print(f"Hello {name}")

greet("Alice")
7. Best Practices: Using functools.wraps

When you wrap a function, the new function (the wrapper) replaces the old one. This means metadata like the function's name and docstring are lost. To fix this, Python provides functools.wraps.

import functools

def my_decorator(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        """I am the wrapper docstring"""
        return func(*args, **kwargs)
    return wrapper

@my_decorator
def complex_logic():
    """I am the original docstring"""
    pass

print(complex_logic.__name__)  # Outputs: complex_logic (instead of 'wrapper')
print(complex_logic.__doc__)   # Outputs: I am the original docstring

Summary

  • Decorators are functions that modify other functions.
  • They use closures to remember the original function.
  • The @decorator syntax is syntactic sugar.
  • Always use functools.wraps to preserve metadata.

Scientific Dev
Scientific Dev
Educator & Developer