Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python Enhancement Proposals

PEP 728 – TypedDict mit zusätzlichen getypten Elementen

Autor:
Zixuan James Li <p359101898 at gmail.com>
Sponsor:
Jelle Zijlstra <jelle.zijlstra at gmail.com>
Discussions-To:
Discourse thread
Status:
Akzeptiert
Typ:
Standards Track
Thema:
Typisierung
Erstellt:
12. Sep 2023
Python-Version:
3.15
Post-History:
09. Feb 2024
Resolution:
15. Aug 2025

Inhaltsverzeichnis

Zusammenfassung

Dieser PEP fügt zwei Klassenparameter hinzu, closed und extra_items, um die zusätzlichen Elemente eines TypedDict zu typisieren. Dies adressiert die Notwendigkeit, geschlossene TypedDict-Typen zu definieren oder eine Teilmenge von Schlüsseln zu typisieren, die in einem dict erscheinen könnten, während zusätzliche Elemente eines bestimmten Typs zugelassen werden.

Motivation

Ein typing.TypedDict kann den Werttyp jedes bekannten Elements in einem Dictionary annotieren. Aufgrund der strukturellen Zuweisbarkeit kann ein TypedDict zusätzliche Elemente enthalten, die über seinen Typ nicht sichtbar sind. Es gibt derzeit keine Möglichkeit, die Typen von Elementen einzuschränken, die in den konsistenten Untertypen des TypedDict-Typs vorhanden sein könnten.

Explizites Verwerfen zusätzlicher Elemente

Das aktuelle Verhalten von TypedDict hindert Benutzer daran, einen TypedDict-Typ zu definieren, wenn erwartet wird, dass der Typ keine zusätzlichen Elemente enthält.

Aufgrund der möglichen Anwesenheit zusätzlicher Elemente können Typenprüfer keine präziseren Rückgabetypen für .items() und .values() eines TypedDict ableiten. Dies kann durch die Definition eines geschlossenen TypedDict-Typs behoben werden.

Ein weiterer möglicher Anwendungsfall dafür ist eine fundierte Möglichkeit, die Typenverengung mit der in Prüfung zu aktivieren.

class Movie(TypedDict):
    name: str
    director: str

class Book(TypedDict):
    name: str
    author: str

def fun(entry: Movie | Book) -> None:
    if "author" in entry:
        reveal_type(entry)  # Revealed type is still 'Movie | Book'

Nichts hindert ein dict, das mit Movie zuweisbar ist, daran, den Schlüssel author zu haben, und gemäß der aktuellen Spezifikation wäre es für den Typenprüfer falsch, seinen Typ zu verengen.

Zulassen zusätzlicher Elemente eines bestimmten Typs

Für die Unterstützung von API-Schnittstellen oder Legacy-Codebasen, bei denen nur ein Teil der möglichen Schlüssel bekannt ist, wäre es nützlich, zusätzliche Elemente bestimmter Werttypen explizit anzugeben.

Die Typspezifikation ist jedoch restriktiver bei der Prüfung der Erstellung eines TypedDicts, was Benutzer daran hindert, dies zu tun.

class MovieBase(TypedDict):
    name: str

def foo(movie: MovieBase) -> None:
    # movie can have extra items that are not visible through MovieBase
    ...

movie: MovieBase = {"name": "Blade Runner", "year": 1982}  # Not OK
foo({"name": "Blade Runner", "year": 1982})  # Not OK

Während die Einschränkung bei der Erstellung eines TypedDicts durchgesetzt wird, kann das TypedDict aufgrund der strukturellen Zuweisbarkeit zusätzliche Elemente enthalten, die über seinen Typ nicht sichtbar sind. Zum Beispiel

class Movie(MovieBase):
    year: int

movie: Movie = {"name": "Blade Runner", "year": 1982}
foo(movie)  # OK

Es ist nicht möglich, die Existenz der zusätzlichen Elemente über in Prüfungen zu erkennen und darauf zuzugreifen, ohne die Typsicherheit zu verletzen, obwohl sie von einigen konsistenten Untertypen von MovieBase aus vorhanden sein könnten.

def bar(movie: MovieBase) -> None:
    if "year" in movie:
        reveal_type(movie["year"])  # Error: TypedDict 'MovieBase' has no key 'year'

Einige Workarounds wurden bereits implementiert, um zusätzliche Elemente zuzulassen, aber keiner davon ist ideal. Für mypy unterdrückt --disable-error-code=typeddict-unknown-key einen Typenprüfungsfehler speziell für unbekannte Schlüssel auf TypedDict. Dies opfert Typsicherheit zugunsten von Flexibilität und bietet keine Möglichkeit anzugeben, dass der TypedDict-Typ zusätzliche Schlüssel erwartet, deren Werttypen mit einem bestimmten Typen zuweisbar sind.

Unterstützung zusätzlicher Schlüssel für Unpack

PEP 692 fügt eine Möglichkeit hinzu, die Typen einzelner Schlüsselwortargumente, die von **kwargs repräsentiert werden, mithilfe von TypedDict mit Unpack präzise zu annotieren. Da TypedDict jedoch nicht so definiert werden kann, dass es beliebige zusätzliche Elemente akzeptiert, ist es nicht möglich, zusätzliche Schlüsselwortargumente zuzulassen, die zum Zeitpunkt der Definition des TypedDict nicht bekannt sind.

