Python Dictionaries sind bequem, bis sie heimlich zur Datenstruktur deiner ganzen Anwendung werden.
Es beginnt meistens mit einem kleinen Script. Ein paar Keys, eine JSON Response, eine schnelle Transformation. Alles wirkt lesbar. Dann wird aus dem Script ein Service, aus dem Service ein Produkt, und ploetzlich wird dieselbe Dict-Struktur in Dutzenden Dateien erzeugt, veraendert und erraten.
Das Problem ist nicht, dass Dictionaries schlecht sind. Das Problem ist, sie als unsichtbare Domain Models zu benutzen.
Wo Dicts anfangen weh zu tun
Ein Dictionary kann fast alles unter fast jedem Key speichern. Diese Flexibilitaet ist fuer Transportdaten nuetzlich, aber fuer Kernlogik gefaehrlich.
Wenn eine Funktion firstName erwartet, eine andere first_name, und eine dritte still permissions entfernt, gibt es keinen klaren Ort mehr, an dem man die finale Form versteht.
Das Datenmodell wird zu implizitem Wissen. IDE Autocomplete wird schwaecher. Refactoring wird langsamer. Bugs erscheinen als KeyError, falsche Defaults oder fehlende Felder erst dann, wenn der Codepfad in Produktion laeuft.
Dicts als Transport behandeln, nicht als Domain Objects
Rohe Dictionaries sind perfekt an Systemgrenzen:
- eingehendes JSON von einer API,
- decodierte Message-Queue-Payloads,
- locker strukturierte Konfiguration,
- temporaere Key-Value-Mappings.
Aber sobald die Daten in deine Anwendung kommen, sollten sie in etwas mit Name, Feldern und Verhalten umgewandelt werden.
Stell dir vor, eine User API liefert diese Payload:
{
"user_id": 42,
"profile": {
"firstName": "Alice",
"lastName": "Wonderland"
},
"permissions": ["read", "write", "admin"]
}
Dieses rohe Dictionary ueberall weiterzugeben ist verlockend. Genau so wird aber jede spaetere Feldumbenennung schmerzhaft.
Option 1: Eine kleine Domain Class
Der einfachste Fix ist eine Klasse, die das Konzept beschreibt, das deine Anwendung wirklich benutzt.
import requests
class User:
def __init__(
self,
user_id: int,
first_name: str,
last_name: str,
permissions: list[str],
):
self.user_id = user_id
self.first_name = first_name
self.last_name = last_name
self.permissions = permissions
def full_name(self) -> str:
return f"{self.first_name} {self.last_name}"
def fetch_user(user_id: int) -> User:
response = requests.get(f"https://example.com/api/users/{user_id}", timeout=10)
response.raise_for_status()
data = response.json()
return User(
user_id=data["user_id"],
first_name=data["profile"]["firstName"],
last_name=data["profile"]["lastName"],
permissions=data["permissions"],
)
Der Code ist etwas laenger, aber die Umwandlung ist jetzt zentral. Wenn sich die externe API aendert, passt du fetch_user an, statt die ganze Codebase zu durchsuchen.
Option 2: Dataclasses
Fuer einfache Datenobjekte entfernen dataclasses viel Boilerplate.
from dataclasses import dataclass
@dataclass(frozen=True)
class User:
user_id: int
first_name: str
last_name: str
permissions: list[str]
def full_name(self) -> str:
return f"{self.first_name} {self.last_name}"
frozen=True macht das Objekt immutable. Das ist nuetzlich, wenn du versehentliche Mutation nach dem Parsen verhindern willst.
Wenn du eine veraenderte Version brauchst, erzeugst du eine neue Instanz oder nutzt dataclasses.replace.
Option 3: Pydantic
Wenn du Validation, Type Conversion, Aliases, Nested Models oder Serialization brauchst, ist Pydantic oft die staerkste Wahl.
from pydantic import BaseModel, ConfigDict, Field
class Profile(BaseModel):
first_name: str = Field(alias="firstName")
last_name: str = Field(alias="lastName")
class User(BaseModel):
model_config = ConfigDict(frozen=True)
user_id: int
profile: Profile
permissions: list[str]
def full_name(self) -> str:
return f"{self.profile.first_name} {self.profile.last_name}"
Ungueltiger Input scheitert damit an der Grenze. Der Rest der Anwendung bekommt ein vorhersagbares Objekt statt eines mysterioesen Dictionaries.
Option 4: TypedDict fuer schrittweises Refactoring
Manchmal kannst du Dictionaries nicht sofort ersetzen. Ein Legacy-Projekt gibt Dicts vielleicht schon durch zu viele Schichten.
TypedDict gibt dir einen Migrationsschritt. Zur Runtime bleibt es ein normales Dictionary, aber Static Checker koennen fehlende Keys und falsche Value Types finden.
from typing import TypedDict
class UserDict(TypedDict):
user_id: int
first_name: str
last_name: str
permissions: list[str]
user_data: UserDict = {
"user_id": 42,
"first_name": "Alice",
"last_name": "Wonderland",
"permissions": ["read", "write"],
}
Das ist nicht so stark wie ein echtes Modell, aber deutlich besser als ein untypisiertes dict[str, object].
Wann Dictionaries trotzdem richtig sind
Dictionaries sind weiterhin hervorragend fuer echte Mappings:
api_keys: dict[str, str] = {
"service_a": "12345ABC",
"service_b": "ABCDE12345",
}
Wenn die Struktur wirklich ein Key-Value Store ist, bleibt ein Mapping sinnvoll. Du kannst APIs auch mit Mapping[str, str] oder MutableMapping[str, str] annotieren, um zu zeigen, ob Caller mutieren duerfen.
Die Kernfrage ist einfach: Ist das eine Lookup Table, oder tut das Dict nur so, als waere es eine Entity?
Praktische Regel fuer 2026
Nutze Dictionaries an den Raendern.
Wandle externe Payloads so frueh wie moeglich in benannte Modelle um.
Nutze dataclasses fuer leichte interne Strukturen, Pydantic fuer validation-heavy Daten und TypedDict, wenn du einen schrittweisen Weg aus dict-lastigem Legacy Code brauchst.
Je frueher du Datenformen explizit machst, desto leichter wird dein Python-Projekt zu refactoren, zu testen und zu warten.


