Python Docs

Decorators

Decorators are a powerful feature in Python that let you modify or extend the behavior of functions or classes without changing their original code. They're perfect for cross-cutting concerns like logging, timing, caching, and authentication.

Introduction to Decorators

A decorator is basically a function that takes another function and returns a new function that adds extra behavior around it.

Why Use Decorators?

  • Add functionality to existing functions or methods.
  • Reduce duplication (DRY principle).
  • Implement logging, timing, caching, authentication, etc.
  • Modify behavior dynamically at import/runtime.
  • Create reusable, composable components.

Functions as First-Class Objects

To understand decorators, you must be comfortable with functions as values: you can assign them, pass them, and return them.

Functions as Objects

# Functions are objects
def greet(name):
    return f"Hello, {name}!"

# Assign function to variable
say_hello = greet
print(say_hello("Alice"))  # Hello, Alice!

# Pass function as argument
def call_function(func, arg):
    return func(arg)

result = call_function(greet, "Bob")
print(result)  # Hello, Bob!

Nested Functions & Returning Functions

# Function inside function
def outer():
    message = "Hello"

    def inner():
        return message + " World!"

    return inner()

print(outer())  # Hello World!

# Returning inner function (no call)
def outer():
    def inner():
        return "Hello from inner"
    return inner  # Return function object, not result

func = outer()
print(func())  # Hello from inner

Basic Decorators

Simple Decorator (Manual Application)

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

# Apply decorator manually
def say_hello():
    print("Hello!")

say_hello = my_decorator(say_hello)
say_hello()
# Output:
# Before function call
# Hello!
# After function call

Using @ Syntax

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

# Apply decorator with @ syntax
@my_decorator
def say_hello():
    print("Hello!")

say_hello()
# Output:
# Before function call
# Hello!
# After function call

Decorators That Accept Arguments

Use *args and **kwargs in your wrapper so the decorator works with any function signature.

def my_decorator(func):
    def wrapper(*args, **kwargs):
        print(f"Arguments: {args}, {kwargs}")
        result = func(*args, **kwargs)
        print(f"Result: {result}")
        return result
    return wrapper

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

@my_decorator
def greet(name, greeting="Hello"):
    return f"{greeting}, {name}!"

add(5, 3)
greet("Alice", greeting="Hi")

Practical Decorators

Timing Decorator

import time

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

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

result = slow_function()

Logging Decorator

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

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

add(3, 5)

Caching / Memoization

from functools import wraps, lru_cache

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)

print(fibonacci(30))  # Fast due to caching

# Python's built-in version
@lru_cache(maxsize=None)
def fibonacci_v2(n):
    if n < 2:
        return n
    return fibonacci_v2(n - 1) + fibonacci_v2(n - 2)

Decorators With Their Own Parameters

A "decorator factory" returns a decorator. This gives you extra configuration (like how many times to repeat).

def repeat(times):
    def decorator(func):
        def wrapper(*args, **kwargs):
            result = None
            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!

Preserving Metadata with @wraps

Without @wraps, the wrapped function loses its original __name__, docstring, etc.

from functools import wraps

def debug(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__}")
        return func(*args, **kwargs)
    return wrapper

@debug
def greet(name: str) -> str:
    """Say hello to someone."""
    return f"Hello, {name}"

print(greet.__name__)  # greet
print(greet.__doc__)   # Say hello to someone.

Built-in Decorators

@property

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

    @property
    def radius(self):
        return self._radius

    @radius.setter
    def radius(self, value):
        if value < 0:
            raise ValueError("Radius must be positive")
        self._radius = value

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

c = Circle(5)
print(c.radius)  # 5
print(c.area)    # 78.53975
c.radius = 10
print(c.area)    # 314.159

@staticmethod and @classmethod

class MathOperations:
    multiplier = 2

    @staticmethod
    def add(a, b):
        """Static method - no access to class or instance"""
        return a + b

    @classmethod
    def multiply_by_class_var(cls, x):
        """Class method - access to class via cls"""
        return x * cls.multiplier

print(MathOperations.add(5, 3))               # 8
print(MathOperations.multiply_by_class_var(10))  # 20

Summary & Best Practices

Decorators let you wrap reusable behavior around functions and methods in a clean, readable way.

  • Use decorators for logging, timing, caching, auth, validation, etc.
  • Always use @wraps in custom decorators so metadata isn't lost.
  • Keep decorators small and focused; avoid hiding heavy logic inside them.
  • For configurable decorators, use a decorator factory (repeat(times) style).