Angesichts der Verwendung von Typannotationen vor PEP 692 für **kwargs in bestehenden Codebasen ist es sinnvoll, zusätzliche Elemente für TypedDict zu akzeptieren und zu typisieren, damit das alte Typieverhalten in Kombination mit Unpack unterstützt werden kann.

Frühere Diskussionen

Die in diesem PEP eingeführten neuen Funktionen würden mehrere seit langem bestehende Funktionsanfragen im Typsystem adressieren. Frühere Diskussionen umfassen

  • Mypy-Problem, das nach einem „finalen TypedDict“ fragt (2019). Während sich die Diskussion auf den @final Dekorator konzentriert, wäre die zugrundeliegende Funktionsanfrage durch diesen PEP adressiert.
  • Thread in Mailingliste, der nach einer Möglichkeit fragt, anzugeben, dass ein TypedDict beliebige zusätzliche Schlüssel enthalten kann (2020).
  • Diskussion über eine Erweiterung des durch PEP 692 eingeführten Unpack-Mechanismus (2023).
  • PEP 705 schlug in einem früheren Entwurf eine ähnliche Funktion vor (2023); sie wurde entfernt, um diesen PEP einfacher zu halten.
  • Diskussion über einen „exakten“ TypedDict (2024).

Begründung

Angenommen, wir möchten einen Typ, der zusätzliche Elemente vom Typ str auf einem TypedDict zulässt.

Index-Signaturen in TypeScript erlauben dies.

type Foo = {
    a: string
    [key: string]: string
}

Dieser Vorschlag zielt darauf ab, eine ähnliche Funktion ohne Syntaxänderungen zu unterstützen, und bietet eine natürliche Erweiterung der bestehenden Zuweisbarkeitsregeln.

Wir schlagen vor, einen Klassenparameter extra_items zu TypedDict hinzuzufügen. Er akzeptiert einen Typausdruck als Argument; wenn er vorhanden ist, sind zusätzliche Elemente zulässig, und ihre Werttypen müssen mit dem Wertausdruck zuweisbar sein.

Eine Anwendung davon ist, zusätzliche Elemente zu verwerfen. Wir schlagen vor, einen Klassenparameter closed hinzuzufügen, der nur ein Literal True oder False als Argument akzeptiert. Es sollte ein Laufzeitfehler sein, wenn closed und extra_items gleichzeitig verwendet werden.

Anders als bei Index-Signaturen müssen die Typen der bekannten Elemente nicht mit dem extra_items Argument zuweisbar sein.

Es gibt einige Vorteile bei diesem Ansatz

  • Wir können auf den Zuweisbarkeitsregeln aufbauen, die in der Typspezifikation definiert sind, wobei extra_items als Pseudoelement behandelt werden kann.
  • Es ist keine Grammatikänderung erforderlich, um den Typ der zusätzlichen Elemente anzugeben.
  • Wir können die zusätzlichen Elemente präzise typisieren, ohne dass die Werttypen der bekannten Elemente mit extra_items zuweisbar sein müssen.
  • Wir verlieren keine Abwärtskompatibilität, da sowohl extra_items als auch closed nur opt-in Features sind.

Spezifikation

Diese Spezifikation ist so strukturiert, dass sie PEP 589 parallelisiert, um Änderungen an der ursprünglichen TypedDict-Spezifikation hervorzuheben.

Wenn extra_items angegeben ist, werden zusätzliche Elemente als nicht erforderliche Elemente behandelt, die mit dem extra_items Argument übereinstimmen, deren Schlüssel bei der Bestimmung unterstützter und nicht unterstützter Operationen zulässig sind.

Der Klassenparameter extra_items

Standardmäßig ist extra_items nicht gesetzt. Für einen TypedDict-Typ, der extra_items angibt, wird beim Erstellen erwartet, dass der Werttyp jedes unbekannten Elements nicht erforderlich und mit dem extra_items Argument zuweisbar ist. Zum Beispiel

class Movie(TypedDict, extra_items=bool):
    name: str

a: Movie = {"name": "Blade Runner", "novel_adaptation": True}  # OK
b: Movie = {
    "name": "Blade Runner",
    "year": 1982,  # Not OK. 'int' is not assignable to 'bool'
}

Hier gibt extra_items=bool an, dass Elemente außer 'name' einen Werttyp von bool haben und nicht erforderlich sind.

Die alternative Inline-Syntax wird ebenfalls unterstützt

Movie = TypedDict("Movie", {"name": str}, extra_items=bool)

Der Zugriff auf zusätzliche Elemente ist erlaubt. Typenprüfer müssen ihren Werttyp aus dem extra_items Argument ableiten.

def f(movie: Movie) -> None:
    reveal_type(movie["name"])              # Revealed type is 'str'
    reveal_type(movie["novel_adaptation"])  # Revealed type is 'bool'

extra_items wird durch Vererbung übernommen.

class MovieBase(TypedDict, extra_items=ReadOnly[int | None]):
    name: str

class Movie(MovieBase):
    year: int

