Last updated: May 5, 2026
Prerequisites
- Python 3.x installed and a terminal or REPL to run examples
- Familiarity with defining functions in Python
- Basic understanding that functions in Python can be passed as arguments (helpful but not essential – explained below)
You open a Flask tutorial. The route works, the app runs, but hovering above the function definition is a line you didn’t write yourself: @app.route('/'). You copy it, accept it, and move on. But that single @ symbol is doing far more than decoration – it is wiring together routing logic, view functions, and the HTTP lifecycle in a single readable line.
The Stack Overflow question “What does the @ symbol do in Python?” has been viewed over 788,000 times since 2011, making it one of the most persistent sources of confusion for Python learners at every level. This python decorator tutorial will change that. By the end, you will understand exactly what the @ symbol does, how to write your own decorators from scratch, and why frameworks like Flask and Django rely on them so heavily.
What the @ Symbol Actually Means in Python

Image: Stack Overflow
The @ symbol in Python, when placed at the start of a line above a function or class definition, is a decorator – a piece of syntax introduced in PEP 318 with Python 2.4. It is syntactic sugar, meaning it is a shorthand for something you could write manually.
Consider the long form first:
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
The decorator takes say_hello, wraps it in new behaviour, and reassigns the result back to the same name. The @ syntax collapses that last line into a single, readable annotation:
@my_decorator
def say_hello():
print("Hello!")
These two forms are exactly equivalent. @my_decorator above say_hello is just Python executing say_hello = my_decorator(say_hello) for you automatically.
Think of a decorator like a security badge reader at an office entrance. The badge reader (decorator) does not change what the employee (function) does at their desk. It wraps the entry process – checking credentials, logging the visit, maybe redirecting unauthorised visitors – before letting the original function proceed as normal.
Python Decorator Tutorial: Writing and Using Decorators Step by Step
Step 1 – Define a basic decorator
A decorator is any callable that accepts a function and returns a function. Start with the simplest possible example:
def shout(func):
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return str(result).upper()
return wrapper
@shout
def greet(name):
return f"hello, {name}"
print(greet("world"))
Output:
HELLO, WORLD
Using *args and **kwargs in the wrapper ensures your decorator works on functions with any signature – not just zero-argument functions.
Common mistake: Writing the wrapper without
*args, **kwargs. If the decorated function accepts arguments and the wrapper does not forward them, you will see aTypeError: wrapper() takes 0 positional arguments but 1 was givenerror at call time.
Step 2 – Preserve the original function’s metadata
When you wrap a function, Python replaces it with the wrapper. That means greet.__name__ now returns "wrapper" rather than "greet". Fix this with functools.wraps:
import functools
def shout(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return str(result).upper()
return wrapper
Always use @functools.wraps. Debugging decorated functions without it is a frustrating exercise in tracing back through anonymous wrapper chains.
Step 3 – Use decorators with arguments
To pass arguments to a decorator itself (not just the wrapped function), add an outer factory function:
import functools
def repeat(times):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for _ in range(times):
func(*args, **kwargs)
return wrapper
return decorator
@repeat(times=3)
def announce(message):
print(message)
announce("Ready!")
Output:
Ready!
Ready!
Ready!
The three levels of nesting – factory, decorator, wrapper – are the standard pattern for parametrised decorators. If you see @decorator(argument) in a codebase, this is what is happening underneath.
Step 4 – Meet the three built-in Python decorators
Python ships with three decorators you will encounter constantly:
class Circle:
def __init__(self, radius):
self._radius = radius
@property
def area(self):
import math
return math.pi * self._radius ** 2
@classmethod
def unit(cls):
return cls(1)
@staticmethod
def is_valid_radius(r):
return r > 0
c = Circle(5)
print(c.area) # Called like an attribute, not a method
print(Circle.unit()) # Called on the class, not an instance
print(Circle.is_valid_radius(-1)) # No self or cls needed
@propertyturns a method into a read-only attribute.@classmethodreceives the class as the first argument instead of an instance.@staticmethodreceives neither – it is a plain function scoped to the class namespace.
Where Decorators Appear in Real Frameworks
Flask and Django use decorators as their primary API surface – once you understand the pattern, reading framework code becomes significantly clearer.
Flask’s most recognisable decorator is @app.route('/'). Internally, calling it is equivalent to app.add_url_rule('/', view_func=hello). The decorator pattern lets Flask register routes with minimal boilerplate:
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello():
return "Hello, world!"
Before/after comparison – without the decorator:
def hello():
return "Hello, world!"
app.add_url_rule('/', view_func=hello)
Both work identically. The decorator version simply makes the association between URL and function visually explicit at the point of definition.
Django provides built-in view decorators for access control and HTTP behaviour. If you are learning Django, a solid guide like How to Learn Django (2026) will walk you through where these fit in the framework’s architecture. Common examples include @login_required, @require_http_methods(["GET", "POST"]), and @cache_page(60 * 15) for adding caching headers – each one wrapping the original view function without modifying its internal logic.
If you see
@login_requiredand it redirects you to a login page, it means: the decorator ran before your view function, checked authentication, and short-circuited the response. The view function itself never executed.
Instance-based decorators also exist. When @instance appears above a function definition, Python passes the function to the instance’s __call__ method. Flask’s app object works exactly this way – app.route is a method on the Flask instance that returns a decorator.
The Nuance Most Tutorials Skip: @ Is Not Always a Decorator
One detail that catches experienced developers off guard: the @ symbol has two completely separate meanings in Python, depending on where it appears.
At the start of a line, @ is a decorator as described throughout this guide. In the middle of an expression, @ is the matrix multiplication operator, introduced in Python 3.5 via PEP 465:
import numpy as np
A = np.array([[1, 2], [3, 4]])
B = np.array([[5, 6], [7, 8]])
C = A @ B # Matrix multiplication - NOT a decorator
print(C)
Output:
[[19 22]
[43 50]]
If you work in data science or numerical computing, you will encounter both usages in the same codebase. The position in the line – start versus middle – is the only disambiguation you need.
For developers coming from other languages, it is also worth noting that decorators in Python are more general than annotations in Java or attributes in C#. They are executable code that runs at import time, not passive metadata tags. This makes them more powerful – and also means that a badly written decorator can silently break function signatures if you are not careful with functools.wraps.
Bringing It Together
The @ symbol is not magic. It is a shorthand that Python executes at class or module load time: take the function defined below, pass it to the callable after @, and bind the result back to the original name. That single insight unlocks Flask routes, Django view decorators, Python’s own @property, and any custom cross-cutting behaviour you need to add across your codebase – authentication, logging, caching, retrying – without touching the functions themselves.
Start by writing a simple timing decorator on your next project. Then look at how Building VS Code Extensions with MCP uses similar composition patterns in a TypeScript context, and notice how the decorator concept crosses language boundaries even when the syntax differs. Once you can read @app.route('/') and trace it back to add_url_rule, you are reading framework code rather than just copying it.
If you are building something in Python and want experienced help architecting clean, maintainable code, get in touch with the team at drs-web.co.uk/contact – we are happy to help.
Frequently Asked Questions
Q: What does the @ symbol do in Python?
A: At the start of a line, @ marks a decorator – it passes the function or class defined below it as an argument to the callable after the @ sign. It is shorthand for func = decorator(func). In the middle of an expression, @ performs matrix multiplication (Python 3.5+).
Q: What is the difference between @classmethod and @staticmethod?
A: @classmethod receives the class itself as the first argument (cls), making it useful for alternative constructors. @staticmethod receives no implicit first argument – it is a regular function that lives in the class namespace purely for organisational reasons.
Q: Can I stack multiple decorators on one function?
A: Yes. Decorators are applied bottom-up, so @a on top of @b means the function is first wrapped by b, then that result is wrapped by a. Each decorator must accept and return a callable for stacking to work correctly.
Q: Why does my decorated function show the wrong __name__?
A: Without @functools.wraps(func) inside your wrapper, Python replaces the function’s metadata with the wrapper’s. Add import functools and decorate your inner wrapper function with @functools.wraps(func) to preserve the original name, docstring, and signature.
Q: What is the @ symbol in Flask routes?
A: Flask’s @app.route('/') is a decorator that registers a URL rule on the Flask application instance. It is equivalent to calling app.add_url_rule('/', view_func=your_function) manually – the decorator syntax just keeps the route and its handler visually co-located in the source file.
Source: https://stackoverflow.com/questions/6392739/what-does-the-at-symbol-do-in-python
This article was researched and written with AI assistance, then reviewed for accuracy and quality. Kev Parker uses AI tools to help produce content faster while maintaining editorial standards.
Need help with your web project?
From one-day launches to full-scale builds, DRS Web Development delivers modern, fast websites.



