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

Python Enhancement Proposals

PEP 764 – Inline Typed Dictionaries

Autor:
Victorien Plot <contact at vctrn.dev>
Sponsor:
Eric Traut <erictr at microsoft.com>
Discussions-To:
Discourse thread
Status:
Entwurf
Typ:
Standards Track
Thema:
Typisierung
Erstellt:
25-Okt-2024
Python-Version:
3.15
Post-History:
29-Jan-2025

Inhaltsverzeichnis

Zusammenfassung

PEP 589 definiert eine klassenbasierte und eine funktionale Syntax zum Erstellen typisierter Wörterbücher. In beiden Szenarien ist die Definition einer Klasse oder die Zuweisung zu einem Wert erforderlich. In einigen Situationen kann dies unnötigen Boilerplate-Code hinzufügen, insbesondere wenn das typisierte Wörterbuch nur einmal verwendet wird.

Dieses PEP schlägt die Hinzufügung einer neuen Inline-Syntax durch Indizierung des Typs TypedDict vor.

from typing import TypedDict

def get_movie() -> TypedDict[{'name': str, 'year': int}]:
    return {
        'name': 'Blade Runner',
        'year': 1982,
    }

Motivation

Python-Wörterbücher sind eine wesentliche Datenstruktur der Sprache. Oft werden sie verwendet, um strukturierte Daten in Funktionen zurückzugeben oder zu empfangen. Es kann jedoch mühsam werden, TypedDict-Klassen zu definieren.

  • Ein typisiertes Wörterbuch benötigt einen Namen, der möglicherweise nicht relevant ist.
  • Verschachtelte Wörterbücher erfordern mehr als eine Klassendefinition.

Ein einfaches Beispiel für eine Funktion, die verschachtelte strukturierte Daten zurückgibt

from typing import TypedDict

class ProductionCompany(TypedDict):
    name: str
    location: str

class Movie(TypedDict):
    name: str
    year: int
    production: ProductionCompany


def get_movie() -> Movie:
    return {
        'name': 'Blade Runner',
        'year': 1982,
        'production': {
            'name': 'Warner Bros.',
            'location': 'California',
        }
    }

Begründung

Die neue Inline-Syntax kann verwendet werden, um diese Probleme zu lösen.

def get_movie() -> TypedDict[{'name': str, 'year': int, 'production': TypedDict[{'name': str, 'location': str}]}]:
    ...

Obwohl weniger nützlich (da die funktionale oder sogar die klassenbasierte Syntax verwendet werden kann), können Inline-typisierte Wörterbücher einer Variablen als Alias zugewiesen werden.

InlineTD = TypedDict[{'name': str}]

def get_movie() -> InlineTD:
    ...

Spezifikation

Die spezielle Form TypedDict wird subscriptable und akzeptiert ein einzelnes Typargument, das ein dict sein muss, was den gleichen Semantiken wie die funktionale Syntax folgt (die Wörterbuchschlüssel sind Zeichenfolgen, die die Feldnamen darstellen, und Werte sind gültige Annotationsausdrücke). Nur die durch Kommas getrennte Liste von key: value-Paaren innerhalb der geschweiften Klammernkonstruktion ({k: <type>}) ist zulässig und sollte direkt als Typargument angegeben werden (d. h. es ist nicht zulässig, eine Variable zu verwenden, der zuvor eine dict-Instanz zugewiesen wurde).

Inline typisierte Wörterbücher können als *anonym* bezeichnet werden, was bedeutet, dass sie keinen spezifischen Namen haben (siehe Abschnitt Laufzeitverhalten).

Es ist möglich, ein verschachteltes Inline-Wörterbuch zu definieren.

Movie = TypedDict[{'name': str, 'production': TypedDict[{'location': str}]}]

# Note that the following is invalid as per the updated `type_expression` grammar:
Movie = TypedDict[{'name': str, 'production': {'location': str}}]

Obwohl keine Klassenargumente wie total angegeben werden können, kann für einzelne Felder jeder Typqualifizierer verwendet werden.