a: Movie = {"name": "Blade Runner", "year": None}  # Not OK. 'None' is incompatible with 'int'
b: Movie = {
    "name": "Blade Runner",
    "year": 1982,
    "other_extra_key": None,
}  # OK

Hier ist 'year' in a ein zusätzlicher Schlüssel, der auf Movie definiert ist und dessen Werttyp int ist. 'other_extra_key' in b ist ein weiterer zusätzlicher Schlüssel, dessen Werttyp mit dem Wert von extra_items, das auf MovieBase definiert ist, zuweisbar sein muss.

Der Klassenparameter closed

Wenn weder extra_items noch closed=True angegeben ist, wird closed=False angenommen. Der TypedDict sollte nicht erforderliche zusätzliche Elemente vom Werttyp ReadOnly[object] während der Vererbung oder Zuweisbarkeitsprüfungen zulassen, um das Standardverhalten von TypedDict beizubehalten. Zusätzliche Schlüssel, die bei der Erstellung von TypedDict-Objekten enthalten sind, sollten immer noch abgefangen werden, wie in der TypedDict-Typenspezifikation erwähnt.

Wenn closed=True gesetzt ist, sind keine zusätzlichen Elemente zulässig. Dies ist äquivalent zu extra_items=Never, da es keinen Werttyp geben kann, der zu Never zuweisbar ist. Es ist ein Laufzeitfehler, die Parameter closed und extra_items in derselben TypedDict-Definition zu verwenden.

Ähnlich wie bei total wird nur ein Literal True oder False als Wert für das Argument closed unterstützt. Typenprüfer sollten jeden nicht-literalen Wert ablehnen.

Das Übergeben von closed=False fordert explizit das Standardverhalten von TypedDict an, bei dem beliebige andere Schlüssel vorhanden sein können und Unterklassen beliebige Elemente hinzufügen können. Es ist ein Fehler des Typenprüfers, closed=False zu übergeben, wenn eine Oberklasse closed=True hat oder extra_items setzt.

Wenn closed nicht angegeben ist, wird das Verhalten von der Oberklasse geerbt. Wenn die Oberklasse TypedDict selbst ist oder die Oberklasse nicht closed=True oder den Parameter extra_items hat, wird das vorherige Verhalten von TypedDict beibehalten: beliebige zusätzliche Elemente sind zulässig. Wenn die Oberklasse closed=True hat, ist die Kindklasse ebenfalls geschlossen.

class BaseMovie(TypedDict, closed=True):
    name: str

class MovieA(BaseMovie):  # OK, still closed
    pass

class MovieB(BaseMovie, closed=True):  # OK, but redundant
    pass

class MovieC(BaseMovie, closed=False):  # Type checker error
    pass

Als Folge davon, dass closed=True äquivalent zu extra_items=Never ist, gelten dieselben Regeln, die für extra_items=Never gelten, auch für closed=True. Obwohl beide die gleiche Wirkung haben, wird closed=True gegenüber extra_items=Never bevorzugt.

Es ist möglich, closed=True bei der Unterklassifizierung zu verwenden, wenn das Argument extra_items ein schreibgeschützter Typ ist.

class Movie(TypedDict, extra_items=ReadOnly[str]):
    pass

class MovieClosed(Movie, closed=True):  # OK
    pass

class MovieNever(Movie, extra_items=Never):  # OK, but 'closed=True' is preferred
    pass

Dies wird in einem späteren Abschnitt weiter diskutiert.

closed wird auch mit der funktionalen Syntax unterstützt

Movie = TypedDict("Movie", {"name": str}, closed=True)

Interaktion mit Totality

Es ist ein Fehler, Required[] oder NotRequired[] mit extra_items zu verwenden. total=False und total=True haben keine Auswirkung auf extra_items selbst.

Die zusätzlichen Elemente sind nicht erforderlich, unabhängig von der Totalität des TypedDict. Operationen, die für NotRequired-Elemente verfügbar sind, sollten auch für zusätzliche Elemente verfügbar sein.

class Movie(TypedDict, extra_items=int):
    name: str

def f(movie: Movie) -> None:
    del movie["name"]  # Not OK. The value type of 'name' is 'Required[str]'
    del movie["year"]  # OK. The value type of 'year' is 'NotRequired[int]'

Interaktion mit Unpack

Für die Typenprüfung sollte Unpack[SomeTypedDict] mit zusätzlichen Elementen wie seine Entsprechung in regulären Parametern behandelt werden, und die bestehenden Regeln für Funktionsparameter gelten weiterhin.

class MovieNoExtra(TypedDict):
    name: str

class MovieExtra(TypedDict, extra_items=int):
    name: str

def f(**kwargs: Unpack[MovieNoExtra]) -> None: ...
def g(**kwargs: Unpack[MovieExtra]) -> None: ...

# Should be equivalent to:
def f(*, name: str) -> None: ...
def g(*, name: str, **kwargs: int) -> None: ...

f(name="No Country for Old Men", year=2007) # Not OK. Unrecognized item
g(name="No Country for Old Men", year=2007) # OK

Interaktion mit schreibgeschützten Elementen

Wenn das Argument extra_items mit dem Tyqualifizierer ReadOnly[] annotiert ist, haben die zusätzlichen Elemente des TypedDict die Eigenschaften von schreibgeschützten Elementen. Dies interagiert mit den Vererbungsregeln, die in Schreibgeschützten Elementen angegeben sind.

