Python Decorators

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 runs
Hello!
After the function runs

We 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_decorator
def say_hello():
print("Hello!")
say_hello()

Output

Before the function runs
Hello!
After the function runs

The @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_decorator
def add(a, b):
return a + b
print(add(5, 3))

Output

Before
After
8

We 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
@timer
def slow_function():
time.sleep(1)
print("Done")
slow_function()

Output

Done
slow_function took 1.0001 seconds

That 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
@logger
def multiply(a, b):
return a * b
multiply(5, 3)

Output

Calling multiply with (5, 3) {}
multiply returned 15

That 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 wrapper

This returns None. So the decorated function becomes None.

Fix:

return wrapper

Mistake 2: Not handling arguments

def my_decorator(func):
def wrapper():
func()
return wrapper
@my_decorator
def add(a, b):
return a + b

This 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_decorator
def add(a, b):
return a + b
print(add.__name__)

Output

wrapper

The __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 wrapper

Now add.__name__ is add.


Next, we will learn about Python Closures and create functions that remember values from their enclosing scope.