Movie = TypedDict[{'name': NotRequired[str], 'year': ReadOnly[int]}]

Inline typisierte Wörterbücher sind implizit *total*, was bedeutet, dass alle Schlüssel vorhanden sein müssen. Die Verwendung des Required-Typqualifizierers ist daher überflüssig.

Typvariablen sind in Inline-typisierten Wörterbüchern zulässig, vorausgesetzt, sie sind an einen äußeren Gültigkeitsbereich gebunden.

class C[T]:
    inline_td: TypedDict[{'name': T}]  # OK, `T` is scoped to the class `C`.

reveal_type(C[int]().inline_td['name'])  # Revealed type is 'int'


def fn[T](arg: T) -> TypedDict[{'name': T}]: ...  # OK: `T` is scoped to the function `fn`.

reveal_type(fn('a')['name'])  # Revealed type is 'str'


type InlineTD[T] = TypedDict[{'name': T}]  # OK, `T` is scoped to the type alias.


T = TypeVar('T')

InlineTD = TypedDict[{'name': T}]  # OK, same as the previous type alias, but using the old-style syntax.


def func():
    InlineTD = TypedDict[{'name': T}]  # Not OK: `T` refers to a type variable that is not bound to the scope of `func`.

Inline typisierte Wörterbücher können erweitert werden.

InlineTD = TypedDict[{'a': int}]

class SubTD(InlineTD):
    pass

Änderungen der Typisierungsspezifikation

Das Inline-typisierte Wörterbuch fügt eine neue Art von Typausdruck hinzu. Daher wird die Produktion type_expression aktualisiert, um die Inline-Syntax aufzunehmen.

new-type_expression ::=  type_expression
                         | <TypedDict> '[' '{' (string: ':' annotation_expression ',')* '}' ']'
                               (where string is any string literal)

Laufzeitverhalten

Das Erstellen eines Inline-typisierten Wörterbuchs führt zu einer neuen Klasse, sodass T1 und T2 vom gleichen Typ sind.

from typing import TypedDict

T1 = TypedDict('T1', {'a': int})
T2 = TypedDict[{'a': int}]

Da Inline-typisierte Wörterbücher *anonym* sein sollen, wird ihr __name__-Attribut auf den Zeichenfolgenliteral <inline TypedDict> gesetzt. In Zukunft könnte ein explizites Klassenattribut hinzugefügt werden, um sie von benannten Klassen zu unterscheiden.

Obwohl TypedDict als Klasse dokumentiert ist, ist die Art und Weise, wie sie definiert wird, ein Implementierungsdetail. Die Implementierung muss angepasst werden, damit TypedDict subscriptable gemacht werden kann.

Abwärtskompatibilität

Dieses PEP führt keine rückwärtskompatiblen Änderungen ein.

Sicherheitsimplikationen

Es sind keine bekannten Sicherheitsfolgen aus diesem PEP bekannt.

Wie man das lehrt

Die neue Inline-Syntax wird sowohl in der Dokumentation des Moduls typing als auch in der Typisierungsspezifikation dokumentiert.

Wenn komplexe Wörterbuchstrukturen verwendet werden, kann die Definition von allem auf einer einzigen Zeile die Lesbarkeit beeinträchtigen. Code-Formatierer können helfen, indem sie das Inline-Typ-Wörterbuch über mehrere Zeilen formatieren.

def edit_movie(
    movie: TypedDict[{
        'name': str,
        'year': int,
        'production': TypedDict[{
            'location': str,
        }],
    }],
) -> None:
    ...

Referenzimplementierung

Mypy unterstützt eine ähnliche Syntax als experimentelle Funktion.

def test_values() -> {"int": int, "str": str}:
    return {"int": 42, "str": "test"}

Die Unterstützung für dieses PEP wurde in diesem Pull Request hinzugefügt.

Pyright fügte die Unterstützung für die neue Syntax in Version 1.1.387 hinzu.

Laufzeitimplementierung

