Applying Decorators to Functions and Methods in Python
Course: Python Training
Topic: Applying Decorators to Functions and Methods
Course Link: Python Training Course
Demo Registration: Register for a Course Demo
JustAcademy Homepage: Visit JustAcademy
1. Introduction to Decorators
Decorators are an important Python feature that allows developers to extend or modify the behavior of functions and methods without changing their original source code. They are widely used for logging, authentication, authorization, timing, caching, input validation, and access control.
A decorator takes a function or another callable as input and returns a callable. In many common cases, it creates a wrapper function that executes additional logic before or after calling the original function.
Decorators rely on Python's support for first-class functions, higher-order functions, and closures. Understanding these concepts makes it easier to write reusable and maintainable decorators.
2. Learning Objectives
- Understand the purpose and syntax of Python decorators.
- Learn how to create and apply custom decorators.
- Understand wrapper functions and the role of closures.
- Apply decorators to ordinary functions and class methods.
- Use decorators with arguments and return values.
- Understand the built-in
@staticmethod, @classmethod, and @property decorators.
- Preserve function metadata with
functools.wraps.
- Recognize common decorator mistakes and follow best practices.
3. What Is a Decorator?
A decorator is a callable that receives another callable and returns a callable, often with additional behavior. The original function can then be used through the decorated version.
Python provides a convenient decorator syntax using the @ symbol followed by the decorator name.
Basic Syntax
def my_decorator(function):
def wrapper():
# Additional behavior
return function()
return wrapper
@my_decorator
def greet():
print("Hello, Python!")
greet()
Output:
Hello, Python!
The expression @my_decorator is effectively equivalent to assigning the decorated function back to its original name:
def greet():
print("Hello, Python!")
greet = my_decorator(greet)
The decorator receives the original function and returns the wrapper. Calling greet() then invokes the decorated callable.
4. Why Are Decorators Useful?
Many applications need common behavior to run around several functions. For example, multiple functions may need logging, execution-time measurement, or permission checks. Repeating the same code in every function creates duplication and makes maintenance harder.
Decorators allow this shared behavior to be implemented once and reused across many functions.
| Use Case |
Purpose |
| Logging |
Record when functions are called and what they return. |
| Authentication |
Check whether a user is signed in. |
| Authorization |
Check whether a user has permission to perform an operation. |
| Performance monitoring |
Measure function execution time. |
| Caching |
Reuse results for repeated calls with the same arguments. |
| Validation |
Check input values before executing the original function. |
| Retries |
Retry certain operations when a recoverable failure occurs. |
| Transactions |
Apply shared transaction-handling logic around operations. |
5. Understanding Wrapper Functions
A wrapper is an inner function that surrounds a call to the original function. It can execute additional logic before or after the original function and can return the original result to the caller.
Example: Adding Messages Before and After a Function
def my_decorator(function):
def wrapper():
print("Before function execution")
result = function()
print("After function execution")
return result
return wrapper
@my_decorator
def say_hello():
print("Hello!")
say_hello()
Output:
Before function execution
Hello!
After function execution
Here, the wrapper adds two messages around the original function call. Returning result is important because it allows the decorated function to preserve the original function's return value.
6. Applying a Decorator to a Function with Arguments
Functions may accept positional and keyword arguments. A reusable decorator should normally support both. The conventional solution is to define the wrapper using *args and **kwargs.
Example
def log_call(function):
def wrapper(*args, **kwargs):
print("Calling:", function.__name__)
result = function(*args, **kwargs)
print("Function completed")
return result
return wrapper
@log_call
def add(a, b):
return a + b
print(add(10, 20))
Output:
Calling: add
Function completed
30
*args collects positional arguments into a tuple, while **kwargs collects keyword arguments into a dictionary. Passing both to the original function allows the decorator to work with a wide variety of function signatures.
7. Preserving Function Metadata with functools.wraps
When a decorator returns a wrapper, the decorated function's name and documentation may otherwise appear to belong to the wrapper. Python provides functools.wraps to preserve important metadata from the original function.
Example Without wraps
def simple_decorator(function):
def wrapper(*args, **kwargs):
return function(*args, **kwargs)
return wrapper
@simple_decorator
def greet():
"""Display a greeting."""
print("Hello!")
print(greet.__name__)
print(greet.__doc__)
Without metadata preservation, the name will usually be wrapper, and the documentation string will usually be None.
Example With wraps
from functools import wraps
def simple_decorator(function):
@wraps(function)
def wrapper(*args, **kwargs):
return function(*args, **kwargs)
return wrapper
@simple_decorator
def greet():
"""Display a greeting."""
print("Hello!")
print(greet.__name__)
print(greet.__doc__)
Output:
greet
Display a greeting.
Using @wraps(function) is a recommended practice when creating decorators. It helps documentation tools, debugging, testing, and introspection work more naturally.
```html
8. Creating a Decorator with Arguments
Sometimes a decorator needs configuration values, such as a retry limit, a permission name, or a logging level. In this situation, a decorator factory is used. The outer function receives the configuration and returns the actual decorator.
Example: Configurable Prefix Decorator
from functools import wraps
def add_prefix(prefix):
def decorator(function):
@wraps(function)
def wrapper(*args, **kwargs):
result = function(*args, **kwargs)
return f"{prefix}{result}"
return wrapper
return decorator
@add_prefix("[INFO] ")
def get_message():
return "Application started"
print(get_message())
Output:
[INFO] Application started
The decorator works in three stages:
add_prefix() receives the configuration value.
decorator() receives the original function.
wrapper() adds the prefix to the function's result.
9. Applying Multiple Decorators
Python allows multiple decorators to be applied to one function. They are applied from the bottom upward, so the decorator closest to the function is applied first.
Example
from functools import wraps
def uppercase(function):
@wraps(function)
def wrapper(*args, **kwargs):
return function(*args, **kwargs).upper()
return wrapper
def add_exclamation(function):
@wraps(function)
def wrapper(*args, **kwargs):
return function(*args, **kwargs) + "!"
return wrapper
@uppercase
@add_exclamation
def greet():
return "hello"
print(greet())
Output:
HELLO!
The decoration is equivalent to greet = uppercase(add_exclamation(greet)). Understanding this order helps when combining logging, validation, caching, and other decorators.
10. Applying Decorators to Instance Methods
An instance method is a function defined inside a class that normally receives the object instance as its first argument, conventionally named self. A decorator applied to an instance method must allow that instance argument to pass through to the original method.
Example: Logging an Instance Method
from functools import wraps
def log_method(function):
@wraps(function)
def wrapper(*args, **kwargs):
print("Executing method:", function.__name__)
return function(*args, **kwargs)
return wrapper
class Calculator:
@log_method
def add(self, a, b):
return a + b
calculator = Calculator()
print(calculator.add(10, 20))
Output:
Executing method: add
30
When calculator.add(10, 20) is called, Python supplies the instance as the first argument. The wrapper accepts that argument through *args and passes it to the original method.
11. Applying Decorators to Class Methods
A class method receives the class itself as its first argument, conventionally named cls, rather than a particular object instance. The built-in @classmethod decorator makes this behavior possible.
Example: Creating an Alternative Constructor
class Employee:
def __init__(self, name, salary):
self.name = name
self.salary = salary
@classmethod
def from_string(cls, employee_data):
name, salary = employee_data.split(",")
return cls(name, float(salary))
employee = Employee.from_string("Asha,55000")
print(employee.name)
print(employee.salary)
Output:
Asha
55000.0
The method uses cls to create an instance of the class. This is useful for alternative constructors that create objects from strings, dictionaries, configuration files, or API data.
Combining a Custom Decorator with @classmethod
from functools import wraps
def log_call(function):
@wraps(function)
def wrapper(*args, **kwargs):
print("Class method called")
return function(*args, **kwargs)
return wrapper
class Product:
@classmethod
@log_call
def category(cls):
return cls.__name__
print(Product.category())
Output:
Class method called
Product
Here, the custom decorator wraps the underlying function, and @classmethod binds it to the class. Decorator order matters, particularly when combining custom decorators with built-in method decorators.
12. Applying Decorators to Static Methods
A static method does not automatically receive either the instance or the class as its first argument. It is useful for a function logically associated with a class but independent of instance-specific and class-specific state.
Example
class MathOperations:
@staticmethod
def multiply(a, b):
return a * b
print(MathOperations.multiply(5, 4))
math = MathOperations()
print(math.multiply(3, 6))
Output:
20
18
Both the class and its instances can call the static method. It does not receive an implicit self or cls argument.
Combining a Custom Decorator with @staticmethod
from functools import wraps
def log_call(function):
@wraps(function)
def wrapper(*args, **kwargs):
print("Running calculation")
return function(*args, **kwargs)
return wrapper
class MathOperations:
@staticmethod
@log_call
def multiply(a, b):
return a * b
print(MathOperations.multiply(4, 5))
Output:
Running calculation
20
Using @staticmethod outside the custom decorator is a common pattern because it first wraps the function and then exposes it as a static method.
13. Understanding the @property Decorator
The built-in @property decorator allows a method to be accessed like an attribute. It is useful for computed values and for controlling access to an object's internal data.
Example: Calculating an Area
class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height
@property
def area(self):
return self.width * self.height
rectangle = Rectangle(10, 5)
print(rectangle.area)
Output:
50
The property is accessed as rectangle.area, without parentheses. This makes a computed value behave like a normal attribute.
Using a Property Setter
class Temperature:
def __init__(self, celsius):
self.celsius = celsius
@property
def celsius(self):
return self._celsius
@celsius.setter
def celsius(self, value):
if value < -273.15:
raise ValueError("Temperature cannot be below absolute zero")
self._celsius = value
temperature = Temperature(25)
print(temperature.celsius)
temperature.celsius = 30
print(temperature.celsius)
Output:
25
30
The setter validates new values before storing them. A property is a built-in descriptor, so it is related to Python's attribute-access mechanism as well as decorators. Read the Python Descriptor Guide.
14. Common Built-in Decorators
| Decorator |
Purpose |
Typical Use |
@staticmethod |
Defines a method without implicit instance or class binding. |
Utility functions associated with a class. |
@classmethod |
Receives the class as the first argument. |
Alternative constructors and class-level operations. |
@property |
Exposes a method as an attribute. |
Computed attributes and controlled data access. |
@functools.wraps |
Preserves metadata from a wrapped function. |
Custom decorator implementations. |
@functools.lru_cache |
Caches results for eligible repeated calls. |
Expensive calculations with reusable results. |
@functools.cache |
Provides an unbounded cache for function results. |
Memoization when unbounded caching is appropriate. |
15. Caching Results with a Decorator
Caching stores results so that repeated calls with the same eligible arguments can reuse a previous result instead of recalculating it. Python provides caching decorators in the functools module.
Example: Using lru_cache
from functools import lru_cache
@lru_cache(maxsize=128)
def fibonacci(number):
if number < 2:
return number
return fibonacci(number - 1) + fibonacci(number - 2)
print(fibonacci(10))
print(fibonacci.cache_info())
Output begins with:
55
CacheInfo(...)
The exact cache statistics depend on the calls made. The cache can reduce repeated work in recursive calculations. Cached functions should generally behave predictably for their arguments; consider whether their results depend on mutable external state before applying caching.
16. Measuring Execution Time with a Decorator
A timing decorator measures how long a function takes to execute. It can help identify slow operations during development and performance testing.
Example
import time
from functools import wraps
def measure_time(function):
@wraps(function)
def wrapper(*args, **kwargs):
start = time.perf_counter()
try:
return function(*args, **kwargs)
finally:
elapsed = time.perf_counter() - start
print(
f"{function.__name__} took {elapsed:.6f} seconds"
)
return wrapper
@measure_time
def calculate_total(limit):
return sum(range(limit))
print(calculate_total(100000))
The elapsed time varies by computer and system load. The finally block ensures the timing message is produced even if the function raises an exception. The original exception still propagates to the caller.
17. Authentication and Permission Decorators
In web applications and other protected systems, decorators can enforce access rules before a function runs. The following is a simplified educational example, not a complete security system.
Example
from functools import wraps
def require_admin(function):
@wraps(function)
def wrapper(user, *args, **kwargs):
if not user.get("is_admin", False):
raise PermissionError("Administrator access required")
return function(user, *args, **kwargs)
return wrapper
@require_admin
def delete_record(user, record_id):
return f"Record {record_id} deleted"
admin = {"name": "Asha", "is_admin": True}
print(delete_record(admin, 101))
Output:
Record 101 deleted
The decorator checks the user's role before executing the protected function. In real applications, access checks should use trusted authentication and authorization mechanisms; user-supplied dictionaries alone are not a security boundary.
18. Decorator Order and Execution Flow
Multiple decorators are applied from the bottom upward. When the decorated function is called, the outermost wrapper runs first, and each wrapper may invoke the next decorated callable.
@decorator_b
@decorator_a
def example():
pass
This is equivalent to:
example = decorator_b(decorator_a(example))
When combining decorators, check whether each one expects a plain function, a bound method, a class method, or another callable type. This is especially important when using decorators that register functions, enforce method-specific behavior, or alter function signatures.
19. Common Mistakes and Troubleshooting
| Mistake |
Cause |
Solution |
| Wrapper does not accept arguments |
The wrapper was defined without parameters. |
Use *args and **kwargs for a general-purpose wrapper. |
| Return value is lost |
The wrapper calls the original function but does not return its result. |
Return the result of the original function. |
| Function name changes to wrapper |
Metadata was not preserved. |
Use @wraps(function). |
| Unexpected extra argument |
A method decorator mishandles the implicit self or cls argument. |
Ensure the wrapper passes all arguments to the original method. |
| Decorator order causes errors |
Decorators are applied in an unexpected order. |
Remember that application proceeds from bottom to top. |
| Exception is accidentally suppressed |
The wrapper catches an exception without re-raising or handling it intentionally. |
Allow exceptions to propagate unless recovery is explicitly required. |
| Cached result becomes stale |
The function depends on state that changes between calls. |
Review cache suitability and provide a deliberate invalidation strategy. |
| Decorated method behaves incorrectly |
The decorator was designed for a different callable type. |
Test it with ordinary functions, instance methods, and class or static methods as appropriate. |
20. Best Practices for Writing Decorators
- Use descriptive names that explain what the decorator does.
- Use
functools.wraps to preserve metadata.
- Support positional and keyword arguments when the decorator is intended to be reusable.
- Return the original function's result unless changing it is the decorator's purpose.
- Keep wrappers focused on one responsibility.
- Document configuration arguments and any behavior the decorator changes.
- Be deliberate about exception handling and avoid silently hiding failures.
- Test decorated functions with normal inputs, edge cases, and exceptions.
- Consider how decorators interact with descriptors and method binding.
- Use caching only when its memory usage and result validity are appropriate.
- Avoid adding decorators when a simple helper function would be clearer.
21. Practical Project: Employee Management with Decorators
Create an employee management example that uses decorators to log method calls and validates salary values through a property.
Project Code
from functools import wraps
def log_action(function):
@wraps(function)
def wrapper(*args, **kwargs):
print(f"Executing {function.__name__}")
return function(*args, **kwargs)
return wrapper
class Employee:
def __init__(self, name, salary):
self.name = name
self.salary = salary
@property
def salary(self):
return self._salary
@salary.setter
def salary(self, value):
if value < 0:
raise ValueError("Salary cannot be negative")
self._salary = value
@log_action
def increase_salary(self, amount):
if amount < 0:
raise ValueError("Increase cannot be negative")
self.salary += amount
return self.salary
@classmethod
def from_string(cls, data):
name, salary = data.split(",")
return cls(name, float(salary))
employee = Employee.from_string("Asha,50000")
print(employee.name)
print(employee.salary)
print(employee.increase_salary(5000))
print(employee.salary)
Expected Output
Asha
50000.0
Executing increase_salary
55000.0
55000.0
Project Explanation
@property allows salary to be accessed like an attribute.
@salary.setter validates salary changes before saving them.
@log_action adds logging behavior to the instance method.
@classmethod provides an alternative constructor that builds an employee from a string.
@wraps preserves the method's metadata.
22. Practice Exercises
- Create a decorator that prints a message before and after a function call.
- Write a decorator that records the name of the function being executed.
- Create a decorator that validates that a numeric argument is positive.
- Write a configurable decorator that adds a prefix to a returned string.
- Apply two decorators to one function and observe their order.
- Create a timing decorator using
time.perf_counter().
- Use
functools.wraps and compare the decorated function's metadata with and without it.
- Decorate an instance method that accepts positional and keyword arguments.
- Create a class method that constructs an object from a comma-separated string.
- Build a class with a property and setter that validate a value.
- Apply
@lru_cache to a recursive calculation and inspect its cache statistics.
- Extend the Employee Management project with a decorator that checks a user's permission before a protected operation.
23. Interview Questions and Answers
Q1. What is a decorator in Python?
A decorator takes a callable and returns a callable, commonly adding behavior without modifying the original function's source code.
Q2. What does the @ symbol mean in decorator syntax?
It applies a decorator to the function or method defined immediately below it.
Q3. What is a wrapper function?
A wrapper is a function that calls or surrounds another function and may add behavior before or after that call.
Q4. Why are *args and **kwargs used in decorators?
They allow a wrapper to accept arbitrary positional and keyword arguments and pass them to the original callable.
Q5. Why is functools.wraps important?
It preserves metadata such as the original function's name and documentation, making debugging and introspection easier.
Q6. Can decorators accept arguments?
Yes. A decorator factory can accept configuration values and return the decorator that wraps the target function.
Q7. In what order are multiple decorators applied?
They are applied from the bottom upward. The decorator closest to the function is applied first.
Q8. Can a decorator be used with a class method?
Yes. The decorator must handle the callable and method-binding behavior correctly, including the class argument supplied by @classmethod.
Q9. What is the difference between @staticmethod and @classmethod?
A static method receives no implicit instance or class argument. A class method receives the class as its first argument.
Q10. What is the purpose of @property?
It exposes a method as an attribute and can be used to compute values or manage controlled attribute access.
Q11. What is caching in Python?
Caching stores previously computed results so that suitable repeated calls can reuse those results.
Q12. How can a decorator measure execution time?
It can record a start time, call the original function, and calculate the elapsed time afterward, using a finally block if timing must also be recorded when an exception occurs.
Q13. What happens if a wrapper does not return the original result?
The decorated function may return None instead of the original result.
Q14. Are decorators only for functions?
No. Decorator syntax can also be applied to methods, classes, and other supported definitions, depending on what the decorator accepts and returns.
Q15. What are common applications of decorators?
Logging, timing, caching, input validation, registration, authentication, authorization, and other reusable cross-cutting behaviors.
24. Quick Revision Checklist
- A decorator receives a callable and returns a callable.
- The
@decorator syntax applies the decorator to a definition.
- Wrappers can add behavior before and after the original function call.
*args and **kwargs support flexible arguments.
functools.wraps preserves function metadata.
- Decorator factories support configurable decorators.
- Multiple decorators are applied from bottom to top.
- Instance methods normally receive
self.
- Class methods receive
cls.
- Static methods receive no implicit instance or class argument.
@property exposes a method through attribute access.
@lru_cache can reuse results for repeated eligible calls.
- Decorators should be tested with return values, exceptions, and different argument types.
25. Conclusion
Decorators are a powerful Python feature for adding reusable behavior to functions and methods. By using wrapper functions, closures, and tools such as functools.wraps, developers can implement logging, timing, caching, validation, and access checks without repeating the same code throughout an application.
Understanding decorators alongside @classmethod, @staticmethod, and @property also helps clarify how Python methods and managed attributes work. Practise creating simple decorators first, then progress to configurable decorators and class-based examples.
26. Additional Learning Resources