Skip to content

Commit b6d93f5

Browse files
authored
Fix regression in dataclass narrowing for Python >= 3.13 (#21675)
Fixes #21635 On Python >= 3.13, `@dataclass` synthesizes a `__replace__(self, ...) -> Self` method to support `copy.replace()`. When mypy tries to narrow a `type[A]` expression via `issubclass(cls, M)` (or `isinstance`) against a second, unrelated dataclass `M`, it builds an ad-hoc `<subclass of A and M>` type to check whether that narrowing is sound (`intersect_instances` in `checker.py`). Building that ad-hoc type runs `check_multiple_inheritance`, which sees `A`'s synthesized `__replace__` returning `A` and `M`'s returning `M`, and flags them as incompatible. That's a false positive: a real subclass of both (e.g. `class C(M, A)`, itself decorated with `@dataclass`) gets its *own* freshly synthesized, mutually compatible `__replace__`.
1 parent e986558 commit b6d93f5

2 files changed

Lines changed: 22 additions & 1 deletion

File tree

mypy/checker.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3133,7 +3133,7 @@ class C(B, A[int]): ... # this is unsafe because...
31333133
x: A[int] = C()
31343134
x.foo # ...runtime type is (str) -> None, while static type is (int) -> None
31353135
"""
3136-
if name in ("__init__", "__new__", "__init_subclass__"):
3136+
if name in {"__init__", "__new__", "__init_subclass__", "__replace__"}:
31373137
# __init__ and friends can be incompatible -- it's a special case.
31383138
return
31393139
first = base1.names[name]

test-data/unit/check-dataclasses.test

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2609,6 +2609,27 @@ class Y(X):
26092609
[builtins fixtures/tuple.pyi]
26102610

26112611

2612+
[case testDunderReplaceDoesNotBlockPlainIssubclassNarrowing]
2613+
# https://github.com/python/mypy/issues/21635
2614+
# flags: --python-version 3.13
2615+
from dataclasses import dataclass
2616+
2617+
@dataclass
2618+
class A: ...
2619+
@dataclass
2620+
class M: ...
2621+
@dataclass
2622+
class B(A): ...
2623+
@dataclass
2624+
class C(M, A): ...
2625+
2626+
cls: type[A] = C
2627+
if issubclass(cls, M):
2628+
reveal_type(cls) # N: Revealed type is "type[__main__.<subclass of "__main__.A" and "__main__.M">]"
2629+
n: int = 'foo' # E: Incompatible types in assignment (expression has type "str", variable has type "int")
2630+
[builtins fixtures/isinstancelist.pyi]
2631+
2632+
26122633
[case testFrozenWithFinal]
26132634
from dataclasses import dataclass
26142635
from typing import Final

0 commit comments

Comments
 (0)