Insbesondere wenn der TypedDict-Typ extra_items als schreibgeschützt angibt, können Unterklassen des TypedDict-Typs extra_items neu deklarieren.

Da ein nicht geschlossener TypedDict-Typ implizit nicht erforderliche zusätzliche Elemente vom Werttyp ReadOnly[object] zulässt, können seine Unterklassen das Argument extra_items mit spezifischeren Typen überschreiben.

Weitere Details werden in den folgenden Abschnitten besprochen.

Vererbung

extra_items wird ähnlich wie ein reguläres Element key: value_type vererbt. Wie bei den anderen Schlüsseln gelten die Vererbungsregeln und die Vererbungsregeln für Schreibgeschützte Elemente.

Wir müssen diese Regeln neu interpretieren, um zu definieren, wie extra_items mit ihnen interagiert.

  • Das Ändern eines Feldtyps einer übergeordneten TypedDict-Klasse in einer Unterklasse ist nicht zulässig.

Erstens ist es nicht zulässig, den Wert von extra_items in einer Unterklasse zu ändern, es sei denn, er wurde in der Oberklasse als ReadOnly deklariert.

class Parent(TypedDict, extra_items=int | None):
    pass

class Child(Parent, extra_items=int): # Not OK. Like any other TypedDict item, extra_items's type cannot be changed
    pass

Zweitens definiert extra_items=T effektiv den Werttyp aller unbenannten Elemente, die für den TypedDict akzeptiert werden, und markiert sie als nicht erforderlich. Daher gilt die obige Einschränkung für alle zusätzlichen Elemente, die in einer Unterklasse hinzugefügt werden. Für jedes Element, das in einer Unterklasse hinzugefügt wird, müssen alle folgenden Bedingungen gelten:

  • Wenn extra_items schreibgeschützt ist
    • Das Element kann entweder erforderlich oder nicht erforderlich sein.
    • Der Werttyp des Elements ist zuweisbar zu T.
  • Wenn extra_items nicht schreibgeschützt ist
    • Das Element ist nicht erforderlich.
    • Der Werttyp des Elements ist konsistent mit T.
  • Wenn extra_items nicht überschrieben wird, erbt die Unterklasse es unverändert.

Zum Beispiel:

class MovieBase(TypedDict, extra_items=int | None):
    name: str

class MovieRequiredYear(MovieBase):  # Not OK. Required key 'year' is not known to 'MovieBase'
    year: int | None

class MovieNotRequiredYear(MovieBase):  # Not OK. 'int | None' is not consistent with 'int'
    year: NotRequired[int]

class MovieWithYear(MovieBase):  # OK
    year: NotRequired[int | None]

class BookBase(TypedDict, extra_items=ReadOnly[int | str]):
    title: str

class Book(BookBase, extra_items=str):  # OK
    year: int  # OK

Eine wichtige Nebenwirkung der Vererbungsregeln ist, dass wir einen TypedDict-Typ definieren können, der zusätzliche Elemente verbietet.

class MovieClosed(TypedDict, extra_items=Never):
    name: str

Hier gibt die Übergabe des Werts Never an extra_items an, dass es keine anderen Schlüssel in MovieFinal als die bekannten geben kann. Aufgrund seiner potenziellen häufigen Verwendung gibt es eine bevorzugte Alternative

class MovieClosed(TypedDict, closed=True):
    name: str

bei der wir implizit annehmen, dass extra_items=Never.

Zuweisbarkeit

Sei S die Menge der Schlüssel der explizit definierten Elemente in einem TypedDict-Typ. Wenn er extra_items=T angibt, gilt der TypedDict-Typ als eine unendliche Menge von Elementen, die alle die folgenden Bedingungen erfüllen.

  • Wenn extra_items schreibgeschützt ist
    • Der Werttyp des Schlüssels ist zuweisbar zu T.
    • Der Schlüssel ist nicht in S.
  • Wenn extra_items nicht schreibgeschützt ist
    • Der Schlüssel ist nicht erforderlich.
    • Der Werttyp des Schlüssels ist konsistent mit T.
    • Der Schlüssel ist nicht in S.

Für die Typenprüfung sei extra_items ein nicht erforderliches Pseudoelement bei der Prüfung auf Zuweisbarkeit gemäß den Regeln in der Schreibgeschützten Elemente-Sektion, mit einer neuen Regel in Fettdruck wie folgt:

Ein TypedDict-Typ B ist zuweisbar zu einem TypedDict-Typ A, wenn B strukturell zu A zuweisbar ist. Dies ist wahr, wenn und nur wenn alle folgenden Bedingungen erfüllt sind:
  • [Wenn kein Schlüssel mit demselben Namen in ``B`` gefunden wird, wird das Argument „extra_items“ als Werttyp des entsprechenden Schlüssels betrachtet.]
  • Für jedes Element in A hat B den entsprechenden Schlüssel, es sei denn, das Element in A ist schreibgeschützt, nicht erforderlich und hat den obersten Werttyp (ReadOnly[NotRequired[object]]).
  • Für jedes Element in A, wenn B den entsprechenden Schlüssel hat, ist der entsprechende Werttyp in B dem Werttyp in A zuweisbar.
  • Für jedes nicht schreibgeschützte Element in A ist sein Werttyp dem entsprechenden Werttyp in B zuweisbar, und der entsprechende Schlüssel ist in B nicht schreibgeschützt.
  • Für jeden erforderlichen Schlüssel in A ist der entsprechende Schlüssel in B erforderlich.
  • Für jeden nicht erforderlichen Schlüssel in A gilt: Wenn das Element in A nicht schreibgeschützt ist, ist der entsprechende Schlüssel in B nicht erforderlich.

