From 02888d0b9323324d9db7ce82a9ecf29567656ca1 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Thu, 4 Jun 2026 13:32:13 +0100 Subject: [PATCH 1/2] Update TypedDictType.__init__ signature to preserve backward compat The new signature added a new parameter in the middle of the signature, which may break plugins that use positional arguments. Add the new parameter to the end of the parameter list so that existing calls will continue to work. This looks a bit ugly, but backward compatibility is the priority here. --- mypy/copytype.py | 5 ++++- mypy/expandtype.py | 2 +- mypy/exprtotype.py | 2 +- mypy/fastparse.py | 2 +- mypy/join.py | 4 +++- mypy/meet.py | 4 +++- mypy/nativeparse.py | 2 +- mypy/semanal_typeddict.py | 2 +- mypy/test/testtypes.py | 17 +++++++++++++++++ mypy/type_visitor.py | 2 +- mypy/typeanal.py | 2 +- mypy/types.py | 15 +++++++++++---- 12 files changed, 45 insertions(+), 14 deletions(-) diff --git a/mypy/copytype.py b/mypy/copytype.py index 3ec512193bece..12dda9d263975 100644 --- a/mypy/copytype.py +++ b/mypy/copytype.py @@ -107,7 +107,10 @@ def visit_tuple_type(self, t: TupleType) -> ProperType: def visit_typeddict_type(self, t: TypedDictType) -> ProperType: return self.copy_common( - t, TypedDictType(t.items, t.required_keys, t.readonly_keys, t.is_closed, t.fallback) + t, + TypedDictType( + t.items, t.required_keys, t.readonly_keys, t.fallback, is_closed=t.is_closed + ), ) def visit_literal_type(self, t: LiteralType) -> ProperType: diff --git a/mypy/expandtype.py b/mypy/expandtype.py index 186429abd36a9..fd507216a6be9 100644 --- a/mypy/expandtype.py +++ b/mypy/expandtype.py @@ -350,7 +350,7 @@ def _possible_callable_kwargs(cls, repl: Parameters, dict_type: Instance) -> Pro return Instance(dict_type.type, [dict_type.args[0], extra_items]) # TODO: when PEP 728 `extra_items` is implemented, pass extra_items below. is_closed = extra_items is None - return TypedDictType(kwargs, required_names, set(), is_closed, fallback=dict_type) + return TypedDictType(kwargs, required_names, set(), dict_type, is_closed=is_closed) def visit_type_var_tuple(self, t: TypeVarTupleType) -> Type: # Sometimes solver may need to expand a type variable with (a copy of) itself diff --git a/mypy/exprtotype.py b/mypy/exprtotype.py index 5fecf7b6fba9f..1c9323be056dd 100644 --- a/mypy/exprtotype.py +++ b/mypy/exprtotype.py @@ -277,7 +277,7 @@ def expr_to_unanalyzed_type( value, options, allow_new_syntax, expr, lookup_qualified=lookup_qualified ) result = TypedDictType( - items, set(), set(), False, Instance(MISSING_FALLBACK, ()), expr.line, expr.column + items, set(), set(), Instance(MISSING_FALLBACK, ()), expr.line, expr.column ) result.extra_items_from = extra_items_from return result diff --git a/mypy/fastparse.py b/mypy/fastparse.py index c2d2243d7555e..d9e2d5df8f4c1 100644 --- a/mypy/fastparse.py +++ b/mypy/fastparse.py @@ -2143,7 +2143,7 @@ def visit_Dict(self, n: ast3.Dict) -> Type: continue return self.invalid_type(n) items[item_name.value] = self.visit(value) - result = TypedDictType(items, set(), set(), False, _dummy_fallback, n.lineno, n.col_offset) + result = TypedDictType(items, set(), set(), _dummy_fallback, n.lineno, n.col_offset) result.extra_items_from = extra_items_from return result diff --git a/mypy/join.py b/mypy/join.py index b99ed190cc9da..c609b303bb087 100644 --- a/mypy/join.py +++ b/mypy/join.py @@ -663,7 +663,9 @@ def visit_typeddict_type(self, t: TypedDictType) -> ProperType: fallback = self.s.create_anonymous_fallback() is_closed = self.s.is_closed and t.is_closed - return TypedDictType(items, required_keys, readonly_keys, is_closed, fallback) + return TypedDictType( + items, required_keys, readonly_keys, fallback, is_closed=is_closed + ) elif isinstance(self.s, Instance): return join_types(self.s, t.fallback) else: diff --git a/mypy/meet.py b/mypy/meet.py index 57b79c51f8e11..bfc6d88a1e209 100644 --- a/mypy/meet.py +++ b/mypy/meet.py @@ -1186,7 +1186,9 @@ def visit_typeddict_type(self, t: TypedDictType) -> ProperType: fallback = self.s.create_anonymous_fallback() required_keys = self.s.required_keys | t.required_keys - return TypedDictType(items, required_keys, readonly_keys, is_closed, fallback) + return TypedDictType( + items, required_keys, readonly_keys, fallback, is_closed=is_closed + ) elif isinstance(self.s, Instance) and is_subtype(t, self.s): return t else: diff --git a/mypy/nativeparse.py b/mypy/nativeparse.py index f371746cab8b8..414426580fa73 100644 --- a/mypy/nativeparse.py +++ b/mypy/nativeparse.py @@ -926,7 +926,7 @@ def read_type(state: State, data: ReadBuffer) -> Type: extra_items_from.append(val) else: td_items[key] = val - typeddict_type = TypedDictType(td_items, set(), set(), False, _dummy_fallback) + typeddict_type = TypedDictType(td_items, set(), set(), _dummy_fallback) typeddict_type.extra_items_from = extra_items_from read_loc(data, typeddict_type) expect_end_tag(data) diff --git a/mypy/semanal_typeddict.py b/mypy/semanal_typeddict.py index 5f152ba0e8988..b1ba9f6c3abec 100644 --- a/mypy/semanal_typeddict.py +++ b/mypy/semanal_typeddict.py @@ -785,7 +785,7 @@ def build_typeddict_typeinfo( assert fallback is not None info = existing_info or self.api.basic_new_typeinfo(name, fallback, line) typeddict_type = TypedDictType( - item_types, required_keys, readonly_keys, is_closed, fallback + item_types, required_keys, readonly_keys, fallback, is_closed=is_closed ) any_placeholder = has_placeholder(typeddict_type) if typeddict_data: diff --git a/mypy/test/testtypes.py b/mypy/test/testtypes.py index ec9af3e669344..6de922cb7f234 100644 --- a/mypy/test/testtypes.py +++ b/mypy/test/testtypes.py @@ -39,6 +39,7 @@ ProperType, TupleType, Type, + TypedDictType, TypeOfAny, TypeType, TypeVarId, @@ -223,6 +224,22 @@ def test_indirection_no_infinite_recursion(self) -> None: modules = visitor.modules assert modules == {"__main__", "builtins"} + def test_typeddict_type_constructor_signature(self) -> None: + typ = TypedDictType({"x": self.fx.o}, {"x"}, set(), self.fx.a, 10, 20) + + assert typ.fallback is self.fx.a + assert_equal(typ.line, 10) + assert_equal(typ.column, 20) + assert not typ.is_closed + + closed = TypedDictType({"x": self.fx.o}, {"x"}, set(), self.fx.a, is_closed=True) + assert closed.is_closed + + with self.assertRaises(TypeError): + TypedDictType( # type: ignore[call-arg] + {"x": self.fx.o}, {"x"}, set(), self.fx.a, 10, 20, True + ) + class TypeOpsSuite(Suite): def setUp(self) -> None: diff --git a/mypy/type_visitor.py b/mypy/type_visitor.py index 7a486d6603a00..7052c80118710 100644 --- a/mypy/type_visitor.py +++ b/mypy/type_visitor.py @@ -284,11 +284,11 @@ def visit_typeddict_type(self, t: TypedDictType, /) -> Type: items, t.required_keys, t.readonly_keys, - t.is_closed, # TODO: This appears to be unsafe. cast(Any, t.fallback.accept(self)), t.line, t.column, + is_closed=t.is_closed, ) self.set_cached(t, result) return result diff --git a/mypy/typeanal.py b/mypy/typeanal.py index aa5d14bddc65a..ff3c8bd2816e1 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -1416,7 +1416,7 @@ def visit_typeddict_type(self, t: TypedDictType) -> Type: fallback = t.fallback is_closed = t.is_closed return TypedDictType( - items, required_keys, readonly_keys, is_closed, fallback, t.line, t.column + items, required_keys, readonly_keys, fallback, t.line, t.column, is_closed=is_closed ) def visit_raw_expression_type(self, t: RawExpressionType) -> Type: diff --git a/mypy/types.py b/mypy/types.py index 324135df014d3..6e934f64315c0 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -3048,10 +3048,11 @@ def __init__( items: dict[str, Type], required_keys: set[str], readonly_keys: set[str], - is_closed: bool, fallback: Instance, line: int = -1, column: int = -1, + *, + is_closed: bool = False, ) -> None: super().__init__(line, column) self.items = items @@ -3112,8 +3113,8 @@ def deserialize(cls, data: JsonDict) -> TypedDictType: {n: deserialize_type(t) for (n, t) in data["items"]}, set(data["required_keys"]), set(data["readonly_keys"]), - bool(data["is_closed"]), Instance.deserialize(data["fallback"]), + is_closed=bool(data["is_closed"]), ) def write(self, data: WriteBuffer) -> None: @@ -3133,8 +3134,8 @@ def read(cls, data: ReadBuffer) -> TypedDictType: read_type_map(data), set(read_str_list(data)), set(read_str_list(data)), - read_bool(data), fallback, + is_closed=read_bool(data), ) assert read_tag(data) == END_TAG return ret @@ -3178,7 +3179,13 @@ def copy_modified( items = {k: v for (k, v) in items.items() if k in item_names} required_keys &= set(item_names) return TypedDictType( - items, required_keys, readonly_keys, is_closed, fallback, self.line, self.column + items, + required_keys, + readonly_keys, + fallback, + self.line, + self.column, + is_closed=is_closed, ) def zip(self, right: TypedDictType) -> Iterable[tuple[str, Type, Type]]: From bfa3d70ce8534e87914edaead9921ef9f844c400 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Thu, 4 Jun 2026 13:36:51 +0100 Subject: [PATCH 2/2] Fix error code --- mypy/test/testtypes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/test/testtypes.py b/mypy/test/testtypes.py index 6de922cb7f234..ac6d24b1ef4c0 100644 --- a/mypy/test/testtypes.py +++ b/mypy/test/testtypes.py @@ -236,7 +236,7 @@ def test_typeddict_type_constructor_signature(self) -> None: assert closed.is_closed with self.assertRaises(TypeError): - TypedDictType( # type: ignore[call-arg] + TypedDictType( # type: ignore[misc] {"x": self.fx.o}, {"x"}, set(), self.fx.a, 10, 20, True )