Today we're going deep into how to use python decorators like a pro. No fluff, no filler — just clear explanations and working code examples that you can copy, modify, and use right away.
Table of Contents
What Are Decorators?
A decorator is a function that takes another function, adds some functionality, and returns it. Think of it as a wrapper around your function.
# A decorator is just a function that wraps another function
def timer(func):
import time
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
elapsed = time.time() - 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() # Prints: "slow_function took 1.0012s"
# @timer is syntactic sugar for:
# slow_function = timer(slow_function)
Building Useful Decorators
import functools
import time
# 1. Retry decorator
def retry(max_attempts=3, delay=1):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(1, max_attempts + 1):
try:
return func(*args, **kwargs)
except Exception as e:
if attempt == max_attempts:
raise
print(f"Attempt {{attempt}} failed: {{e}}. Retrying in {{delay}}s...")
time.sleep(delay)
return wrapper
return decorator
@retry(max_attempts=3, delay=2)
def fetch_data(url):
import urllib.request
return urllib.request.urlopen(url).read()
# 2. Cache/Memoize decorator
def memoize(func):
cache = {{}}
@functools.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(100)) # Instant! Without memoize this would take forever
# 3. Validate arguments
def validate_types(**expected_types):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for param, expected in expected_types.items():
value = kwargs.get(param)
if value is not None and not isinstance(value, expected):
raise TypeError(f"{{param}} must be {{expected.__name__}}, got {{type(value).__name__}}")
return func(*args, **kwargs)
return wrapper
return decorator
@validate_types(name=str, age=int)
def create_user(name, age):
return {{'name': name, 'age': age}}
Decorators with Classes
# Class-based decorator
class RateLimit:
def __init__(self, calls_per_second=5):
self.calls_per_second = calls_per_second
self.timestamps = []
def __call__(self, func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
now = time.time()
# Remove timestamps older than 1 second
self.timestamps = [t for t in self.timestamps if now - t < 1]
if len(self.timestamps) >= self.calls_per_second:
wait = 1 - (now - self.timestamps[0])
if wait > 0:
time.sleep(wait)
self.timestamps.append(time.time())
return func(*args, **kwargs)
return wrapper
@RateLimit(calls_per_second=3)
def api_call(endpoint):
print(f"Calling {{endpoint}}")
return {{'status': 'ok'}}
# Decorating class methods
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 to database...")
self.connected = True
db1 = Database() # "Connecting to database..."
db2 = Database() # No output — returns same instance
print(db1 is db2) # True
Stacking Multiple Decorators
import functools
import logging
logging.basicConfig(level=logging.INFO)
def log_calls(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
logging.info(f"Calling {{func.__name__}} with args={{args}}, kwargs={{kwargs}}")
result = func(*args, **kwargs)
logging.info(f"{{func.__name__}} returned {{result}}")
return result
return wrapper
def require_auth(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
user = kwargs.get('user')
if not user or not user.get('authenticated'):
raise PermissionError("Authentication required")
return func(*args, **kwargs)
return wrapper
# Decorators are applied bottom-up
@log_calls # 2nd: logs the call
@require_auth # 1st: checks auth
def delete_account(user_id, user=None):
return f"Account {{user_id}} deleted"
# This means: delete_account = log_calls(require_auth(delete_account))
delete_account(42, user={{'authenticated': True, 'name': 'Admin'}})
Pro Tip: Always use
@functools.wraps(func) in your decorators. Without it, the wrapped function loses its name, docstring, and other metadata.Built-in Decorators You Should Know
# @property — getter/setter pattern
class Temperature:
def __init__(self, celsius=0):
self._celsius = celsius
@property
def celsius(self):
return self._celsius
@celsius.setter
def celsius(self, value):
if value < -273.15:
raise ValueError("Temperature below absolute zero!")
self._celsius = value
@property
def fahrenheit(self):
return self._celsius * 9/5 + 32
temp = Temperature(100)
print(temp.fahrenheit) # 212.0
temp.celsius = -300 # ValueError!
# @staticmethod and @classmethod
class MathUtils:
PI = 3.14159
@staticmethod
def add(a, b):
return a + b # No access to class/instance
@classmethod
def circle_area(cls, radius):
return cls.PI * radius ** 2 # Accesses class variable
# @functools.lru_cache — built-in memoization
from functools import lru_cache
@lru_cache(maxsize=128)
def expensive_query(user_id):
# Simulates database query
import time
time.sleep(0.1)
return {{'id': user_id, 'name': f'User {{user_id}}'}}
# First call: slow (0.1s), subsequent calls: instant
expensive_query(1) # Slow
expensive_query(1) # Instant (cached)
AM
Arjun Mehta
Full-Stack Developer & Technical Writer at DRIXO
Full-Stack Developer & Technical Writer at DRIXO
Full-stack developer with 5+ years of experience in Python and JavaScript. I love breaking down complex concepts into simple, practical tutorials. When I'm not coding, you'll find me contributing to open-source projects.
Comments