Die folgenden Beispiele veranschaulichen diese Prüfungen in Aktion.

extra_items legt verschiedene Einschränkungen für zusätzliche Elemente für Zuweisbarkeitsprüfungen fest.

class Movie(TypedDict, extra_items=int | None):
    name: str

class MovieDetails(TypedDict, extra_items=int | None):
    name: str
    year: NotRequired[int]

details: MovieDetails = {"name": "Kill Bill Vol. 1", "year": 2003}
movie: Movie = details  # Not OK. While 'int' is assignable to 'int | None',
                        # 'int | None' is not assignable to 'int'

class MovieWithYear(TypedDict, extra_items=int | None):
    name: str
    year: int | None

details: MovieWithYear = {"name": "Kill Bill Vol. 1", "year": 2003}
movie: Movie = details  # Not OK. 'year' is not required in 'Movie',
                        # but it is required in 'MovieWithYear'

wobei MovieWithYear (B) gemäß dieser Regel nicht zu Movie (A) zuweisbar ist.

  • Für jeden nicht erforderlichen Schlüssel in A gilt: Wenn das Element in A nicht schreibgeschützt ist, ist der entsprechende Schlüssel in B nicht erforderlich.

Wenn extra_items für einen TypedDict-Typ als schreibgeschützt angegeben ist, ist es möglich, dass ein Element einen engeren Typ als das extra_items Argument hat.

class Movie(TypedDict, extra_items=ReadOnly[str | int]):
    name: str

class MovieDetails(TypedDict, extra_items=int):
    name: str
    year: NotRequired[int]

details: MovieDetails = {"name": "Kill Bill Vol. 2", "year": 2004}
movie: Movie = details  # OK. 'int' is assignable to 'str | int'.

Dies verhält sich genauso, als wäre year: ReadOnly[str | int] ein explizit definiertes Element in Movie.

extra_items als Pseudoelement folgt denselben Regeln wie andere Elemente. Wenn also beide TypedDict-Typen extra_items angeben, wird diese Prüfung natürlich durchgesetzt.

class MovieExtraInt(TypedDict, extra_items=int):
    name: str

class MovieExtraStr(TypedDict, extra_items=str):
    name: str

extra_int: MovieExtraInt = {"name": "No Country for Old Men", "year": 2007}
extra_str: MovieExtraStr = {"name": "No Country for Old Men", "description": ""}
extra_int = extra_str  # Not OK. 'str' is not assignable to extra items type 'int'
extra_str = extra_int  # Not OK. 'int' is not assignable to extra items type 'str'

Ein nicht geschlossener TypedDict-Typ erlaubt implizit nicht erforderliche zusätzliche Schlüssel vom Werttyp ReadOnly[object]. Die Anwendung der Zuweisbarkeitsregeln zwischen diesem Typ und einem geschlossenen TypedDict-Typ ist zulässig.

class MovieNotClosed(TypedDict):
    name: str

extra_int: MovieExtraInt = {"name": "No Country for Old Men", "year": 2007}
not_closed: MovieNotClosed = {"name": "No Country for Old Men"}
extra_int = not_closed  # Not OK.
                        # 'extra_items=ReadOnly[object]' implicitly on 'MovieNotClosed'
                        # is not assignable to with 'extra_items=int'
not_closed = extra_int  # OK

Interaktion mit Konstruktoren

TypedDicts, die zusätzliche Elemente vom Typ T zulassen, erlauben auch beliebige Schlüsselwortargumente dieses Typs, wenn sie durch Aufruf des Klassenobjekts erstellt werden.

class NonClosedMovie(TypedDict):
    name: str

NonClosedMovie(name="No Country for Old Men")  # OK
NonClosedMovie(name="No Country for Old Men", year=2007)  # Not OK. Unrecognized item

class ExtraMovie(TypedDict, extra_items=int):
    name: str

ExtraMovie(name="No Country for Old Men")  # OK
ExtraMovie(name="No Country for Old Men", year=2007)  # OK
ExtraMovie(
    name="No Country for Old Men",
    language="English",
)  # Not OK. Wrong type for extra item 'language'

# This implies 'extra_items=Never',
# so extra keyword arguments would produce an error
class ClosedMovie(TypedDict, closed=True):
    name: str

ClosedMovie(name="No Country for Old Men")  # OK
ClosedMovie(
    name="No Country for Old Men",
    year=2007,
)  # Not OK. Extra items not allowed

Unterstützte und nicht unterstützte Operationen

Diese Aussage aus der Typspezifikation gilt weiterhin.

Operationen mit beliebigen str-Schlüsseln (anstelle von Zeichenkettenliteralen oder anderen Ausdrücken mit bekannten Zeichenkettenwerten) sollten im Allgemeinen abgelehnt werden.

Operationen, die bereits für NotRequired-Elemente gelten, sollten im Allgemeinen auch für zusätzliche Elemente gelten, gemäß der gleichen Begründung aus der Typspezifikation.

Die genauen Regeln für die Typenprüfung obliegen jedem Typenprüfer. In einigen Fällen können potenziell unsichere Operationen akzeptiert werden, wenn die Alternative darin besteht, falsch-positive Fehler für idiomatischen Code zu generieren.

Einige Operationen, einschließlich indizierter Zugriffe und Zuweisungen mit beliebigen str-Schlüsseln, können zulässig sein, da der TypedDict zu Mapping[str, VT] oder dict[str, VT] zuweisbar ist. Die beiden folgenden Abschnitte werden dies näher erläutern.

Interaktion mit Mapping[str, VT]

Ein TypedDict-Typ ist zu einem Typ der Form Mapping[str, VT] zuweisbar, wenn alle Werttypen der Elemente im TypedDict zu VT zuweisbar sind. Für die Zwecke dieser Regel wird ein TypedDict, das extra_items= oder closed= nicht gesetzt hat, als ein Element mit dem Werttyp ReadOnly[object] betrachtet. Dies erweitert die aktuelle Zuweisbarkeitsregel aus der Typspezifikation.

Zum Beispiel:

class MovieExtraStr(TypedDict, extra_items=str):
    name: str

extra_str: MovieExtraStr = {"name": "Blade Runner", "summary": ""}
str_mapping: Mapping[str, str] = extra_str  # OK

class MovieExtraInt(TypedDict, extra_items=int):
    name: str

extra_int: MovieExtraInt = {"name": "Blade Runner", "year": 1982}
int_mapping: Mapping[str, int] = extra_int  # Not OK. 'int | str' is not assignable with 'int'
int_str_mapping: Mapping[str, int | str] = extra_int  # OK

Typenprüfer sollten die genauen Signaturen von values() und items() für solche TypedDict-Typen ableiten.

def foo(movie: MovieExtraInt) -> None:
    reveal_type(movie.items())  # Revealed type is 'dict_items[str, str | int]'
    reveal_type(movie.values())  # Revealed type is 'dict_values[str, str | int]'

Durch die Erweiterung dieser Zuweisbarkeitsregel können Typenprüfer indizierte Zugriffe mit beliebigen str-Schlüsseln zulassen, wenn extra_items oder closed=True angegeben ist. Zum Beispiel

def bar(movie: MovieExtraInt, key: str) -> None:
    reveal_type(movie[key])  # Revealed type is 'str | int'

Die Definition des Typenverengungsverhaltens für TypedDict ist nicht Gegenstand dieses PEP. Dies überlässt es dem Typenprüfer, flexibler zu sein, wenn es um indizierte Zugriffe mit beliebigen str-Schlüsseln geht. Zum Beispiel kann ein Typenprüfer strengere Regeln anwenden, indem er eine explizite Prüfung 'x' in d verlangt.

Interaktion mit dict[str, VT]

Da die Anwesenheit von extra_items in einem geschlossenen TypedDict-Typ zusätzliche erforderliche Schlüssel in seinen strukturellen Untertypen verbietet, können wir statisch analysieren, ob der TypedDict-Typ und seine strukturellen Untertypen jemals erforderliche Schlüssel haben werden.

Der TypedDict-Typ ist zu dict[str, VT] zuweisbar, wenn alle Elemente des TypedDict-Typs die folgenden Bedingungen erfüllen:

  • Der Werttyp des Elements ist konsistent mit VT.
  • Das Element ist nicht schreibgeschützt.
  • Das Element ist nicht erforderlich.

Zum Beispiel:

class IntDict(TypedDict, extra_items=int):
    pass

class IntDictWithNum(IntDict):
    num: NotRequired[int]

def f(x: IntDict) -> None:
    v: dict[str, int] = x  # OK
    v.clear()  # OK

not_required_num_dict: IntDictWithNum = {"num": 1, "bar": 2}
regular_dict: dict[str, int] = not_required_num_dict  # OK
f(not_required_num_dict)  # OK

In diesem Fall sind Methoden, die auf einem TypedDict zuvor nicht verfügbar waren, mit Signaturen erlaubt, die dict[str, VT] entsprechen (z. B.: __setitem__(self, key: str, value: VT) -> None).

not_required_num_dict.clear()  # OK

reveal_type(not_required_num_dict.popitem())  # OK. Revealed type is 'tuple[str, int]'

def f(not_required_num_dict: IntDictWithNum, key: str):
    not_required_num_dict[key] = 42  # OK
    del not_required_num_dict[key]  # OK

Die Hinweise zu indizierten Zugriffen aus dem vorherigen Abschnitt gelten weiterhin.

dict[str, VT] ist keinem TypedDict-Typ zuweisbar, da ein solches Dict eine Unterart von dict sein kann.

class CustomDict(dict[str, int]):
    pass

def f(might_not_be_a_builtin_dict: dict[str, int]):
    int_dict: IntDict = might_not_be_a_builtin_dict # Not OK

not_a_builtin_dict = CustomDict({"num": 1})
f(not_a_builtin_dict)

