InsightsAdvance Python : Part 2
Python

Advance Python : Part 2

Gaurav ChopraGaurav Chopra·November 15, 2025
PART 2 OF 3

Python Decorators

Elegant Code Enhancement

🎯 What You'll Master

Decorators are one of Python's most elegant features, allowing you to modify or enhance functions and classes without changing their source code. In this part, you'll learn to write clean, maintainable, and powerful code using decorators for everything from logging and caching to authentication and validation. By the end, you'll be crafting sophisticated decorator patterns used by popular frameworks like Flask and FastAPI.

MODULE 5

Function Fundamentals for Decorators

5.1 First-Class Functions

  • Functions as objects - understanding the fundamental concept
  • Assigning functions to variables and storing them in data structures
  • Passing functions as arguments to other functions
  • Returning functions from functions - the foundation of decorators
# Functions are first-class objects in Python def greet(name): return f"Hello, {name}!"# Assign to variable say_hello = greet print(say_hello("Alice")) # Hello, Alice!# Pass as argument def execute_function(func, arg): return func(arg)result = execute_function(greet, "Bob") # Hello, Bob!

5.2 Closures and Scope

  • Understanding the nonlocal keyword and when to use it
  • How closures capture and remember variables from outer scope
  • Practical closure examples for data encapsulation
  • The difference between closure and global scope
def outer_function(x): # x is captured in the closure def inner_function(y): return x + y # inner can access outer's x return inner_function# Create closures with different captured values add_five = outer_function(5) add_ten = outer_function(10)print(add_five(3)) # 8 print(add_ten(3)) # 13

Key Concepts to Master

First-Class Functions Closures Scope (LEGB) nonlocal Keyword Function References

🏋️ Lab Exercise: Simple Memoization Function

Task: Implement a simple memoization function using closures that caches function results to avoid redundant calculations.

Requirements:

  • Create a memoize function that takes another function as input
  • Use a closure to maintain a cache dictionary
  • Cache function results based on arguments
  • Return cached results when the same arguments are provided
  • Demonstrate proper closure usage and scope management

Hint: Your solution structure should look like:

  • Outer function: memoize(func)
  • Create an empty cache dictionary in the outer scope
  • Inner function: wrapper(*args)
  • Check if args are in cache; if yes, return cached value
  • If not in cache, call the function, cache the result, and return it
  • Test with an expensive function like fibonacci
MODULE 6

Decorator Basics

6.1 What Are Decorators?

  • The decorator design pattern vs Python's decorator syntax
  • Understanding the @decorator_name syntactic sugar
  • How decorators wrap functions to modify behavior
  • The relationship between decorators, closures, and higher-order functions
# Simple decorator structure def my_decorator(func): def wrapper(*args, **kwargs): print("Before function call") result = func(*args, **kwargs) print("After function call") return result return wrapper# Using the @ syntax @my_decorator def say_hello(): print("Hello!")# Equivalent to: say_hello = my_decorator(say_hello)

6.2 Building Your First Decorator

  • Simple logging decorator - tracking function calls
  • Timer/performance decorator - measuring execution time
  • Common pitfalls: losing function metadata
  • Using functools.wraps to preserve function information
