Python Decorators
Table of Contents
In the last lesson, we learned about Python Generators. Now let us use decorators to add behavior to functions without changing their code.
What Is a Decorator in Python?
A decorator is a function that wraps another function. It adds some behavior before or after the original function runs.
Think of it like gift wrapping. The gift inside stays the same. But the wrapping adds something extra.
Why Do We Need Decorators in Python?
Decorators let us add functionality to existing functions without modifying them. That is useful for logging, timing, authentication, and more.
We can apply the same decorator to many functions. That saves us from writing the same code again and again.
How to Create a Decorator in Python
A decorator is a function that takes a function as an argument. It defines an inner function that calls the original function. It returns the inner function.
def my_decorator(func): def wrapper(): print("Before the function runs") func() print("After the function runs") return wrapper
def say_hello(): print("Hello!")
say_hello = my_decorator(say_hello)say_hello()Output
Before the function runsHello!After the function runsWe manually wrapped say_hello with my_decorator. But Python has a nicer syntax.
Using the @ Syntax for Decorators
We can use @ to apply a decorator.
def my_decorator(func): def wrapper(): print("Before the function runs") func() print("After the function runs") return wrapper
@my_decoratordef say_hello(): print("Hello!")
say_hello()Output
Before the function runsHello!After the function runsThe @my_decorator line is equivalent to say_hello = my_decorator(say_hello). It is cleaner and easier to read.
Decorators with Arguments in Python
If the function we want to decorate takes arguments, we need to handle them in the wrapper.
def my_decorator(func): def wrapper(*args, **kwargs): print("Before") result = func(*args, **kwargs) print("After") return result return wrapper
@my_decoratordef add(a, b): return a + b
print(add(5, 3))Output
BeforeAfter8We use *args and **kwargs to accept any arguments and pass them to the original function.
A Real-Life Example – Timing a Function
Let us create a decorator that measures how long a function takes.
import time
def timer(func): def wrapper(*args, **kwargs): start = time.time() result = func(*args, **kwargs) end = time.time() print(f"{func.__name__} took {end - start:.4f} seconds") return result return wrapper
@timerdef slow_function(): time.sleep(1) print("Done")
slow_function()Output
Doneslow_function took 1.0001 secondsThat is a useful decorator.
Another Example – Logging Decorator
def logger(func): def wrapper(*args, **kwargs): print(f"Calling {func.__name__} with {args} {kwargs}") result = func(*args, **kwargs) print(f"{func.__name__} returned {result}") return result return wrapper
@loggerdef multiply(a, b): return a * b
multiply(5, 3)Output
Calling multiply with (5, 3) {}multiply returned 15That helps us debug and understand what is happening.
Common Mistakes with Decorators in Python
Mistake 1: Forgetting to return the wrapper
def my_decorator(func): def wrapper(): print("Before") func() print("After") # forgot to return wrapperThis returns None. So the decorated function becomes None.
Fix:
return wrapperMistake 2: Not handling arguments
def my_decorator(func): def wrapper(): func() return wrapper
@my_decoratordef add(a, b): return a + bThis gives an error when we call add(5, 3). Because the wrapper does not accept arguments.
Fix: Use *args and **kwargs.
Mistake 3: Losing the function’s metadata
def my_decorator(func): def wrapper(*args, **kwargs): return func(*args, **kwargs) return wrapper
@my_decoratordef add(a, b): return a + b
print(add.__name__)Output
wrapperThe __name__ is now wrapper, not add. That is because the wrapper function replaced the original.
Fix: Use functools.wraps.
import functools
def my_decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): return func(*args, **kwargs) return wrapperNow add.__name__ is add.
Next, we will learn about Python Closures and create functions that remember values from their enclosing scope.