When Type Annotations Lie: Recursive Aliases in helm
Recursive type aliases like Mapping[str, JsonType] create infinite recursion in mypy — the fix replaces the self-reference with Any at the boundary.
The bottom line: Recursive type aliases like
Mapping[str, JsonType]create infinite recursion in mypy — the fix replaces the self-reference withAnyat the boundary.
The Problem
helm/helm issue #32047 exposes a subtle failure mode in Python’s type system. The codebase defines a recursive type alias — a type that references itself — and mypy enters infinite recursion trying to resolve it under --strict mode. The fix is only 30 lines, but the pattern behind it applies across any Python project using complex generic types.
PR: https://github.com/helm/helm/pull/32337
Status: Submitted (awaiting review)
Why Recursive Aliases Break Mypy
Type checkers like mypy evaluate type aliases by expanding them inline. When a type alias references itself — like JsonType = str | int | float | bool | None | Mapping[str, JsonType] | list[Any] — mypy substitutes the alias with its definition, which contains the alias again, which it substitutes again, infinitely. This is not a bug in mypy; it’s a fundamental constraint of how Python’s type system works at runtime. Unlike Haskell or Rust, Python does not support iso-recursive types natively — the type alias is a textual substitution, not a fixed-point operator.
# Before: recursive type alias that crashes mypy --strict
JsonType = str | int | float | bool | None | Mapping[str, 'JsonType'] | list[Any]
# After: Any breaks the recursion
JsonType = str | int | float | bool | None | Mapping[str, Any] | list[Any]
The mapping values in JsonType need to be typed, but mypy cannot expand the alias indefinitely. By replacing the self-reference with Any, we tell mypy “the values here can be anything, including nested structures” without forcing it to resolve the full recursive tree.
How This Surfaces in Practice
Recursive type aliases typically appear in:
- JSON/Dict parsing — where deeply nested structures follow a self-similar pattern
- Tree data structures — AST nodes, DOM trees, configuration hierarchies
- Serialization schemas — recursive schema definitions for nested payloads
- Plugin systems — where a plugin’s output type is the same as its input type
The telltale sign is a mypy crash with a RecursionError or an unresponsive type-checker process. Running mypy --strict on a codebase with recursive aliases will either hang indefinitely or fail with a stack overflow. The fix is always the same: introduce Any at the recursive boundary.
Testing for Recursive Alias Issues
Add a smoke test to your CI pipeline that runs mypy with a timeout:
timeout 30 mypy --strict src/ || exit 1
This catches recursive alias crashes before they reach a developer’s workstation. Without the timeout, mypy hangs silently until manually killed — which means the issue can persist for days in a busy codebase if no one runs --strict locally.
Key Takeaway
Type annotations are not just documentation. They are executable constraints that the type checker evaluates at analysis time. When mypy can’t resolve a recursive alias, replacing it with Any at the boundary preserves intent while satisfying the checker. This is a pragmatic tradeoff: you lose some specificity at the recursive boundary, but you gain a working type-check that prevents real bugs everywhere else.
Discovered while fixing helm/helm#32047. View the fix post for the specific diff.