A decorator is a function that wraps another function, method, or class to modify their behavior without changing their source code.
It is typically used to add functionality to existing code in a clean and reusable way without altering the original function's structure.
Syntax:
@decorator_name
def function():
pass
Is equivalent to: function = decorator_name(function)
Functions | Examples: @lru_cache, @wraps, custom decorators
Methods | Examples: @classmethod, @staticmethod, @property
Classes | Examples: @dataclass, @total_ordering, custom
❌ No decorators for variables/attributes
Control how methods are bound to classes and instances
Purpose: Bind method to the class, not the instance
First parameter: cls (the class itself)
Use case: Factory methods, alternative constructors, class-level operations
class Person:
population = 0
def __init__(self, name, age):
self.name = name
self.age = age
Person.population += 1
@classmethod
def from_birth_year(cls, name, birth_year):
"""Alternative constructor from birth year"""
age = 2025 - birth_year
return cls(name, age)
@classmethod
def get_population(cls):
"""Access class-level data"""
return cls.population
# Usage
p1 = Person.from_birth_year("Alice", 1990)
print(Person.get_population()) # 1
Purpose: Define a utility function within class namespace
First parameter: None (no automatic self or cls)
Use case: Helper functions logically related to the class but don't need class/instance data
class MathUtils:
@staticmethod
def add(x, y):
return x + y
@staticmethod
def is_even(n):
return n % 2 == 0
# Usage
result = MathUtils.add(5, 3) # 8
print(MathUtils.is_even(4)) # True
Purpose: Make methods behave like attributes with controlled access
Use case: Validation, computed attributes, encapsulation
class Temperature:
def __init__(self, celsius):
self._celsius = celsius
@property
def celsius(self):
"""Getter: read temperature"""
return self._celsius
@celsius.setter
def celsius(self, value):
"""Setter: validate before assignment"""
if value < -273.15:
raise ValueError("Temperature below absolute zero!")
self._celsius = value
@property
def fahrenheit(self):
"""Computed property"""
return self._celsius * 9/5 + 32
@fahrenheit.setter
def fahrenheit(self, value):
self.celsius = (value - 32) * 5/9
# Usage
temp = Temperature(25)
print(temp.celsius) # 25 (calls getter)
temp.celsius = 30 # calls setter with validation
print(temp.fahrenheit) # 86.0 (computed)
temp.fahrenheit = 100 # converts and sets celsius
Add functionality to functions without modifying their code
Purpose: Cache function results for performance
Use case: Expensive computations, recursive functions
from functools import lru_cache
@lru_cache(maxsize=128)
def fibonacci(n):
if n < 2:
return n
return fibonacci(n-1) + fibonacci(n-2)
# Without cache: fib(35) takes ~2 seconds
# With cache: fib(35) takes microseconds
print(fibonacci(100)) # Instant!
Purpose: Preserve metadata when creating custom decorators
Use case: Writing well-behaved decorators
from functools import wraps
import time
def timer(func):
@wraps(func) # Preserves func's __name__, __doc__, etc.
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():
"""This docstring is preserved"""
time.sleep(1)
return "done"
print(slow_function.__name__) # 'slow_function' (not 'wrapper')
print(slow_function.__doc__) # 'This docstring is preserved'
def debug(func):
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__} with {args} {kwargs}")
return func(*args, **kwargs)
return wrapper
@debug # Replace 'add' with the wrapped version: add = debug(add)
def add(a, b):
return a + b
add(5, 7)
Another example:
def repeat(times):
"""Decorator factory: returns a decorator"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(3)
def greet(name):
print(f"Hello, {name}!")
greet("Alice")
# Output:
# Hello, Alice!
# Hello, Alice!
# Hello, Alice!
Transform entire classes
Purpose: Auto-generate special methods
Generates: __init__, __repr__, __eq__, etc.
from dataclasses import dataclass, field
@dataclass
class Point:
x: int
y: int
label: str = "origin"
def distance_from_origin(self):
return (self.x**2 + self.y**2)**0.5
# Auto-generated __init__, __repr__, __eq__
p = Point(3, 4)
print(p) # Point(x=3, y=4, label='origin')
def singleton(cls):
"""Ensure only one instance exists"""
instances = {}
@wraps(cls) # Keep the original class’s metadata (like __name__, docstring)
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):
self.connection = "Connected to DB"
db1 = Database()
db2 = Database()
print(db1 is db2) # True — same instance!
Enforce interface contracts
Purpose: Require subclasses to implement methods
Use case: Defining interfaces and contracts
from abc import ABC, abstractmethod
# Inheriting from ABC makes Shape an abstract class
# It cannot be instantiated
class Shape(ABC):
@abstractmethod
def area(self):
"""Subclasses must implement this"""
pass
@abstractmethod
def perimeter(self):
pass
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
return 3.14159 * self.radius ** 2
def perimeter(self):
return 2 * 3.14159 * self.radius
# shape = Shape() # Error! Can't instantiate abstract class
circle = Circle(5) # OK — implements required methods
Route handling and middleware
from flask import Flask
app = Flask(__name__)
@app.route('/home')
def home():
return "Home Page"
@app.route('/user/<username>')
def profile(username):
return f"Profile: {username}"
from fastapi import FastAPI
app = FastAPI()
@app.get("/items/{item_id}")
async def read_item(item_id: int):
return {"item_id": item_id}
import pytest
@pytest.mark.parametrize("a,b,expected", [
(2, 3, 5),
(10, 20, 30),
(-1, 1, 0),
])
def test_addition(a, b, expected):
assert a + b == expected
from unittest.mock import patch
@patch('module.function_to_mock')
def test_something(mock_func):
mock_func.return_value = 42
# test code here
def my_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
# Code before function call
print(f"Calling {func.__name__}")
result = func(*args, **kwargs)
# Code after function call
print(f"Finished {func.__name__}")
return result
return wrapper
@my_decorator
def say_hello(name):
print(f"Hello, {name}!")
say_hello("Bob")
# Output:
# Calling say_hello
# Hello, Bob!
# Finished say_hello
def validate_range(min_val, max_val):
"""Decorator factory"""
def decorator(func):
@wraps(func)
def wrapper(value):
if not min_val <= value <= max_val:
raise ValueError(f"Value must be between {min_val} and {max_val}")
return func(value)
return wrapper
return decorator
@validate_range(0, 100)
def set_percentage(value):
return f"Set to {value}%"
print(set_percentage(50)) # OK
# set_percentage(150) # ValueError!