2025, Nov 09 03:00

Covariant TypeVars in Parameter Positions: the Union Loophole in Python Type Checkers

Learn why mypy, Pyright and Pyre accept unions with a covariant TypeVar in parameter positions, why it's unsafe, and how to design variance-safe Python APIs.

Covariance and contravariance in Python’s type system look simple on paper, but there’s a subtle edge-case that slips through mainstream static analyzers. Method parameters should be contravariant, which means a covariant type variable isn’t supposed to appear in an input position. Yet mypy, pyright, and pyre-check all accept a method parameter that is a union including a covariant type variable. Let’s unpack what’s going on and how to handle it in real code.

The setup

The situation is easy to reproduce. A covariant TypeVar is rejected when used directly as a parameter type, but no error is reported when the same TypeVar is nested inside a union.

from typing import TypeVar, Generic, Any

U_cov = TypeVar("U_cov", covariant=True)

class Bar(Generic[U_cov]):

    def alpha(self, param: U_cov) -> Any:  # Expected to be rejected
        ...

    def beta(self, param: int | U_cov) -> Any:  # Accepted by popular type checkers
        ...

Why this looks wrong

By design, parameter types are contravariant. If a type variable is declared covariant, it shouldn’t appear in input positions because that undermines the usual subtyping guarantees. With a direct annotation like in the first method, type checkers flag the issue. The surprise is that wrapping the same covariant variable in a union results in silence.

What the type checkers actually do

This behavior stems from how current tools implement their checks rather than from a deeper type-theoretic exemption. In one implementation, the logic rejects covariant type variables on parameters but explicitly notes a pending task to examine inner type variables, which covers unions and other nested forms. Another implementation originally blocked invalid nested occurrences but then relaxed the rule to only flag direct covariant type variables. As stated in the change description, unions that contain covariant type variables were allowed without further rationale.

Made the check less strict for the use of covariant type vars within a function input parameter annotation. In particular, unions that contain covariant type vars are now permitted.

The practical consequence is consistent across tools: the direct use is rejected, the union form is accepted.

The resolution today

This is a missing feature. The tools currently don’t diagnose nested appearances of a covariant type variable in a parameter type, and in one case the rule was intentionally loosened to allow unions. There isn’t an endorsed exception for this pattern; it’s simply not enforced in these cases.

What to do in your code

Don’t rely on this acceptance. If your mental model says the annotation should be disallowed, treat the union case the same way in your codebase. Avoid placing a covariant type variable in parameter positions, even when nested in a union. That keeps your intent aligned with variance rules regardless of what a checker misses today.

Why this matters

Type systems are communication tools for humans and machines. When there’s a gap between the intended rule and what tools currently verify, it’s easy for teams to build accidental assumptions into APIs. You might think a construct is sound because it passes CI, while the underlying rule would flag it if the checker dug one level deeper. Being explicit about variance-sensitive positions helps prevent these mismatches.

Takeaways

Covariant type variables and parameter positions don’t mix, whether direct or wrapped in a union. Popular Python type checkers currently allow the union pattern due to incomplete or intentionally relaxed checks, not because it’s a special, safe case. Code as if the stricter rule applied, and track updates in your preferred checker if and when recursive validation of nested types lands.

The article is based on a question from StackOverflow by Daraan and an answer by InSync.