Skip to content

Commit 4de0caa

Browse files
authored
Allow @final on TypedDict (#13557)
Allow a `TypedDict` to be decorated with `@final`. Like a regular class, mypy will emit an error if a final `TypedDict` is subclassed. Allow `@final` to be applied to a `TypedDict`, and have mypy emit an error if class is derived from a final `TypedDict`. This goes some way towards closing #7981 and closing a feature gap with pyright, though not the whole way, as #7981 also asks for additional type narrowing for a final `TypedDict`.
1 parent cc59b56 commit 4de0caa

File tree

3 files changed

+27
-5
lines changed

3 files changed

+27
-5
lines changed

mypy/semanal.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -1478,8 +1478,8 @@ def analyze_typeddict_classdef(self, defn: ClassDef) -> bool:
14781478
for decorator in defn.decorators:
14791479
decorator.accept(self)
14801480
if isinstance(decorator, RefExpr):
1481-
if decorator.fullname in FINAL_DECORATOR_NAMES:
1482-
self.fail("@final cannot be used with TypedDict", decorator)
1481+
if decorator.fullname in FINAL_DECORATOR_NAMES and info is not None:
1482+
info.is_final = True
14831483
if info is None:
14841484
self.mark_incomplete(defn.name, defn)
14851485
else:

mypy/semanal_typeddict.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
from typing_extensions import Final
66

7-
from mypy import errorcodes as codes
7+
from mypy import errorcodes as codes, message_registry
88
from mypy.errorcodes import ErrorCode
99
from mypy.exprtotype import TypeTranslationError, expr_to_unanalyzed_type
1010
from mypy.messages import MessageBuilder
@@ -79,6 +79,9 @@ def analyze_typeddict_classdef(self, defn: ClassDef) -> tuple[bool, TypeInfo | N
7979
self.api.accept(base_expr)
8080
if base_expr.fullname in TPDICT_NAMES or self.is_typeddict(base_expr):
8181
possible = True
82+
if isinstance(base_expr.node, TypeInfo) and base_expr.node.is_final:
83+
err = message_registry.CANNOT_INHERIT_FROM_FINAL
84+
self.fail(err.format(base_expr.node.name).value, defn, code=err.code)
8285
if not possible:
8386
return False, None
8487
existing_info = None

test-data/unit/check-typeddict.test

+21-2
Original file line numberDiff line numberDiff line change
@@ -2012,16 +2012,35 @@ v = {bad2: 2} # E: Extra key "bad" for TypedDict "Value"
20122012
[builtins fixtures/dict.pyi]
20132013
[typing fixtures/typing-typeddict.pyi]
20142014

2015-
[case testCannotUseFinalDecoratorWithTypedDict]
2015+
[case testCannotSubclassFinalTypedDict]
20162016
from typing import TypedDict
20172017
from typing_extensions import final
20182018

2019-
@final # E: @final cannot be used with TypedDict
2019+
@final
20202020
class DummyTypedDict(TypedDict):
20212021
int_val: int
20222022
float_val: float
20232023
str_val: str
20242024

2025+
class SubType(DummyTypedDict): # E: Cannot inherit from final class "DummyTypedDict"
2026+
pass
2027+
2028+
[builtins fixtures/dict.pyi]
2029+
[typing fixtures/typing-typeddict.pyi]
2030+
2031+
[case testCannotSubclassFinalTypedDictWithForwardDeclarations]
2032+
from typing import TypedDict
2033+
from typing_extensions import final
2034+
2035+
@final
2036+
class DummyTypedDict(TypedDict):
2037+
forward_declared: "ForwardDeclared"
2038+
2039+
class SubType(DummyTypedDict): # E: Cannot inherit from final class "DummyTypedDict"
2040+
pass
2041+
2042+
class ForwardDeclared: pass
2043+
20252044
[builtins fixtures/dict.pyi]
20262045
[typing fixtures/typing-typeddict.pyi]
20272046

0 commit comments

Comments
 (0)