FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

Implement PEP 728: TypedDict with Typed Extra Items · Issue #137840 · python/cpython · GitHub

Repository navigation

Implement PEP 728: TypedDict with Typed Extra Items #137840

Closed

Description

Feature 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

Activity

  1. added
    triagedThe issue has been accepted as valid by a triager.
    on Aug 16, 2025
  2. angela-tarantula commented on Aug 17, 2025

    Contributor

    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 whenclosed and extra_items are "used at the same time" also trigger when closed=False is passed explicitly, or only when closed=True?
  3. 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.

  4. angela-tarantula commented on Aug 17, 2025

    Contributor

    Thanks. I'll prepare a PR!

  5. JelleZijlstra commented on Aug 18, 2025

    MemberAuthor

    The TypedDict[{...}] special form (_TypedDictSpecialForm), which the PEP does not specify.

    Yes, this is a separate PEP, which hasn't been accepted yet.

  6. added a commit that references this issue on Oct 3, 2025
  7. hugovk commented on Jan 15, 2026

    Member

    What's left to do for implementing PEP 728? At least docs and What's New are still needed.

  8. StanFromIreland commented on Apr 17, 2026

    Member

    A little ping @JelleZijlstra.

  9. added a commit that references this issue on May 5, 2026
  10. 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.

  11. JelleZijlstra commented on May 30, 2026

    MemberAuthor

    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.

  12. 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    3.15bugs and security fixestopic-typingtriagedThe issue has been accepted as valid by a triager.type-featureA feature request or enhancement

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions


      Back | FazBrowse Home | New Git URL