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()
# 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))
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

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

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

print(complex_function.__name__)  # 'complex_function'
print(complex_function.__doc__)   # Original docstring
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")
# 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):
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        print(f"{func.__name__} took {end - start:.4f}s")
        return result
    return wrapper

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

# 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)

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()
print(db1 is db2)  # 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)  # 10
obj.value = 20    # Uses setter

Chaining Multiple Decorators

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

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}"

print(greet("Alice"))  # "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
from functools import wraps
import inspect

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))
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