From bd2c051852a75eecce1ba47cb086570c8ed0bd3d Mon Sep 17 00:00:00 2001 From: Xuehai Pan Date: Mon, 24 Aug 2026 13:56:17 +0800 Subject: [PATCH 1/2] fix: classify __slots__ as class variables Mark ordinary and plugin-generated __slots__ symbols as class variables so inheritance does not produce spurious instance/class variable override errors. Add regression coverage for ordinary classes, dataclasses, and attrs classes. --- mypy/plugins/attrs.py | 4 +++- mypy/plugins/dataclasses.py | 1 + mypy/semanal.py | 1 + test-data/unit/check-dataclasses.test | 12 ++++++++++++ test-data/unit/check-plugin-attrs.test | 13 +++++++++++++ test-data/unit/check-slots.test | 11 +++++++++++ 6 files changed, 41 insertions(+), 1 deletion(-) diff --git a/mypy/plugins/attrs.py b/mypy/plugins/attrs.py index 1593c73cd2bfe..bf8cc1dd81aeb 100644 --- a/mypy/plugins/attrs.py +++ b/mypy/plugins/attrs.py @@ -960,7 +960,9 @@ def _add_slots(ctx: mypy.plugin.ClassDefContext, attributes: list[Attribute]) -> [ctx.api.named_type("builtins.str") for _ in attributes], fallback=ctx.api.named_type("builtins.tuple"), ) - add_attribute_to_class(api=ctx.api, cls=ctx.cls, name="__slots__", typ=slots_type) + add_attribute_to_class( + api=ctx.api, cls=ctx.cls, name="__slots__", typ=slots_type, is_classvar=True + ) def _add_match_args(ctx: mypy.plugin.ClassDefContext, attributes: list[Attribute]) -> None: diff --git a/mypy/plugins/dataclasses.py b/mypy/plugins/dataclasses.py index a511e714ac6b4..af1d5438547f6 100644 --- a/mypy/plugins/dataclasses.py +++ b/mypy/plugins/dataclasses.py @@ -475,6 +475,7 @@ def add_slots(self, info: TypeInfo, attributes: list[DataclassAttribute]) -> Non self._cls, "__slots__", slots_type, + is_classvar=True, overwrite_existing=slots_defined_by_plugin, ) diff --git a/mypy/semanal.py b/mypy/semanal.py index 7f961687a8aee..7ffb505710d6f 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -4648,6 +4648,7 @@ def make_name_lvalue_var( assert self.type is not None v.info = self.type v.is_initialized_in_class = True + v.is_classvar = name == "__slots__" v.allow_incompatible_override = name in ALLOW_INCOMPATIBLE_OVERRIDE if kind != LDEF: v._fullname = self.qualified_name(name) diff --git a/test-data/unit/check-dataclasses.test b/test-data/unit/check-dataclasses.test index f43ac255373e6..78dc62406de0d 100644 --- a/test-data/unit/check-dataclasses.test +++ b/test-data/unit/check-dataclasses.test @@ -1514,6 +1514,18 @@ class Some: self.y = 1 # E: Trying to assign name "y" that is not in "__slots__" of type "__main__.Some" [builtins fixtures/dataclasses.pyi] +[case testDataclassGeneratedSlotsClassVariableOverride] +from dataclasses import dataclass +from typing import ClassVar + +@dataclass(slots=True) +class Base: + value: int + +class Child(Base): + __slots__: ClassVar[tuple[str, ...]] = () +[builtins fixtures/tuple.pyi] + [case testDataclassWithSlotsDef] from dataclasses import dataclass diff --git a/test-data/unit/check-plugin-attrs.test b/test-data/unit/check-plugin-attrs.test index 5e6dd4d83ce02..ad2622adae108 100644 --- a/test-data/unit/check-plugin-attrs.test +++ b/test-data/unit/check-plugin-attrs.test @@ -1745,6 +1745,19 @@ class C: self.c = 2 # E: Trying to assign name "c" that is not in "__slots__" of type "__main__.C" [builtins fixtures/plugin_attrs.pyi] +[case testAttrsGeneratedSlotsClassVariableOverride] +from typing import ClassVar + +import attrs + +@attrs.define(slots=True) +class Base: + value: int + +class Child(Base): + __slots__: ClassVar[tuple[str, ...]] = () +[builtins fixtures/plugin_attrs.pyi] + [case testAttrsClassWithSlotsDerivedFromNonSlots] import attrs diff --git a/test-data/unit/check-slots.test b/test-data/unit/check-slots.test index 25dd630e1cbed..ae8bf7d040cf2 100644 --- a/test-data/unit/check-slots.test +++ b/test-data/unit/check-slots.test @@ -85,6 +85,17 @@ A.b = 1 [builtins fixtures/tuple.pyi] +[case testSlotsClassVariableOverride] +from typing import ClassVar + +class Base: + __slots__ = () + +class Child(Base): + __slots__: ClassVar[tuple[str, ...]] = () +[builtins fixtures/tuple.pyi] + + [case testSlotsDefinitionMultipleVars1] class A: __slots__ = __fields__ = ("a", "b") From 1486925a735d359e8fdb067f0abd277ea3d3f590 Mon Sep 17 00:00:00 2001 From: Xuehai Pan Date: Mon, 24 Aug 2026 15:14:34 +0800 Subject: [PATCH 2/2] fix(mypyc): skip __slots__ class variable emission Keep __slots__ classified as a class variable for type checking, but do not attach it to native classes at runtime. Native classes use their own layout, and exposing inherited slots prevents compiled subclasses from being created. Add a compiled inheritance regression that reproduces the CI failure. --- mypyc/irbuild/classdef.py | 3 +++ mypyc/test-data/run-classes.test | 17 +++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/mypyc/irbuild/classdef.py b/mypyc/irbuild/classdef.py index 61587b46161f7..238b628a6fb21 100644 --- a/mypyc/irbuild/classdef.py +++ b/mypyc/irbuild/classdef.py @@ -329,6 +329,9 @@ def add_attr(self, lvalue: NameExpr, stmt: AssignmentStmt) -> None: # Variable declaration with no body if isinstance(stmt.rvalue, TempNode): return + # Native classes use mypyc's own layout, so don't emit __slots__ at runtime. + if lvalue.name == "__slots__": + return # Only treat marked class variables as class variables. if not (is_class_var(lvalue) or stmt.is_final_def): return diff --git a/mypyc/test-data/run-classes.test b/mypyc/test-data/run-classes.test index 8203de6feee2d..676da9c0f75d1 100644 --- a/mypyc/test-data/run-classes.test +++ b/mypyc/test-data/run-classes.test @@ -1175,6 +1175,23 @@ assert f() == 10 A.x = 200 assert f() == 200 +[case testSlotsNotEmittedAsClassVar] +class Base: + __slots__ = ("value",) + value: int + +class Child(Base): + pass + +def make_child() -> Child: + child = Child() + child.value = 1 + return child +[file driver.py] +from native import make_child + +assert make_child().value == 1 + [case testClassVarDoesNotShadowMethodGlobal] from typing import ClassVar from testutil import assertRaises