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
- Motivation
- Begründung
- Spezifikation
- Der Klassenparameter
extra_items - Der Klassenparameter
closed - Interaktion mit Totality
- Interaktion mit
Unpack - Interaktion mit schreibgeschützten Elementen
- Vererbung
- Zuweisbarkeit
- Interaktion mit Konstruktoren
- Unterstützte und nicht unterstützte Operationen
- Interaktion mit Mapping[str, VT]
- Interaktion mit dict[str, VT]
- Laufzeitverhalten
- Der Klassenparameter
- Wie man das lehrt
- Abwärtskompatibilität
- Abgelehnte Ideen
- Verwenden Sie
@finalanstelle des Klassenparametersclosed - Verwenden Sie einen speziellen Schlüssel
__extra_items__mit dem Klassenparameterclosed - Unterstützung einer neuen Syntax zur Angabe von Schlüsseln
- Zulassen zusätzlicher Elemente ohne Angabe des Typs
- Unterstützung zusätzlicher Elemente mit Schnittmenge
- Anforderung der Typprüfung bekannter Elemente mit
extra_items
- Verwenden Sie
- Referenzimplementierung
- Danksagungen
- Urheberrecht
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
@finalDekorator konzentriert, wäre die zugrundeliegende Funktionsanfrage durch diesen PEP adressiert. - Thread in Mailingliste, der nach einer Möglichkeit fragt, anzugeben, dass ein
TypedDictbeliebige 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_itemsals 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_itemszuweisbar sein müssen. - Wir verlieren keine Abwärtskompatibilität, da sowohl
extra_itemsals auchclosednur 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_itemsschreibgeschützt ist- Das Element kann entweder erforderlich oder nicht erforderlich sein.
- Der Werttyp des Elements ist zuweisbar zu
T.
- Wenn
extra_itemsnicht schreibgeschützt ist- Das Element ist nicht erforderlich.
- Der Werttyp des Elements ist konsistent mit
T.
- Wenn
extra_itemsnicht ü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_itemsschreibgeschützt ist- Der Werttyp des Schlüssels ist zuweisbar zu
T. - Der Schlüssel ist nicht in
S.
- Der Werttyp des Schlüssels ist zuweisbar zu
- Wenn
extra_itemsnicht 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-TypBist zuweisbar zu einem TypedDict-TypA, wennBstrukturell zuAzuweisbar 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
AhatBden entsprechenden Schlüssel, es sei denn, das Element inAist schreibgeschützt, nicht erforderlich und hat den obersten Werttyp (ReadOnly[NotRequired[object]]).- Für jedes Element in
A, wennBden entsprechenden Schlüssel hat, ist der entsprechende Werttyp inBdem Werttyp inAzuweisbar.- Für jedes nicht schreibgeschützte Element in
Aist sein Werttyp dem entsprechenden Werttyp inBzuweisbar, und der entsprechende Schlüssel ist inBnicht schreibgeschützt.- Für jeden erforderlichen Schlüssel in
Aist der entsprechende Schlüssel inBerforderlich.- Für jeden nicht erforderlichen Schlüssel in
Agilt: Wenn das Element inAnicht schreibgeschützt ist, ist der entsprechende Schlüssel inBnicht 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
Agilt: Wenn das Element inAnicht schreibgeschützt ist, ist der entsprechende Schlüssel inBnicht 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: eindictmit einer festen Menge von Schlüsseln und Werttypen. NotRequired,Requiredundtotal=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
_: boolden TypedDict speziell macht, im Vergleich zum Hinzufügen eines Klassenarguments wieextra_items=bool. - Es ist rückwärts inkompatibel mit bestehenden TypedDicts, die den Schlüssel
_: boolverwenden. 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.
Urheberrecht
Dieses Dokument wird in die Public Domain oder unter die CC0-1.0-Universal-Lizenz gestellt, je nachdem, welche Lizenz permissiver ist.
Quelle: https://github.com/python/peps/blob/main/peps/pep-0728.rst
Zuletzt geändert: 2025-08-18 20:22:33 GMT