Know if you're actually ready. Take the Python quiz → get your AI readiness report.Take the free test →

Study guide · Python

Python Decorators: How They Work & What Exams Test

Decorators look like magic but they're just functions that wrap other functions. Here's everything exams ask — including the stacking order trap.

What is a decorator?

A decorator is a function that takes another function as input and returns a modified version of it. The @ syntax is shorthand for passing a function as an argument.

def shout(func):
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        return str(result).upper()
    return wrapper

@shout
def greet(name):
    return f"hello, {name}"

print(greet("alice"))  # "HELLO, ALICE"

# Equivalent without @ syntax:
greet = shout(greet)
Note@shout above def greet is exactly the same as greet = shout(greet) after the definition.

Preserving metadata with functools.wraps

Without functools.wraps, the decorated function loses its original __name__ and __doc__. Always use @wraps(func) inside your decorator.

from functools import wraps

def my_decorator(func):
    @wraps(func)          # preserves __name__, __doc__, etc.
    def wrapper(*args, **kwargs):
        print("Before")
        result = func(*args, **kwargs)
        print("After")
        return result
    return wrapper

@my_decorator
def add(a, b):
    """Add two numbers."""
    return a + b

print(add.__name__)  # 'add'  (without @wraps: 'wrapper')
print(add.__doc__)   # 'Add two numbers.'

Decorator with arguments

To pass arguments to a decorator, add another layer of nesting — a factory function that returns the actual decorator.

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

@repeat(3)
def say_hi():
    print("Hi!")

say_hi()  # prints "Hi!" three times

Stacking decorators

When you stack multiple decorators, they apply bottom-up (closest to the function first), but execute top-down when the function is called.

def bold(func):
    @wraps(func)
    def wrapper():
        return "<b>" + func() + "</b>"
    return wrapper

def italic(func):
    @wraps(func)
    def wrapper():
        return "<i>" + func() + "</i>"
    return wrapper

@bold        # applied second
@italic      # applied first
def text():
    return "Hello"

print(text())  # <b><i>Hello</i></b>
NoteApplication order: bottom decorator wraps first. So @italic wraps text(), then @bold wraps that result.

Built-in decorators: @property, @staticmethod, @classmethod

Python ships three essential built-in decorators used in class definitions.

class Circle:
    def __init__(self, radius):
        self._radius = radius

    @property              # getter — access like an attribute
    def area(self):
        return 3.14159 * self._radius ** 2

    @staticmethod          # no self or cls — independent utility
    def unit_circle():
        return Circle(1)

    @classmethod           # receives the class (cls) not the instance
    def from_diameter(cls, d):
        return cls(d / 2)

c = Circle(5)
print(c.area)             # 78.53... (no parentheses needed)
c2 = Circle.from_diameter(10)
Exam tipStacking order is the top exam trap: decorators apply bottom-up but execute top-down. Also: without functools.wraps, func.__name__ returns the wrapper's name — exams test this directly.

Want a person to help?

Struggling with Python? Work 1:1 with a vetted Python tutor on Wyzant. (affiliate link)

Find a Python tutor →
← All Python guides