Decorators in Python
A decorator is a function that takes another function, adds some extra work before or after it, and gives back a new function, without changing the original function's code.
Think of it like a gift wrapper: the gift (your function) stays the same, the wrapper (decorator) adds something around it.
Decorators are used everywhere in real projects: logging, timing, login checks, caching and validation. Frameworks such as FastAPI and Flask use them for routes, for example @app.get("/").
Setup
import time
from functools import wraps
Step 1: Functions are objects
A function can be stored in a variable, just like a number or string.
def say_hello():
print("Hello!")
greet = say_hello # no brackets -> we are NOT calling it
print("Step 1:")
greet() # now we call it -> prints Hello!
Step 2: Pass a function to another function
def call_twice(func):
func()
func()
print("\nStep 2:")
call_twice(say_hello)
Step 3: Function inside a function
def outer():
def inner():
print("I am inner function")
return inner # return the function itself (not inner())
print("\nStep 3:")
my_func = outer()
my_func()
Step 4: Our first decorator (manual way)
def my_decorator(func):
def wrapper():
print("Before the function runs")
func()
print("After the function runs")
return wrapper
def say_hi():
print("Hi Students")
print("\nStep 4:")
say_hi = my_decorator(say_hi) # wrap say_hi
say_hi()
Step 5: the @ symbol (short way)
@my_decorator is the same as say_bye = my_decorator(say_bye)
@my_decorator
def say_bye():
print("Bye Students")
print("\nStep 5:")
say_bye()
Step 6: Decorator for functions with arguments
*args and **kwargs accept any number of arguments.
def show_arguments(func):
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__} with {args} {kwargs}")
return func(*args, **kwargs)
return wrapper
@show_arguments
def add(a, b):
return a + b
print("\nStep 6:")
result = add(10, 20)
print("Result:", result)
Step 7: Returning a value from the decorated function
Always "return func(...)" inside wrapper, otherwise the result is lost.
def double_result(func):
def wrapper(*args, **kwargs):
value = func(*args, **kwargs)
return value * 2
return wrapper
@double_result
def get_number(n):
return n
print("\nStep 7:")
print("get_number(5) ->", get_number(5)) # 10
Step 8: Real example - timer decorator
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_task():
time.sleep(1)
print("Task finished")
print("\nStep 8:")
slow_task()
Step 9: Real example - login check
def login_required(func):
def wrapper(user):
if user.get("logged_in"):
return func(user)
else:
print("Access denied! Please login first.")
return wrapper
@login_required
def view_profile(user):
print("Welcome", user["name"])
print("\nStep 9:")
view_profile({"name": "Sudheer", "logged_in": True})
view_profile({"name": "Guest", "logged_in": False})
Step 10: Decorator with its own argument
Need one more outer function to receive the argument.
def repeat(times):
def decorator(func):
def wrapper(*args, **kwargs):
for _ in range(times):
func(*args, **kwargs)
return wrapper
return decorator
@repeat(3)
def cheer(name):
print("Well done,", name)
print("\nStep 10:")
cheer("Divyani")
Step 11: Using more than one decorator
The decorator closest to the function runs first (bottom to top).
def bold(func):
def wrapper():
return "<b>" + func() + "</b>"
return wrapper
def italic(func):
def wrapper():
return "<i>" + func() + "</i>"
return wrapper
@bold
@italic
def text():
return "Python"
print("\nStep 11:")
print(text()) # <b><i>Python</i></b>
Step 12: functools.wraps (good practice)
Without @wraps, the function name becomes "wrapper".
def no_wraps(func):
def wrapper():
return func()
return wrapper
def with_wraps(func):
@wraps(func) # keeps original name and docstring
def wrapper():
return func()
return wrapper
@no_wraps
def first():
"""This is first function"""
@with_wraps
def second():
"""This is second function"""
print("\nStep 12:")
print("Without wraps:", first.__name__) # wrapper
print("With wraps :", second.__name__) # second
Summary
- A decorator takes a function and returns a new function.
@decorator_nameabove a function = func = decorator_name(func)- Use *args, **kwargs in wrapper so it works with any function.
- Use "return func(...)" so the result is not lost.
- Use
@wraps(func) to keep the original function's name. - Common uses: logging, timing, login checks, caching, validation.