Die notwendigen Änderungen wurden zuerst in typing_extensions in diesem Pull Request implementiert.

Abgelehnte Ideen

Verwendung der funktionalen Syntax in Annotationen

Die alternative funktionale Syntax könnte direkt als Annotation verwendet werden.

def get_movie() -> TypedDict('Movie', {'title': str}): ...

Aufruf-Ausdrücke werden derzeit aus verschiedenen Gründen (teuer zu verarbeiten, ihre Auswertung ist nicht standardisiert) in einem solchen Kontext nicht unterstützt.

Dies würde auch einen Namen erfordern, der manchmal nicht relevant ist.

Verwendung von dict oder typing.Dict mit einem einzelnen Typargument

Wir könnten dict oder typing.Dict mit einem einzelnen Typargument wiederverwenden, um dasselbe Konzept auszudrücken.

def get_movie() -> dict[{'title': str}]: ...

Dies würde zwar die Notwendigkeit vermeiden, TypedDict aus typing zu importieren, diese Lösung hat jedoch mehrere Nachteile.

  • Für Typ-Checker ist dict eine normale Klasse mit zwei Typvariablen. dict mit einem einzelnen Typargument zu parametrisieren, würde eine spezielle Behandlung durch Typ-Checker erfordern, da es keine Möglichkeit gibt, Parametrisierungs-Overloads auszudrücken. TypedDict ist hingegen bereits eine Spezialform.
  • Wenn zukünftige Arbeiten die Möglichkeiten von Inline-typisierten Wörterbüchern erweitern, müssen wir uns keine Sorgen um die Auswirkungen der gemeinsamen Nutzung des Symbols mit dict machen.
  • typing.Dict wurde durch PEP 585 als veraltet markiert (obwohl keine Entfernung geplant ist). Es wäre verwirrend für Benutzer, es für ein neues Typisierungsmerkmal zu verwenden (und würde Änderungen an Code-Linting-Tools erfordern).

Verwendung eines einfachen Wörterbuchs

Anstatt die Klasse TypedDict zu indizieren, könnte ein einfaches Wörterbuch als Annotation verwendet werden.

def get_movie() -> {'title': str}: ...

Jedoch fügte PEP 584 Union-Operatoren für Wörterbücher hinzu und PEP 604 führte Union-Typen ein. Beide Merkmale nutzen den Bitweise-ODER-Operator (|), was die folgenden Anwendungsfälle inkompatibel macht, insbesondere für die Laufzeit-Introspektion.

# Dictionaries are merged:
def fn() -> {'a': int} | {'b': str}: ...

# Raises a type error at runtime:
def fn() -> {'a': int} | int: ...

Erweiterung anderer typisierter Wörterbücher

Verschiedene Syntaxvarianten könnten verwendet werden, um die Fähigkeit zu haben, andere typisierte Wörterbücher zu erweitern.

InlineBase = TypedDict[{'a': int}]

Inline = TypedDict[InlineBase, {'b': int}]
# or, by providing a slice:
Inline = TypedDict[{'b': int} : (InlineBase,)]

Da Inline-typisierte Wörterbücher nur eine Teilmenge der bestehenden Syntax unterstützen sollen, ist die Hinzufügung dieses Erweiterungsmechanismus angesichts der zusätzlichen Komplexität nicht überzeugend genug, um unterstützt zu werden.

Wenn Schnittmengen in das Typsystem aufgenommen würden, könnte dies diesen Anwendungsfall abdecken.

Offene Fragen

Inline typisierte Wörterbücher und zusätzliche Elemente

PEP 728 führt das Konzept von *geschlossenen* Typ-Wörterbüchern ein. Wenn dieses PEP angenommen würde, wären Inline-typisierte Wörterbücher standardmäßig *geschlossen*. Dies bedeutet, dass PEP 728 zuerst behandelt werden muss, damit dieses PEP entsprechend aktualisiert werden kann.


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

Zuletzt geändert: 2025-05-06 22:54:17 GMT