Python Decorators

Elegantly modify and enhance functions and classes.

What are Decorators?

Decorators are a powerful Python feature that allows you to modify or enhance functions and classes without changing their source code. They use the @syntax to wrap functions, adding functionality before, after, or around the original code.

Basic Function Decorators

A decorator is a function that takes another function as an argument and returns a new function with added behavior.

def my_decorator(func):
    def wrapper():
        print("Before function call")
        func()
        print("After function call")
    return wrapper

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

say_hello()
Expected Output:
Before function call
Hello!
After function call
Key concept: The @decorator syntax is syntactic sugar for func = decorator(func)

Decorators with Function Arguments

Use *args and **kwargs to make decorators work with any function signature.

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

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

print(add(5, 3))
Expected Output:
Calling add with:
  args: (5, 3)
  kwargs: {}
  returned: 8
8
Best practice: Always use *args, **kwargs in wrapper to maintain flexibility

Preserving Function Metadata

Use functools.wraps to preserve the original function's metadata.

from functools import wraps

# Without @wraps: the wrapper's own identity leaks out
def plain_decorator(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

# With @wraps: the original function's metadata is copied over
def my_decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@plain_decorator
def lost_identity():
    """This docstring disappears"""
    pass

@my_decorator
def complex_function():
    """This function does something complex"""
    pass

print(lost_identity.__name__)
print(lost_identity.__doc__)

print(complex_function.__name__)
print(complex_function.__doc__)
Expected Output:
wrapper
None
complex_function
This function does something complex
Important: Always use @wraps(func) to avoid debugging headaches

Decorators with Parameters

Create decorators that accept arguments by adding another level of nesting.

from functools import wraps

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

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

greet("Alice")
Expected Output:
Hello, Alice!
Hello, Alice!
Hello, Alice!
Pattern: Parameterized decorators: params → decorator → wrapper → function

Practical Decorator Examples

Common real-world use cases for decorators.

import time
from functools import wraps

# Timing decorator
def timer(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        # perf_counter is the right clock for measuring durations:
        # it is monotonic and higher resolution than time.time()
        start = time.perf_counter()
        result = func(*args, **kwargs)
        end = time.perf_counter()
        print(f"{func.__name__} took {end - start:.2f}s")
        return result
    return wrapper

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

print(slow_function())

# Memoization decorator
def memoize(func):
    cache = {}
    @wraps(func)
    def wrapper(*args):
        if args not in cache:
            cache[args] = func(*args)
        return cache[args]
    return wrapper

@memoize
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n-1) + fibonacci(n-2)

# Without the cache this recursion is exponential and would not finish;
# memoized, each n is computed exactly once.
print(fibonacci(50))
Expected Output:
slow_function took 1.00s
Done
12586269025
Note: the timing line is a real measurement, so it can read 1.01s on a busy machine. fibonacci(50) returns instantly only because of the cache: drop @memoize and the same call makes roughly 250 recursive calls, which will not finish.

Class Decorators

Decorators can also modify or enhance entire classes.

def singleton(cls):
    instances = {}
    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("Creating database connection")

db1 = Database()
db2 = Database()   # no second "Creating..." line: the instance is reused
print(db1 is db2)
Expected Output:
Creating database connection
True

Built-in Method Decorators

Python provides several built-in decorators for class methods.

class MyClass:
    def __init__(self, value):
        self._value = value

    @classmethod
    def from_string(cls, string):
        value = int(string)
        return cls(value)

    @staticmethod
    def static_method():
        return "Static method called"

    @property
    def value(self):
        return self._value

    @value.setter
    def value(self, new_value):
        if new_value < 0:
            raise ValueError("Must be positive")
        self._value = new_value

obj = MyClass(10)
print(obj.value)          # @property: reads like an attribute

obj.value = 20            # goes through the setter
print(obj.value)

# @classmethod: an alternative constructor
print(MyClass.from_string("42").value)

# @staticmethod: no self, no cls
print(MyClass.static_method())

# The setter validates, so this is rejected
try:
    obj.value = -5
except ValueError as e:
    print(f"ValueError: {e}")
Expected Output:
10
20
42
Static method called
ValueError: Must be positive

Chaining Multiple Decorators

Stack multiple decorators on a single function. Applied bottom-to-top.

from functools import wraps

def uppercase(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        return result.upper()
    return wrapper

def exclaim(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        return result + "!"
    return wrapper

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

# exclaim runs first (it is closest to the function), then uppercase
print(greet("Alice"))
Expected Output:
HELLO, ALICE!

How @uppercase @exclaim greet('Alice') flows

greet('Alice')uppercasewrapperexclaimwrappergreet()original'hello, Alice'+ '!'.upper()

Figure 1: The call travels inward (solid) through the outer decorator first; results bubble back out (dashed), each layer transforming them: greet returns 'hello, Alice', exclaim appends '!', uppercase upper-cases it → 'HELLO, ALICE!'.

Critical: Decorator order matters! Applied from bottom to top (inside out)

Key Takeaways

  • Decorators modify functions without changing source code
  • @decorator is sugar for func = decorator(func)
  • Always use @wraps(func) to preserve metadata
  • Use *args, **kwargs for flexibility
  • Parameterized decorators require extra nesting level
  • Common uses: timing, caching, authentication, logging
  • Class decorators can modify entire classes
  • Multiple decorators applied bottom-to-top

Bonus: Useful Decorator Libraries

core-mixins is a Python library providing common functions, decorators, and mixin classes for professional Python development.

The library includes:

  • Caching decorators - Built-in memoization and result caching
  • Performance monitoring - Timing and profiling decorators
  • Retry logic - Automatic retry with backoff strategies
  • Common mixins - Reusable class mixins for extending functionality
Check out the library:core-mixins on GitLab

Other popular decorator libraries:

  • functools - Built-in utilities (lru_cache, wraps)
  • decorator - Simplifies decorator creation
  • wrapt - Advanced decorator library with proper signature preservation
  • tenacity - Flexible retry logic with multiple strategies

Quick Reference

TypeSyntaxUse Case
Basic@decoratorWrap function behavior
With Parameters@decorator(args)Configurable decorators
Class Decorator@decorator class XModify entire classes
@property@property def xGetter as attribute
@classmethod@classmethod def xClass-level methods
@staticmethod@staticmethod def xUtility functions

Practice Exercise

Challenge: Create a @validate decorator that checks function arguments against validation rules and raises ValueError if validation fails.

Show Solution
import inspect
from functools import wraps

def validate(**validators):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            sig = inspect.signature(func)
            bound = sig.bind(*args, **kwargs)
            bound.apply_defaults()

            for param, validator in validators.items():
                if param in bound.arguments:
                    value = bound.arguments[param]
                    if not validator(value):
                        raise ValueError(
                            f"Validation failed for {param}"
                        )
            return func(*args, **kwargs)
        return wrapper
    return decorator

@validate(
    age=lambda x: 18 <= x <= 100,
    name=lambda x: len(x) > 0
)
def create_user(name, age):
    return f"User {name} ({age}) created"

print(create_user("Alice", 25))

# Out-of-range age fails the age validator
try:
    create_user("Bob", 15)
except ValueError as e:
    print(f"ValueError: {e}")

# Empty name fails the name validator
try:
    create_user("", 30)
except ValueError as e:
    print(f"ValueError: {e}")
Expected Output:
User Alice (25) created
ValueError: Validation failed for age
ValueError: Validation failed for name
What's Next?

You've learned the power of decorators! Next, we'll explore how to work with different file formats and serialize data.

  • JSON, CSV, XML - Master reading and writing structured data formats
  • Data Serialization - Convert Python objects to storable formats and back
  • Binary Files - Work with pickle and other binary serialization methods