import functools import timedef timer(func): @functools.wraps(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_function(): time.sleep(1) return "Done"

6.3 Decorators with Arguments

  • Creating configurable decorators with parameters
  • Understanding the decorator factory pattern (three levels of nesting)
  • Practical example: @repeat(times=3)
  • When and why to use parameterized decorators
def repeat(times): # Decorator factory - returns the actual decorator def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): results = [] for _ in range(times): result = func(*args, **kwargs) results.append(result) return results return wrapper return decorator@repeat(times=3) def greet(name): return f"Hello, {name}!"print(greet("Alice")) # ['Hello, Alice!', 'Hello, Alice!', 'Hello, Alice!']

Key Concepts to Master

@ Syntax Wrapper Functions functools.wraps Decorator Factories *args, **kwargs

🏋️ Lab Exercise: Debug Decorator

Task: Build a @debug decorator that prints function calls with their arguments and return values.

Requirements:

  • Create a decorator that logs function name, arguments, and return value
  • Handle both positional and keyword arguments properly
  • Use functools.wraps to preserve function metadata
  • Format the output to be readable (e.g., "Calling function_name(arg1, arg2, kwarg1=value)")
  • Print the return value after function execution

Hint: Your solution structure should look like:

  • Import functools
  • Define debug(func) decorator
  • Use @functools.wraps(func) on the wrapper
  • In wrapper, format args and kwargs as strings
  • Print "Calling {func.__name__}(...)"
  • Call the function and capture result
  • Print "Returned: {result}"
  • Return the result
MODULE 7

Advanced Decorator Patterns

7.1 Class-Based Decorators

  • Using the __call__ method to create callable classes
  • When to use class decorators vs function decorators
  • Maintaining state across multiple function calls
  • Advantages: readability and state management for complex decorators
class CountCalls: def __init__(self, func): self.func = func self.count = 0 def __call__(self, *args, **kwargs): self.count += 1 print(f"Call {self.count} of {self.func.__name__}") return self.func(*args, **kwargs)@CountCalls def process_data(data): return data.upper()

7.2 Method Decorators

  • Decorating class methods and handling the self parameter
  • Deep dive into @staticmethod, @classmethod, @property
  • Creating custom method decorators for validation and authorization
  • Differences between decorating functions and methods
def require_login(method): @functools.wraps(method) def wrapper(self, *args, **kwargs): if not self.is_authenticated: raise PermissionError("User must be logged in") return method(self, *args, **kwargs) return wrapperclass UserAccount: def __init__(self): self.is_authenticated = False @require_login def view_balance(self): return "Your balance: $1000"

7.3 Class Decorators

  • Decorating entire classes to modify their behavior
  • Adding methods or attributes to classes automatically
  • Implementing the Singleton pattern using class decorators
  • Use cases: registration, monitoring, and automatic method generation
def singleton(cls): instances = {} @functools.wraps(cls) def get_instance(*args, **kwargs): if cls not in instances: instances[cls] = cls(*args, **kwargs) return instances[cls] return get_instance@singleton class Database: def __init__(self): print("Connecting to database...")

7.4 Stacking Multiple Decorators

  • Understanding the order of execution when stacking decorators
  • Bottom-to-top evaluation: how Python processes stacked decorators
  • Designing decorators that work well together (composability)
  • Common patterns and potential pitfalls
@timer @repeat(times=3) @debug def process(data): return data.upper()# Execution order: # 1. debug wraps process # 2. repeat wraps the result # 3. timer wraps everything

Key Concepts to Master

__call__ Method Class Decorators Method Decorators Decorator Stacking Singleton Pattern

🏋️ Lab Exercise: Argument Validation Decorator

Task: Create a @validate decorator for function argument validation that raises appropriate errors when arguments don't meet requirements.

Requirements:

  • Implement both function-based and class-based versions
  • Accept validation rules: type checks, range checks, custom validators
  • Raise ValueError or TypeError with descriptive messages
  • Support validating both positional and keyword arguments
  • Example usage: @validate(x=int, y=(int, lambda y: y > 0))

Hint: Your solution structure should look like:

  • Function-based: Create a decorator factory that accepts validation rules as kwargs
  • Return a decorator that returns a wrapper function
  • In wrapper, inspect function signature and match args to rules
  • Check types and run custom validators
  • Class-based: Use __init__ to store rules, __call__ to wrap function
  • Store the wrapped function and rules as instance attributes
MODULE 8

Real-World Decorator Applications

8.1 Common Use Cases

  • Authentication and authorization - protecting sensitive operations
  • Caching and memoization - optimizing expensive computations
  • Rate limiting and throttling - preventing abuse and overload
  • Retry logic with exponential backoff - handling transient failures
  • Deprecation warnings - gracefully phasing out old code
import time from functools import wrapsdef retry(max_attempts=3, delay=1): def decorator(func): @wraps(func) def wrapper(*args, **kwargs): attempts = 0 while attempts < max_attempts: try: return func(*args, **kwargs) except Exception as e: attempts += 1 if attempts >= max_attempts: raise print(f"Attempt {attempts} failed. Retrying...") time.sleep(delay * attempts) # Exponential backoff return wrapper return decorator@retry(max_attempts=5, delay=2) def fetch_data_from_api(): # Might fail due to network issues pass
def memoize(func): cache = {} @wraps(func) def wrapper(*args): if args not in cache: cache[args] = func(*args) return cache[args] return wrapper@memoize def fibonacci(n): if n < 2: return n return fibonacci(n-1) + fibonacci(n-2)

8.2 Decorators in Popular Frameworks

  • Flask/FastAPI route decorators - mapping URLs to functions
  • Property decorators for getters/setters - clean attribute access
  • Testing decorators: @patch, @mock from unittest
  • Django decorators: @login_required, @permission_required
  • How frameworks leverage decorators for clean, declarative APIs
# Flask-style route decorator class App: def __init__(self): self.routes = {} def route(self, path): def decorator(func): self.routes[path] = func return func return decoratorapp = App()@app.route("/home") def home(): return "Welcome home!"@app.route("/about") def about(): return "About us"
# Property decorators for clean attribute access class Temperature: def __init__(self, celsius): self._celsius = celsius @property def celsius(self): return self._celsius @celsius.setter def celsius(self, value): if value < -273.15: raise ValueError("Temperature below absolute zero!") self._celsius = value @property def fahrenheit(self): return self._celsius * 9/5 + 32

Key Concepts to Master

Authentication Caching Rate Limiting Retry Logic Route Registration Property Pattern

🏋️ Lab Exercise: API Endpoint Decorator System

Task: Build an API endpoint decorator system that combines multiple concerns in a modular, composable way.

Requirements:

  • Create an @auth_required decorator that checks authentication
  • Create a @rate_limit(max_calls=10, window=60) decorator
  • Create a @log_request decorator that logs function calls
  • Create an @handle_errors decorator for automatic error handling
  • Demonstrate stacking these decorators on API endpoint functions
  • Make sure decorators work well together and maintain proper order

Hint: Your solution structure should look like:

  • auth_required: Check for auth token/header, raise exception if missing
  • rate_limit: Use a dictionary to track calls per user/IP with timestamps
  • log_request: Print timestamp, function name, and arguments
  • handle_errors: Wrap in try/except, return error JSON on exception
  • Test by stacking decorators: @handle_errors @rate_limit(10, 60) @auth_required @log_request

🎓 Part 2 Capstone Project: Web Framework Mini

Build a lightweight web framework that demonstrates mastery of decorator patterns!

Project Requirements:

  1. Route Registration System
    • Use decorators to map URLs to handler functions
    • Support multiple HTTP methods (GET, POST, PUT, DELETE)
    • Example: @app.route("/home"), @app.route("/api/users", methods=["POST"])
    • Store route mappings in a dictionary
  2. Middleware System
    • Authentication decorator: @require_auth
    • Logging decorator: @log_requests
    • Timing decorator: @measure_time
    • Middleware should be composable and stackable
  3. Request/Response Validation
    • Create decorators that validate incoming request data
    • Format responses automatically (JSON, HTML, etc.)
    • Example: @validate_json(schema), @returns_json
  4. Caching Decorator
    • Implement @cache(ttl=300) for expensive operations
    • Support configurable time-to-live (TTL)
    • Cache key should be based on function args
    • Automatic cache invalidation
  5. Error Handling & Rate Limiting
    • Decorators that catch exceptions and return HTTP error responses
    • Per-route rate limiting: @rate_limit(calls=100, period=60)
    • Track requests per user/IP address

Example Usage:

app = WebFramework()@app.route("/api/users")
@require_auth
@rate_limit(calls=10, period=60)
@cache(ttl=300)
@returns_json
def get_users():
return User.get_all()@app.route("/api/users", methods=["POST"])
@require_auth
@validate_json(user_schema)
@returns_json
def create_user(data):
return User.create(data)

Evaluation Criteria:

  • ✅ Use both function-based and class-based decorators appropriately
  • ✅ Implement decorator factories for configurable behavior
  • ✅ Demonstrate proper decorator stacking and composition
  • ✅ Include comprehensive error handling and validation
  • ✅ Write unit tests for all decorator functionality
  • ✅ Document code with clear docstrings and type hints

Bonus Challenges:

  • 🌟 Add support for URL parameters and wildcards
  • 🌟 Implement decorator-based permission system with role-based access control
  • 🌟 Create profiling decorator that tracks performance metrics
  • 🌟 Build decorator that auto-generates API documentation from signatures

📚 Part 2 Summary

What You've Learned:

  • Module 5: First-class functions, closures, and scope fundamentals
  • Module 6: Decorator basics, syntax, and creating parameterized decorators
  • Module 7: Advanced patterns including class-based decorators and decorator stacking
  • Module 8: Real-world applications in authentication, caching, and framework design

Key Takeaways:

  1. Decorators are syntactic sugar for function wrappers that modify behavior
  2. Closures allow decorators to maintain state and access outer scope
  3. functools.wraps is essential for preserving function metadata
  4. Decorator factories enable parameterized decorators with configuration
  5. Stacking decorators requires understanding execution order (bottom-to-top)
  6. Class-based decorators use __call__ and are great for stateful decorators

Next Steps:

Ready to continue? Move on to:

  • Part 3: Asynchronous Programming - Master async/await, concurrent task execution, and building high-performance async applications
Gaurav Chopra
Gaurav Chopra

Gaurav is a Co-Founder of Eightgen AI

Work with us

Found this useful? Let's talk about your build.

We write about what we build. If any of this resonates with a challenge you're facing, book a free 30-minute call — no prep needed.