2025, Oct 08 05:00
Why dict[str, str] isn't assignable to Mapping[str | int, str] in Python typing and how to fix it
Learn why mypy and pyright reject passing dict[str, str] to Mapping[str | int, str], how Mapping keys are invariant, and fixes using wider types or unions.
Passing a plain dict into a function that expects Mapping with a wider key type looks harmless, yet type checkers disagree. Here’s why mypy 1.16.0 and pyright 1.1.400 flag this pattern and what you can do about it without fighting the tools.
Reproducing the issue
The function expects a Mapping whose keys may be str or int, but the call site provides a dict with str keys only. Type checkers reject the assignment.
from collections.abc import Mapping
def consume_map(src: Mapping[str | int, str]):
    print(src)
payload = {"a": "b"}
consume_map(payload)
Both mypy and pyright report that dict[str, str] is not assignable to Mapping[str | int, str]. pyright, for example, explains that the Mapping key type parameter is invariant and str is not the same as str | int.
What’s going on
Even though Mapping is immutable, its key type parameter is treated as invariant in Python’s typing. Making Mapping keys covariant has been proposed and rejected more than once. As a result, type checkers consistently enforce that Mapping[K, V] does not accept Mapping[K_sub, V] when K_sub is narrower than K. In our case, dict[str, str] is narrower than Mapping[str | int, str], so the assignment is rejected.
Practical fix
The straightforward way to appease the type checker is to declare the dictionary with the wider key type at the point of definition. That keeps the intent explicit and matches the function signature.
from collections.abc import Mapping
def consume_map(src: Mapping[str | int, str]):
    print(src)
store: dict[str | int, str] = {"a": "b"}
consume_map(store)
If your goal is to accept either all-string-key mappings or all-int-key mappings, another option is to make that intent explicit in the parameter type by using a union of Mapping variants: Mapping[str, str] | Mapping[int, str]. This can serve as a workaround when you don’t actually need a single mapping that mixes both key kinds.
Why this matters
Relying on the intuition that immutability implies safe covariance can lead to confusing errors at review time or CI. The current typing rules are clear: Mapping’s key parameter is invariant. Both mypy and pyright enforce that consistently. Knowing this upfront helps you pick the right annotation strategy and keeps your codebase free of suppressions and ad hoc casts.
Takeaways
Be explicit about key types when designing function boundaries that accept mappings. If a consumer truly needs Mapping[str | int, str], declare the producer with that wider key type. If you need to handle either string-key maps or int-key maps, spell it out as a union of Mapping[str, str] and Mapping[int, str]. Most importantly, don’t assume that immutability changes the variance story here—the proposals to make Mapping keys covariant were rejected, so the invariant rule is what the tools will apply.
The article is based on a question from StackOverflow by Kerrick Staley and an answer by Anerdw.