GadaaLabs
Python Mastery — From Zero to AI Engineering
Lesson 8

Advanced Python — Decorators, Generators & Type Hints

28 min

Closures: The Foundation of Decorators

Before decorators make sense, closures must make sense.

A closure is a function that captures variables from its enclosing scope — even after the enclosing function has returned. Python implements this with __closure__, a tuple of cell objects that hold the captured values.

Python
Click Run to execute — Python runs in your browser via WebAssembly

Decorators: Syntactic Sugar for Higher-Order Functions

A decorator is a callable that takes a function and returns a new function. The @decorator syntax is literally just:

python
@my_decorator
def my_function():
    pass

# Exactly equivalent to:
def my_function():
    pass
my_function = my_decorator(my_function)

The key tool is functools.wraps — without it, the wrapper replaces the original function's __name__, __doc__, and other metadata, which breaks introspection and documentation.

Python
Click Run to execute — Python runs in your browser via WebAssembly

Decorator Factories (Decorators with Arguments)

When you need to configure a decorator, you add another layer: a factory function that takes the configuration and returns the actual decorator.

Python
Click Run to execute — Python runs in your browser via WebAssembly

Class-Based Decorators and Built-in Decorators

Any callable can be a decorator, including classes. A class-based decorator is useful when you need to maintain state across calls.

Python
Click Run to execute — Python runs in your browser via WebAssembly

Generators: Lazy Evaluation at Its Finest

A generator is a function that yields values one at a time, pausing its execution between yields. It doesn't compute everything upfront — it computes the next value when asked. This makes generators:

  • Memory-efficient: a generator over 1 million items uses O(1) memory
  • Composable: generators can be chained into pipelines
  • Interruptible: execution can be paused, resumed, and even injected into with send()
Python
Click Run to execute — Python runs in your browser via WebAssembly

itertools: The Generator Toolkit

Python
Click Run to execute — Python runs in your browser via WebAssembly

Type Hints: Making Python's Dynamic System Explicit

Type hints don't change runtime behavior — Python remains dynamically typed. But they power static analysis tools (mypy, pyright), improve IDE autocomplete, and serve as executable documentation.

Python
Click Run to execute — Python runs in your browser via WebAssembly

Python 3.10+ Pattern Matching

The match statement is structural pattern matching — it's not a C-style switch. It can destructure objects, sequences, and mappings:

Python
Click Run to execute — Python runs in your browser via WebAssembly

PROJECT: Retry Decorator System

Python
Click Run to execute — Python runs in your browser via WebAssembly

PROJECT: Memory-Efficient Data Pipeline

Python
Click Run to execute — Python runs in your browser via WebAssembly

Key Takeaways

  • Closures capture by reference, not by value: the loop variable pitfall (lambda: i in a loop) bites everyone once; fix with a default argument lambda i=i: i
  • functools.wraps is not optional: without it, decorators silently destroy __name__, __doc__, and __wrapped__, breaking reflection, logging, and documentation tools
  • Decorator factories add one more level of indirection: @retry(max_attempts=3) is a three-level structure — factory → decorator → wrapper — which is why the return structure looks nested
  • Generators are pull-based: computation only happens when the caller requests the next value via next() or iteration; a generator pipeline over 1M rows uses O(1) memory
  • send() makes generators two-way channels: you can push values into a paused generator, making them useful for coroutines and state machines
  • itertools is the standard library for lazy sequences: chain, islice, groupby, accumulate, product — learn them and you'll stop writing manual loops
  • Type hints are documentation that runs: they don't enforce types at runtime, but mypy/pyright catches mismatches statically; Protocol enables duck-typed interfaces without inheritance
  • Pattern matching (match) is structural: it's not a switch statement — it can destructure dicts, sequences, and instances, with guard clauses (if condition) for complex filtering