Python Notes
Master advanced Python decorators including decorator factories, class decorators, stacking decorators, functools.wraps, parameterized decorators, and real-world patterns.
Introduction
Decorators are a powerful metaprogramming tool in Python. They allow you to modify existing functions or classes without changing their source code. The @ syntax is simply syntactic sugar.
Decorator = A function that takes another function and returns a modified function
2. functools.wraps — Preserving Metadata
import functools
def better_decorator(func):
@functools.wraps(func) # Metadata preserve karo
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
result = func(*args, **kwargs)
print(f"Done with {func.__name__}")
return result
return wrapper
@better_decorator
def multiply(a, b):
"""Multiply two numbers"""
return a * b
print(multiply.__name__) # 'multiply' — Correct!
print(multiply.__doc__) # 'Multiply two numbers' — Correct!
print(multiply(3, 4))
# Output:
# Calling multiply
# Done with multiply
# 12multiply Multiply two numbers Calling multiply Done with multiply 12
3. Parameterized Decorators (Decorator Factories)
import functools
import time
def repeat(times):
"""Decorator that repeats a function n times"""
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for i in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(3)
def say_hello(name):
print(f"Hello, {name}!")
say_hello("Bob")
# Output:
# Hello, Bob!
# Hello, Bob!
# Hello, Bob!
# Using with different counts
@repeat(1)
def once():
print("Only once")
@repeat(5)
def five_times():
print("Five times!")
once()
five_times()Only once Five times! Five times! Five times! Five times! Five times!
[Heavy Computation] took 0.041234s Result: 499999500000
📝 Hindi Explanation
Decorator Factory uses three levels of nesting: the outer function accepts parameters, the middle function accepts func, and the inner function handles the actual call. This pattern enables configurable decorators.4. Stacking Decorators
import functools
def bold(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
return f"<b>{func(*args, **kwargs)}</b>"
return wrapper
def italic(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
return f"<i>{func(*args, **kwargs)}</i>"
return wrapper
def underline(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
return f"<u>{func(*args, **kwargs)}</u>"
return wrapper
@bold
@italic
@underline
def format_text(text):
return text
print(format_text("Hello"))
# Output: <b><i><u>Hello</u></i></b>
# Execution order: bottom-up application, top-down execution
# 1. underline(format_text) -> underlined_func
# 2. italic(underlined_func) -> italicized_func
# 3. bold(italicized_func) -> bolded_func
# When called: bold wrapper -> italic wrapper -> underline wrapper -> original<b><i><u>Hello</u></i></b>
5. Class-Based Decorators
import functools
class CountCalls:
"""Function call count karne wala class decorator"""
def __init__(self, func):
functools.update_wrapper(self, func)
self.func = func
self.count = 0
def __call__(self, *args, **kwargs):
self.count += 1
print(f"Call #{self.count} to {self.func.__name__}")
return self.func(*args, **kwargs)
@CountCalls
def greet(name):
return f"Hello, {name}!"
print(greet("Alice")) # Call #1 to greet — Hello, Alice!
print(greet("Bob")) # Call #2 to greet — Hello, Bob!
print(greet("Carol")) # Call #3 to greet — Hello, Carol!
print(f"Total calls: {greet.count}") # Total calls: 3Call #1 to greet Hello, Alice! Call #2 to greet Hello, Bob! Call #3 to greet Hello, Carol! Total calls: 3
class Retry:
"""Decorator that retries a failed function"""
def __init__(self, max_retries=3, exceptions=(Exception,), delay=0.5):
self.max_retries = max_retries
self.exceptions = exceptions
self.delay = delay
def __call__(self, func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
import time
last_exception = None
for attempt in range(1, self.max_retries + 1):
try:
return func(*args, **kwargs)
except self.exceptions as e:
last_exception = e
print(f"Attempt {attempt} failed: {e}")
if attempt < self.max_retries:
time.sleep(self.delay)
raise last_exception
return wrapper
# Usage
import random
@Retry(max_retries=3, exceptions=(ValueError,), delay=0.1)
def unreliable_function():
if random.random() < 0.7:
raise ValueError("Random failure!")
return "Success!"
try:
result = unreliable_function()
print(result)
except ValueError:
print("All retries failed!")📝 Hindi Explanation
Class-based Decorator uses__init__()for setup and__call__()for actual wrapping. This pattern is useful when the decorator needs to maintain state (such as call count, cache, timing metrics).
6. Real-World Decorator: Memoization
7. Real-World Decorator: Logging
8. Real-World Decorator: Rate Limiter
9. Decorator for Validation
{'name': 'Alice', 'age': 30, 'salary': 75000.0}
Error: Parameter 'age' must be int, got str📝 Hindi Explanation
Validation Decorator is very useful in production code. Type checking, value range validation, authentication checks — all can be handled with decorators. This makes functions cleaner and separation of concerns better.
10. Property, staticmethod, classmethod Decorators
class Temperature:
def __init__(self, celsius):
self._celsius = celsius
@property
def celsius(self):
"""Getter"""
return self._celsius
@celsius.setter
def celsius(self, value):
"""Setter with validation"""
if value < -273.15:
raise ValueError("Temperature below absolute zero!")
self._celsius = value
@celsius.deleter
def celsius(self):
"""Deleter"""
del self._celsius
@property
def fahrenheit(self):
"""Celsius to Fahrenheit conversion"""
return self._celsius * 9/5 + 32
@staticmethod
def is_valid(temp):
"""Static method — class/instance ki zaroorat nahi"""
return temp >= -273.15
@classmethod
def from_fahrenheit(cls, fahrenheit):
"""Class method — alternative constructor"""
celsius = (fahrenheit - 32) * 5/9
return cls(celsius)
t = Temperature(100)
print(t.celsius) # 100
print(t.fahrenheit) # 212.0
t.celsius = 0
print(t.fahrenheit) # 32.0
# Class method as alternative constructor
t2 = Temperature.from_fahrenheit(98.6)
print(f"{t2.celsius:.1f}°C") # 37.0°C
# Static method
print(Temperature.is_valid(-300)) # False
print(Temperature.is_valid(100)) # True100 212.0 32.0 37.0°C False True
11. Combining Multiple Patterns
[DEBUG] simple_func: 0.0001s -> 42 [PERF] heavy_func: 0.0034s -> 4999950000
Summary
| Decorator Type | Syntax | Use Case |
|---|---|---|
| Simple | @decorator | Basic wrapping |
| Parameterized | @decorator(args) | Configurable behavior |
| Class-based | class Dec: __call__ | Stateful decorators |
| Stacked | @d1 @d2 @d3 | Multiple enhancements |
| Built-in | @property @staticmethod | Python OOP features |
# Decorator template
import functools
def my_decorator(func=None, *, option1=True, option2="default"):
"""Decorator with optional parameters"""
if func is None:
# Called with arguments: @my_decorator(option1=False)
return functools.partial(my_decorator, option1=option1, option2=option2)
# Called without arguments: @my_decorator
@functools.wraps(func)
def wrapper(*args, **kwargs):
# Pre-processing
result = func(*args, **kwargs)
# Post-processing
return result
return wrapper
# Works both ways:
@my_decorator
def func1(): pass
@my_decorator(option1=False)
def func2(): passPro Tip: Always use@functools.wraps(func)in your decorators to preserve the original function's metadata. This is especially important when usinghelp(),inspect, or documentation tools.
⚠️ Common Mistakes
❌ Mistake 1: functools.wraps bhool jaana
# ❌ WRONG - metadata lost ho jayega
def bad_decorator(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
# ✅ RIGHT - metadata preserve hota hai
import functools
def good_decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper🔑 Tip: Withoutfunctools.wraps,__name__,__doc__,__module__will all show the wrapper's info — creating a debugging nightmare!
❌ Mistake 2: Parentheses confusion in parameterized decorators
# ❌ WRONG - parentheses miss kiya
@repeat # TypeError! repeat() expects 'times' argument
def say_hi():
print("Hi!")
# ✅ RIGHT - parentheses lagao
@repeat(3)
def say_hi():
print("Hi!")🔑 Tip: Always include parentheses in the outer call of a parameterized decorator, even if using default values.
❌ Mistake 3: Return value bhool jaana
# ❌ WRONG - return missing, function always returns None
def broken_decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print("Before")
func(*args, **kwargs) # didn't capture the result!
print("After")
return wrapper
# ✅ RIGHT - result return karo
def correct_decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print("Before")
result = func(*args, **kwargs) # capture
print("After")
return result # return!
return wrapper🔑 Tip: Always writereturn func(*args, **kwargs)in the wrapper function. Otherwise the decorated function will always returnNone.
❌ Mistake 4: Mutable default arguments in decorator state
🔑 Tip: Define cache or state inside the closure, not in parameter defaults. Otherwise all functions will share the same cache!
❌ Mistake 5: Stacking order confusion
# Decorators are applied bottom-up, executed top-down
@auth_required # 3rd apply, 1st execute (checked first)
@rate_limit(10) # 2nd apply, 2nd execute
@log_calls # 1st apply, 3rd execute (closest to function)
def api_endpoint():
pass
# ❌ WRONG order thinking: log -> rate_limit -> auth
# ✅ RIGHT execution order: auth -> rate_limit -> log -> function🔑 Tip: In stacking, the topmost decorator executes first. Always put authentication at the top!
❌ Mistake 6: Decorator on async functions without async wrapper
# ❌ WRONG - sync wrapper pe async function
def bad_timer(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs) # Returns coroutine, not result!
print(f"Time: {time.time() - start}")
return result
return wrapper
# ✅ RIGHT - async-aware decorator
def async_timer(func):
@functools.wraps(func)
async def wrapper(*args, **kwargs):
start = time.time()
result = await func(*args, **kwargs)
print(f"Time: {time.time() - start}")
return result
return wrapper🔑 Tip: For async functions, the wrapper must also beasync defand must useawait!
❌ Mistake 7: Not implementing __get__ in decorator classes
# ❌ WRONG - class decorator won't work on methods
class MyDec:
def __init__(self, func):
self.func = func
def __call__(self, *args, **kwargs):
return self.func(*args, **kwargs)
# ✅ RIGHT - descriptor protocol implement karo for methods
class MyDec:
def __init__(self, func):
self.func = func
def __call__(self, *args, **kwargs):
return self.func(*args, **kwargs)
def __get__(self, obj, objtype=None):
if obj is None:
return self
return functools.partial(self, obj)🔑 Tip: Class-based decorators must implement__get__to work on methods, otherwiseselfwon't be passed.
✅ Key Takeaways
- 🎯 Decorator = Higher-order function that accepts a function and returns an enhanced function.
@decoratoris syntactic sugar forfunc = decorator(func).
- 🔒 Always use
@functools.wraps(func)— it preserves the original function's__name__,__doc__,__module__, and__qualname__.
- 🏭 Decorator Factory (parameterized decorator) uses three levels of nesting: outer function → parameters, middle function → func, inner function → actual call.
- 📚 Remember stacking order: Bottom-up application, top-down execution.
@A @B @C def fmeansA(B(C(f)))— A executes first.
- 🏗️ Class-based decorators should be used when state needs to be maintained (call count, cache, timing history). The
__call__method handles wrapping.
- ⚡ Built-in decorators —
@property(getters/setters),@staticmethod(no self),@classmethod(alternative constructors),@functools.lru_cache(memoization) — essential in production code.
- 🔄
functools.partialtrick allows using a decorator both with and without arguments — best practice for a flexible API.
- 🧪 Real-world patterns — logging, timing, retry, rate limiting, validation, authentication — all are implemented with decorators in production applications.
- 🐍 Async decorators require the wrapper function to also be
async— a regular decorator won't work properly on an async function.
- 📏 Single Responsibility — each decorator should do one thing. If you need multiple behaviors, stack them; don't put everything in one.
❓ FAQ
Q1: What is the difference between a Decorator and a Higher-Order Function? > Every decorator is a higher-order function, but not every higher-order function is a decorator. A decorator specifically modifies/enhances a function and returns the same interface.
Q2: Does using a decorator make a function slow? > Yes, every decorator adds an extra function call. But in modern Python, this overhead is measured in nanoseconds — negligible performance impact. Only in tight loops (millions of iterations) does it matter, and even then functools.lru_cache can optimize it.
Q3: What is the difference between @staticmethod and @classmethod? > @staticmethod receives neither self nor cls — it's a regular function that lives in the class namespace. @classmethod receives cls (the class itself) — useful for inheritance and alternative constructors.
Q4: Multiple decorators stack karne pe performance pe asar hota hai? > Each stacked decorator adds a wrapper function layer. 2-3 decorators cause no problems. But if you're stacking 10+ decorators, reconsider — perhaps a class-based approach or middleware pattern would be better.
Q5: Can decorators be used on class methods? > Yes, but note that the decorator must handle the self parameter. Using *args, **kwargs will work. For class-based decorators, implementing the __get__ descriptor method is necessary for proper self binding.
Q6: functools.wraps exactly kya copy karta hai? > It copies __name__, __doc__, __module__, __qualname__, __dict__, and __wrapped__ attributes. __wrapped__ stores a reference to the original function — useful for debugging and introspection.
Q7: How to handle exceptions in a decorator? > Use try/except in the wrapper function. But always re-raise the exception (raise) unless you have a specific reason to suppress it (like a retry decorator). Silently swallowing exceptions creates hidden bugs.
Q8: Can a decorator be removed or changed at runtime? > If you've used @functools.wraps, you can access the original function via decorated_func.__wrapped__. Changing decorators at runtime is technically possible but strongly discouraged in production code.
🎯 Interview Questions
Q1: What is a decorator in Python? Explain with an example. > A decorator is a callable that takes a function as input and returns a modified/enhanced function. It's a form of metaprogramming that allows you to add behavior to functions without modifying their source code. The @ syntax is syntactic sugar: @decorator above a function f is equivalent to f = decorator(f). Decorators leverage closures and higher-order functions.
Q2: What is the difference between @decorator and @decorator()? When would you use each? > @decorator passes the function directly to decorator(func). @decorator() first calls decorator() which returns the actual decorator, then that decorator is applied to the function. Use @decorator() (decorator factory) when you need to pass configuration parameters. The pattern involves three nested functions: factory → decorator → wrapper.
Q3: Why should you always use functools.wraps? What happens if you don't? > Without functools.wraps, the decorated function loses its original __name__, __doc__, __module__, and __qualname__. This breaks introspection tools like help(), documentation generators (Sphinx), debugging tools, logging that uses func.__name__, and serialization frameworks (like Celery task naming). functools.wraps copies these attributes from the original function to the wrapper.
Q4: Explain the execution order when multiple decorators are stacked. > Decorators are applied bottom-up but executed top-down. For @A @B @C def f: first C wraps f, then B wraps the result, then A wraps that → A(B(C(f))). When called, A's wrapper runs first, then B's, then C's, then the original function. This is like layers of an onion — the outermost decorator has first and last access.
Q5: How would you write a decorator that works both with and without arguments? > Use functools.partial trick: check if the first argument is a callable (function). If yes, it was used without parentheses (@dec). If no, it was called with arguments (@dec(args)) — return a partial or nested decorator. Pattern: > ``python > def decorator(func=None, *, option="default"): > if func is None: > return functools.partial(decorator, option=option) > @functools.wraps(func) > def wrapper(*args, **kwargs): > return func(*args, **kwargs) > return wrapper > ``
Q6: What is the difference between function-based and class-based decorators? When would you choose one over the other? > Function-based decorators use closures for state and are simpler for stateless transformations. Class-based decorators use __init__ for setup and __call__ for wrapping — better when you need persistent state (call counts, caches, configuration), need to expose additional methods/attributes, or when the logic is complex enough to benefit from OOP organization. Class decorators need __get__ for method support.
Q7: How does @property decorator work internally? > @property creates a descriptor object with __get__, __set__, and __delete__ methods. When you access obj.attr, Python's descriptor protocol calls the getter. The .setter and .deleter decorators return new property objects with the additional methods set. Internally, it's equivalent to property(fget, fset, fdel, doc).
Q8: Write a thread-safe singleton decorator. > ``python > import functools > import threading > > def singleton(cls): > instances = {} > lock = threading.Lock() > > @functools.wraps(cls) > def get_instance(*args, **kwargs): > if cls not in instances: > with lock: > if cls not in instances: # Double-check locking > instances[cls] = cls(*args, **kwargs) > return instances[cls] > > return get_instance > `` > This uses double-checked locking pattern — first check without lock for performance, second check with lock for thread safety.
Q9: How would you implement a retry decorator with exponential backoff? > ``python > def retry(max_attempts=3, base_delay=1, backoff_factor=2, exceptions=(Exception,)): > def decorator(func): > @functools.wraps(func) > def wrapper(*args, **kwargs): > for attempt in range(max_attempts): > try: > return func(*args, **kwargs) > except exceptions as e: > if attempt == max_attempts - 1: > raise > delay = base_delay * (backoff_factor ** attempt) > time.sleep(delay) > return wrapper > return decorator > `` > Exponential backoff prevents thundering herd problem in distributed systems — each retry waits exponentially longer (1s, 2s, 4s, 8s...).
Q10: What is decorator chaining vs decorator composition? How do they differ? > Chaining is stacking multiple decorators (@A @B @C) — each decorator is independent and wraps the result of the previous one. Composition is combining multiple behaviors into a single decorator, reducing wrapper layers. Composition is better for performance (fewer function call layers) and when decorators have interdependencies. Use functools.reduce or manual composition to merge decorators into one.
Exam Focus
Revise definitions, diagrams, examples, and short-answer points for Advanced Python Decorators.
Interview Use
Prepare one clear explanation, one practical example, and one common mistake for this Python Master Course topic.
Search Terms
python-master-course, python master course, python, master, course, advanced, decorators, advanced python decorators
Related Python Master Course Topics