From 6bf86022e256bad787568f2ee126a111e7697e7d Mon Sep 17 00:00:00 2001 From: STerliakov Date: Fri, 27 Jun 2025 01:30:42 +0200 Subject: [PATCH 1/6] Allow empty list as `__slots__` without annotation --- mypy/checker.py | 3 +++ test-data/unit/check-slots.test | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/mypy/checker.py b/mypy/checker.py index 596564c98a40..8e872d2355b8 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -3316,6 +3316,9 @@ def get_variable_type_context(self, inferred: Var, rvalue: Expression) -> Type | type_contexts.append(base_type) # Use most derived supertype as type context if available. if not type_contexts: + if inferred.name == "__slots__": + str_type = self.named_type("builtins.str") + return self.named_generic_type("typing.Iterable", [str_type]) return None candidate = type_contexts[0] for other in type_contexts: diff --git a/test-data/unit/check-slots.test b/test-data/unit/check-slots.test index b7ce5e596101..b406a3f9dcbb 100644 --- a/test-data/unit/check-slots.test +++ b/test-data/unit/check-slots.test @@ -496,6 +496,11 @@ class A: self.missing = 3 [builtins fixtures/dict.pyi] +[case testSlotsEmptyList] +class A: + __slots__ = [] +[builtins fixtures/dict.pyi] + [case testSlotsWithAny] from typing import Any From cdc2573c32acf57629b81b9eae4ff93afad2349f Mon Sep 17 00:00:00 2001 From: STerliakov Date: Fri, 27 Jun 2025 02:05:16 +0200 Subject: [PATCH 2/6] Limit to class-level __slots__ --- mypy/checker.py | 4 ++-- test-data/unit/check-slots.test | 9 +++++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 8e872d2355b8..302b07a9c4a3 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -3137,7 +3137,7 @@ def check_assignment( else: self.check_getattr_method(signature, lvalue, name) - if name == "__slots__": + if name == "__slots__" and self.scope.active_class() is not None: typ = lvalue_type or self.expr_checker.accept(rvalue) self.check_slots_definition(typ, lvalue) if name == "__match_args__" and inferred is not None: @@ -3316,7 +3316,7 @@ def get_variable_type_context(self, inferred: Var, rvalue: Expression) -> Type | type_contexts.append(base_type) # Use most derived supertype as type context if available. if not type_contexts: - if inferred.name == "__slots__": + if inferred.name == "__slots__" and self.scope.active_class(): str_type = self.named_type("builtins.str") return self.named_generic_type("typing.Iterable", [str_type]) return None diff --git a/test-data/unit/check-slots.test b/test-data/unit/check-slots.test index b406a3f9dcbb..7aaf0a8b1d7e 100644 --- a/test-data/unit/check-slots.test +++ b/test-data/unit/check-slots.test @@ -496,11 +496,16 @@ class A: self.missing = 3 [builtins fixtures/dict.pyi] +[case testSlotsNotInClass] +# Shouldn't be triggered +__slots__ = [1, 2] + +def foo() -> None: + __slots__ = 1 + [case testSlotsEmptyList] class A: __slots__ = [] -[builtins fixtures/dict.pyi] - [case testSlotsWithAny] from typing import Any From 84b8849ee3d7ae6e9c0c09c0a5e244415d24a78d Mon Sep 17 00:00:00 2001 From: STerliakov Date: Fri, 27 Jun 2025 02:16:03 +0200 Subject: [PATCH 3/6] Apply same treatment to __all__ --- mypy/checker.py | 7 ++++++- mypy/checker_shared.py | 4 ++++ test-data/unit/check-modules.test | 23 ++++++++++++++++++++++- 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 302b07a9c4a3..986a87d726bd 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -3316,7 +3316,12 @@ def get_variable_type_context(self, inferred: Var, rvalue: Expression) -> Type | type_contexts.append(base_type) # Use most derived supertype as type context if available. if not type_contexts: - if inferred.name == "__slots__" and self.scope.active_class(): + if ( + inferred.name == "__slots__" + and self.scope.active_class() + or inferred.name == "__all__" + and self.scope.is_top_level() + ): str_type = self.named_type("builtins.str") return self.named_generic_type("typing.Iterable", [str_type]) return None diff --git a/mypy/checker_shared.py b/mypy/checker_shared.py index 2ab4548edfaf..a9cbae643dca 100644 --- a/mypy/checker_shared.py +++ b/mypy/checker_shared.py @@ -334,6 +334,10 @@ def current_self_type(self) -> Instance | TupleType | None: return fill_typevars(item) return None + def is_top_level(self) -> bool: + """Is current scope top-level (no classes or functions)?""" + return len(self.stack) == 1 + @contextmanager def push_function(self, item: FuncItem) -> Iterator[None]: self.stack.append(item) diff --git a/test-data/unit/check-modules.test b/test-data/unit/check-modules.test index 858024e7daf2..4fb1355db26f 100644 --- a/test-data/unit/check-modules.test +++ b/test-data/unit/check-modules.test @@ -423,7 +423,28 @@ import typing __all__ = [1, 2, 3] [builtins fixtures/module_all.pyi] [out] -main:2: error: Type of __all__ must be "Sequence[str]", not "list[int]" +main:2: error: List item 0 has incompatible type "int"; expected "str" +main:2: error: List item 1 has incompatible type "int"; expected "str" +main:2: error: List item 2 has incompatible type "int"; expected "str" + +[case testAllMustBeSequenceStr2] +import typing +__all__ = 1 +[builtins fixtures/module_all.pyi] +[out] +main:2: error: Type of __all__ must be "Sequence[str]", not "int" + +[case testModuleAllEmptyList] +__all__ = [] +[builtins fixtures/module_all.pyi] + +[case testDunderAllNotGlobal] +class A: + __all__ = 1 + +def foo() -> None: + __all__ = 1 +[builtins fixtures/module_all.pyi] [case testUnderscoreExportedValuesInImportAll] import typing From 3139f4953b4dafbe6a048e63428a32676f2fcb7d Mon Sep 17 00:00:00 2001 From: STerliakov Date: Fri, 27 Jun 2025 18:02:00 +0200 Subject: [PATCH 4/6] Address review comments: restrict `__all__` to Sequence --- mypy/checker.py | 10 ++++------ test-data/unit/check-modules.test | 6 +++--- test-data/unit/check-slots.test | 5 +++++ 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 986a87d726bd..c48fe93d55d0 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -3316,14 +3316,12 @@ def get_variable_type_context(self, inferred: Var, rvalue: Expression) -> Type | type_contexts.append(base_type) # Use most derived supertype as type context if available. if not type_contexts: - if ( - inferred.name == "__slots__" - and self.scope.active_class() - or inferred.name == "__all__" - and self.scope.is_top_level() - ): + if inferred.name == "__slots__" and self.scope.active_class() is not None: str_type = self.named_type("builtins.str") return self.named_generic_type("typing.Iterable", [str_type]) + if inferred.name == "__all__" and self.scope.is_top_level(): + str_type = self.named_type("builtins.str") + return self.named_generic_type("typing.Sequence", [str_type]) return None candidate = type_contexts[0] for other in type_contexts: diff --git a/test-data/unit/check-modules.test b/test-data/unit/check-modules.test index 4fb1355db26f..4924b95dcecb 100644 --- a/test-data/unit/check-modules.test +++ b/test-data/unit/check-modules.test @@ -429,13 +429,13 @@ main:2: error: List item 2 has incompatible type "int"; expected "str" [case testAllMustBeSequenceStr2] import typing -__all__ = 1 +__all__ = 1 # E: Type of __all__ must be "Sequence[str]", not "int" +reveal_type(__all__) # N: Revealed type is "builtins.int" [builtins fixtures/module_all.pyi] -[out] -main:2: error: Type of __all__ must be "Sequence[str]", not "int" [case testModuleAllEmptyList] __all__ = [] +reveal_type(__all__) # N: Revealed type is "builtins.list[builtins.str]" [builtins fixtures/module_all.pyi] [case testDunderAllNotGlobal] diff --git a/test-data/unit/check-slots.test b/test-data/unit/check-slots.test index 7aaf0a8b1d7e..3a26064825d0 100644 --- a/test-data/unit/check-slots.test +++ b/test-data/unit/check-slots.test @@ -499,13 +499,18 @@ class A: [case testSlotsNotInClass] # Shouldn't be triggered __slots__ = [1, 2] +reveal_type(__slots__) # N: Revealed type is "builtins.list[builtins.int]" def foo() -> None: __slots__ = 1 + reveal_type(__slots__) # N: Revealed type is "builtins.int" [case testSlotsEmptyList] class A: __slots__ = [] + reveal_type(__slots__) # N: Revealed type is "builtins.list[builtins.str]" + +reveal_type(A.__slots__) # N: Revealed type is "builtins.list[builtins.str]" [case testSlotsWithAny] from typing import Any From e083e5ce9332af12e6b497cc2c938ff985e95420 Mon Sep 17 00:00:00 2001 From: STerliakov Date: Fri, 27 Jun 2025 18:03:25 +0200 Subject: [PATCH 5/6] Add explicit test with Iterable that is not a Sequence for `__all__` --- test-data/unit/check-modules.test | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test-data/unit/check-modules.test b/test-data/unit/check-modules.test index 4924b95dcecb..862cd8ea3905 100644 --- a/test-data/unit/check-modules.test +++ b/test-data/unit/check-modules.test @@ -433,6 +433,13 @@ __all__ = 1 # E: Type of __all__ must be "Sequence[str]", not "int" reveal_type(__all__) # N: Revealed type is "builtins.int" [builtins fixtures/module_all.pyi] +[case testAllMustBeSequenceStr3] +import typing +__all__ = set() # E: Need type annotation for "__all__" (hint: "__all__: set[] = ...") \ + # E: Type of __all__ must be "Sequence[str]", not "set[Any]" +reveal_type(__all__) # N: Revealed type is "builtins.set[Any]" +[builtins fixtures/set.pyi] + [case testModuleAllEmptyList] __all__ = [] reveal_type(__all__) # N: Revealed type is "builtins.list[builtins.str]" From 585bc152a9314dcf6193f8557d776243e4177fae Mon Sep 17 00:00:00 2001 From: STerliakov Date: Fri, 27 Jun 2025 22:50:05 +0200 Subject: [PATCH 6/6] Add set testcase --- test-data/unit/check-slots.test | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test-data/unit/check-slots.test b/test-data/unit/check-slots.test index 3a26064825d0..e924ac9e5f57 100644 --- a/test-data/unit/check-slots.test +++ b/test-data/unit/check-slots.test @@ -512,6 +512,14 @@ class A: reveal_type(A.__slots__) # N: Revealed type is "builtins.list[builtins.str]" +[case testSlotsEmptySet] +class A: + __slots__ = set() + reveal_type(__slots__) # N: Revealed type is "builtins.set[builtins.str]" + +reveal_type(A.__slots__) # N: Revealed type is "builtins.set[builtins.str]" +[builtins fixtures/set.pyi] + [case testSlotsWithAny] from typing import Any