Logo OnlineCBT
PYTHON Tutorial
PYTHON · LESSON 40

Decorators

Wrap callable behaviour while preserving metadata.

Level: Beginner to Advanced
Duration: ~60 Mins Deep-Dive
Updated: 26 Jul 2026

Lesson 40: Decorators

1. Introduction to Functional Metaprogramming

In enterprise software engineering, applications frequently require cross-cutting concerns: repetitive non-business utilities that must be executed across dozens or hundreds of independent functions. Examples include logging function execution parameters, benchmarking runtime performance, validating security authorization tokens, caching expensive calculations, or retrying failed database calls.

If a developer copies and pastes timing or logging logic inside every business function, code clutter increases drastically, breaking the **DRY (Don't Repeat Yourself)** principle and making global maintenance nearly impossible.

Python solves this elegantly through Decorators. A decorator is a structural design pattern that allows modifying or extending the behavior of a function or class dynamically without altering its actual source code.

In this lesson, we will master the mechanics of First-Class Functions, demystify the @decorator syntax sugar, preserve function signatures using functools.wraps, build configurable decorators that accept arguments, engineer Class-Based Decorators using __call__(), and combine decorated functions with Structural Pattern Matching (`match-case`).


2. Prerequisite Foundations: First-Class Functions and Closures

To understand how decorators work under the hood, you must recognize that functions in Python are First-Class Citizens. This means functions are treated as standard data objects in memory:

  • Functions can be assigned to standard variables.
  • Functions can be passed as arguments into other higher-order functions.
  • Functions can be nested inside other functions and returned as return values.
FIRST_CLASS_FUNCTIONS_DEMO.PY
def execution_wrapper(target_func):
    """Higher-order function accepting a function reference as an argument."""
    print(f"[METAPROGRAMMING] Invoking target function object: '{target_func.__name__}'")
    # Executing the function reference passed as a parameter
    return target_func()

def greet_service():
    return "Hello, Enterprise System!"

# Passing 'greet_service' as a raw function object pointer (without parentheses!)
result = execution_wrapper(greet_service)
print("Returned Result:", result)
OUTPUT
[METAPROGRAMMING] Invoking target function object: 'greet_service'
Returned Result: Hello, Enterprise System!

3. The Anatomy of a Decorator (`@decorator` Syntax)

A Decorator is simply a callable higher-order function that takes another target function as an argument, wraps it inside an internal closure function to inject extra behavior, and returns the modified wrapper function object.

1. Manual Wrapping vs `@` Syntactic Sugar

# 1. Manual Wrapping Syntax:
def my_function(): pass
my_function = my_decorator(my_function)

# 2. Syntactic Sugar Equivalent (Using @ symbol):
@my_decorator
def my_function(): pass
    
The `@` Operator: Writing @my_decorator above a function definition is pure syntactic sugar! Python executes my_func = my_decorator(my_func) automatically at module load time, replacing the original function identifier with the wrapped function reference.
BASIC_DECORATOR_DEMO.PY
import time

def performance_timer_decorator(original_func):
    """
    Decorator function that measures and prints execution duration.
    Uses *args and **kwargs to accept arbitrary function signatures!
    """
    def wrapper_closure(*args, **kwargs):
        start_time = time.perf_counter()
        
        # Executing the original target function
        result = original_func(*args, **kwargs)
        
        end_time = time.perf_counter()
        elapsed = end_time - start_time
        print(f"[BENCHMARK] Function '{original_func.__name__}' executed in {elapsed:.6f} seconds.")
        
        return result  # Return original function output
        
    return wrapper_closure  # Return wrapper function pointer

# Applying decorator using @ syntax
@performance_timer_decorator
def compute_sum_of_squares(limit_n: int) -> int:
    return sum(i * i for i in range(limit_n))

# Invoking decorated function
total = compute_sum_of_squares(500_000)
print(f"Calculated Sum: {total}")
OUTPUT
[BENCHMARK] Function 'compute_sum_of_squares' executed in 0.024512 seconds.
Calculated Sum: 41666541666250000

4. Preserving Function Metadata using `functools.wraps`

When you wrap a target function inside a decorator's inner closure, the original function object is replaced by the inner wrapper function in Python's namespace. Consequently, the original function loses its identity metadata: its name (__name__), docstring (__doc__), and parameter annotations!

The Metadata Corruption Bug: Without preserving metadata, inspecting a decorated function (e.g., compute_sum.__name__) returns "wrapper_closure" rather than "compute_sum". This breaks diagnostic tools, auto-generated documentation tools (Sphinx), and debugging stack traces!

1. The Solution: `@functools.wraps`

Python provides the @functools.wraps(original_func) decorator in the standard library. Decorating the internal wrapper closure function with @wraps(original_func) copies all metadata attributes from the original function onto the wrapper object automatically.

METADATA_PRESERVATION_DEMO.PY
from functools import wraps

def audit_logger_decorator(func):
    @wraps(func)  # Preserves func.__name__, func.__doc__, func.__annotations__
    def wrapper(*args, **kwargs):
        print(f"[AUDIT LOG] Calling function '{func.__name__}' with args={args}, kwargs={kwargs}")
        return func(*args, **kwargs)
    return wrapper

@audit_logger_decorator
def transfer_funds(sender_id: str, receiver_id: str, amount: float) -> bool:
    """Transfers financial capital securely between user accounts."""
    print(f"--> Transferred ${amount:,.2f} from {sender_id} to {receiver_id}")
    return True

# Testing function execution
transfer_funds("ACC_101", "ACC_202", 500.00)

print("\n=== Metadata Introspection Checks ===")
print("Function Name     :", transfer_funds.__name__)  # Correctly retains 'transfer_funds'!
print("Function Docstring:", transfer_funds.__doc__)   # Retains original docstring!
OUTPUT
[AUDIT LOG] Calling function 'transfer_funds' with args=('ACC_101', 'ACC_202', 500.0), kwargs={}
--> Transferred $500.00 from ACC_101 to ACC_202

=== Metadata Introspection Checks ===
Function Name     : transfer_funds
Function Docstring: Transfers financial capital securely between user accounts.

5. Configurable Decorators: Accepting Arguments

What if you want a decorator to accept configuration parameters (e.g., specifying a custom retry count @retry(max_attempts=3) or permission tier @require_role("ADMIN"))?

To create a Decorator with Arguments, you must build a 3-level nested function hierarchy:

  1. Outer Level (Decorator Factory): Accepts custom configuration parameters.
  2. Middle Level (Actual Decorator): Accepts the target function object reference.
  3. Inner Level (Wrapper Closure): Accepts runtime function execution arguments (*args, **kwargs).
DECORATOR_WITH_ARGUMENTS.PY
from functools import wraps

# Level 1: Decorator Factory accepting custom configuration arguments
def repeat_execution(num_times: int = 3):
    # Level 2: Actual Decorator accepting target function
    def actual_decorator(func):
        # Level 3: Wrapper Closure executing inner logic
        @wraps(func)
        def wrapper(*args, **kwargs):
            print(f"=== Repeating '{func.__name__}' {num_times} times ===")
            last_result = None
            for attempt in range(1, num_times + 1):
                print(f"  Attempt #{attempt}:")
                last_result = func(*args, **kwargs)
            return last_result
        return wrapper
    return actual_decorator

# Applying Decorator with Arguments
@repeat_execution(num_times=2)
def send_ping_heartbeat():
    print("  --> Ping heartbeat packet sent to server.")
    return True

send_ping_heartbeat()
OUTPUT
=== Repeating 'send_ping_heartbeat' 2 times ===
  Attempt #1:
  --> Ping heartbeat packet sent to server.
  Attempt #2:
  --> Ping heartbeat packet sent to server.

6. Class-Based Decorators (`__call__`)

When decorators require maintaining state across multiple function invocations (such as counting total API calls or managing a local execution cache), stateful Class-Based Decorators provide a cleaner alternative to nested function closures.

A class acts as a decorator by implementing the dunder __call__(self, *args, **kwargs) magic method, allowing class instances to be invoked as functions!

CLASS_BASED_DECORATOR_DEMO.PY
from functools import wraps

class ExecutionCounterDecorator:
    """Class-based stateful decorator tracking function call frequency."""
    def __init__(self, func):
        self.func = func
        self.call_count = 0
        wraps(func)(self)  # Preserve function metadata on class instance

    def __call__(self, *args, **kwargs):
        self.call_count += 1
        print(f"[STATEFUL DECORATOR] Calling '{self.func.__name__}' | Total Calls So Far: {self.call_count}")
        return self.func(*args, **kwargs)

@ExecutionCounterDecorator
def execute_database_query(query_str: str):
    print(f"Executing SQL: {query_str}")

# Testing Class-Based Stateful Decorator
execute_database_query("SELECT * FROM users;")
execute_database_query("UPDATE users SET status='ACTIVE';")
print("Preserved Call Count State:", execute_database_query.call_count)
OUTPUT
[STATEFUL DECORATOR] Calling 'execute_database_query' | Total Calls So Far: 1
Executing SQL: SELECT * FROM users;
[STATEFUL DECORATOR] Calling 'execute_database_query' | Total Calls So Far: 2
Executing SQL: UPDATE users SET status='ACTIVE';
Preserved Call Count State: 2

7. Integrating Decorated Functions with `match-case` Pattern Matching

In enterprise microservice architectures, decorated functions are frequently combined with match-case structural pattern matching to parse incoming CLI or web payloads, enforce security validation decorators, and dispatch execution cleanly.

MATCH_CASE_DECORATED_DISPATCHER.PY
from functools import wraps

# Security Validation Decorator
def enforce_admin_role(func):
    @wraps(func)
    def wrapper(user_role: str, *args, **kwargs):
        if user_role.upper() != "ADMIN":
            print(f"[SECURITY DENIAL] Role '{user_role}' lacks permission to execute '{func.__name__}'!")
            return False
        return func(user_role, *args, **kwargs)
    return wrapper

@enforce_admin_role
def purge_system_logs(user_role: str, target_dir: str):
    print(f"SUCCESS: System logs in '{target_dir}' purged by {user_role}.")
    return True

@enforce_admin_role
def update_system_config(user_role: str, config_key: str, new_val: str):
    print(f"SUCCESS: Updated config '{config_key}' to '{new_val}' by {user_role}.")
    return True

def dispatch_service_command(command_payload: list):
    """
    Routes commands using match-case and invokes role-guarded decorated functions.
    Demonstrates combining pattern matching with decorated security methods.
    """
    match command_payload:
        case ["PURGE", role, target_path]:
            return purge_system_logs(role, target_path)
            
        case ["CONFIG", role, key_str, val_str]:
            return update_system_config(role, key_str, val_str)
            
        case _:
            print("ERROR: Unrecognized command payload format.")
            return False

# Testing Decorated Dispatcher
print("=== Attempt 1: Standard User Execution ===")
dispatch_service_command(["PURGE", "MEMBER", "/var/logs"])

print("\n=== Attempt 2: Admin User Execution ===")
dispatch_service_command(["PURGE", "ADMIN", "/var/logs"])
OUTPUT
=== Attempt 1: Standard User Execution ===
[SECURITY DENIAL] Role 'MEMBER' lacks permission to execute 'purge_system_logs'!

=== Attempt 2: Admin User Execution ===
SUCCESS: System logs in '/var/logs' purged by ADMIN.

8. Frequently Asked Interview Questions with Answers

Q1: What is a Decorator in Python, and how does `@decorator` syntactic sugar work?
Answer: A Decorator is a callable higher-order function that accepts another function as an argument, wraps it inside an inner closure function to extend its behavior, and returns the modified function object. The @decorator syntax is syntactic sugar for func = decorator(func), executed automatically at module load time.
Q2: Why is `@functools.wraps` essential when writing custom decorators?
Answer: Wrapping a target function inside a closure replaces the original function object with the inner wrapper function, destroying original metadata like __name__, __doc__, and parameter annotations. Decorating the closure with @wraps(original_func) preserves this metadata on the wrapper object.
Q3: How do you pass custom arguments into a Decorator (e.g., `@retry(max_attempts=3)`)?
Answer: By constructing a 3-level nested function factory: (1) The outer function receives the custom decorator configuration arguments, (2) The middle function receives the target function reference, and (3) The inner wrapper closure receives runtime execution arguments (*args, **kwargs).
Q4: How does a Class-Based Decorator work in Python?
Answer: A class acts as a decorator by storing the target function reference inside its __init__(self, func) constructor and implementing the dunder __call__(self, *args, **kwargs) method. When the decorated function is invoked, Python calls the instance's __call__() method.
Q5: What is the execution order when multiple decorators are stacked on a single function?
Answer: Multiple stacked decorators are applied in bottom-up (inside-out) order! Writing:
@dec_A
@dec_B
def func(): pass
is equivalent to: func = dec_A(dec_B(func)). dec_B wraps func first, and dec_A wraps the resulting object from dec_B.

9. Homework & Practical Assignments

Task 1: Execution Logging Decorator with Metadata Preservation

Create a script named logging_decorator_task.py inside your lesson_40 folder:

  • Import wraps from functools.
  • Define a decorator log_arguments_and_return(func) that logs function name, positional args, keyword kwargs, and the returned result.
  • Apply decorator to a function calculate_payroll(emp_id: str, hours: float, rate: float) -> float.
  • Verify metadata preservation by printing calculate_payroll.__name__ and calculate_payroll.__doc__.

Task 2: Pattern-Matched Configurable Retry Decorator

Create a script named retry_decorator_task.py:

  • Build a 3-level decorator factory retry_on_exception(max_retries: int = 3) that catches exceptions and retries function calls up to max_retries times before re-raising.
  • Define a simulated network function that raises ConnectionError on initial calls.
  • Write a match-case dispatcher to test calling the decorated network function with different retry configs and report success/failure status.

Task 3: Comprehensive Review Project — Enterprise Decorator-Guarded Service OS (`decorator_service_os.py`)

Create a script named decorator_service_os.py inside your lesson_40 folder. This assignment tests and integrates **ALL concepts learned across Lessons 1 through 40**.

Project Architectural Requirements Specification:

  1. Advanced Metaprogramming & Decorators Architecture (Lesson 40):
    • Build a 3-level configurable decorator factory require_permission(role_level="ADMIN") enforcing authorization checks.
    • Build a stateful class-based decorator ServiceCallMonitor tracking function execution frequency and error counts via __call__().
    • Ensure ALL decorators use @functools.wraps to preserve metadata.
  2. Iterators, Generators & Functional Tools (Lesson 39):
    • Implement generator function stream_audit_records(filepath) reading log lines lazily with O(1) memory.
    • Incorporate Generator Expressions for record filtering.
  3. Full OOP Architecture (Lessons 32-38):
    • Define abstract base class BaseServiceEngine(ABC) and concrete Data Classes UserServiceRecord and AuditEntry.
    • Build composite manager EnterpriseOS composing storage drivers and custom context managers.
  4. Observability & Fault Tolerance (Lessons 29-31):
    • Set up multi-handler logging to console and os_audit.log file.
    • Incorporate developer assertions (assert) verifying invariant states.
    • Define domain custom exception hierarchy (ServiceOSError, PermissionDeniedError).
  5. Context Managers & Persistence Layer (Lessons 27-30):
    • Use custom context manager VaultLockSession to manage persistent database lock files.
    • Persist JSON database maps to system_db.json and export CSV reports to service_audit.csv using pathlib.Path.
  6. Nested Collections, Comprehensions & Data Structures (Lessons 21-26):
    • Maintain in-memory registry dictionaries mapping IDs to Data Class Instances.
    • Use List and Dictionary Comprehensions to clean and transform records.
  7. Advanced Function Parameters & Scope (Lessons 15-17):
    • Structure logic into pure modular functions with type hints, docstrings, early return Guard Clauses, and *args / **kwargs logging.
  8. Indefinite & Definite Loops (Lessons 13-14):
    • Run the interactive CLI interface inside a continuous while True menu loop.
    • Iterate through output reports using for loops with enumerate() and .items().
  9. Pattern Matching CLI Dispatcher (Lesson 12):
    • Process user CLI command tokens inside a match-case block:
      • case ["USER", "ADD", role, uid, name, email] → Invoke decorated service method to register user.
      • case ["SYSTEM", "PURGE", role, target_dir] → Invoke require_permission("ADMIN") decorated method.
      • case ["AUDIT", "STREAM", filename] → Stream log records lazily using generator pipeline.
      • case ["EXPORT", "CSV"] → Export active database state to CSV.
      • case ["EXIT" | "QUIT"] → Terminate session safely using a sentinel flag.
      • case _ → Output command error message.
  10. Conditionals, Formatting & Foundations (Lessons 1-11):
    • Sanitize all inputs using .strip() and .upper().
    • Format tabular reports using f-strings with field width alignment specifiers (:<15, :>10) and clear visual borders.

10. File & Workspace Directory Structure

Standard Course Workspace Layout:

Ensure all exercise files are stored inside their corresponding lesson directories:

python_mastery_course/
│
├── lesson_01/ ... lesson_39/
│
└── lesson_40/
    ├── first_class_functions_demo.py
    ├── basic_decorator_demo.py
    ├── metadata_preservation_demo.py
    ├── decorator_with_arguments.py
    ├── class_based_decorator_demo.py
    ├── match_case_decorated_dispatcher.py
    ├── logging_decorator_task.py   <-- (Task 1)
    ├── retry_decorator_task.py     <-- (Task 2)
    └── decorator_service_os.py     <-- (Task 3: Master Review Capstone)
        

11. What We Will Learn Next

Next Up: Lesson 41 — Closures and Function Factories

Now that you master Decorators and metaprogramming, we will dive deeper into advanced functional programming concepts: Closures and Function Factories!

In the next lesson, we will cover:

  • Deep dive into Variable Scope and Lexical Scoping (LEGB Rule: Local, Enclosing, Global, Built-in).
  • Understanding Lexical Closures and the __closure__ dunder attribute.
  • State retention without global variables using the `nonlocal` keyword.
  • Engineering dynamic Function Factories that generate specialized functions at runtime.
  • Combining Function Factories with match-case pattern dispatchers.

📝 Live Lesson Practice

HTML/CSS JavaScript Python C++ C PHP
⌨️ Practice Inputs (लाइव इनपुट भरें) (खाली होने पर RED, भरने पर GREEN underline)
💻 Code Editor (Monaco VS Code Engine)
👀 Live Preview
Address Contact

+91 7877547686

E-mail

onlinecbtportal@gmail.com

Helpline Number

+91 7877547686


Click To Download
Get it on Google Play

ऐप डाउनलोड करने
के लिए Google Play पर
उपलब्ध है

WhatsApp Chat