Skip to main content
Python Decorators

Advanced Python

Ready to move beyond the basics? These features are what distinguish intermediate Python developers from experts. They allow you to write code that is efficient, clean, and “Pythonic”.

1. Decorators

Decorators are a powerful way to modify the behavior of functions or classes without changing their source code. They are essentially wrappers. Think of a decorator like a “middleware” for a function.
def timer(func):
    import time
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs) # Call the original function
        end = time.time()
        print(f"{func.__name__} took {end - start:.4f}s")
        return result
    return wrapper

# Apply the decorator using @
@timer
def slow_function():
    import time
    time.sleep(1)
    print("Done")

slow_function()
# Output:
# Done
# slow_function took 1.0012s

2. Generators

Lists store all their data in memory. If you have a list of 1 billion numbers, you’ll run out of RAM. Generators produce data one item at a time, on demand. They are lazy. They use the yield keyword instead of return.
def fibonacci(n):
    a, b = 0, 1
    for _ in range(n):
        yield a      # Pause here and return 'a'
        a, b = b, a + b

# Usage
for num in fibonacci(10):
    print(num)

# Generator Expression (Like list comp, but with parenthesis)
# This takes almost 0 memory, even for 1 million items!
squares = (x**2 for x in range(1000000))

3. Context Managers (with)

Managing resources (files, network connections, database locks) is hard. You have to remember to close them, even if errors occur. Context Managers handle this automatically.
# The 'with' statement ensures f.close() is called
# even if an error happens inside the block.
with open("file.txt", "w") as f:
    f.write("Hello")

# Creating a custom Context Manager
from contextlib import contextmanager

@contextmanager
def database_connection():
    print("Connecting...")
    yield "db_conn"
    print("Closing...") # This runs after the 'with' block ends

with database_connection() as db:
    print(f"Using {db}")

4. AsyncIO (Asynchronous Programming)

Python is single-threaded. However, using asyncio, you can write concurrent code that handles I/O (like network requests) efficiently. While one request is waiting for a response, Python can start another. This is essential for modern web frameworks like FastAPI.
import asyncio

# Define a coroutine with 'async def'
async def fetch_data(id):
    print(f"Fetching {id}...")
    await asyncio.sleep(1) # Simulate I/O wait (non-blocking)
    print(f"Done {id}")
    return {"id": id}

async def main():
    # Run tasks concurrently!
    # This will take ~1 second total, not 3.
    results = await asyncio.gather(
        fetch_data(1),
        fetch_data(2),
        fetch_data(3)
    )
    print(results)

if __name__ == "__main__":
    asyncio.run(main())

5. Type Hinting (Advanced)

For complex data structures, basic type hints (int, str) aren’t enough. The typing module provides tools to describe shapes of data.
from typing import List, Dict, Optional, Union, Callable

# A list of integers
def process_items(items: List[int]) -> Dict[str, int]:
    return {"sum": sum(items)}

# Optional: Can be string OR None
def find_user(id: int) -> Optional[str]:
    return "Alice" if id == 1 else None

# Union: Can be int OR float
Number = Union[int, float]

def add(a: Number, b: Number) -> Number:
    return a + b

Summary

  • Decorators: Wrap functions to add logic (logging, timing, auth).
  • Generators: Lazy iteration for memory efficiency.
  • Context Managers: with statement for safe resource management.
  • AsyncIO: Concurrency for I/O-bound tasks.
  • Type Hints: Essential for large codebases.
Congratulations! You’ve completed the Python Crash Course. You now possess the tools to write robust, efficient, and modern Python applications.