Repository navigation
Reconsider making Mapping covariant in the key type #445
Description
Confusion with invariant container types is one of the more common questions mypy users have. Often these can be easily worked around by using a covariant type such as Sequence instead of List. However, there is no fully covariant dictionary-like type -- Mapping is invariant in the key type, since the key type is used as an argument type of __getitem__ and get. This is particularly problematic in Python 2, as mappings with unicode keys are common, and a mapping with str keys is not compatible with them, even though they are fine at runtime (at least if the keys are ascii only).
I can see a few things we could do.
1) Don't care about unsafety
We'd make Mapping covariant even though it's known to be unsafe (#273). The argument is that the potential unsafety is less of a problem than the confusing user experience. This wouldn't help with certain related things like using a unicode key to access Mapping[str, X], which is arguably okay.
Note that this was proposed and rejected earlier.
2) Use object in Mapping argument types
We'd make Mapping covariant and use object in the argument type of __getitem__ and get. This would result in less effective type checking, but I'd consider this to still be correct at least for get, since it can always fall back to the default. Also, __getitem__ would typically only generate a KeyError if given a key of an unrelated type, and this is arguably not a type error. Indexing operations on mappings can already fail with KeyError, and this isn't considered a type safety issue.
We could also recommend that type checkers special case type checking of Mapping.__getitem__ and Mapping.get (and also the corresponding methods in subclasses such as Dict). A reasonable rule would be to require that the actual key type in code like d[k] overlaps with the declared key type. This would allow things like using unicode keys for Mapping[str, int], which should perhaps be fine in Python 2. We could still reject things like d[1] if the declared key type is str, since int and str are not overlapping.
Subclasses of Mapping that only support specific runtime key types would now be rejected. For example, this wouldn't work any more:
class IntMap(Mapping[int, int]):
def __getitem__(self, k: int) -> int: ... # needs an object argument
...However, it would be easy enough to refactor this to type check:
class IntMap(Mapping[int, int]):
def __getitem__(self, k: object) -> int: ...
assert isinstance(k, int) # sorry, can't deal with non-int keys
...
...I think that this is reasonable -- the required assert (or cast) makes it clear that this is potentially unsafe.
3) Just teach users to work around using unions or type variables
Maybe keeping Mapping is invariant in the key type is reasonable. Often the lack of covariance can be worked around by using other type system features. Here are some examples:
- Use
Union[Mapping[str, int], Mapping[unicode, int]]instead ofMapping[unicode, int]. - Use
Mapping[AnyStr, int]in Python 2 andMapping[str, int]in Python 3 using a conditional type alias. - Define a type variable like
AnyText = TypeVar('AnyText', str, Text)and useMapping[AnyText, int]. This is a little awkward, since in Python 3 the values of the type variable would bestrandstr.
I don't like these for a few reasons:
- These are not intuitive.
- These are inconsistent with other containers such as
Sequence. - These are more verbose than just
Mapping[unicode, int].
Activity
ilevkivskyi commentedon Jun 28, 2017
I think this is a crucial point. I don't see a good solution without adding some internal special-casing in type checkers.
JukkaL commentedon Jun 28, 2017
Pinging @rowillia, since he had issues with invariance recently.
ilevkivskyi commentedon Jul 20, 2017
@JukkaL
There appeared an idea, see http://bugs.python.org/issue25988 to have a base class of
Mappingthat would be contravariant in the key type. It seems to me that sometimes contravariance rather than covariance is something that is wanted. For example (see b.p.o. issue):JukkaL commentedon Jul 21, 2017
It's unclear to me how useful
BaseMapwould be. For any new feature, I'd like to see how it would help with real-world code. For example, maybe actual use cases often iterate over the keys, which wouldn't be supported byBaseMap. If that's the case, usingMapping[AnyStr, int]orUnion[Mapping[str, int], Mapping[unicode, int]]would be more reasonable, and both of these already work.ilevkivskyi commentedon Jul 21, 2017
Actually the original proposal in http://bugs.python.org/issue25988 was to add an ABC with two methods
__getitem__and__iter__, but it would be invariant, so this will not help with this problem. TBH my main motivation here is to have a protocol intypingthat has__getitem__and is not "bulky". Now we haveMappingandSequencebut those have 7-8 methods each.JukkaL commentedon Jul 21, 2017
Well, it's probably only worth adding such a protocol if it's useful often enough -- defining one manually is easy in case it would only be used occasionally. I don't know whether this is the case, though.
ilevkivskyi commentedon Jul 21, 2017
OK, right, let us postpone this then. It will be easy to add one later, if we will see that it is actually needed.
Do we need to say something about this in the rejected/postponed section of PEP 544?
JukkaL commentedon Jul 21, 2017
This is small enough that it probably won't need to be mentioned.
LunarLanding commentedon Sep 4, 2020
Hi, this might add to the cases where the key not being covariant fails, not sure:
Python 3.8 using pylance for typechecking.
srittau commentedon Nov 4, 2021
I believe it's now too late to fix this.
Mappingis also problematic as it is not a protocol, but a concrete class, as opposed to most other similar classes.harahu commentedon May 19, 2022
I encountered this issue when adding type annotations to Streamlit. I came across a function that looked like
Annotating this function, I just followed my instincts based on naming, and annotated it as
Mypy quickly pointed out that
Dictis not covariant with regards to key, upon which I thought:My next attempt looked like this:
Imagine my surprise when this did not work out for the same reason as with
Dict. This led me to a search, both for a covariant mapping type and for answers as to whyMappingisn't covariant. I came across this issue, which answered the "why" part (__getitem__andget), which, once I understand it, seems perfectly reasonable. The remaining question was just: what does this mean for me in practice?I never found my covariant mapping type. Instead, I landed on this solution:
Here, I add a free type variable for the key, effectively making the mapping covariant. For the purposes of type checking, this covers what I need, but I don't think it is elegant. It forces me to introduce yet another type variable (which, confusingly, is only used once*).
The issue here, really, is that
Mappingis a too specific type in light of what I actually do with it in the function body. My only interaction consists ofdict.items(), which has covariant behavior. I could rewrite the function such that it takes anItemsViewinstead, but that makes for an ugly API, IMO. Or, preferably, I really just need a handy littleSupportsItemsprotocol. Which, it seems, someone at typeshed has seen the need for as well.Given that
Mappingwas the type I, and many others, reached for in order to achieve covariance, I'd like the following:Easily accessible, granular protocols, for the covariant methods on
Mapping. In the simplest case, that could be all of the covariant methods grouped into a single Protocol. Perhaps something like:My gut tells me this belongs in
typing.Improved documentation for the mapping type, at
typing.Mapping,collections.abc.Mapping, or, preferably both. I would want a section that can be paraphrased as:@srittau I think this is a solution that would fix most of the issue (for developers mature enough to realize why
Mappingcannot be covariant), while still being something that can be done today, without changing behavior.* The "normal" pattern is to use it at least twice, in order to indicate dependency between types.
TL;DR: Given my experience, I desire better documentation for the
Mappingtype, as well as covariant protocols for the APIs ofMappingthat are covariant.srittau commentedon May 19, 2022
Better documentation for typing would be desirable overall and there is some first ideas in the typing repository: https://github.com/python/typing/issues?q=is%3Aopen+is%3Aissue+label%3A%22topic%3A+documentation%22
We have some very tight protocols in
_typeshedthat can be used a bit awkwardly:We are currently working to bring some of these types into
typing_extensionsso they are more easily accessible at runtime (python/typing_extensions#6). But I agree that a "mapping-like" protocol like the one you suggested, that's a bit more comprehensive would also be a good idea. Would you like to open an issue in /python/typing for typing-extensions? We could discuss it in detail over there.BlueGlassBlock commentedon Jul 19, 2022
Not sure whether it belongs here, but it's quite reasonable to implement "type based callback map" in this way:
Type checker assumed that the key type of
cbpassed intoCaseistype[TypeA | TypeB], which is acceptable.However it tried to apply
TypeA | TypeBto the callback values, resulting in errors.covariant populations and generators
Change FieldExpr to TypeVar to work around Mappings being invariant i…
Support covariance of collection types, and use StarlarkType.assignab…
Support covariance of collection types, and use StarlarkType.assignab…