Mastering Python Decorators: From Basics to Advanced Use Cases
This article explains Python decorators—functions that return functions—to decouple logic, improve reusability, and extend behavior without modifying original code, covering basic syntax, parameterized and nested decorators, functools utilities, built‑in decorators, and practical examples like logging, timing, permission checks, and caching.
What is a Decorator?
In Python, functions are first‑class objects, meaning they can be assigned, passed as arguments, and returned. A decorator is a function that returns a function . Using the @ syntax, a function is passed to another function (the decorator) which returns a new function that replaces the original.
Why Use Decorators?
Add behavior before or after a function call.
Modify a function’s input or output.
Attach metadata to a function.
Common scenarios include logging, permission checks, performance timing, and caching. Decorators promote code reuse, maintainability, separation of concerns, and flexibility.
2.1 Basic Structure
def decorator(func):
def wrapper():
print("Before function call")
func() # call original function
print("After function call")
return wrapper
@decorator
def say_hello():
print("Hello!")
say_hello()Explanation: decorator receives func as a parameter. wrapper adds custom behavior before and after calling func(). @decorator is equivalent to say_hello = decorator(say_hello).
Output:
Before function call
Hello!
After function call2.2 Parameterized Decorator
def decorator(func):
def wrapper(*args, **kwargs):
print("Before function call")
result = func(*args, **kwargs)
print("After function call")
return result
return wrapper
@decorator
def add(a, b):
return a + b
result = add(3, 5)
print("Result:", result)Explanation: wrapper forwards *args and **kwargs to the original function, allowing any signature.
The result of func is returned unchanged.
Output:
Before function call
After function call
Result: 82.3 Nested Decorators
def decorator1(func):
def wrapper(*args, **kwargs):
print("Decorator 1 - Before function call")
result = func(*args, **kwargs)
print("Decorator 1 - After function call")
return result
return wrapper
def decorator2(func):
def wrapper(*args, **kwargs):
print("Decorator 2 - Before function call")
result = func(*args, **kwargs)
print("Decorator 2 - After function call")
return result
return wrapper
@decorator1
@decorator2
def say_hello():
print("Hello!")
say_hello()Explanation:
Decorators are applied from bottom to top: @decorator2 first, then @decorator1.
Calling say_hello() triggers decorator2 then decorator1.
Output:
Decorator 2 - Before function call
Decorator 1 - Before function call
Hello!
Decorator 1 - After function call
Decorator 2 - After function call2.4 Preserving Metadata with functools.wraps
import functools
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print("Before function call")
return func(*args, **kwargs)
return wrapper
@decorator
def say_hello():
"""This is the say_hello function."""
print("Hello!")
print(say_hello.__name__) # prints: say_hello
print(say_hello.__doc__) # prints: This is the say_hello function. @functools.wrapsensures the wrapper keeps the original function’s name and docstring.
2.5 Decorators with Arguments (Three‑Level Nesting)
def decorator_with_args(arg):
def decorator(func):
def wrapper(*args, **kwargs):
print(f"Decorator argument: {arg}")
return func(*args, **kwargs)
return wrapper
return decorator
@decorator_with_args("Hello")
def greet(name):
print(f"Greetings, {name}!")
greet("Alice")Explanation: decorator_with_args receives a parameter and returns the actual decorator.
The inner decorator receives the target function.
Calling greet prints the decorator argument before executing the function.
Output:
Decorator argument: Hello
Greetings, Alice!2.6 Decorating Class, Instance, and Static Methods
When decorating methods, the wrapper must accept self (instance) or cls (class) as the first argument.
class MyClass:
def decorator(func):
def wrapper(self, *args, **kwargs):
print("Before calling instance method")
result = func(self, *args, **kwargs)
print("After calling instance method")
return result
return wrapper
@decorator
def greet(self, name):
print(f"Hello, {name}!")
obj = MyClass()
obj.greet("Alice")Static‑method example:
class MyClass:
@staticmethod
def decorator(func):
def wrapper(*args, **kwargs):
print("Before calling static method")
result = func(*args, **kwargs)
print("After calling static method")
return result
return wrapper
@decorator
@staticmethod
def greet(name):
print(f"Hello, {name}!")
MyClass.greet("Alice")3 Built‑in Decorators
@staticmethod– defines a static method that does not receive self or cls. @classmethod – defines a class method that receives cls as the first argument. @property – turns a method into a read‑only attribute. @functools.lru_cache – caches function results to avoid recomputation. @functools.wraps – preserves original metadata when writing custom decorators. @abstractmethod – marks a method as abstract in an abstract base class. @property.setter – defines a setter for a property. @property.deleter – defines a deleter for a property.
Examples
Static method:
class MyClass:
@staticmethod
def greet(name):
print(f"Hello, {name}!")
MyClass.greet("Alice")Class method:
class MyClass:
count = 0
@classmethod
def increment_count(cls):
cls.count += 1
print(f"Count: {cls.count}")
MyClass.increment_count()Property and setter:
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
c = Circle(5)
print(c.radius)
c.radius = 10
print(c.radius)LRU cache:
import functools
@functools.lru_cache(maxsize=None)
def fibonacci(n):
if n < 2:
return n
return fibonacci(n-1) + fibonacci(n-2)
print(fibonacci(35))4 Real‑World Use Cases
Logging
def log_decorator(func):
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__} with args {args} and kwargs {kwargs}")
result = func(*args, **kwargs)
print(f"{func.__name__} returned {result}")
return result
return wrapper
@log_decorator
def add(a, b):
return a + b
@log_decorator
def multiply(a, b):
return a * b
add(1, 2)
multiply(3, 4)Output shows function calls and results, keeping logging separate from business logic.
Performance Monitoring
import time
def time_decorator(func):
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f"Function {func.__name__} took {end - start} seconds")
return result
return wrapper
@time_decorator
def slow_function():
time.sleep(2)
@time_decorator
def fast_function():
time.sleep(0.5)
slow_function()
fast_function()Permission Validation
def permission_required(func):
def wrapper(*args, **kwargs):
if not has_permission():
raise PermissionError("You do not have permission to access this resource.")
return func(*args, **kwargs)
return wrapper
def has_permission():
return False
@permission_required
def sensitive_data():
return "This is sensitive data."
try:
sensitive_data()
except PermissionError as e:
print(e)Caching Expensive Calls
from functools import lru_cache
@lru_cache(maxsize=None)
def expensive_function(x):
print(f"Calculating {x}...")
return x * 2
print(expensive_function(4)) # first call, computes
print(expensive_function(4)) # second call, cachedDecorators in Python provide a powerful, flexible way to modify or extend function and method behavior while keeping core logic clean and maintainable.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
