Kodokon kodokon.com

Type Hints und Dataclasses: modernes Python ernsthaft typisieren

Verstehe, was Annotationen zur Laufzeit wirklich tun, und hol das Beste aus Dataclasses heraus - mit frozen, slots und default_factory.

10 Min. · 3 Fragen

Diese Lektion in Kodokon öffnen

Typ-Annotationen sind Metadaten, keine Einschränkungen: Der Interpreter speichert sie in __annotations__ und prüft sie zur Laufzeit nie. Das Prüfen ist die Aufgabe eines statischen Analysewerkzeugs wie mypy oder pyright, vor der Ausführung. Die direkte Folge: Ein schlecht typisiertes Programm läuft ohne Murren - und das ist so gewollt. Schrittweise Typisierung erlaubt dir, eine bestehende Codebasis nach und nach zu annotieren, ohne irgendetwas zu zerbrechen.

PYTHON
from typing import get_type_hints

def add(a: int, b: int) -> int:
    return a + b

print(add("py", "thon"))
print(add.__annotations__)
print(get_type_hints(add))
Annotationen blockieren nichts: Der "falsche" Aufruf gelingt.

Seit Python 3.9 sind eingebaute Typen direkt generisch: Schreib list[int] oder dict[str, float], ohne typing.List zu importieren. Unions schreibt man seit 3.10 als int | None. Um den Eingabetyp an den Ausgabetyp zu koppeln, nutze einen TypeVar - Python 3.12 bietet sogar die kompakte Syntax def first[T](items: list[T]) -> T. Ein oft übersehener Punkt: from __future__ import annotations macht jede Annotation lazy (als Strings gespeichert), was Verweise auf weiter unten definierte Klassen erlaubt und die Kosten beim Import senkt.

PYTHON
from collections.abc import Iterable, Iterator
from typing import TypeVar

T = TypeVar("T")

def dedupe(items: Iterable[T]) -> Iterator[T]:
    seen: set[T] = set()
    for item in items:
        if item not in seen:
            seen.add(item)
            yield item

print(list(dedupe([3, 1, 3, 2, 1])))
Der TypeVar garantiert, dass dedupe(list[int]) wirklich ints liefert.
PYTHON
from dataclasses import dataclass, field

@dataclass(frozen=True, slots=True)
class Point:
    x: float
    y: float

@dataclass
class Basket:
    items: list[str] = field(default_factory=list)
    total: int = 0

    def __post_init__(self) -> None:
        self.total = len(self.items)

point = Point(1.0, 2.0)
print(point, hash(point))
print(Basket(["book", "pen"]))
Point ist unveränderlich, hashbar und hat kein __dict__ pro Instanz.

Drei Parameter machen aus @dataclass ein Werkzeug für den Produktivbetrieb. frozen=True macht Instanzen unveränderlich und, zusammen mit der generierten Gleichheit, hashbar - nutzbar als Dictionary-Schlüssel. slots=True (Python 3.10+) entfernt das __dict__ pro Instanz: weniger Speicher, schnellerer Attributzugriff, und jede unerwartete Attributzuweisung schlägt fehl. Schließlich erzwingt kw_only=True explizite Aufrufe mit Schlüsselwörtern. In __post_init__ gehören Invarianten und abgeleitete Felder. Für ein einfaches unveränderliches Aggregat ist NamedTuple weiterhin sinnvoll; sobald es Verhalten oder Validierung gibt, bevorzuge die Dataclass.

Wissenscheck

Stelle sicher, dass du die wichtigsten Punkte dieser Lektion behalten hast.

  1. Was passiert zur Laufzeit, wenn du add('py', 'thon') aufrufst, während die Funktion (a: int, b: int) -> int annotiert ist?
    • Python löst sofort einen TypeError aus
    • Der Aufruf läuft normal: Annotationen werden zur Laufzeit nie geprüft
    • Python gibt eine RuntimeWarning aus und führt den Aufruf dann aus
    • Das Modul verweigert den Import
  2. Warum ist items: list[str] = [] als Standardwert eines Dataclass-Feldes verboten?
    • Weil list[str] keine gültige Annotation für ein Feld ist
    • Weil Dataclasses nur Felder ohne Standardwert dulden
    • Weil dieser veränderbare Standardwert über alle Instanzen geteilt würde: Die Dataclass löst ValueError aus und verlangt field(default_factory=list)
    • Weil Standardwerte in __post_init__ gesetzt werden müssen
  3. Was macht frozen=True mit einer Dataclass?
    • Instanzen werden unveränderlich und, mit der generierten Gleichheit, hashbar - nutzbar als Dictionary-Schlüssel
    • Die Klasse kann nicht mehr abgeleitet werden
    • Die Felder werden zu geteilten Klassenattributen
    • Annotationen werden bei jeder Zuweisung geprüft