Laufzeitverhalten

Zur Laufzeit ist es ein Fehler, sowohl das Argument closed als auch das Argument extra_items in derselben TypedDict-Definition zu übergeben, sei es über die Klassensyntax oder die funktionale Syntax. Zur Vereinfachung prüft die Laufzeit keine anderen ungültigen Kombinationen, die Vererbung betreffen.

Zur Introspektion werden die Argumente closed und extra_items auf zwei neue Attribute des resultierenden TypedDict-Objekts abgebildet: __closed__ und __extra_items__. Diese Attribute spiegeln exakt wider, was an den TypedDict-Konstruktor übergeben wurde, ohne Superklassen zu berücksichtigen.

Wenn closed nicht übergeben wird, ist der Wert von __closed__ None. Wenn extra_items nicht übergeben wird, ist der Wert von __extra_items__ das neue Sentinel-Objekt typing.NoExtraItems. (Es kann nicht None sein, da extra_items=None eine gültige Definition ist, die angibt, dass alle zusätzlichen Elemente None sein müssen.)

Wie man das lehrt

Die in diesem PEP eingeführten neuen Funktionen können zusammen mit dem Konzept der Vererbung für TypedDict vermittelt werden. Eine mögliche Gliederung könnte sein:

  • Grundlagen von TypedDict: ein dict mit einer festen Menge von Schlüsseln und Werttypen.
  • NotRequired, Required und total=False: Schlüssel, die fehlen dürfen.
  • ReadOnly: Schlüssel, die nicht geändert werden können.
  • Vererbung: Unterklassen können neue Schlüssel hinzufügen. Als Folgerung kann ein Wert eines TypedDict-Typs zur Laufzeit zusätzliche Schlüssel enthalten, die im Typ nicht spezifiziert sind.
  • closed=True: Zulassen zusätzlicher Schlüssel wird verboten und Vererbung eingeschränkt.
  • extra_items=VT: Zulassen zusätzlicher Schlüssel mit einem angegebenen Werttyp.

Das Konzept eines geschlossenen TypedDict sollte auch in der Dokumentation für verwandte Konzepte Querverweise enthalten. Zum Beispiel funktioniert die Typenverengung mit dem in-Operator anders, vielleicht intuitiver, mit geschlossenen TypedDict-Typen. Darüber hinaus kann bei Verwendung von Unpack für Schlüsselwortargumente ein geschlossenes TypedDict nützlich sein, um die erlaubten Schlüsselwortargumente einzuschränken.

Abwärtskompatibilität

Da extra_items eine Opt-in-Funktion ist, wird keine bestehende Codebasis durch diese Änderung unterbrochen.

Beachten Sie, dass closed und extra_items als Schlüsselwortargumente nicht mit anderen Schlüsseln kollidieren, wenn etwas wie TD = TypedDict("TD", foo=str, bar=int) verwendet wird, da diese Syntax in Python 3.13 bereits entfernt wurde.

Da dies eine Typüberwachungsfunktion ist, kann sie für ältere Versionen verfügbar gemacht werden, solange der Typenprüfer sie unterstützt.

Abgelehnte Ideen

Verwenden Sie @final anstelle des Klassenparameters closed

Dies wurde hier diskutiert.

Zitat eines relevanten Kommentars von Eric Traut

Der Klassen-Decorator @final zeigt an, dass eine Klasse nicht unterklassenfähig ist. Das ist sinnvoll für Klassen, die nominale Typen definieren. TypedDict ist jedoch ein struktureller Typ, ähnlich einem Protokoll. Das bedeutet, dass zwei TypedDict-Klassen mit unterschiedlichen Namen, aber denselben Felddefinitionen, äquivalente Typen sind. Ihre Namen und Hierarchien spielen keine Rolle bei der Bestimmung der Typenkonsistenz. Aus diesem Grund hat @final keinen Einfluss auf die Typenkonsistenzregeln von TypedDict und sollte auch das Verhalten von Elementen oder Werten nicht ändern.

Verwenden Sie einen speziellen Schlüssel __extra_items__ mit dem Klassenparameter closed

In einer früheren Überarbeitung dieses Vorschlags diskutierten wir einen Ansatz, der den Werttyp von __extra_items__ zur Angabe des Typs von akzeptierten zusätzlichen Elementen nutzen würde, wie folgt:

class IntDict(TypedDict, closed=True):
    __extra_items__: int

wobei closed=True erforderlich ist, damit __extra_items__ speziell behandelt wird, um Schlüsselkollisionen zu vermeiden.

Einige Mitglieder der Community äußerten Bedenken hinsichtlich der Eleganz der Syntax. Praktisch kann die Schlüsselkollision mit einem regulären Schlüssel durch Workarounds gemildert werden, aber da die Verwendung eines reservierten Schlüssels zentral für diesen Vorschlag ist, gibt es nur begrenzte Möglichkeiten, die Bedenken auszuräumen.

Unterstützung einer neuen Syntax zur Angabe von Schlüsseln

Durch die Einführung einer neuen Syntax, die die Angabe von String-Schlüsseln ermöglicht, könnten wir die funktionale Syntax zur Definition von TypedDict-Typen als veraltet kennzeichnen und die Probleme mit Schlüsselkonflikten angehen, falls wir uns entscheiden, einen speziellen Schlüssel für die Typisierung zusätzlicher Elemente zu reservieren.

