import typing
a: dict[int, int] = {}
b: dict[int, int | str] = a
c: typing.Mapping[int, int | str] = a
d: typing.Mapping[int | str, int] = a
Pylance reports an error for b: dict[int, int | str] = a
:
Expression of type "dict[int, int]" is incompatible with declared type "dict[int, int | str]"
"dict[int, int]" is incompatible with "dict[int, int | str]"
Type parameter "_VT@dict" is invariant, but "int" is not the same as "int | str"
Consider switching from "dict" to "Mapping" which is covariant in the value type
But c: typing.Mapping[int, int | str] = a
is OK.
Additionally, d: typing.Mapping[int | str, int] = a
also gets an error:
Expression of type "dict[int, int]" is incompatible with declared type "Mapping[int | str, int]"
"dict[int, int]" is incompatible with "Mapping[int | str, int]"
Type parameter "_KT@Mapping" is invariant, but "int" is not the same as "int | str"
Why are these types hint incompatible?
If a function declares a parameter of type dict[int, int | str]
, how can I pass a dict[int, int]
object as its parameter?
dict
type was designed to be completely invariant on key and value. Hence when you assign dict[int, int]
to dict[int, int | str]
, you make the type system raise errors. [1]
Mapping
type on the other hand wasn’t designed to be completely invariant but rather is invariant on key and covariant on value. Hence you can assign one Mapping
type (dict[int, int]
) to another (Mapping[int, int | str]
) if they are both covariant on value. if they are invariant on key, you can assign them else you cannot. Hence when you assign dict[int, int]
to Mapping[int | str, int]
, you make the type system raise errors. [2][3]
There is a good reason for the above design in the type system and I will give a few:
1. dict
type is a concrete type so it will actually get used in a program.
2. Because of the above mentioned, it was designed the way it was to avoid things like this:
a: dict[int, int] = {}
b: dict[int, int | str] = a
b[0] = 0xDEADBEEF
b[1] = "Bull"
dict
s are assigned by reference [4] hence any mutation to b
is actually a mutation to a
. So if one reads a
as follows:
x: int = a[0]
assert isinstance(x, int)
y: int = a[1]
assert isinstance(y, int)
One gets unexpected results. x
passes but y
doesn’t. It then seems like the type system is contradicting itself. This can cause worse problems in a program.
For posterity, to correctly type a dictionary in Python, use Mapping
type to denote a readonly dictionary and use MutableMapping
type to denote a read-write dictionary.
[1] Of course Python’s type system doesn’t influence program’s running behaviour but at least linters have some use of this.
[2] dict
type is a Mapping
type but Mapping
type is not a dict
type.
[3] Keep in mind that the ordering of types is important in type theory.
[4] All variable names in Python are references to values.