Skip to content

False positive when mixing Mapping and Union #6001

Closed

Description

@wkschwartz

A function that expects a heterogeneous dictionary and gets a dictionary with a subset of the expected types seems to cause problems.

from typing import Any, Mapping, Union
def f(arg: Mapping[      str      , Any]) -> None: pass
def g(arg: Mapping[Union[str, int], Any]) -> None: pass
f({'b': 'c'})
g({'b': 'c'})
d = {'b': 'c'}
f(d)
g(d)  # <--- Line 8

Mypy's output is

mypy-test.py:8: error: Argument 1 to "g" has incompatible type "Dict[str, str]"; expected "Mapping[Union[str, int], Any]"

I'm using Python 3.7.1 and Mypy 0.641. Possibly related to #5849?

Activity

  1. JelleZijlstra commented on Dec 4, 2018

    @JelleZijlstra
    Member

    This is because Mapping is invariant in its key type. See https://mypy.readthedocs.io/en/latest/common_issues.html#invariance-vs-covariance.

  2. wkschwartz commented on Dec 4, 2018

    @wkschwartz
    ContributorAuthor

    I'm having a hard time understanding how this isn't a bug:

    1. str should satisfy Union[str, int]: str is not a subclass of Union[str, int] so variance shouldn't matter.
    2. g({'b': 'c'}) should be equivalent to d = {'b': 'c'}; g(d).
    3. The docs you linked says using immutable types mitigates the unexpected behavior—Mapping is an immutable type.
  3. ilevkivskyi commented on Dec 4, 2018

    @ilevkivskyi
    Member

    Mutability is not a root cause, the root cause is invariance. Mutable collections are typically invariant, but an immutable collection can be invariant as well.

  4. JelleZijlstra commented on Dec 4, 2018

    @JelleZijlstra
    Member

    python/typing#273 discusses why Mapping's key type is invariant.

  5. wkschwartz commented on Dec 4, 2018

    @wkschwartz
    ContributorAuthor

    What about items 1 and 2? How can invariance prevent str from satisfying Union[str, int]? Why would it affect variables but not literals?

  6. JelleZijlstra commented on Dec 4, 2018

    @JelleZijlstra
    Member
    1. is due to type context, where mypy infers a different type if you're accessing something in a context where a particular type is expected.

    2. My understanding is that str is a subtype (though not subclass) of Union[str, int].

  7. bluenote10 commented on Jun 20, 2022

    @bluenote10
    Contributor

    FYI related discussion python/typing#445 and feature request python/typing_extensions#5

  8. randolf-scholz commented on Nov 30, 2022

    @randolf-scholz
    Contributor

    This is not type safe for the following reason:

    • g may be using the arg.__getitem__ function.
    • Imagine, we had a UserMapping class that is a Mapping[UserString, Any] type that used UserString keys, a subclass of str with an additional method transliterate. Furthermore, assume UserMapping made use of that UserString.transliterate method in the UserMapping.__getitem__ call. (Imagine for example that the Mapping transliterates Greek to Latin characters before attempting a lookup.)
    • Then, if we pass an instance of this UserMapping to g, g might attempt to perform a lookup with a regular string that lacks the .transliterate method. Thus it is not type safe.

    In essence, because Mapping has both methods where the keys appear as input and output, Mapping needs to be invariant in the keys.


    A way around this is:

    from typing import Any, Mapping, Union, TypeVar
    
    StrVar = TypeVar("StrVar", bound=str)
    StrOrIntVar = TypeVar("StrOrIntVar", bound=Union[str, int])
    
    def f(arg: Mapping[      StrVar      , Any]) -> None: pass
    def g(arg: Mapping[StrOrIntVar, Any]) -> None: pass
    f({'b': 'c'})
    g({'b': 'c'})
    d = {'b': 'c'}
    f(d)
    g(d)

    Using a TypeVar kind of acts as an @overload for every possible str class.


    Of course, in the particular case of str as a subtype of Union[str, int] it would be nice if the type-checker were a bit smarter. Because here really str is kind of stronger than just a subtype.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions