From cbea6f83873647f8149021732f6c4af67ea03efc Mon Sep 17 00:00:00 2001 From: Angela Liss <59097311+angela-tarantula@users.noreply.github.com> Date: Sun, 17 Aug 2025 18:33:26 -0400 Subject: [PATCH 01/20] Introduce NoExtraItems as a C-level singleton Implemented _Py_NoExtraItemsStruct and _Py_NoExtraItemsStruct --- Objects/typevarobject.c | 54 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/Objects/typevarobject.c b/Objects/typevarobject.c index cead6e69af5451..403ac547546489 100644 --- a/Objects/typevarobject.c +++ b/Objects/typevarobject.c @@ -123,6 +123,60 @@ PyTypeObject _PyNoDefault_Type = { PyObject _Py_NoDefaultStruct = _PyObject_HEAD_INIT(&_PyNoDefault_Type); +/* NoExtraItems: a marker object for TypeDict extra-items when it's unset. */ + +static PyObject * +NoExtraItems_repr(PyObject *op) +{ + return PyUnicode_FromString("typing.NoExtraItems"); +} + +static PyObject * +NoExtraItems_reduce(PyObject *op, PyObject *Py_UNUSED(ignored)) +{ + return PyUnicode_FromString("NoExtraItems"); +} + +static PyMethodDef noextraitems_methods[] = { + {"__reduce__", NoExtraItems_reduce, METH_NOARGS, NULL}, + {NULL, NULL} +}; + +static PyObject * +noextraitems_new(PyTypeObject *type, PyObject *args, PyObject *kwargs) +{ + if (PyTuple_GET_SIZE(args) || (kwargs && PyDict_GET_SIZE(kwargs))) { + PyErr_SetString(PyExc_TypeError, "NoExtraItemsType takes no arguments"); + return NULL; + } + return (PyObject *)&_Py_NoExtraItemsStruct; +} + +static void +noextraitems_dealloc(PyObject *obj) +{ + /* Immortal singleton: never actually deallocates. */ + _Py_SetImmortal(obj); +} + +PyDoc_STRVAR(noextraitems_doc, +"NoExtraItemsType()\n" +"--\n\n" +"The type of the NoExtraItems singleton."); + +PyTypeObject _PyNoExtraItems_Type = { + PyVarObject_HEAD_INIT(&PyType_Type, 0) + "NoExtraItemsType", + .tp_dealloc = noextraitems_dealloc, + .tp_repr = NoExtraItems_repr, + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_doc = noextraitems_doc, + .tp_methods = noextraitems_methods, + .tp_new = noextraitems_new, +}; + +PyObject _Py_NoExtraItemsStruct = _PyObject_HEAD_INIT(&_PyNoExtraItems_Type); + typedef struct { PyObject_HEAD PyObject *value; From 4cde2f923cd58035710cd6ee06c19f2103555e35 Mon Sep 17 00:00:00 2001 From: Angela Liss <59097311+angela-tarantula@users.noreply.github.com> Date: Sun, 17 Aug 2025 18:47:52 -0400 Subject: [PATCH 02/20] expose NoExtraItems via _typing --- Include/internal/pycore_typevarobject.h | 2 ++ Modules/_typingmodule.c | 3 +++ 2 files changed, 5 insertions(+) diff --git a/Include/internal/pycore_typevarobject.h b/Include/internal/pycore_typevarobject.h index 4d7556e68cdaee..255b716846f535 100644 --- a/Include/internal/pycore_typevarobject.h +++ b/Include/internal/pycore_typevarobject.h @@ -21,6 +21,8 @@ extern int _Py_typing_type_repr(PyUnicodeWriter *, PyObject *); extern PyTypeObject _PyTypeAlias_Type; extern PyTypeObject _PyNoDefault_Type; extern PyObject _Py_NoDefaultStruct; +extern PyTypeObject _PyNoExtraItems_Type; +extern PyObject _Py_NoExtraItemsStruct; #ifdef __cplusplus } diff --git a/Modules/_typingmodule.c b/Modules/_typingmodule.c index e51279c808a2e1..89d2a956eeb721 100644 --- a/Modules/_typingmodule.c +++ b/Modules/_typingmodule.c @@ -70,6 +70,9 @@ _typing_exec(PyObject *m) if (PyModule_AddObjectRef(m, "NoDefault", (PyObject *)&_Py_NoDefaultStruct) < 0) { return -1; } + if (PyModule_AddObjectRef(m, "NoExtraItems", (PyObject *)&_Py_NoExtraItemsStruct) < 0) { + return -1; + } return 0; } From 06de7a94006a33569ddaf9274368a2c925b5bf32 Mon Sep 17 00:00:00 2001 From: Angela Liss <59097311+angela-tarantula@users.noreply.github.com> Date: Sun, 17 Aug 2025 19:02:38 -0400 Subject: [PATCH 03/20] Clearer comments in the C internals --- Objects/typevarobject.c | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Objects/typevarobject.c b/Objects/typevarobject.c index 403ac547546489..a09c005ac0667f 100644 --- a/Objects/typevarobject.c +++ b/Objects/typevarobject.c @@ -123,7 +123,7 @@ PyTypeObject _PyNoDefault_Type = { PyObject _Py_NoDefaultStruct = _PyObject_HEAD_INIT(&_PyNoDefault_Type); -/* NoExtraItems: a marker object for TypeDict extra-items when it's unset. */ +/* NoExtraItems: a marker object for TypeDict extra_items when it's unset. */ static PyObject * NoExtraItems_repr(PyObject *op) @@ -155,7 +155,10 @@ noextraitems_new(PyTypeObject *type, PyObject *args, PyObject *kwargs) static void noextraitems_dealloc(PyObject *obj) { - /* Immortal singleton: never actually deallocates. */ + /* This should never get called, but we also don't want to SEGV if + * we accidentally decref NoDefault out of existence. Instead, + * since NoDefault is an immortal object, re-set the reference count. + */ _Py_SetImmortal(obj); } From 407bf7488d83ac5bd60541dc6230990c1d50d2ab Mon Sep 17 00:00:00 2001 From: Angela Liss <59097311+angela-tarantula@users.noreply.github.com> Date: Sun, 17 Aug 2025 19:08:34 -0400 Subject: [PATCH 04/20] Import NoExtraItems into typing --- Lib/typing.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Lib/typing.py b/Lib/typing.py index 8c1d265019bb94..19b0a528c0252f 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -40,6 +40,7 @@ Generic, Union, NoDefault, + NoExtraItems, ) # Please keep __all__ alphabetized within each category. @@ -141,6 +142,7 @@ 'no_type_check', 'no_type_check_decorator', 'NoDefault', + 'NoExtraItems', 'NoReturn', 'NotRequired', 'overload', From f9755d3e05f54478f323cf88ae941e4d9391d56f Mon Sep 17 00:00:00 2001 From: Angela Liss <59097311+angela-tarantula@users.noreply.github.com> Date: Sun, 17 Aug 2025 19:18:55 -0400 Subject: [PATCH 05/20] Update _TypedDictMeta and TypedDict They now both accept `closed` and `extra_items` as parameters. For introspection, these arguments are also mapped to 2 new attributes: `__closed__` and `__extra_items__`. --- Lib/typing.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/Lib/typing.py b/Lib/typing.py index 19b0a528c0252f..441a64dfb0ad2e 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -3080,7 +3080,8 @@ def _get_typeddict_qualifiers(annotation_type): class _TypedDictMeta(type): - def __new__(cls, name, bases, ns, total=True): + def __new__(cls, name, bases, ns, total=True, closed=None, + extra_items=NoExtraItems): """Create a new typed dict class object. This method is called when TypedDict is subclassed, @@ -3092,6 +3093,8 @@ def __new__(cls, name, bases, ns, total=True): if type(base) is not _TypedDictMeta and base is not Generic: raise TypeError('cannot inherit from both a TypedDict type ' 'and a non-TypedDict base class') + if closed is not None and extra_items is not NoExtraItems: + raise TypeError(f"Cannot combine closed={closed!r} and extra_items") if any(issubclass(b, Generic) for b in bases): generic_base = (Generic,) @@ -3203,6 +3206,8 @@ def __annotate__(format): tp_dict.__readonly_keys__ = frozenset(readonly_keys) tp_dict.__mutable_keys__ = frozenset(mutable_keys) tp_dict.__total__ = total + tp_dict.__closed__ = closed + tp_dict.__extra_items__ = extra_items return tp_dict __call__ = dict # static method @@ -3214,7 +3219,8 @@ def __subclasscheck__(cls, other): __instancecheck__ = __subclasscheck__ -def TypedDict(typename, fields, /, *, total=True): +def TypedDict(typename, fields, /, *, total=True, closed=None, + extra_items=NoExtraItems): """A simple typed namespace. At runtime it is equivalent to a plain dict. TypedDict creates a dictionary type such that a type checker will expect all @@ -3275,7 +3281,8 @@ class DatabaseUser(TypedDict): # Setting correct module is necessary to make typed dict classes pickleable. ns['__module__'] = module - td = _TypedDictMeta(typename, (), ns, total=total) + td = _TypedDictMeta(typename, (), ns, total=total, closed=closed, + extra_items=extra_items) td.__orig_bases__ = (TypedDict,) return td From 385812a5294956baba3cb653f9b1f0f22b08fb96 Mon Sep 17 00:00:00 2001 From: Angela Liss <59097311+angela-tarantula@users.noreply.github.com> Date: Sun, 17 Aug 2025 19:37:32 -0400 Subject: [PATCH 06/20] fix typo --- Objects/typevarobject.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Objects/typevarobject.c b/Objects/typevarobject.c index a09c005ac0667f..540811000dd9be 100644 --- a/Objects/typevarobject.c +++ b/Objects/typevarobject.c @@ -156,7 +156,7 @@ static void noextraitems_dealloc(PyObject *obj) { /* This should never get called, but we also don't want to SEGV if - * we accidentally decref NoDefault out of existence. Instead, + * we accidentally decref NoExtraItems out of existence. Instead, * since NoDefault is an immortal object, re-set the reference count. */ _Py_SetImmortal(obj); From bdbae49abca5b645007edce6aca500a292bf2ffc Mon Sep 17 00:00:00 2001 From: Angela Liss <59097311+angela-tarantula@users.noreply.github.com> Date: Sun, 17 Aug 2025 19:57:55 -0400 Subject: [PATCH 07/20] Port over the relevant tests --- Lib/test/test_typing.py | 99 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 98 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 6317d4657619f0..8c6cc2c08b68dc 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -41,7 +41,7 @@ from typing import Self, LiteralString from typing import TypeAlias from typing import ParamSpec, Concatenate, ParamSpecArgs, ParamSpecKwargs -from typing import TypeGuard, TypeIs, NoDefault +from typing import TypeGuard, TypeIs, NoDefault, NoExtraItems import abc import textwrap import typing @@ -8770,6 +8770,32 @@ class ChildWithInlineAndOptional(Untotal, Inline): class Wrong(*bases): pass + def test_closed_values(self): + class Implicit(TypedDict): ... + class ExplicitTrue(TypedDict, closed=True): ... + class ExplicitFalse(TypedDict, closed=False): ... + + self.assertIsNone(Implicit.__closed__) + self.assertIs(ExplicitTrue.__closed__, True) + self.assertIs(ExplicitFalse.__closed__, False) + + def test_extra_items_class_arg(self): + class TD(TypedDict, extra_items=int): + a: str + + self.assertIs(TD.__extra_items__, int) + self.assertEqual(TD.__annotations__, {'a': str}) + self.assertEqual(TD.__required_keys__, frozenset({'a'})) + self.assertEqual(TD.__optional_keys__, frozenset()) + + class NoExtra(TypedDict): + a: str + + self.assertIs(NoExtra.__extra_items__, NoExtraItems) + self.assertEqual(NoExtra.__annotations__, {'a': str}) + self.assertEqual(NoExtra.__required_keys__, frozenset({'a'})) + self.assertEqual(NoExtra.__optional_keys__, frozenset()) + def test_is_typeddict(self): self.assertIs(is_typeddict(Point2D), True) self.assertIs(is_typeddict(Union[str, int]), False) @@ -9097,6 +9123,71 @@ class AllTheThings(TypedDict): }, ) + def test_closed_inheritance(self): + class Base(TypedDict, extra_items=ReadOnly[Union[str, None]]): + a: int + + self.assertEqual(Base.__required_keys__, frozenset({"a"})) + self.assertEqual(Base.__optional_keys__, frozenset({})) + self.assertEqual(Base.__readonly_keys__, frozenset({})) + self.assertEqual(Base.__mutable_keys__, frozenset({"a"})) + self.assertEqual(Base.__annotations__, {"a": int}) + self.assertEqual(Base.__extra_items__, ReadOnly[Union[str, None]]) + self.assertIsNone(Base.__closed__) + + class Child(Base, extra_items=int): + a: str + + self.assertEqual(Child.__required_keys__, frozenset({'a'})) + self.assertEqual(Child.__optional_keys__, frozenset({})) + self.assertEqual(Child.__readonly_keys__, frozenset({})) + self.assertEqual(Child.__mutable_keys__, frozenset({'a'})) + self.assertEqual(Child.__annotations__, {"a": str}) + self.assertIs(Child.__extra_items__, int) + self.assertIsNone(Child.__closed__) + + class GrandChild(Child, closed=True): + a: float + + self.assertEqual(GrandChild.__required_keys__, frozenset({'a'})) + self.assertEqual(GrandChild.__optional_keys__, frozenset({})) + self.assertEqual(GrandChild.__readonly_keys__, frozenset({})) + self.assertEqual(GrandChild.__mutable_keys__, frozenset({'a'})) + self.assertEqual(GrandChild.__annotations__, {"a": float}) + self.assertIs(GrandChild.__extra_items__, NoExtraItems) + self.assertIs(GrandChild.__closed__, True) + + class GrandGrandChild(GrandChild): + ... + self.assertEqual(GrandGrandChild.__required_keys__, frozenset({'a'})) + self.assertEqual(GrandGrandChild.__optional_keys__, frozenset({})) + self.assertEqual(GrandGrandChild.__readonly_keys__, frozenset({})) + self.assertEqual(GrandGrandChild.__mutable_keys__, frozenset({'a'})) + self.assertEqual(GrandGrandChild.__annotations__, {"a": float}) + self.assertIs(GrandGrandChild.__extra_items__, NoExtraItems) + self.assertIsNone(GrandGrandChild.__closed__) + + def test_implicit_extra_items(self): + class Base(TypedDict): + a: int + + self.assertIs(Base.__extra_items__, NoExtraItems) + self.assertIsNone(Base.__closed__) + + class ChildA(Base, closed=True): + ... + + self.assertEqual(ChildA.__extra_items__, NoExtraItems) + self.assertIs(ChildA.__closed__, True) + + def test_cannot_combine_closed_and_extra_items(self): + with self.assertRaisesRegex( + TypeError, + "Cannot combine closed=True and extra_items" + ): + class TD(TypedDict, closed=True, extra_items=range): + x: str + def test_annotations(self): # _type_check is applied with self.assertRaisesRegex(TypeError, "Plain typing.Final is not valid as type argument"): @@ -9326,6 +9417,12 @@ class A(typing.Match): class B(typing.Pattern): pass + def test_typed_dict_signature(self): + self.assertListEqual( + list(inspect.signature(TypedDict).parameters), + ['typename', 'fields', 'total', 'closed', 'extra_items'] + ) + class AnnotatedTests(BaseTestCase): From e68c2edf9630855f73c7a781b96e8510f93f246f Mon Sep 17 00:00:00 2001 From: Angela Liss <59097311+angela-tarantula@users.noreply.github.com> Date: Sun, 17 Aug 2025 23:23:09 -0400 Subject: [PATCH 08/20] Updated docstring and error messages --- Lib/typing.py | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/Lib/typing.py b/Lib/typing.py index 441a64dfb0ad2e..b68db337e28e4f 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -3091,10 +3091,10 @@ def __new__(cls, name, bases, ns, total=True, closed=None, """ for base in bases: if type(base) is not _TypedDictMeta and base is not Generic: - raise TypeError('cannot inherit from both a TypedDict type ' + raise TypeError('Cannot inherit from both a TypedDict type ' 'and a non-TypedDict base class') if closed is not None and extra_items is not NoExtraItems: - raise TypeError(f"Cannot combine closed={closed!r} and extra_items") + raise TypeError(f"Cannot use both closed and extra_items") if any(issubclass(b, Generic) for b in bases): generic_base = (Generic,) @@ -3274,6 +3274,26 @@ class DatabaseUser(TypedDict): id: ReadOnly[int] # the "id" key must not be modified username: str # the "username" key can be changed + The *closed* argument controls whether the TypedDict allows additional + non-required items during inheritance and assignability checks. + If closed=True, the TypedDict is closed to additional items:: + + Point2D = TypedDict('Point2D', {'x': int, 'y': int}, closed=True) + class Point3D(Point2D): + z: int # Type checker error + + Passing closed=False explicitly requests TypedDict's default open behavior. + If closed is not provided, the behavior is inherited from the superclass. + + The *extra_items* argument can instead be used to specify the type of + additional non-required keys:: + + Point2D = TypedDict('Point2D', {'x': int, 'y': int}, extra_items=int) + class Point3D(Point2D): + z: int # OK + label: str # Type checker error + + See PEP 728 for more information about closed and extra_items. """ ns = {'__annotations__': dict(fields)} module = _caller() From 8203bc19104b10842008b848da3332f7279cacfc Mon Sep 17 00:00:00 2001 From: Angela Liss <59097311+angela-tarantula@users.noreply.github.com> Date: Mon, 18 Aug 2025 07:11:57 -0400 Subject: [PATCH 09/20] NEWS entry --- .../Library/2025-08-18-07-10-55.gh-issue-137840.9b7AnG.rst | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2025-08-18-07-10-55.gh-issue-137840.9b7AnG.rst diff --git a/Misc/NEWS.d/next/Library/2025-08-18-07-10-55.gh-issue-137840.9b7AnG.rst b/Misc/NEWS.d/next/Library/2025-08-18-07-10-55.gh-issue-137840.9b7AnG.rst new file mode 100644 index 00000000000000..5ea25a86204318 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-08-18-07-10-55.gh-issue-137840.9b7AnG.rst @@ -0,0 +1,3 @@ +:class:`typing.TypedDict` now supports the ``closed`` and ``extra_items`` +keyword arguments (as described in :pep:`728`) to control whether additional +non-required keys are allowed and to specify their value type. From 1b3ce5258fdd217ba9f56939c77210a8fc3f0a0c Mon Sep 17 00:00:00 2001 From: Angela Liss <59097311+angela-tarantula@users.noreply.github.com> Date: Mon, 18 Aug 2025 22:44:35 -0400 Subject: [PATCH 10/20] New draft of docstring and error message --- Lib/typing.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/Lib/typing.py b/Lib/typing.py index b68db337e28e4f..d7150d09017352 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -3094,7 +3094,7 @@ def __new__(cls, name, bases, ns, total=True, closed=None, raise TypeError('Cannot inherit from both a TypedDict type ' 'and a non-TypedDict base class') if closed is not None and extra_items is not NoExtraItems: - raise TypeError(f"Cannot use both closed and extra_items") + raise TypeError(f"Closed cannot be specified with extra_items") if any(issubclass(b, Generic) for b in bases): generic_base = (Generic,) @@ -3274,7 +3274,7 @@ class DatabaseUser(TypedDict): id: ReadOnly[int] # the "id" key must not be modified username: str # the "username" key can be changed - The *closed* argument controls whether the TypedDict allows additional + The closed argument controls whether the TypedDict allows additional non-required items during inheritance and assignability checks. If closed=True, the TypedDict is closed to additional items:: @@ -3284,15 +3284,21 @@ class Point3D(Point2D): Passing closed=False explicitly requests TypedDict's default open behavior. If closed is not provided, the behavior is inherited from the superclass. + A type checker is only expected to support a literal False or True as the + value of the closed argument. - The *extra_items* argument can instead be used to specify the type of - additional non-required keys:: + The extra_items argument can instead be used to specify the assignable type + of unknown non-required keys:: Point2D = TypedDict('Point2D', {'x': int, 'y': int}, extra_items=int) class Point3D(Point2D): z: int # OK label: str # Type checker error + The extra_items argument is also inherited through subclassing. It is unset + by default, and it may not be used with the closed argument at the same + time. + See PEP 728 for more information about closed and extra_items. """ ns = {'__annotations__': dict(fields)} From 2f5c42e37fd5fd4f6ce4f43f402ce6535bf8eef2 Mon Sep 17 00:00:00 2001 From: Angela Liss <59097311+angela-tarantula@users.noreply.github.com> Date: Mon, 18 Aug 2025 22:49:10 -0400 Subject: [PATCH 11/20] just use the original error message --- Lib/typing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/typing.py b/Lib/typing.py index d7150d09017352..1a7c0e6eef295e 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -3094,7 +3094,7 @@ def __new__(cls, name, bases, ns, total=True, closed=None, raise TypeError('Cannot inherit from both a TypedDict type ' 'and a non-TypedDict base class') if closed is not None and extra_items is not NoExtraItems: - raise TypeError(f"Closed cannot be specified with extra_items") + raise TypeError(f"Cannot combine closed={closed!r} and extra_items") if any(issubclass(b, Generic) for b in bases): generic_base = (Generic,) From 1bd3a6d3d88a87f6707ba71f2ed3cb6af443c587 Mon Sep 17 00:00:00 2001 From: Angela Liss <59097311+angela-tarantula@users.noreply.github.com> Date: Mon, 18 Aug 2025 23:25:10 -0400 Subject: [PATCH 12/20] fix typo --- Objects/typevarobject.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Objects/typevarobject.c b/Objects/typevarobject.c index 540811000dd9be..66f0b5575baf64 100644 --- a/Objects/typevarobject.c +++ b/Objects/typevarobject.c @@ -123,7 +123,7 @@ PyTypeObject _PyNoDefault_Type = { PyObject _Py_NoDefaultStruct = _PyObject_HEAD_INIT(&_PyNoDefault_Type); -/* NoExtraItems: a marker object for TypeDict extra_items when it's unset. */ +/* NoExtraItems: a marker object for TypeDict extra_items when its unset. */ static PyObject * NoExtraItems_repr(PyObject *op) @@ -157,7 +157,7 @@ noextraitems_dealloc(PyObject *obj) { /* This should never get called, but we also don't want to SEGV if * we accidentally decref NoExtraItems out of existence. Instead, - * since NoDefault is an immortal object, re-set the reference count. + * since NoExtraItems is an immortal object, re-set the reference count. */ _Py_SetImmortal(obj); } From 42a1aac75927dde9911867a0d0da23b6ebf1c77e Mon Sep 17 00:00:00 2001 From: Angela Liss <59097311+angela-tarantula@users.noreply.github.com> Date: Mon, 18 Aug 2025 23:35:22 -0400 Subject: [PATCH 13/20] undo capitalization --- Lib/typing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/typing.py b/Lib/typing.py index 1a7c0e6eef295e..7c45563be720a3 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -3091,7 +3091,7 @@ def __new__(cls, name, bases, ns, total=True, closed=None, """ for base in bases: if type(base) is not _TypedDictMeta and base is not Generic: - raise TypeError('Cannot inherit from both a TypedDict type ' + raise TypeError('cannot inherit from both a TypedDict type ' 'and a non-TypedDict base class') if closed is not None and extra_items is not NoExtraItems: raise TypeError(f"Cannot combine closed={closed!r} and extra_items") From 36ee4d2f4594087dbf3bc8ae6a1acb7a0e1ea768 Mon Sep 17 00:00:00 2001 From: Angela Liss <59097311+angela-tarantula@users.noreply.github.com> Date: Tue, 19 Aug 2025 00:47:21 -0400 Subject: [PATCH 14/20] fix typo x2 --- Objects/typevarobject.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Objects/typevarobject.c b/Objects/typevarobject.c index 66f0b5575baf64..bb13fa9513afe6 100644 --- a/Objects/typevarobject.c +++ b/Objects/typevarobject.c @@ -123,7 +123,7 @@ PyTypeObject _PyNoDefault_Type = { PyObject _Py_NoDefaultStruct = _PyObject_HEAD_INIT(&_PyNoDefault_Type); -/* NoExtraItems: a marker object for TypeDict extra_items when its unset. */ +/* NoExtraItems: a marker object for TypeDict extra_items when it's unset. */ static PyObject * NoExtraItems_repr(PyObject *op) From 503b82584f1c73bd7aea45906c549b03f72b6140 Mon Sep 17 00:00:00 2001 From: Angela Liss <59097311+angela-tarantula@users.noreply.github.com> Date: Tue, 19 Aug 2025 23:52:02 -0400 Subject: [PATCH 15/20] fix typo x3 --- Objects/typevarobject.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Objects/typevarobject.c b/Objects/typevarobject.c index bb13fa9513afe6..8ffa00c332dddb 100644 --- a/Objects/typevarobject.c +++ b/Objects/typevarobject.c @@ -123,7 +123,7 @@ PyTypeObject _PyNoDefault_Type = { PyObject _Py_NoDefaultStruct = _PyObject_HEAD_INIT(&_PyNoDefault_Type); -/* NoExtraItems: a marker object for TypeDict extra_items when it's unset. */ +/* NoExtraItems: a marker object for TypedDict extra_items when it's unset. */ static PyObject * NoExtraItems_repr(PyObject *op) From 1f23caa67a2b8309123062af8e55d819edd0c470 Mon Sep 17 00:00:00 2001 From: Angela Liss <59097311+angela-tarantula@users.noreply.github.com> Date: Wed, 20 Aug 2025 00:49:41 -0400 Subject: [PATCH 16/20] Hush the c-analyzer: NoExtraItems is a singleton --- Tools/c-analyzer/cpython/ignored.tsv | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Tools/c-analyzer/cpython/ignored.tsv b/Tools/c-analyzer/cpython/ignored.tsv index 4fdb7b3cd1abf2..f686a1f9a9a380 100644 --- a/Tools/c-analyzer/cpython/ignored.tsv +++ b/Tools/c-analyzer/cpython/ignored.tsv @@ -344,6 +344,8 @@ Objects/obmalloc.c - obmalloc_state_main - Objects/obmalloc.c - obmalloc_state_initialized - Objects/typeobject.c - name_op - Objects/typeobject.c - slotdefs - +Objects/typevarobject.c - _PyNoExtraItems_Type - +Objects/typevarobject.c - _Py_NoExtraItemsStruct - Objects/unicodeobject.c - stripfuncnames - Objects/unicodeobject.c - utf7_category - Objects/unicodeobject.c unicode_decode_call_errorhandler_wchar argparse - From 9a9c5c59a2bbd202fb8bb86b6e406affe49124d5 Mon Sep 17 00:00:00 2001 From: Angela Liss <59097311+angela-tarantula@users.noreply.github.com> Date: Mon, 25 Aug 2025 21:21:17 -0400 Subject: [PATCH 17/20] remove c-level NoExtraItems singleton --- Include/internal/pycore_typevarobject.h | 2 - Lib/test/test_typing.py | 2 +- Lib/typing.py | 1 - Modules/_typingmodule.c | 3 -- Objects/typevarobject.c | 57 ------------------------- Tools/c-analyzer/cpython/ignored.tsv | 2 - 6 files changed, 1 insertion(+), 66 deletions(-) diff --git a/Include/internal/pycore_typevarobject.h b/Include/internal/pycore_typevarobject.h index 255b716846f535..4d7556e68cdaee 100644 --- a/Include/internal/pycore_typevarobject.h +++ b/Include/internal/pycore_typevarobject.h @@ -21,8 +21,6 @@ extern int _Py_typing_type_repr(PyUnicodeWriter *, PyObject *); extern PyTypeObject _PyTypeAlias_Type; extern PyTypeObject _PyNoDefault_Type; extern PyObject _Py_NoDefaultStruct; -extern PyTypeObject _PyNoExtraItems_Type; -extern PyObject _Py_NoExtraItemsStruct; #ifdef __cplusplus } diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 8c6cc2c08b68dc..d1c305b3b9c10c 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -41,7 +41,7 @@ from typing import Self, LiteralString from typing import TypeAlias from typing import ParamSpec, Concatenate, ParamSpecArgs, ParamSpecKwargs -from typing import TypeGuard, TypeIs, NoDefault, NoExtraItems +from typing import TypeGuard, TypeIs, NoDefault import abc import textwrap import typing diff --git a/Lib/typing.py b/Lib/typing.py index 7c45563be720a3..268537b2f87136 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -40,7 +40,6 @@ Generic, Union, NoDefault, - NoExtraItems, ) # Please keep __all__ alphabetized within each category. diff --git a/Modules/_typingmodule.c b/Modules/_typingmodule.c index 89d2a956eeb721..e51279c808a2e1 100644 --- a/Modules/_typingmodule.c +++ b/Modules/_typingmodule.c @@ -70,9 +70,6 @@ _typing_exec(PyObject *m) if (PyModule_AddObjectRef(m, "NoDefault", (PyObject *)&_Py_NoDefaultStruct) < 0) { return -1; } - if (PyModule_AddObjectRef(m, "NoExtraItems", (PyObject *)&_Py_NoExtraItemsStruct) < 0) { - return -1; - } return 0; } diff --git a/Objects/typevarobject.c b/Objects/typevarobject.c index 8ffa00c332dddb..cead6e69af5451 100644 --- a/Objects/typevarobject.c +++ b/Objects/typevarobject.c @@ -123,63 +123,6 @@ PyTypeObject _PyNoDefault_Type = { PyObject _Py_NoDefaultStruct = _PyObject_HEAD_INIT(&_PyNoDefault_Type); -/* NoExtraItems: a marker object for TypedDict extra_items when it's unset. */ - -static PyObject * -NoExtraItems_repr(PyObject *op) -{ - return PyUnicode_FromString("typing.NoExtraItems"); -} - -static PyObject * -NoExtraItems_reduce(PyObject *op, PyObject *Py_UNUSED(ignored)) -{ - return PyUnicode_FromString("NoExtraItems"); -} - -static PyMethodDef noextraitems_methods[] = { - {"__reduce__", NoExtraItems_reduce, METH_NOARGS, NULL}, - {NULL, NULL} -}; - -static PyObject * -noextraitems_new(PyTypeObject *type, PyObject *args, PyObject *kwargs) -{ - if (PyTuple_GET_SIZE(args) || (kwargs && PyDict_GET_SIZE(kwargs))) { - PyErr_SetString(PyExc_TypeError, "NoExtraItemsType takes no arguments"); - return NULL; - } - return (PyObject *)&_Py_NoExtraItemsStruct; -} - -static void -noextraitems_dealloc(PyObject *obj) -{ - /* This should never get called, but we also don't want to SEGV if - * we accidentally decref NoExtraItems out of existence. Instead, - * since NoExtraItems is an immortal object, re-set the reference count. - */ - _Py_SetImmortal(obj); -} - -PyDoc_STRVAR(noextraitems_doc, -"NoExtraItemsType()\n" -"--\n\n" -"The type of the NoExtraItems singleton."); - -PyTypeObject _PyNoExtraItems_Type = { - PyVarObject_HEAD_INIT(&PyType_Type, 0) - "NoExtraItemsType", - .tp_dealloc = noextraitems_dealloc, - .tp_repr = NoExtraItems_repr, - .tp_flags = Py_TPFLAGS_DEFAULT, - .tp_doc = noextraitems_doc, - .tp_methods = noextraitems_methods, - .tp_new = noextraitems_new, -}; - -PyObject _Py_NoExtraItemsStruct = _PyObject_HEAD_INIT(&_PyNoExtraItems_Type); - typedef struct { PyObject_HEAD PyObject *value; diff --git a/Tools/c-analyzer/cpython/ignored.tsv b/Tools/c-analyzer/cpython/ignored.tsv index f686a1f9a9a380..4fdb7b3cd1abf2 100644 --- a/Tools/c-analyzer/cpython/ignored.tsv +++ b/Tools/c-analyzer/cpython/ignored.tsv @@ -344,8 +344,6 @@ Objects/obmalloc.c - obmalloc_state_main - Objects/obmalloc.c - obmalloc_state_initialized - Objects/typeobject.c - name_op - Objects/typeobject.c - slotdefs - -Objects/typevarobject.c - _PyNoExtraItems_Type - -Objects/typevarobject.c - _Py_NoExtraItemsStruct - Objects/unicodeobject.c - stripfuncnames - Objects/unicodeobject.c - utf7_category - Objects/unicodeobject.c unicode_decode_call_errorhandler_wchar argparse - From 18b929bb058f591ef906c00adc87503daf492c24 Mon Sep 17 00:00:00 2001 From: Angela Liss <59097311+angela-tarantula@users.noreply.github.com> Date: Mon, 25 Aug 2025 21:47:57 -0400 Subject: [PATCH 18/20] Update Lib/typing.py Co-authored-by: Jelle Zijlstra --- Lib/typing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/typing.py b/Lib/typing.py index 268537b2f87136..312f3d40fd58ac 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -3275,7 +3275,7 @@ class DatabaseUser(TypedDict): The closed argument controls whether the TypedDict allows additional non-required items during inheritance and assignability checks. - If closed=True, the TypedDict is closed to additional items:: + If closed=True, the TypedDict does not allow additional items:: Point2D = TypedDict('Point2D', {'x': int, 'y': int}, closed=True) class Point3D(Point2D): From 611b3e6c91afb71434dda8d832b67e695a8f52e7 Mon Sep 17 00:00:00 2001 From: Angela Liss <59097311+angela-tarantula@users.noreply.github.com> Date: Mon, 25 Aug 2025 22:36:49 -0400 Subject: [PATCH 19/20] define NoExtraItems singleton in Python --- Lib/test/test_typing.py | 2 +- Lib/typing.py | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index d1c305b3b9c10c..8362664f101055 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -34,7 +34,7 @@ from typing import dataclass_transform from typing import no_type_check, no_type_check_decorator from typing import Type -from typing import NamedTuple, NotRequired, Required, ReadOnly, TypedDict +from typing import NamedTuple, NotRequired, Required, ReadOnly, TypedDict, NoExtraItems from typing import IO, TextIO, BinaryIO from typing import Pattern, Match from typing import Annotated, ForwardRef diff --git a/Lib/typing.py b/Lib/typing.py index 312f3d40fd58ac..8ba731efc939d0 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -3056,6 +3056,20 @@ def _namedtuple_mro_entries(bases): NamedTuple.__mro_entries__ = _namedtuple_mro_entries +class _NoExtraItemsType: + """The type of the NoExtraItems singleton.""" + + __slots__ = () + + def __repr__(self): + return 'typing.NoExtraItems' + + def __reduce__(self): + return 'NoExtraItems' + +NoExtraItems = _NoExtraItemsType() + + def _get_typeddict_qualifiers(annotation_type): while True: annotation_origin = get_origin(annotation_type) From a6250f4730796c65180d49839e7a8c872c27a104 Mon Sep 17 00:00:00 2001 From: Angela Liss <59097311+angela-tarantula@users.noreply.github.com> Date: Tue, 26 Aug 2025 08:05:46 -0400 Subject: [PATCH 20/20] enforce singleton qualities on NoExtraItems --- Lib/typing.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/Lib/typing.py b/Lib/typing.py index 8ba731efc939d0..cc506e6338b13d 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -3056,11 +3056,22 @@ def _namedtuple_mro_entries(bases): NamedTuple.__mro_entries__ = _namedtuple_mro_entries -class _NoExtraItemsType: +class _SingletonMeta(type): + def __setattr__(cls, attr, value): + # TypeError is consistent with the behavior of NoneType + raise TypeError( + f"cannot set {attr!r} attribute of immutable type {cls.__name__!r}" + ) + + +class _NoExtraItemsType(metaclass=_SingletonMeta): """The type of the NoExtraItems singleton.""" __slots__ = () + def __new__(cls): + return globals().get("NoExtraItems") or object.__new__(cls) + def __repr__(self): return 'typing.NoExtraItems' @@ -3068,6 +3079,8 @@ def __reduce__(self): return 'NoExtraItems' NoExtraItems = _NoExtraItemsType() +del _NoExtraItemsType +del _SingletonMeta def _get_typeddict_qualifiers(annotation_type):