tl  tr
  Home | Tutorials | Articles | Videos | Products | Tools | Search
Interviews | Open Source | Tag Cloud | Follow Us | Bookmark | Contact   
 Modern Python > Type Hints and Dataclasses > Python Decorators

Python Decorators

Author: Venkata Sudhakar

A Python decorator is a function that wraps another function to extend or modify its behaviour without changing its source code. The @ syntax is syntactic sugar for passing the decorated function as an argument to the decorator function. Decorators are one of the most powerful patterns in Python - they are used extensively throughout popular frameworks: Flask uses @app.route() to register URL handlers, FastAPI uses @app.get() for API endpoints, Pydantic uses @field_validator() for schema validation, and LangChain uses @tool to define agent tools. Understanding how to write your own decorators is essential for building clean, reusable Python code.

The key to understanding decorators is that functions in Python are first-class objects - they can be passed as arguments, returned from other functions, and assigned to variables. A decorator is simply a callable that takes a function, wraps it in a new function (called a wrapper), and returns the wrapper. The functools.wraps decorator should always be applied to the wrapper function to preserve the original function name, docstring, and signature - without it, debugging and introspection tools see the wrapper instead of the original function.

The below example shows four practical decorators used in real data migration and AI pipelines: a timer, a logger, a retry decorator, and a cache decorator.


It gives the following output,

[LOG] Calling migrate_table('customers', batch_size=500)
[TIMER] migrate_table completed in 0.1002s
[LOG] migrate_table returned {'table': 'customers', 'rows_migrated': 125000, 'batches': 125}
Result: {'table': 'customers', 'rows_migrated': 125000, 'batches': 125}

It gives the following output,

[RETRY] call_migration_api attempt 1 failed: Connection timeout on attempt 1. Retrying in 0.5s...
[RETRY] call_migration_api attempt 2 failed: Connection timeout on attempt 2. Retrying in 1.0s...
API result: {'job_id': 'MIG-1042', 'status': 'RUNNING'}

[CACHE] get_table_schema('customers') computed and cached
  (fetching schema from DB for customers...)
[CACHE] get_table_schema('customers') returned from cache
[CACHE] get_table_schema('orders') computed and cached
  (fetching schema from DB for orders...)

Decorator factory pattern (parameterised decorators):

When you need to pass arguments to a decorator, you need a decorator factory - a function that returns a decorator. The retry decorator above is an example: @retry(max_attempts=3) means Python calls retry(max_attempts=3) first, which returns the decorator function, which is then applied to the function. This is the same pattern used by @app.route("/users") in Flask and @app.get("/users") in FastAPI - they are all decorator factories.

Class-based decorators:

Decorators can also be classes that implement __call__. This is useful when the decorator needs to maintain state across calls. The functools.lru_cache decorator is a built-in example of a sophisticated stateful decorator - use @functools.lru_cache(maxsize=128) instead of writing your own cache decorator for production code, as it is thread-safe and handles complex cache eviction.


 
  


  
bl  br