GeezerVison is showing github.com — Implement PEP 728: TypedDict with Typed Extra Items · Issue #137840 · python/cpython
Implement PEP 728: TypedDict with Typed Extra Items #137840DescriptionFeature or enhancement
Proposal:
PEP-728 was just accepted by the Steering Council, so we get to implement it in CPython!
The implementation can likely be largely copied from typing-extensions.
cc @PIG208.
Has this already been discussed elsewhere?
No response given
Links to previous discussion of this feature:
No response
Linked PRs
Reacted by Alex Waygood, Zixuan Li, Edgar Ramírez Mondragón, Shahriar Heidrich, Lucas Colombo and Nick Murphy Activityangela-tarantula commented on Aug 17, 2025 I’d be happy to take this on and finish it, if that works for you, @JelleZijlstra and @PIG208. I’m really excited about this upgrade and would like to keep contributing to CPython.
Plan
- Port the core
extra_items functionality from typing_extensions into stdlib typing.
- Exclude extensions-only code:
- Backwards-compatibility paths (e.g.
__extra_items__ special annotation).
- The TypedDict[{...}] special form (
_TypedDictSpecialForm), which the PEP does not specify.
- Introduce
typing.NoExtraItems as a C-level immortal singleton (mirroring NoDefault), to be exported via _typing.
- Ensures process-wide singleton identity, consistency across typing, etc.
- Update
_TypedDictMeta and TypedDict to accept closed and extra_items, and set __closed__ and __extra_items__ on the resulting type.
- Port the relevant tests from test_typing_extensions:
test_closed_values, test_extra_items_class_arg, test_closed_inheritance, test_implicit_extra_items, test_cannot_combine_closed_and_extra_items, plus test_typed_dict_signature (adjusted to remove kwargs).
- Update documentation, maybe in a separate PR?
Open question about closed:
- Should the runtime error for when
closed and extra_items are "used at the same time" also trigger when closed=False is passed explicitly, or only when closed=True?
Reacted by Zixuan Li Reacted by Jelle Zijlstra Reacted by Zixuan Li PIG208 commented on Aug 17, 2025
Should the runtime error for whenclosed and extra_items are "used at the same time" also trigger when closed=False is passed explicitly, or only when closed=True?
There should be an error whenever both are specified at the same time, according to the section for closed:
It is a runtime error to use the closed and extra_items parameters in the same TypedDict definition.
angela-tarantula commented on Aug 17, 2025 Thanks. I'll prepare a PR! JelleZijlstra commented on Aug 18, 2025
The TypedDict[{...}] special form (_TypedDictSpecialForm), which the PEP does not specify.
Yes, this is a separate PEP, which hasn't been accepted yet. added a commit that references this issue on Oct 3, 2025gh-137840: Implement PEP 728 (closed and extra_items in typing.TypedD… hugovk commented on Jan 15, 2026 What's left to do for implementing PEP 728? At least docs and What's New are still needed. StanFromIreland commented on Apr 17, 2026 RobinSobczyk commented on May 30, 2026 Hi, coming a bit late as I see this issue has been closed a month ago, and hope it's not too late as 3.15 is not yet released, but I stumbled upon this problem while working on higher kinded types :
from typing import TypedDict, Never
class RealEmpty(TypedDict, closed=True):
pass
class FalseEmpty(TypedDict, extra_items=Never):
pass
class NotEmpty(TypedDict, closed=True):
x : Never
assert issubclass(NotEmpty, FalseEmpty) # should be okay per the doc
assert not issubclass(NotEmpty, RealEmpty) # as well
# yet RealEmpty and FalseEmpty should be equivalent per the docs
This comes from the fact that the doc states
extra_items: indicates that the TypedDict has extra items. The argument must be a annotation expression specifying the type of the extra items. The type qualifier ReadOnly[] may be used to indicate that the extra items are read-only. Other type qualifiers are not allowed. If the extra items type is Never, no extra items are allowed, so this is equivalent to closed=True.
In particular, this is impactful for type checking in the following situation :
def fun(*, x: Any) -> None:
pass
def correct_wrapper(**kwargs : Unpack[NotEmpty])->None:
fun(**kwargs)
def wrong_wrapper(**kwargs : Unpack[RealEmpty])->None:
fun(**kwargs)
def unclear_wrapper(**kwargs : Unpack[FalseEmpty])->None:
fun(**kwargs)
I think Never causes a lot of other weird behaviors here and there otherwise, and it's a shame it was never properly specified, but it is not the subject. My point is mainly that I think having extra_items=Never is not equivalent to having closed=True. Note also that RealEmpty and FalseEmpty are both inhabitated by only the empty dictionnary (as long as Never is uninhabitated), while NotEmpty is uninhabitated. Also, this makes RealEmpty and FalseEmpty equal types from a set-based type theory, but I also have arguments to say that set-based type theory is not what matches Python, namely there are uninhabitated types that are not Never. JelleZijlstra commented on May 30, 2026 I'm not entirely sure what the problem is here (and it's perhaps better discussed on Discourse). First, issubclass isn't meaningful for structural types. Did you mean to use the is_subtype relation from the typing spec?
I would argue that NotEmpty is equivalent to Never (because it has a required field of type Never, and therefore no values of it can exist). Therefore, it is indeed a subtype of every other TypedDict type, somewhat trivially. FalseEmpty and RealEmpty are indeed equivalent; I'm not sure why you think that NotEmpty is a subtype of only one of the two.
I do agree there are some problems with the way Never interacts with the gradual type system (https://discuss.python.org/t/interactions-with-never-leading-to-undesirable-assignability/99445), though I don't think PEP 728 makes those problems particularly more acute. RobinSobczyk commented on May 30, 2026
I'm not entirely sure what the problem is here (and it's perhaps better discussed on Discourse).
Just to be sure, you meant there ?
First, issubclass isn't meaningful for structural types. Did you mean to use the is_subtype relation from the typing spec?
Not sure about the difference, I'll look into it later, but you're probably right.
EDIT : After checking, I found :
- structural types mostly include Protocols, TypedDict and Callable. Callable can be used in
issubclass, so issubclass could be used on structural types. Code for demonstration :
from typing import Callable
class MyCallable:
def __call__(self):
pass
assert issubclass(MyCallable, Callable)
- Protocols can also benefit from
issubclass thanks to @runtime_checkable, however limited to non-data Protocols. Fine, but TypedDict aren't Protocols, as Protocols can't be instantiated but TypedDicts can
issubclass doc does not state that it cannot be used with structural types (and this is normal, as it could, and need to, for virtual inheritance).
So I wouldn't say that it isn't meaningful, but more that either some features are not implemented or that it hasn't been made obvious why those features cannot be implemented (e.g. mentionning this in the unsupported operations section of the doc). I do understand that data Protocols aren't checkable "because some data attributes can be set on an instance in constructor and this information is not always available on the class object", and honestly it's fair to say it is the same about TypedDict. At the end of the day, indeed, I was talking about the subtyping between TypedDicts part of the documentation, irrelevantly of if it is implemented or not. The question here is more about type/documentation soundness.
Additionnally, I also think it's reasonable to ask who the subtypes of TypedDict are. Having this point of view might highlight interesting limits.
For example, applying structural subtyping to its core tells me that a subtype of a TypedDict type A is just any type with corresponding typing guarantees (e.g. throug overloads) on its __getitem__ method. In particular, from this point of view, it is meaningful to say that keys that are not specified (extra_items)/unspecifiable (closed=True) should return Never. But this has the very unwanted side effect of making dict[str, object] a structural type :
class AllStrObj(TypedDict, extra_items = object):
pass
AllStrObj is indeed structural and theoretically equivalent to dict[str, object].
Because dict is not structural, I think there is an upper bound on the structural typing that can be applied to understand what can be assigned to TypedDicts. I haven't seen this detailed so far, and am very curious about it, as it might really clear up some limits of structural subtyping, which might help me define a more robust theory for sound higher kinded types.
I'm not sure why you think that NotEmpty is a subtype of only one of the two.
Because blindly applying the rules from the doc tells me so. More specifically, the answer of why I think so is that the doc states :
A TypedDict type B is a subtype of a TypedDict type A if and only if all of the conditions below are satisfied
- [...]
- If A is closed, B must also be closed, and it must not contain any items that are not present in A.
- [...]
Here, NotEmpty structurally contains an item, x. Indeed, this item can never be instantiated, forcing NotEmpty to be uninhabitated. However, the following should reasonably typecheck but crash at runtime :
def fun(**kwargs : Unpack[NotEmpty]) -> None:
reveal_type(kwargs["x"]) # should reveal Never
while doing so on RealEmpty should fail at type checking, because RealEmpty has no key by definition.
On the other hand, by applying the same rules from the doc, it can be derived that NotEmpty is a subtype of FalseEmpty. Indeed, FalseEmpty being empty except from extra_items leads to trivial truth of universal properties and leaves only one condition :
Additionally, for any items in B that are not present in A, the item type must be a subtype of the extra items type in A.
Additionally, for any items in B that are not present in A, the item type must be equivalent to the extra items type in A.
which is verified.
MetadataMetadataDevelopmentNo branches or pull requestsIssue actions
|