Back to Python tutorials
Advanced17 min read

Decorators & Generators

Write reusable decorators, lazy generators, and custom iterators for expressive Python.

Function Decorators

Decorators wrap functions to add logging, caching, authentication, or timing. @decorator syntax applies wrapper at definition time.

Use functools.wraps to preserve wrapped function metadata for introspection and debugging.

Parameterized decorators are factories returning the actual decorator—three levels of nested functions.

  • Keep decorator overhead minimal on hot paths
  • Document decorator side effects in docstrings
  • Test decorated functions behave like originals
def retry(times=3):
    def decorator(fn):
        @wraps(fn)
        def wrapper(*args, **kwargs):
            for attempt in range(times):
                try:
                    return fn(*args, **kwargs)
                except TransientError:
                    if attempt == times - 1:
                        raise
        return wrapper
    return decorator

Generators and yield

Generators produce values lazily with yield, pausing execution between items. They consume constant memory for large sequences versus building lists.

Generator expressions mirror list comprehensions: (x*x for x in range(10)). Use when you iterate once and do not need random access.

yield from delegates to sub-generators, flattening nested iteration cleanly.

  • Close resources in generators with try/finally or context managers
  • Materialize to list only when reuse is required
  • Use itertools for advanced generator composition
def read_lines(path):
    with open(path) as f:
        for line in f:
            yield line.rstrip("\n")

Custom Iterators

Classes implementing __iter__ returning self and __next__ raising StopIteration are iterators. Often generators are simpler than manual iterator classes.

Infinite generators model streams: paginated API results, sensor readings. Consumers should break explicitly to avoid infinite loops.

Async generators with async for integrate with asyncio for streaming HTTP or websocket data.

  • Prefer yield-based generators over manual __next__
  • Document whether iterators are single-pass or reusable
  • Use tee() sparingly—it buffers consumed values

Get In Touch


Ready to discuss your next project? Drop me a message.