Skip to content

Dataclasses fail to narrow with issubclass() in Python >= 3.13 #21635

Closed

Description

@ygale

Bug Report

With Python >= 3.13, given cls: type[A] and if issubclass(cls, M) where A and M are dataclasses, mypy is unable to narrow the type of cls to type[M]. Instead, the guarded block is not typechecked at all.

When iterating over a list[type[A]] in a comprehension with if issubclass(cls, M) where A and M are dataclasses, mypy is unable to narrow the type of cls to type[M]. Instead, the comprehension result expression is typechecked with cls at its original unnarrowed type type[A].

To Reproduce

from dataclasses import dataclass
from typing import reveal_type

@dataclass
class A:
  pass

@dataclass
class M:
  pass

@dataclass
class B(A):
  pass

@dataclass
class C(M, A):
  pass

cls: type[A] = C
if issubclass(cls, M):
  reveal_type(cls)
  n: int = 'foo'

Expected Behavior

The type of cls is revealed as type[M], followed by a type error for the assignment of a str to a variable of type int.

Actual Behavior

No type is revealed, silent success.

For comprehensions:

from dataclasses import dataclass
from typing import reveal_type

@dataclass
class A:
  pass

@dataclass
class M:
  pass

@dataclass
class B(A):
  pass

@dataclass
class C(M, A):
  pass

alist: list[type[A]] = [B, C]
mlist = [cls for cls in alist if issubclass(cls, M)]
reveal_type(mlist)

Expected Behavior

note: Revealed type is "list[type[<subclass of "A" and "M">]]"

And if we annotate mlist: list[type[M]] it still typechecks.

Actual Behavior

note: Revealed type is "list[type[A]]"

And if we annotate mlist: list[type[M]] we get:

error: List comprehension has incompatible type List[type[A]]; expected List[type[M]]  [misc]

Notes

This error DOES NOT occur if the classes are not dataclasses.

This error DOES NOT occur in a regular for loop:

mlist: list[type[M]] = []
cls: type[A]
for cls in alist:
  if issubclass(cls, M):
    mlist.append(cls)

This error DOES still occur if issubclass() is wrapped in a TypeIs guard function:

def is_m(t: type) -> TypeIs[type[M]]:
  return issubclass(t, M)

alist: list[type[A]] = [B, C]
mlist: list[type[M]] = [cls for cls in alist
  if is_m(cls)]

Your Environment

  • Mypy version used: mypy 2.1.0 (compiled: no)
  • Mypy command-line flags: none
  • Mypy configuration options from mypy.ini (and other config files): nothing special
  • Python version used: 3.13.13

Activity

  1. changed the title [-]issubclass() fails to narrow dataclasses in comprehensions[/-] [+]Dataclasses fail to narrow in comprehensions[/+] on Jul 5, 2026
  2. ygale commented on Jul 5, 2026

    @ygale
    ContributorAuthor

    @apoorvdarshan Thanks for the PR. However, it does not solve the problem. It only masks the type error by incorrectly typing the classes as Never. In fact, this is not an impossible type - as you can see from the repro in the description, the intersection of the types is not empty.

    Thanks to your PR, I have made the issue title and repro more precise. Note that the PR does not include the repro in its tests. If it included the original repro the test would have passed, but only because the error is masked. The current more precise repro would still fail.

    The PR still might have merit for the case where the type is actually impossible, as in the tests supplied in the PR. But that discussion would be out of scope here; perhaps open a separate issue for that case.

  3. changed the title [-]Dataclasses fail to narrow in comprehensions[/-] [+]Dataclasses fail to narrow in Python >= 3.13[/+] on Jul 5, 2026
  4. changed the title [-]Dataclasses fail to narrow in Python >= 3.13[/-] [+]Dataclasses fail to narrow with `issubclass()` in Python >= 3.13[/+] on Jul 5, 2026
  5. ygale commented on Jul 5, 2026

    @ygale
    ContributorAuthor

    This turns out to be a regression for dataclasses in Python >= 3.13, caused by the introduction of auto-generated __replace__() methods.

    Comprehensions are not required to hit this bug, though the effect of the bug is different for comprehensions.

    Root cause: When mypy compares the types of two unrelated dataclasses in issubclass(), the new auto-generated __replace__() methods always cause it to think that the dataclasses are incompatible and assigns the type Never. This causes the broken behaviors described in the reproductions.

    I updated the title and description yet again to make them even more precise. I submitted a PR.

    The "regular for loop" case in the description works only because the guarded block is not typechecked at all. I added a better reproduction for that, so this case is no longer interesting.

    The TypeIs case in the description turns out to be a completely different failure mode and so out of scope here. I will open a separate issue for that.

    There is a similar failure for NamedTuple. That too is a completely different failure mode and out of scope here. I'll open a separate issue also for that one.

  6. added a commit that references this issue on Jul 7, 2026
    b6d93f5
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