As part of my #BuildInPublic journey building an AI chatbot backend on my MacBook, I stumbled across a syntax pattern that looks a bit like magic at first glance: the @ symbol sitting above functions.
These are Decorators.
After diving deep into how they work under the hood, I wanted to document exactly what they are, why we use them, and how they power major frameworks like FastAPI. If you've ever been confused by nested functions or *args and **kwargs, this guide is for you!
What is a Decorator? π
Think of a decorator like a gift wrapper.
Your function is the gift inside. The wrapper doesn't alter what the gift is, but it adds extra features on the outsideβlike wrapping paper, a fancy bow, or a security tag.
In technical terms, a decorator is a design pattern that allows you to modify or extend the behavior of a function without changing its actual source code.
The Superpower: Functions are First-Class Citizens
To understand decorators, you have to understand one rule in Python: Functions are just objects.
Just like strings or integers, you can pass a function into another function as an argument, and you can even return a function from a function.
Step-by-Step: Building a Custom Logger Decorator
Let's build a decorator that automatically logs a timestamp whenever a function runs. This allows us to track user behavior without cluttering our main core logic.
import datetime
# 1. The Outer Function acts like a factory. It takes your blueprint.
def my_logger(original_function):
# 2. The Inner Function acts like a "pause button".
# It catches arguments and executes code ONLY when the function is actually called.
def wrapper(*args, **kwargs):
print(f"β° [{datetime.datetime.now()}] '{original_function.__name__}' is starting...")
# 3. This executes your original code
result = original_function(*args, **kwargs)
print(f"β
'{original_function.__name__}' finished successfully.")
return result
return wrapper # 4. Returns the customized wrapper package
Wait, what are *args and **kwargs? π€
Because this logger needs to work on any function in our app, we use wildcards:
-
*argscatches any standard positional arguments (like a list of numbers:1, 2, 3). -
**kwargscatches keyword arguments (like named options:status="active"). This makes our decorator universally compatible!
Applying the Decorator
Now, instead of manually writing print statements inside every single function, we just drop the @ symbol on top:
@my_logger
def process_payment(amount):
print(f"π³ Processing payment of ${amount}...")
@my_logger
def generate_chatbot_report():
print("π Compiling chatbot usage analytics report...")
# Triggering the code:
process_payment(50)
The Terminal Output:
β° [2026-09-13 15:35:12] 'process_payment' is starting...
π³ Processing payment of $50...
β
'process_payment' finished successfully.
We completely modified the behavior of process_payment without touching a single line of code inside it. Clean, reusable, and elegant.
How this connects to FastAPI and Web Development
If you're building backends with FastAPI, you see decorators constantly. For example:
@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
...
Is @app.websocket built into Python? No! Python just provides the syntax (@).
The FastAPI developers pre-wrote specialized decorators attached to the app instance [://tiangolo.com]. When we use @app.websocket("/ws"), we are handing our function over to FastAPI's engine, telling it: "Hey, whenever a user connects to the /ws URL, run this function to manage their chat stream." [://tiangolo.com]
Summary
Decorators help you keep your code DRY (Don't Repeat Yourself). Use them for logging, authentication, performance timing, or routing.
Are you using decorators in your current projects? Let me know in the comments below how you utilize them!
Top comments (1)
Some comments may only be visible to logged-in visitors. Sign in to view all comments.