SD
sophia-dcruz_
Back to Articles
PythonJuly 10, 2026

Mastering Python Generators and Decorators

A deep dive into Python's powerful generator expressions and decorator patterns — with an interactive live decorator simulator you can run in your browser.

#python#generators#decorators#advanced
Mastering Python Generators and Decorators

Python is renowned for being readable and expressive — but its real power lies in advanced features like generators and decorators that let you write elegant, memory-efficient, and reusable code. Whether you’re a second-year student encountering these for the first time, or a final-year developer sharpening your craft, this guide will make these concepts crystal clear.


What Are Generators?

A generator is a function that yields values one at a time using the yield keyword, rather than returning all values at once. This makes them highly memory-efficient when dealing with large sequences.

Basic Generator Example

def count_up(limit):
    n = 0
    while n < limit:
        yield n
        n += 1

# Usage
for number in count_up(5):
    print(number)  # 0, 1, 2, 3, 4

Instead of storing all numbers in memory (like a list), the generator produces each number on demand.

Generator Expressions

Much like list comprehensions, you can write compact generator expressions:

# List comprehension – stores ALL squares in memory
squares_list = [x**2 for x in range(1000000)]

# Generator expression – produces squares ONE AT A TIME
squares_gen = (x**2 for x in range(1000000))

print(next(squares_gen))  # 0
print(next(squares_gen))  # 1

💡 Teaching Note: Show students how sys.getsizeof() dramatically differs between the list and generator versions — this makes the memory benefit tangible.


The send() Method in Generators

Generators can be two-way communication channels using .send():

def accumulator():
    total = 0
    while True:
        value = yield total
        if value is None:
            break
        total += value

acc = accumulator()
next(acc)        # Prime the generator
print(acc.send(10))   # 10
print(acc.send(25))   # 35
print(acc.send(5))    # 40

Understanding Decorators

A decorator is a function that wraps another function, adding behaviour before or after it runs — without modifying its source code.

The Core Mechanics

def my_decorator(func):
    def wrapper(*args, **kwargs):
        print("⚡ Before the function call")
        result = func(*args, **kwargs)
        print("✅ After the function call")
        return result
    return wrapper

@my_decorator
def greet(name):
    print(f"Hello, {name}!")

greet("Sophia")
# Output:
# ⚡ Before the function call
# Hello, Sophia!
# ✅ After the function call

The @my_decorator syntax is syntactic sugar for greet = my_decorator(greet).

Practical Decorators

1. Timing Decorator

import time
import functools

def timer(func):
    @functools.wraps(func)  # Preserves original function metadata
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        elapsed = time.perf_counter() - start
        print(f"'{func.__name__}' executed in {elapsed:.4f}s")
        return result
    return wrapper

@timer
def compute_sum(n):
    return sum(range(n))

compute_sum(1_000_000)
# 'compute_sum' executed in 0.0423s

2. Retry Decorator

def retry(max_attempts=3):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(1, max_attempts + 1):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    print(f"Attempt {attempt} failed: {e}")
            raise RuntimeError(f"Failed after {max_attempts} attempts")
        return wrapper
    return decorator

@retry(max_attempts=4)
def unstable_network_call():
    import random
    if random.random() < 0.7:
        raise ConnectionError("Network error!")
    return "Data received!"

3. Caching / Memoization

from functools import lru_cache

@lru_cache(maxsize=None)
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

print(fibonacci(50))  # Computed instantly via caching

Chaining Multiple Decorators

Decorators are applied bottom-up:

@timer
@retry(3)
def fetch_data(url):
    # ...simulated fetch
    pass

# Equivalent to: timer(retry(3)(fetch_data))(url)

Generator + Decorator Synergy

Generators and decorators work beautifully together. Here’s a decorator that logs generator values:

def log_generator(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        gen = func(*args, **kwargs)
        for value in gen:
            print(f"  → Generated: {value}")
            yield value
    return wrapper

@log_generator
def countdown(n):
    while n > 0:
        yield n
        n -= 1

list(countdown(3))
# → Generated: 3
# → Generated: 2
# → Generated: 1

🤖 Interactive AI Widget: Decorator Simulator

Use the live simulator below to build and test your own Python decorator logic. Type a function body, apply a decorator, and see the wrapped output — right here in your browser.

⚡ Live Decorator Simulator


Key Takeaways

Concept Key Benefit Use Case
Generator Memory efficiency Large data pipelines, infinite streams
yield Lazy evaluation Pagination, file reading
Decorator Code reusability Logging, auth, retry, caching
@lru_cache Auto memoization Recursive functions, expensive computations
functools.wraps Metadata preservation All production decorators

Practice Exercises

  1. Write a generator that produces the Fibonacci sequence infinitely.
  2. Build a @validate decorator that checks if all arguments to a function are positive integers.
  3. Combine a @timer decorator with a Fibonacci generator and measure performance with and without lru_cache.

📚 Recommended Reading: Fluent Python by Luciano Ramalho — Chapter 7 (Functions as First-Class Objects) and Chapter 14 (Iterables, Iterators, and Generators).

// Related Articles