When Type Annotations Lie: Recursive Aliases in scikit-learn
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’] cause mypy to enter infinite recursion during type evaluation. The fix replaces the self-reference with
Anyat the recursive boundary, preserving intent while keeping the checker happy.
The Problem
scikit-learn/scikit-learn issue #34258 exposes a subtle edge case in how Python type checkers handle self-referential type aliases. The fix is only 6 lines, but the pattern behind it applies across any Python project using recursive type definitions.
PR: https://github.com/scikit-learn/scikit-learn/pull/34478
Status: Submitted (awaiting review)
Type checkers like mypy evaluate type aliases by expanding them inline. When a type alias references itself — like JsonType = Mapping[str, 'JsonType'] — mypy enters an infinite recursion trying to resolve the full type tree. This manifests as a stack overflow or a hung type-checking process on mypy --strict.
# 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 key insight is that Mapping[str, Any] is semantically identical to the unbounded recursive case in practice — you cannot statically enumerate every possible nesting depth of a recursive dictionary. The Any at the boundary signals “we know this could be anything at runtime, stop trying to prove it.”
Why This Pattern Matters
Recursive type aliases appear more often than most teams realize:
- JSON-like data structures — Any API that accepts arbitrary nested payloads needs a recursive JSON type. Without the
Anyboundary, mypy stalls. - Abstract syntax trees — AST node types often reference child nodes recursively. Libraries like
ast,lark, and custom DSL parsers hit this pattern. - Configuration schemas — Nested
dictchains in dynamic configuration systems benefit from a bounded recursive type.
The Any replacement is not a loss of type safety. A recursive alias that mypy cannot evaluate provides zero type safety anyway — the checker simply cannot complete its analysis. Replacing the recursive arm with Any at least allows type checking on the non-recursive parts of the alias to proceed.
# Alternative: forward reference with a type variable
from typing import TypeVar
T = TypeVar('T')
JsonType = str | int | float | bool | None | Mapping[str, 'T'] | list[T]
This variant lets callers specify the value type while still capping recursion. For scikit-learn’s use case, Any was the pragmatic choice — the type is used in configuration parsing where exact value types vary at runtime.
Key Takeaway
Type annotations are not just documentation. When mypy can’t resolve a recursive alias, replacing it with Any at the recursive boundary preserves intent while satisfying the checker. The tradeoff is minimal: a type checker that cannot complete offers zero guarantees, while a bounded type with Any at the recursion point still catches real bugs in the non-recursive parts of your API surface.
Discovered while fixing scikit-learn/scikit-learn#34258. View the fix post for the specific diff.
📖 Related Reads
- ToolBrain — tool reviews, LLM comparisons, and AI workflow guides
Cross-links automatically generated from CodeIntel Log.