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 innerBasic 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 callUsing @ 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 callDecorators 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)) # 20Summary & 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
@wrapsin 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).