Zum Inhalt springen
aviral gupta

// A2.4 · ca. 30 Min. · Vertiefung

TypedDict, Literal und Final

Nach dieser Lektion beschreiben Sie Dict-Datensätze mit TypedDict, schränken Werte mit Literal auf feste Optionen ein und markieren Konstanten, Methoden und Klassen als final. Und Sie wissen, was Python davon zur Laufzeit prüft: nichts.

Lektion 4 von 6 in A2 Typisierung

Danach können Sie

  • Dict-Datensätze mit TypedDict beschreiben, einschließlich total=False und NotRequired
  • Werte mit Literal auf feste Optionen einschränken und vorhersagen, wie mypy sie eingrenzt
  • Namen, Methoden und Klassen mit Final und @final als final markieren
  1. Aufwärmen · Aufgabe 1 von 7

    Aufwärmen aus Lektion A2.1: Welcher Hint passt am besten zum JSON-Datensatz {"title": "Alien", "year": 1979}, wenn Sie nur eingebaute Generics verwenden?

  2. Vorhersagen · Aufgabe 2 von 7

    Sagen Sie es vorher, bevor Sie weiterlesen: year ist als int deklariert, der Aufruf übergibt einen String. Was wird ausgegeben?

    from typing import TypedDict
    
    
    class Movie(TypedDict):
        title: str
        year: int
    
    
    m = Movie(title="Alien", year="1979")
    print(type(m).__name__, m["year"])
  3. Üben · Aufgabe 3 von 7

    Viele Filme haben noch keine Bewertung. Ergänzen Sie den Qualifier, sodass der Schlüssel rating fehlen darf, während title Pflicht bleibt.

    class Movie(TypedDict):
        title: str
        rating: ____[float]
    rating: [float]
  4. Üben · Aufgabe 4 von 7

    mypy meldet einen Fehler für die letzte Zeile. Welchen Fehlercode?

    from typing import TypedDict
    
    
    class Movie(TypedDict):
        title: str
        year: int
    
    
    m: Movie = {"title": "Alien", "year": 1979, "rating": 8.5}
  5. Üben · Aufgabe 5 von 7

    Ordnen Sie jedem Konstrukt zu, was es dem Type Checker sagt.

  6. Denksport · Aufgabe 6 von 7

    Knobelaufgabe. d enthält "up", was das Literal erlaubt, trotzdem lehnt mypy move(d) ab. Welchen Typ hat d laut mypy?

    from typing import Literal
    
    
    def move(direction: Literal["up", "down"]) -> None:
        print("moving", direction)
    
    
    d = "up"
    move(d)
  7. Anwenden · Aufgabe 7 von 7

    Mini-Aufgabe. Bestellungen kommen als JSON: eine id, ein status, der "open", "paid" oder "shipped" ist, und manchmal eine note. Beschreiben Sie sie mit einem TypedDict Order, den Status mit einem Literal-Alias und die Notiz mit NotRequired. Schreiben Sie summary(order), das #1 paid liefert oder #2 open (gift), wenn es eine Notiz gibt. Prüfen Sie es mit mypy --strict.

    Prüfen Sie Ihr Ergebnis anhand dieser Liste

Selbst programmieren

Lesen Sie das ausgearbeitete Beispiel und lösen Sie dann die Übungen. Ihr Code läuft in Ihrem Browser oder auf Ihrem Computer und wird nie hochgeladen.

Ausgearbeitetes Beispiel

Typisierte Log-Ereignisse

Ein Event ist ein TypedDict, dessen level ein Literal-Alias ist und dessen code optional ist. MAX_MESSAGE ist eine Final-Konstante, Formatter eine @final-Klasse. mypy würde ein level wie "debug", eine fehlende message, MAX_MESSAGE = 50 oder eine Unterklasse von Formatter ablehnen. Die letzten Zeilen zeigen, was zur Laufzeit übrig bleibt.

main.py

from typing import Final, Literal, NotRequired, TypedDict, final, get_args

type Level = Literal["info", "warning", "error"]
MAX_MESSAGE: Final = 30


class Event(TypedDict):
    level: Level
    message: str
    code: NotRequired[int]


@final
class Formatter:
    def format(self, event: Event) -> str:
        text = event["message"][:MAX_MESSAGE]
        if "code" in event:
            text += f" [code {event['code']}]"
        return f"{event['level'].upper()}: {text}"


events: list[Event] = [
    {"level": "info", "message": "server started"},
    {"level": "error", "message": "disk full on /var/data/cache/images", "code": 28},
]
formatter = Formatter()
for event in events:
    print(formatter.format(event))
print(get_args(Level.__value__), sorted(Event.__optional_keys__))
print(type(events[0]).__name__, getattr(Formatter, "__final__", False))

Ausführen mit

python main.py

Ausgabe

INFO: server started
ERROR: disk full on /var/data/cache/i [code 28]
('info', 'warning', 'error') ['code']
dict True
  • Mit "code" in event prüfen Sie einen NotRequired-Schlüssel, bevor Sie ihn lesen; nach diesem Test akzeptiert mypy den Zugriff.
  • get_args(Level.__value__) liest die erlaubten Werte aus dem Alias, sie stehen also nur an einer Stelle.
  • Die Ereignisse sind gewöhnliche Dicts: Das TypedDict existiert für den Type Checker.
  • @final hinterlässt nur das Flag __final__ = True; eine Unterklasse von Formatter würde trotzdem laufen.
Ändern und ausführen

Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.

Beim ersten Ausführen lädt Ihr Browser Python herunter (bis zu 6.5 MB) und speichert es im Cache. Ihr Code bleibt auf Ihrem Gerät.

Übungen

Übung 1 von 2

Ein typisierter Buchdatensatz

Buchdatensätze kommen aus JSON. Machen Sie isbn im TypedDict Book mit NotRequired optional; title, author und year bleiben Pflicht. Korrigieren Sie dann label(book): Es liefert Titel (Autor, Jahr), gefolgt von ISBN und der Nummer nur dann, wenn der Datensatz eine isbn hat. load_books liest das JSON bereits ein.

Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.

Beim ersten Ausführen lädt Ihr Browser Python herunter (bis zu 6.5 MB) und speichert es im Cache. Ihr Code bleibt auf Ihrem Gerät.

Hinweise
  1. Hinweis 1

    from typing import NotRequired, dann isbn: NotRequired[str].

  2. Hinweis 2

    book["isbn"] löst bei einem Datensatz ohne isbn einen KeyError aus. Prüfen Sie zuerst "isbn" in book.

  3. Hinweis 3

    Bauen Sie den Text ohne ISBN und hängen Sie im if " ISBN " und die Nummer an.

Eine Lösung zeigen

Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.

import json
from typing import NotRequired, TypedDict


class Book(TypedDict):
    title: str
    author: str
    year: int
    isbn: NotRequired[str]


def load_books(text: str) -> list[Book]:
    books: list[Book] = json.loads(text)
    return books


def label(book: Book) -> str:
    text = f"{book['title']} ({book['author']}, {book['year']})"
    if "isbn" in book:
        text += f" ISBN {book['isbn']}"
    return text
Auf dem eigenen Computer ausführen

Installieren Sie Python 3.14 oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.

main.py

import json
from typing import TypedDict


class Book(TypedDict):
    title: str
    author: str
    year: int
    isbn: str


def load_books(text: str) -> list[Book]:
    books: list[Book] = json.loads(text)
    return books


def label(book: Book) -> str:
    return f"{book['title']} ({book['author']}, {book['year']}) ISBN {book['isbn']}"

test_main.py

from typing import is_typeddict

from main import Book, label, load_books

DATA = '[{"title": "Dune", "author": "Frank Herbert", "year": 1965, "isbn": "9780441013593"}, {"title": "Emma", "author": "Jane Austen", "year": 1815}]'


def test_keys():
    """title, author und year sind Pflicht; isbn ist optional"""
    assert is_typeddict(Book), "Book sollte ein TypedDict sein"
    got = sorted(Book.__required_keys__), sorted(Book.__optional_keys__)
    assert got == (['author', 'title', 'year'], ['isbn']), f"Pflicht- und optionale Schlüssel sind {got!r}"


def test_label_with_isbn():
    """Ein Buch mit isbn zeigt sie am Ende"""
    got = label(load_books(DATA)[0])
    assert got == "Dune (Frank Herbert, 1965) ISBN 9780441013593", f"label lieferte {got!r}"


def test_label_without_isbn():
    """Ein Buch ohne isbn hat keinen ISBN-Teil"""
    got = label(load_books(DATA)[1])
    assert got == "Emma (Jane Austen, 1815)", f"label lieferte {got!r}"

Unter macOS und Linux tippen Sie python3, wo in diesen Befehlen python steht, wie in der ersten Lektion.

Programm ausführen:

python main.py

Prüfungen ausführen (learnrun.py muss im selben Ordner liegen):

python learnrun.py test
learnrun.py herunterladen

Übung 2 von 2

Ampeln

Definieren Sie type Light = Literal["red", "green", "yellow"]. Machen Sie LIGHTS zu einem Final[tuple[Light, ...]] und NEXT zu einem Final[dict[Light, Light]] (red zu green, green zu yellow, yellow zu red). next_light liefert die nächste Farbe. parse_light(text) entfernt Leerraum, wandelt in Kleinbuchstaben um, liefert das passende Light aus LIGHTS und löst für alles andere ValueError aus.

Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.

Beim ersten Ausführen lädt Ihr Browser Python herunter (bis zu 6.5 MB) und speichert es im Cache. Ihr Code bleibt auf Ihrem Gerät.

Hinweise
  1. Hinweis 1

    type Light = Literal["red", "green", "yellow"], dann LIGHTS: Final[tuple[Light, ...]] = (...).

  2. Hinweis 2

    Ein einfaches NEXT = {...} hat den Typ dict[str, str], und mypy lehnt dann NEXT[light] als Rückgabe vom Typ Light ab. Annotieren Sie es als Final[dict[Light, Light]].

  3. Hinweis 3

    Gehen Sie LIGHTS durch und geben Sie die Farbe zurück, die dem bereinigten Text gleicht: So weiß mypy, dass das Ergebnis ein Light ist.

Eine Lösung zeigen

Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.

from typing import Final, Literal

type Light = Literal["red", "green", "yellow"]

LIGHTS: Final[tuple[Light, ...]] = ("red", "green", "yellow")
NEXT: Final[dict[Light, Light]] = {"red": "green", "green": "yellow", "yellow": "red"}


def next_light(light: Light) -> Light:
    return NEXT[light]


def parse_light(text: str) -> Light:
    cleaned = text.strip().lower()
    for light in LIGHTS:
        if light == cleaned:
            return light
    raise ValueError(f"unknown light: {text!r}")
Auf dem eigenen Computer ausführen

Installieren Sie Python 3.14 oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.

main.py

from typing import Final, Literal

# Define the alias Light, and annotate the constants as Final.
LIGHTS = ("red", "green", "yellow")
NEXT = {"red": "green", "green": "yellow", "yellow": "red"}


def next_light(light: str) -> str:
    return NEXT[light]


def parse_light(text: str) -> str:
    return text

test_main.py

from typing import Final, get_args, get_origin

import main


def test_light_alias():
    """Light ist ein Alias für Literal["red", "green", "yellow"]"""
    light = getattr(main, "Light", None)
    assert light is not None, "main.py definiert kein Light"
    got = get_args(light.__value__)
    assert got == ("red", "green", "yellow"), f"Light erlaubt {got!r}"


def test_constants_are_final():
    """LIGHTS und NEXT sind mit Final[...] annotiert"""
    hints = getattr(main, "__annotations__", {})
    for name in ("LIGHTS", "NEXT"):
        assert get_origin(hints.get(name)) is Final, f"{name} ist als {hints.get(name)!r} annotiert, erwartet Final[...]"


def test_next_light():
    """red, green und yellow folgen im Kreis aufeinander"""
    got = [main.next_light(light) for light in ("red", "green", "yellow")]
    assert got == ["green", "yellow", "red"], f"next_light lieferte {got!r}"


def test_parse_light():
    """parse_light bereinigt den Text: " Green " wird zu "green\""""
    got = main.parse_light(" Green ")
    assert got == "green", f"parse_light(' Green ') lieferte {got!r}"


def test_parse_rejects_unknown():
    """parse_light("blue") löst ValueError aus"""
    try:
        main.parse_light("blue")
    except ValueError:
        return
    assert False, "parse_light('blue') hat keinen ValueError ausgelöst"

Unter macOS und Linux tippen Sie python3, wo in diesen Befehlen python steht, wie in der ersten Lektion.

Programm ausführen:

python main.py

Prüfungen ausführen (learnrun.py muss im selben Ordner liegen):

python learnrun.py test
learnrun.py herunterladen

Häufige Fehler

isinstance() mit einem TypedDict

from typing import TypedDict


class Movie(TypedDict):
    title: str


print(isinstance({"title": "Alien"}, Movie))

Was Python ausgibt

TypeError: TypedDict does not support instance and class checks

Warum, und die Lösung

Ein TypedDict-Wert ist ein gewöhnliches Dict, es gibt also keine Klasse zum Prüfen. Prüfen Sie selbst, was Sie brauchen: isinstance(data, dict) und die nötigen Schlüssel, zum Beispiel Movie.__required_keys__ <= data.keys(), und lassen Sie mypy den Rest dort prüfen, wo das Dict entsteht.

Einen Pflichtschlüssel weglassen

from typing import TypedDict


class Movie(TypedDict):
    title: str
    year: int


m = Movie(title="Alien")
print(m["year"])

Was Python ausgibt

KeyError: 'year'

Warum, und die Lösung

Python baut das Dict ohne Beschwerde, der Fehler kommt erst beim Lesen des Schlüssels. mypy meldet die Zeile, in der das Dict entsteht: Missing key "year" for TypedDict "Movie" [typeddict-item]. Geben Sie den Schlüssel an, oder deklarieren Sie ihn als NotRequired[int], wenn er wirklich fehlen darf, und prüfen Sie "year" in m vor dem Lesen.

isinstance() mit einem Literal

from typing import Literal

Mode = Literal["r", "w"]
print(isinstance("r", Mode))

Was Python ausgibt

TypeError: Subscripted generics cannot be used with class and instance checks

Warum, und die Lösung

Ein Literal ist eine Menge von Werten, keine Klasse. Um Eingaben zur Laufzeit zu prüfen, testen Sie die Zugehörigkeit zu seinen Werten: from typing import get_args, dann if mode in get_args(Mode):. So stehen die Werte an einer Stelle, im Literal, und die Prüfung wird daraus abgeleitet.

Python im Browser: Pyodide 314.0.7, MPL-2.0. Lizenz und Quellcode

Abschlussquiz

5 Fragen, ohne Hinweise. Ab 80 % ist die Lektion abgeschlossen.

Erledigen Sie zuerst alle Aufgaben oben, um das Abschlussquiz freizuschalten.

Problem melden

Etwas ist falsch oder unklar? Beschreiben Sie es kurz, dann wird es geprüft und korrigiert.

#

Mindestens 20 Zeichen.

Nur, wenn Sie eine Antwort wünschen.

Kernideen

TypedDict: ein Dict mit bekannter Form

class Movie(TypedDict): title: str; year: int beschreibt Dicts mit festen String-Schlüsseln, jeder mit eigenem Werttyp, etwa Datensätze aus JSON. mypy meldet fehlende, unbekannte und falsch geschriebene Schlüssel sowie falsche Werttypen. Jeder Schlüssel ist Pflicht, außer er ist als NotRequired[...] markiert; total=False macht alle optional, Required[...] macht einen wieder zur Pflicht. Zur Laufzeit ist ein Movie ein gewöhnliches Dict: Movie(title=..., year=...) liefert ein dict, und isinstance() mit einem TypedDict löst TypeError aus.

Literal: genau diese Werte

def move(direction: Literal["up", "down"]) nimmt nur diese beiden Strings an; mypy lehnt move("left") ab. Eine Variable, der "up" zugewiesen wird, hat den Typ str, also scheitert auch sie, außer sie ist Final oder mit dem Literal-Typ annotiert. Nach if level == "low": return grenzt mypy den Rest der Funktion auf Literal["high"] ein. Zur Laufzeit liefert typing.get_args() die erlaubten Werte, praktisch für die Prüfung von Eingaben.

Final und @final

MAX: Final = 3 sagt mypy, dass der Name nicht neu zugewiesen werden darf: MAX = 4 meldet es als Cannot assign to final name "MAX". Final[int] funktioniert auch für Klassenattribute, die Unterklassen nicht überschreiben dürfen. Der Decorator @final verbietet Unterklassen einer Klasse oder das Überschreiben einer Methode. Zur Laufzeit wird nichts davon geprüft: Neuzuweisung und Unterklasse laufen, und @final setzt nur __final__ = True.

Quellen

Zuletzt geprüft am 29. September 2026