Skip to content

Commit bec5cad

Browse files
authored
Do not allow ClassVar and Final in TypedDict and NamedTuple (#18281)
Closes #18220
1 parent 40730c9 commit bec5cad

File tree

10 files changed

+80
-7
lines changed

10 files changed

+80
-7
lines changed

mypy/checker.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3565,7 +3565,7 @@ def check_final(self, s: AssignmentStmt | OperatorAssignmentStmt | AssignmentExp
35653565
else:
35663566
lvs = [s.lvalue]
35673567
is_final_decl = s.is_final_def if isinstance(s, AssignmentStmt) else False
3568-
if is_final_decl and self.scope.active_class():
3568+
if is_final_decl and (active_class := self.scope.active_class()):
35693569
lv = lvs[0]
35703570
assert isinstance(lv, RefExpr)
35713571
if lv.node is not None:
@@ -3579,6 +3579,9 @@ def check_final(self, s: AssignmentStmt | OperatorAssignmentStmt | AssignmentExp
35793579
# then we already reported the error about missing r.h.s.
35803580
isinstance(s, AssignmentStmt)
35813581
and s.type is not None
3582+
# Avoid extra error message for NamedTuples,
3583+
# they were reported during semanal
3584+
and not active_class.is_named_tuple
35823585
):
35833586
self.msg.final_without_value(s)
35843587
for lv in lvs:

mypy/semanal.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3646,7 +3646,12 @@ def unwrap_final(self, s: AssignmentStmt) -> bool:
36463646
invalid_bare_final = False
36473647
if not s.unanalyzed_type.args:
36483648
s.type = None
3649-
if isinstance(s.rvalue, TempNode) and s.rvalue.no_rhs:
3649+
if (
3650+
isinstance(s.rvalue, TempNode)
3651+
and s.rvalue.no_rhs
3652+
# Filter duplicate errors, we already reported this:
3653+
and not (self.type and self.type.is_named_tuple)
3654+
):
36503655
invalid_bare_final = True
36513656
self.fail("Type in Final[...] can only be omitted if there is an initializer", s)
36523657
else:
@@ -7351,6 +7356,7 @@ def type_analyzer(
73517356
allow_unpack: bool = False,
73527357
report_invalid_types: bool = True,
73537358
prohibit_self_type: str | None = None,
7359+
prohibit_special_class_field_types: str | None = None,
73547360
allow_type_any: bool = False,
73557361
) -> TypeAnalyser:
73567362
if tvar_scope is None:
@@ -7370,6 +7376,7 @@ def type_analyzer(
73707376
allow_param_spec_literals=allow_param_spec_literals,
73717377
allow_unpack=allow_unpack,
73727378
prohibit_self_type=prohibit_self_type,
7379+
prohibit_special_class_field_types=prohibit_special_class_field_types,
73737380
allow_type_any=allow_type_any,
73747381
)
73757382
tpan.in_dynamic_func = bool(self.function_stack and self.function_stack[-1].is_dynamic())
@@ -7394,6 +7401,7 @@ def anal_type(
73947401
allow_unpack: bool = False,
73957402
report_invalid_types: bool = True,
73967403
prohibit_self_type: str | None = None,
7404+
prohibit_special_class_field_types: str | None = None,
73977405
allow_type_any: bool = False,
73987406
) -> Type | None:
73997407
"""Semantically analyze a type.
@@ -7429,6 +7437,7 @@ def anal_type(
74297437
allow_unpack=allow_unpack,
74307438
report_invalid_types=report_invalid_types,
74317439
prohibit_self_type=prohibit_self_type,
7440+
prohibit_special_class_field_types=prohibit_special_class_field_types,
74327441
allow_type_any=allow_type_any,
74337442
)
74347443
tag = self.track_incomplete_refs()

mypy/semanal_namedtuple.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,7 @@ def check_namedtuple_classdef(
191191
stmt.type,
192192
allow_placeholder=not self.api.is_func_scope(),
193193
prohibit_self_type="NamedTuple item type",
194+
prohibit_special_class_field_types="NamedTuple",
194195
)
195196
if analyzed is None:
196197
# Something is incomplete. We need to defer this named tuple.
@@ -483,6 +484,7 @@ def parse_namedtuple_fields_with_types(
483484
type,
484485
allow_placeholder=not self.api.is_func_scope(),
485486
prohibit_self_type="NamedTuple item type",
487+
prohibit_special_class_field_types="NamedTuple",
486488
)
487489
# Workaround #4987 and avoid introducing a bogus UnboundType
488490
if isinstance(analyzed, UnboundType):

mypy/semanal_shared.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,7 @@ def anal_type(
185185
allow_placeholder: bool = False,
186186
report_invalid_types: bool = True,
187187
prohibit_self_type: str | None = None,
188+
prohibit_special_class_field_types: str | None = None,
188189
) -> Type | None:
189190
raise NotImplementedError
190191

mypy/semanal_typeddict.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,7 @@ def analyze_typeddict_classdef_fields(
330330
allow_typed_dict_special_forms=True,
331331
allow_placeholder=not self.api.is_func_scope(),
332332
prohibit_self_type="TypedDict item type",
333+
prohibit_special_class_field_types="TypedDict",
333334
)
334335
if analyzed is None:
335336
return None, [], [], set(), set() # Need to defer
@@ -561,6 +562,7 @@ def parse_typeddict_fields_with_types(
561562
allow_typed_dict_special_forms=True,
562563
allow_placeholder=not self.api.is_func_scope(),
563564
prohibit_self_type="TypedDict item type",
565+
prohibit_special_class_field_types="TypedDict",
564566
)
565567
if analyzed is None:
566568
return None

mypy/typeanal.py

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,7 @@ def __init__(
229229
allow_unpack: bool = False,
230230
report_invalid_types: bool = True,
231231
prohibit_self_type: str | None = None,
232+
prohibit_special_class_field_types: str | None = None,
232233
allowed_alias_tvars: list[TypeVarLikeType] | None = None,
233234
allow_type_any: bool = False,
234235
alias_type_params_names: list[str] | None = None,
@@ -275,6 +276,8 @@ def __init__(
275276
# Names of type aliases encountered while analysing a type will be collected here.
276277
self.aliases_used: set[str] = set()
277278
self.prohibit_self_type = prohibit_self_type
279+
# Set when we analyze TypedDicts or NamedTuples, since they are special:
280+
self.prohibit_special_class_field_types = prohibit_special_class_field_types
278281
# Allow variables typed as Type[Any] and type (useful for base classes).
279282
self.allow_type_any = allow_type_any
280283
self.allow_type_var_tuple = False
@@ -596,11 +599,18 @@ def try_analyze_special_unbound_type(self, t: UnboundType, fullname: str) -> Typ
596599
elif fullname == "typing.Any" or fullname == "builtins.Any":
597600
return AnyType(TypeOfAny.explicit, line=t.line, column=t.column)
598601
elif fullname in FINAL_TYPE_NAMES:
599-
self.fail(
600-
"Final can be only used as an outermost qualifier in a variable annotation",
601-
t,
602-
code=codes.VALID_TYPE,
603-
)
602+
if self.prohibit_special_class_field_types:
603+
self.fail(
604+
f"Final[...] can't be used inside a {self.prohibit_special_class_field_types}",
605+
t,
606+
code=codes.VALID_TYPE,
607+
)
608+
else:
609+
self.fail(
610+
"Final can be only used as an outermost qualifier in a variable annotation",
611+
t,
612+
code=codes.VALID_TYPE,
613+
)
604614
return AnyType(TypeOfAny.from_error)
605615
elif fullname == "typing.Tuple" or (
606616
fullname == "builtins.tuple"
@@ -668,6 +678,12 @@ def try_analyze_special_unbound_type(self, t: UnboundType, fullname: str) -> Typ
668678
self.fail(
669679
"Invalid type: ClassVar nested inside other type", t, code=codes.VALID_TYPE
670680
)
681+
if self.prohibit_special_class_field_types:
682+
self.fail(
683+
f"ClassVar[...] can't be used inside a {self.prohibit_special_class_field_types}",
684+
t,
685+
code=codes.VALID_TYPE,
686+
)
671687
if len(t.args) == 0:
672688
return AnyType(TypeOfAny.from_omitted_generics, line=t.line, column=t.column)
673689
if len(t.args) != 1:

test-data/unit/check-namedtuple.test

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1441,3 +1441,22 @@ def bar() -> None:
14411441
misspelled_var_name # E: Name "misspelled_var_name" is not defined
14421442
[builtins fixtures/tuple.pyi]
14431443
[typing fixtures/typing-namedtuple.pyi]
1444+
1445+
1446+
[case testNamedTupleFinalAndClassVar]
1447+
from typing import NamedTuple, Final, ClassVar
1448+
1449+
class My(NamedTuple):
1450+
a: Final # E: Final[...] can't be used inside a NamedTuple
1451+
b: Final[int] # E: Final[...] can't be used inside a NamedTuple
1452+
c: ClassVar # E: ClassVar[...] can't be used inside a NamedTuple
1453+
d: ClassVar[int] # E: ClassVar[...] can't be used inside a NamedTuple
1454+
1455+
Func = NamedTuple('Func', [
1456+
('a', Final), # E: Final[...] can't be used inside a NamedTuple
1457+
('b', Final[int]), # E: Final[...] can't be used inside a NamedTuple
1458+
('c', ClassVar), # E: ClassVar[...] can't be used inside a NamedTuple
1459+
('d', ClassVar[int]), # E: ClassVar[...] can't be used inside a NamedTuple
1460+
])
1461+
[builtins fixtures/tuple.pyi]
1462+
[typing fixtures/typing-namedtuple.pyi]

test-data/unit/check-typeddict.test

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4053,3 +4053,22 @@ d: D = {"a": 1, "b": "x"}
40534053
c: C = d # E: Incompatible types in assignment (expression has type "D", variable has type "C")
40544054
[builtins fixtures/dict.pyi]
40554055
[typing fixtures/typing-typeddict.pyi]
4056+
4057+
4058+
[case testTypedDictFinalAndClassVar]
4059+
from typing import TypedDict, Final, ClassVar
4060+
4061+
class My(TypedDict):
4062+
a: Final # E: Final[...] can't be used inside a TypedDict
4063+
b: Final[int] # E: Final[...] can't be used inside a TypedDict
4064+
c: ClassVar # E: ClassVar[...] can't be used inside a TypedDict
4065+
d: ClassVar[int] # E: ClassVar[...] can't be used inside a TypedDict
4066+
4067+
Func = TypedDict('Func', {
4068+
'a': Final, # E: Final[...] can't be used inside a TypedDict
4069+
'b': Final[int], # E: Final[...] can't be used inside a TypedDict
4070+
'c': ClassVar, # E: ClassVar[...] can't be used inside a TypedDict
4071+
'd': ClassVar[int], # E: ClassVar[...] can't be used inside a TypedDict
4072+
})
4073+
[builtins fixtures/dict.pyi]
4074+
[typing fixtures/typing-typeddict.pyi]

test-data/unit/fixtures/typing-namedtuple.pyi

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ Optional = 0
88
Self = 0
99
Tuple = 0
1010
ClassVar = 0
11+
Final = 0
1112

1213
T = TypeVar('T')
1314
T_co = TypeVar('T_co', covariant=True)

test-data/unit/fixtures/typing-typeddict.pyi

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ Required = 0
2828
NotRequired = 0
2929
ReadOnly = 0
3030
Self = 0
31+
ClassVar = 0
3132

3233
T = TypeVar('T')
3334
T_co = TypeVar('T_co', covariant=True)

0 commit comments

Comments
 (0)