Zum Beispiel:

class Foo(TypedDict):
    name: str  # Regular item
    _: bool    # Type of extra items
    __items__ = {
        "_": int,   # Literal "_" as a key
        "class": str,  # Keyword as a key
        "tricky.name?": float,  # Arbitrary str key
    }

Dies wurde hier von Jukka vorgeschlagen. Der Schlüssel '_' wird gewählt, da kein neuer Name erfunden werden muss und er Ähnlichkeit mit der match-Anweisung hat.

Dies würde es uns ermöglichen, die funktionale Syntax zur Definition von TypedDict-Typen vollständig als veraltet zu kennzeichnen, hat aber einige Nachteile. Zum Beispiel

  • Es ist für einen Leser weniger offensichtlich, dass _: bool den TypedDict speziell macht, im Vergleich zum Hinzufügen eines Klassenarguments wie extra_items=bool.
  • Es ist rückwärts inkompatibel mit bestehenden TypedDicts, die den Schlüssel _: bool verwenden. Obwohl solche Benutzer eine Möglichkeit haben, das Problem zu umgehen, ist es immer noch ein Problem für sie, wenn sie Python (oder typing-extensions) aktualisieren.
  • Die Typen erscheinen nicht in einem Annotationskontext, daher wird ihre Auswertung nicht verzögert.

Zulassen zusätzlicher Elemente ohne Angabe des Typs

extra=True wurde ursprünglich vorgeschlagen, um einen TypedDict zu definieren, der zusätzliche Elemente unabhängig vom Typ akzeptiert, ähnlich wie total=True funktioniert.

class ExtraDict(TypedDict, extra=True):
    pass

Da es keine Möglichkeit bot, den Typ der zusätzlichen Elemente anzugeben, müssen die Typenprüfer annehmen, dass der Typ der zusätzlichen Elemente Any ist, was die Typsicherheit beeinträchtigt. Darüber hinaus erlaubt das aktuelle Verhalten von TypedDict bereits, dass untypisierte zusätzliche Elemente zur Laufzeit vorhanden sind, aufgrund der strukturellen Zuweisbarkeit. closed=True spielt eine ähnliche Rolle im aktuellen Vorschlag.

Unterstützung zusätzlicher Elemente mit Schnittmenge

Die Unterstützung von Schnitten im Typsystem von Python erfordert viele sorgfältige Überlegungen, und es kann lange dauern, bis die Community zu einem Konsens über ein vernünftiges Design gelangt.

Idealerweise sollten zusätzliche Elemente in TypedDict nicht durch die Arbeit an Schnitten blockiert werden, noch müssen sie unbedingt durch Schnitte unterstützt werden.

Darüber hinaus ist der Schnitt zwischen Mapping[...] und TypedDict nicht äquivalent zu einem TypedDict-Typ mit dem vorgeschlagenen extra_items-Spezialelement, da der Werttyp aller bekannten Elemente in TypedDict die Untertyp-Beziehung mit dem Werttyp von Mapping[...] erfüllen muss.

Anforderung der Typprüfung bekannter Elemente mit extra_items

extra_items schränkt den Werttyp für Schlüssel ein, die für den TypedDict-Typ *unbekannt* sind. Der Werttyp eines *bekannten* Elements ist also nicht notwendigerweise dem von extra_items zuweisbar, und extra_items ist nicht notwendigerweise allen bekannten Elementen zuweisbar.

Dies unterscheidet sich von der Index-Signaturen-Syntax von TypeScript, die verlangt, dass die Typen aller Eigenschaften mit dem Typ der String-Index-Signatur übereinstimmen. Zum Beispiel:

interface MovieWithExtraNumber {
    name: string // Property 'name' of type 'string' is not assignable to 'string' index type 'number'.
    [index: string]: number
}

interface MovieWithExtraNumberOrString {
    name: string // OK
    [index: string]: number | string
}

Während diese Einschränkung indizierte Zugriffe mit beliebigen Schlüsseln ermöglicht, bringt sie Benutzerfreundlichkeitseinschränkungen mit sich, die in TypeScript's Issue Tracker diskutiert werden. Ein Vorschlag war, die definierten Schlüssel von der Index-Signatur auszuschließen, um einen Typ wie MovieWithExtraNumber zu definieren. Dies beinhaltet wahrscheinlich Subtraktionstypen, was über den Rahmen dieses PEPs hinausgeht.

Referenzimplementierung

Dies wird in pyright 1.1.386 unterstützt, und eine frühere Überarbeitung wird in pyanalyze 0.12.0 unterstützt.

Dies wird auch in typing-extensions 4.13.0 unterstützt.

Danksagungen

Dank an Jelle Zijlstra für das Sponsoring dieses PEP und das Bereitstellen von Überprüfungsfeedback, Eric Traut, der das ursprüngliche Design vorgeschlagen hat, auf dem dieses PEP aufbaut, und Alice Purcell für ihre Perspektive als Autorin von PEP 705.


Quelle: https://github.com/python/peps/blob/main/peps/pep-0728.rst

Zuletzt geändert: 2025-08-18 20:22:33 GMT