Decorators modify or enhance functions and classes without changing their source code. They’re used for logging, timing, authentication, caching, and more.

Functions as First-Class Objects

Functions can be passed as arguments and returned from other functions:

  def greet(name):
    return f"Hello, {name}!"

def shout(func):
    def wrapper(name):
        result = func(name)
        return result.upper()
    return wrapper

loud_greet = shout(greet)
loud_greet("Alice")  # "HELLO, ALICE!"
  

Basic Decorator Syntax

@decorator is syntactic sugar for reassigning the function:

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

@timer
def slow_function():
    import time
    time.sleep(1)
    return "done"

slow_function()  # slow_function took 1.0012s
  

Preserving Metadata with functools.wraps

Decorators replace the original function, losing its name and docstring:

  import functools

def my_decorator(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        """Wrapper docstring — not what we want exposed."""
        return func(*args, **kwargs)
    return wrapper

@my_decorator
def important_function():
    """This docstring should be preserved."""
    pass

print(important_function.__name__)  # important_function
print(important_function.__doc__)   # This docstring should be preserved.
  

Always use @functools.wraps(func) in decorators.

Decorators with Arguments

Create decorator factories when you need configurable behavior:

  import functools
import time

def retry(max_attempts=3, delay=1):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(max_attempts):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    if attempt == max_attempts - 1:
                        raise
                    print(f"Attempt {attempt + 1} failed: {e}. Retrying...")
                    time.sleep(delay)
        return wrapper
    return decorator

@retry(max_attempts=3, delay=2)
def fetch_data():
    ...
  

Built-in Decorators

@property — Computed Attributes

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

    @property
    def area(self):
        return 3.14159 * self.radius ** 2

    @property
    def diameter(self):
        return self.radius * 2

c = Circle(5)
print(c.area)       # 78.54 — accessed like an attribute
print(c.diameter)   # 10
  

@staticmethod and @classmethod

  class Date:
    def __init__(self, year, month, day):
        self.year, self.month, self.day = year, month, day

    @classmethod
    def from_string(cls, date_str):
        year, month, day = map(int, date_str.split("-"))
        return cls(year, month, day)

    @staticmethod
    def is_leap_year(year):
        return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)

d = Date.from_string("2024-06-15")
Date.is_leap_year(2024)  # True
  

Class Decorators

  def singleton(cls):
    instances = {}
    @functools.wraps(cls)
    def get_instance(*args, **kwargs):
        if cls not in instances:
            instances[cls] = cls(*args, **kwargs)
        return instances[cls]
    return get_instance

@singleton
class Database:
    def __init__(self):
        print("Connecting...")

db1 = Database()  # Connecting...
db2 = Database()  # (no output — same instance)
assert db1 is db2
  

Stacking Decorators

Decorators apply bottom-up:

  @decorator_a
@decorator_b
def func():
    pass

# Equivalent to: func = decorator_a(decorator_b(func))
  

Practical Examples

Access Control

  def require_auth(func):
    @functools.wraps(func)
    def wrapper(user, *args, **kwargs):
        if not user.get("authenticated"):
            raise PermissionError("Login required")
        return func(user, *args, **kwargs)
    return wrapper
  

Caching

  @functools.lru_cache(maxsize=128)
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)
  

Decorators are everywhere in Python frameworks — Flask routes (@app.route), pytest fixtures (@pytest.fixture), and dataclass generation (@dataclass) all use them.