Welcome to DRIXO — Your Coding Journey Starts Here
DRIXO Code • Learn • Build

How to Use Python Decorators Like a Pro

February 11, 2026 8 min read 0 Comments
How to Use Python Decorators Like a Pro
Python

How to Use Python Decorators Like a Pro

DRIXO

Code · Learn · Build

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.

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