Fun with Python Mapping
In which we learn that the novice and expert can both choose the simpler answer for different reasons.
mypy
is a static type-checking tool. It's like a very pedantic linter and I have a personal goal to try and satisfy it's --strict
mode demands.
It took me a while to get the hang of TypeScript types, so I thought I kinda got all the practical type theory I'd need, but alas I didn't. Turns out python's types are just different.
Unsupported target for indexed assignment #
Usually when mypy
complains about something, I basically know what the problem is and how to fix it. Here's a minimal example of my first real head-scratcher:
from typing import Any, Mapping
box: Mapping[str, Any]
box["this"] = "fail"
# => error: Unsupported target for indexed assignment
# ("Mapping[str, Any]") [index]
Why doesn't this work? Well, it turns out that Mapping
is intended to represent a read-only structure. That's why there's a MutableMapping
(I always wondered). Ok, so I switched to MutableMapping
. Here's another minimal example of the next problem:
from typing import Any, MutableMapping
def test(cls: MutableMapping[Any, Any]) -> None:
cls()
# => error: "MutableMapping[Any, Any]" not
# callable [operator]
Why not!? Because I didn't specify that I want the class of this type, not a concrete instance of this type.
from typing import Any, MutableMapping, Type
def test(cls: Type[MutableMapping[Any, Any]]) -> None:
cls()
This works! Ok, on to the next problem:
from typing import Any, Mapping, SupportsIndex, Union
general: Mapping[Union[str, SupportsIndex, slice], Any]
specific: Mapping[str, Any] = {}
general = specific
# => error: Incompatible types in assignment
# (expression has type "Mapping[str, Any]",
# variable has type "Mapping[Union[str,
# SupportsIndex, slice], Any]") [assignment]
As an aside, why am I using SupportsIndex
instead of int
? Because I'm working on a version of a Mapping
that supports the same signature as list.__getitem__
and that's what it expects for it's index.
So why is str
not compatible with Union[str, ...]
? Maybe because I don't have a Union
for the specific
type?
general: Mapping[Union[str, SupportsIndex, slice], Any]
specific: Mapping[Union[str, int], Any] = {}
general = specific
# => error: Incompatible types in assignment
# (expression has type "Mapping[Union[str, int], Any]",
# variable has type "Mapping[Union[str,
# SupportsIndex, slice], Any]") [assignment]
Nope. Turns out Mapping
is "invariant in its key type". Reading the relevant mypy docs and the long python github thread there is some real complexity in trying to use python's type system to describe how Mapping
should work.
I don't totally understand the discussion, but I'm guessing this means something like "the type of the key cannot change" and so this isn't going to be remedied soon. Therefore, I'm going to have to go back to my trusted solution:
general: Mapping[Any, Any]
specific: Mapping[Union[str, int], Any] = {}
general = specific
This works.