Skip to content

Commit b1fe23f

Browse files
Support TypedDict functional syntax as class base type (#16703)
Fixes #16701 This PR allows `TypedDict(...)` calls to be used as a base class. This fixes the error emitted by mypy described in #16701 .
1 parent 186ace3 commit b1fe23f

File tree

3 files changed

+33
-4
lines changed

3 files changed

+33
-4
lines changed

mypy/semanal.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2169,8 +2169,16 @@ def analyze_base_classes(
21692169
if (
21702170
isinstance(base_expr, RefExpr)
21712171
and base_expr.fullname in TYPED_NAMEDTUPLE_NAMES + TPDICT_NAMES
2172+
) or (
2173+
isinstance(base_expr, CallExpr)
2174+
and isinstance(base_expr.callee, RefExpr)
2175+
and base_expr.callee.fullname in TPDICT_NAMES
21722176
):
21732177
# Ignore magic bases for now.
2178+
# For example:
2179+
# class Foo(TypedDict): ... # RefExpr
2180+
# class Foo(NamedTuple): ... # RefExpr
2181+
# class Foo(TypedDict("Foo", {"a": int})): ... # CallExpr
21742182
continue
21752183

21762184
try:

mypy/semanal_typeddict.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@ def analyze_typeddict_classdef(self, defn: ClassDef) -> tuple[bool, TypeInfo | N
7979
"""
8080
possible = False
8181
for base_expr in defn.base_type_exprs:
82+
if isinstance(base_expr, CallExpr):
83+
base_expr = base_expr.callee
8284
if isinstance(base_expr, IndexExpr):
8385
base_expr = base_expr.base
8486
if isinstance(base_expr, RefExpr):
@@ -117,7 +119,13 @@ def analyze_typeddict_classdef(self, defn: ClassDef) -> tuple[bool, TypeInfo | N
117119
typeddict_bases: list[Expression] = []
118120
typeddict_bases_set = set()
119121
for expr in defn.base_type_exprs:
120-
if isinstance(expr, RefExpr) and expr.fullname in TPDICT_NAMES:
122+
ok, maybe_type_info, _ = self.check_typeddict(expr, None, False)
123+
if ok and maybe_type_info is not None:
124+
# expr is a CallExpr
125+
info = maybe_type_info
126+
typeddict_bases_set.add(info.fullname)
127+
typeddict_bases.append(expr)
128+
elif isinstance(expr, RefExpr) and expr.fullname in TPDICT_NAMES:
121129
if "TypedDict" not in typeddict_bases_set:
122130
typeddict_bases_set.add("TypedDict")
123131
else:
@@ -176,19 +184,22 @@ def add_keys_and_types_from_base(
176184
required_keys: set[str],
177185
ctx: Context,
178186
) -> None:
187+
base_args: list[Type] = []
179188
if isinstance(base, RefExpr):
180189
assert isinstance(base.node, TypeInfo)
181190
info = base.node
182-
base_args: list[Type] = []
183-
else:
184-
assert isinstance(base, IndexExpr)
191+
elif isinstance(base, IndexExpr):
185192
assert isinstance(base.base, RefExpr)
186193
assert isinstance(base.base.node, TypeInfo)
187194
info = base.base.node
188195
args = self.analyze_base_args(base, ctx)
189196
if args is None:
190197
return
191198
base_args = args
199+
else:
200+
assert isinstance(base, CallExpr)
201+
assert isinstance(base.analyzed, TypedDictExpr)
202+
info = base.analyzed.info
192203

193204
assert info.typeddict_type is not None
194205
base_typed_dict = info.typeddict_type

test-data/unit/check-typeddict.test

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3438,3 +3438,13 @@ class TotalInTheMiddle(TypedDict, a=1, total=True, b=2, c=3): # E: Unexpected k
34383438
...
34393439
[builtins fixtures/dict.pyi]
34403440
[typing fixtures/typing-typeddict.pyi]
3441+
3442+
[case testCanCreateClassWithFunctionBasedTypedDictBase]
3443+
from mypy_extensions import TypedDict
3444+
3445+
class Params(TypedDict("Params", {'x': int})):
3446+
pass
3447+
3448+
p: Params = {'x': 2}
3449+
reveal_type(p) # N: Revealed type is "TypedDict('__main__.Params', {'x': builtins.int})"
3450+
[builtins fixtures/dict.pyi]

0 commit comments

Comments
 (0)