diff --git a/mypy/semanal.py b/mypy/semanal.py index aa74122be255..2f620eca993c 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -3325,7 +3325,11 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None: # This should be safe as generally semantic analyzer is idempotent. with self.allow_unbound_tvars_set(): s.rvalue.accept(self) - + if self.is_class_scope() and not self.is_stub_file and not self.is_typeshed_stub_file: + for lvalue in s.lvalues: + if isinstance(lvalue, NameExpr) and lvalue.name == "__qualname__": + if not isinstance(s.rvalue, StrExpr): + self.fail('"__qualname__" must be str', s) # The r.h.s. is now ready to be classified, first check if it is a special form: special_form = False # * type alias diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test index 5a66eff2bd3b..fbc3d8d47d62 100644 --- a/test-data/unit/check-classes.test +++ b/test-data/unit/check-classes.test @@ -9412,3 +9412,9 @@ from typ import NT def f() -> NT: return NT(x='') [builtins fixtures/tuple.pyi] + +[case test_qualname_must_be_str] +class X: + __qualname__ = 5 +[out] +main:2: error: "__qualname__" must be str