Skip to content

issubclass doesn't narrow down types #10978

Open

Description

@Illviljan

Bug Report

issubclass appears to not narrow down possible types.

To Reproduce

from typing import Any, Mapping, MutableMapping


def test_mutablemapping(obj: Mapping[Any, Any]) -> MutableMapping[Any, Any]:
    if issubclass(type(obj), MutableMapping):
        return obj  # error: Incompatible types in assignment (expression has type "Mapping[Any, Any]", variable has type "MutableMapping[Any, Any]")
    else:
        return dict(obj)

Expected Behavior

To narrow down in similar fashion to isinstance:

from typing import Any, Mapping, MutableMapping


def test_mutablemapping2(obj: Mapping[Any, Any]) -> MutableMapping[Any, Any]:
    if isinstance(obj, MutableMapping):
        return obj
    else:
        return dict(obj)

Your Environment

  • Mypy version used: 0.910
  • Mypy command-line flags: default
  • Mypy configuration options from mypy.ini (and other config files): default
  • Python version used: 3.8.8
  • Operating system and version: Windows 10

Activity

  1. erictraut commented on Aug 14, 2021

    @erictraut

    Mypy is able to narrow the type of a variable using issubclass, but in your example, the expression passed as the first argument to issubclass is not a variable but a call expression (type(obj)). I wouldn't expect that to work.

    If you assign the result of this expression to a variable, mypy does narrow its type as expected.

        obj_t = type(obj)
        if issubclass(obj_t, MutableMapping):
            reveal_type(obj_t) # Type[MultableMapping[Any, Any]]
  2. Illviljan commented on Aug 14, 2021

    @Illviljan
    Author

    This example fails as well:

    from typing import Any, Mapping, MutableMapping
    
    
    def test_mutablemapping(obj: Mapping[Any, Any]) -> MutableMapping[Any, Any]:
        obj_type = type(obj)
        if issubclass(obj_type, MutableMapping):
            return obj  # error: Incompatible return value type (got "Mapping[Any, Any]", expected "MutableMapping[Any, Any]")
        else:
            return dict(obj)

    And that's because obj hasn't been narrowed down, but a completely different variable (even though obj_type is a function of obj but mypy doesn't remember that) has been narrowed down?

    So the only way of getting the result I want is using the isinstance checks?

  3. erictraut commented on Aug 14, 2021

    @erictraut

    You can get the result you want using an isinstance check, but perhaps there was some other reason you want to use issubclass here?

    Another option is to use a user-defined type guard. Unfortunately, this appears to hit a bug in mypy's TypeGuard support. This works fine in pyright.

    from typing import Any, Mapping, MutableMapping, Type, TypeVar
    from typing_extensions import TypeGuard
    
    _T = TypeVar("_T")
    
    def is_type_subclass_of(val: object, t: Type[_T]) -> TypeGuard[_T]:
        return issubclass(type(val), t)
    
    def test_mutablemapping(obj: Mapping[Any, Any]) -> MutableMapping[Any, Any]:
        if is_type_subclass_of(obj, MutableMapping):
            return obj
        else:
            return dict(obj)
  4. Illviljan commented on Aug 14, 2021

    @Illviljan
    Author

    TypeGuard seems interesting! I have not looked into that one much. I'm mostly trying to improve performance and isinstance checks are rather slow.

    class Test2(MutableMapping):
        def __init__(self, *args, **kwargs):
            self.data = dict(*args, **kwargs)
    
        def __getitem__(self, key):
            pass
    
        def __setitem__(self, key, value):
            pass
    
        def __delitem__(self, key):
            pass
    
        def __iter__(self):
            pass
    
        def __len__(self):
            pass
    
    b = Test2()
    
    %timeit issubclass(type(b), MutableMapping)
    711 ns ± 5.33 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
    
    %timeit isinstance(b, MutableMapping)
    853 ns ± 6.29 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
    
    853/711
    Out[51]: 1.19971870604782

    If you want to get really fast you can check for one of the required attributes MutableMapping has

    %timeit hasattr(b, "update")
    82.6 ns ± 0.181 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)

    But this is (admittedly) not that mypy friendly, but will probably do the job just fine.

  5. erictraut commented on Aug 14, 2021

    @erictraut

    Yeah, performing a runtime protocol check (regardless of whether it uses isinstance or issubclass) is not something I'd recommend doing in code that is performance critical.

  6. sg495 commented on Dec 3, 2023

    @sg495

    Just adding one more example of this issue, involving a type variable bound to type. With Mypy --strict, the code snippet below raises the following error:

    Mypy(return-value): Incompatible return value type (got "type[object]", expected "ClassT | None")
    
    from typing import TypeVar
    
    ClassT = TypeVar("ClassT", bound=type)
    
    def directed_meet(cls0: ClassT, cls1: ClassT) -> ClassT|None:
        if issubclass(cls1, cls0):
            return cls1 # error Mypy(return-value)
        if issubclass(cls0, cls1):
            return cls0 # error Mypy(return-value)
        return None

    However, the problem disappears if type is replaced by Type[Any]:

    from typing import Any, Type, TypeVar
    
    ClassT = TypeVar("ClassT", bound=Type[Any])
    
    def directed_meet(cls0: ClassT, cls1: ClassT) -> ClassT|None:
        if issubclass(cls1, cls0):
            return cls1 # error Mypy(return-value)
        if issubclass(cls0, cls1):
            return cls0 # error Mypy(return-value)
        return None

    This was tested with mypy 1.7.0 (compiled: yes) and Python 3.12.0.

  7. hauntsaninja commented on Feb 10, 2026

    @hauntsaninja
    Collaborator

    sg495 case was fixed in #19183

    For OP's case, they should probably just use isinstance, but it is a thing we could potentially add narrowing for in the future

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

    bugmypy got something wrongtopic-type-narrowingConditional type narrowing / binder

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions