Aufwärmen · Aufgabe 1 von 7
// 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.
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
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"])Ü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]Ü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}Üben · Aufgabe 5 von 7
Ordnen Sie jedem Konstrukt zu, was es dem Type Checker sagt.
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)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.pyAusgabe
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
Hinweis 1
from typing import NotRequired, dann isbn: NotRequired[str].
Hinweis 2
book["isbn"] löst bei einem Datensatz ohne isbn einen KeyError aus. Prüfen Sie zuerst "isbn" in book.
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.pyPrüfungen ausführen (learnrun.py muss im selben Ordner liegen):
python learnrun.py testlearnrun.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
Hinweis 1
type Light = Literal["red", "green", "yellow"], dann LIGHTS: Final[tuple[Light, ...]] = (...).
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]].
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.pyPrüfungen ausführen (learnrun.py muss im selben Ordner liegen):
python learnrun.py testlearnrun.py herunterladenHä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 checksWarum, 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 checksWarum, 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.