Python Konstruktor - __init__, __new__ & Dataclasses meistern

Zwei Kreise mit Text: "__INIT__()" und "__NEW__()". Pfeile zeigen von links nach rechts. Thema: Konstruktor Python.

Geschrieben von

Thilo Arndt

Veröffentlicht am

16. Juni 2026

Inhaltsverzeichnis

Ein sauberer Python-Konstruktor entscheidet oft darüber, ob eine Klasse später leicht zu warten ist oder nur scheinbar funktioniert. Ich zeige hier, wie Python Objekte tatsächlich aufbaut, warum `__init__` im Alltag meist den Konstruktor meint, wo `__new__` ins Spiel kommt und wann `dataclasses` die elegantere Lösung sind. Dazu kommen typische Fehler, Vererbungsregeln und praktische Entscheidungen, die in Web- und Infrastrukturprojekten wirklich zählen.

Das Wichtigste zur Initialisierung in Python auf einen Blick

  • Klassen sind in Python aufrufbar: Erst wird ein Objekt erzeugt, dann initialisiert.
  • `__init__` setzt den Zustand, `__new__` erzeugt das Objekt selbst.
  • Im Alltag meint man mit „Konstruktor“ meist `__init__`, technisch ist das etwas ungenau.
  • Mutable Standardwerte wie `[]` oder `{}` sind ein häufiger Fehler in Konstruktoren.
  • Für reine Datenobjekte ist `@dataclass` oft kürzer, lesbarer und robuster.
  • Bei Vererbung sollte Initialisierung bewusst mit `super().__init__()` weitergereicht werden.

Python-Konstruktor:

Wie Python Objekte beim Aufruf einer Klasse erzeugt

Die offizielle Python-Dokumentation trennt hier sauber zwischen Erzeugung und Initialisierung. Wenn ich eine Klasse aufrufe, passiert nicht einfach „magisch“ alles in einem Schritt: Zuerst wird das Objekt gebaut, danach bekommt es seinen Zustand. Genau deshalb ist der Begriff „Konstruktor“ in Python im Alltag zwar verbreitet, technisch aber etwas unpräzise.

In der Praxis laufen zwei Sondermethoden zusammen: __new__ erzeugt die Instanz, __init__ initialisiert sie. Für die meisten Klassen reicht es völlig, nur __init__ zu definieren. __new__ brauche ich vor allem dann, wenn ich unveränderliche Typen erweitere, die Objektentstehung gezielt beeinflussen will oder in Spezialfällen Metaklassen und interne Erzeugungslogik anpasse.

Mechanismus Aufgabe Wann sinnvoll Wichtige Einschränkung
__new__ Erzeugt das Objekt Bei int, str, tuple oder Spezialfällen Soll ein Objekt zurückgeben, nicht nur Zustand setzen
__init__ Initialisiert die Instanz Fast immer bei normalen Klassen Darf nichts zurückgeben außer None
@dataclass Erzeugt __init__ automatisch Für Datencontainer und Konfigurationsobjekte Komplexe Lebenszyklen bleiben Handarbeit
Factory-Funktion Wählt die passende Instanz aus Wenn Eingaben zu mehreren Varianten führen Ist kein Ersatz für saubere Klassenstruktur

Was ich daraus ableite: Ein Python-Konstruktor ist kein Ort für „alles, was man gerade noch unterbringen kann“. Er sollte die Instanz in einen gültigen Anfangszustand bringen. Mehr nicht, aber auch nicht weniger. Diese Trennung macht Klassen vorhersagbar, und genau das zahlt sich später in Tests, Wartung und Fehleranalyse aus.

So sieht ein sauberer Konstruktor in der Praxis aus

Ich halte Konstruktoren gern kurz, eindeutig und streng in der Verantwortung. Sie sollen Pflichtdaten prüfen, Defaults setzen und Invarianten sichern. Wenn eine Klasse etwa eine Konfiguration für einen Service, einen API-Client oder einen internen Worker beschreibt, ist das exakt der richtige Ort für eine minimale, gut lesbare Initialisierung.

class ServiceConfig:
    def __init__(self, host: str, port: int = 443, tls: bool = True) -> None:
        if not host:
            raise ValueError("host darf nicht leer sein")
        if not 1 <= port <= 65535:
            raise ValueError("port muss zwischen 1 und 65535 liegen")

        self.host = host.strip()
        self.port = port
        self.tls = tls

    def base_url(self) -> str:
        scheme = "https" if self.tls else "http"
        return f"{scheme}://{self.host}:{self.port}"

Das Beispiel zeigt drei Dinge, die ich in Projekten immer wieder empfehle. Erstens: Pflichtwerte sofort validieren. Zweitens: Eingaben normalisieren, bevor sie in der Instanz landen. Drittens: Den Zustand so ablegen, dass spätere Methoden ihn ohne Überraschungen nutzen können. Ein guter Konstruktor sorgt dafür, dass die restliche Klasse nicht ständig defensiv prüfen muss, ob sie überhaupt sauber gestartet wurde.

Gerade in Web-Anwendungen oder Infrastrukturcode ist das hilfreich, weil dort Konfigurationsfehler schnell teuer werden. Wenn ein Objekt gültig erzeugt wird, ist der Rest des Codes deutlich einfacher zu lesen. Und wenn etwas nicht stimmt, sollte der Fehler früh, klar und direkt im Konstruktor auftauchen.

Typische Fehler bei der Initialisierung

Die meisten Probleme entstehen nicht bei der Syntax, sondern bei der Verantwortung. Ich sehe in Codebasen immer wieder dieselben Fehler, und fast alle davon lassen sich mit etwas Disziplin vermeiden.

  • Mutable Standardwerte wie [] oder {}: Sie werden zwischen Instanzen geteilt, wenn man sie direkt als Default verwendet.
  • Zu viel Logik im Konstruktor: Netzwerkzugriffe, Dateizugriffe oder teure Berechnungen machen Objekte schwer testbar und unvorhersehbar.
  • Vergessenes super().__init__(): In Vererbungshierarchien fehlt dann oft ein Teil des Basiszustands.
  • Rückgabewerte aus __init__: Das führt in Python zu einem Fehler, weil Initialisierung keinen Wert zurückgeben darf.
  • Zu viele Parameter: Wenn der Konstruktor wie ein Formular aussieht, ist das häufig ein Zeichen für ein zu großes Objekt.

Bei veränderlichen Defaults nutze ich fast immer den sicheren Weg mit None als Platzhalter:

class Team:
    def __init__(self, members=None):
        self.members = [] if members is None else list(members)

Der Unterschied ist klein, die Wirkung groß. So bekommt jede Instanz ihre eigene Liste, und ich vermeide schwer erklärbare Nebeneffekte. Genau solche Details machen im Alltag den Unterschied zwischen stabiler Software und späterem Debugging-Schmerz.

Ein weiterer Punkt: Wenn ein Konstruktor externe Systeme anfasst, verliere ich Kontrolle über Fehlerszenarien. Dann ist oft eine explizite Methode besser, etwa connect(), load() oder start(). Der Konstruktor bleibt dadurch leichtgewichtig, und der Lebenszyklus des Objekts wird klarer.

Wann dataclasses die bessere Wahl sind

Die Dataclasses-Dokumentation zeigt sehr deutlich, dass Python für reine Datenobjekte eine eigene, elegante Abkürzung mitbringt: @dataclass kann unter anderem einen passenden __init__ automatisch erzeugen. Für mich ist das immer dann die erste Wahl, wenn eine Klasse vor allem Daten trägt und nur etwas Zusatzlogik braucht.

from dataclasses import dataclass

@dataclass(slots=True)
class InventoryItem:
    name: str
    unit_price: float
    quantity_on_hand: int = 0

    def total_cost(self) -> float:
        return self.unit_price * self.quantity_on_hand

Der Nutzen liegt nicht nur in weniger Code. Die Signatur bleibt klar, die Klasse ist schnell verständlich und die automatische Repräsentation hilft beim Debuggen. Wenn ich zusätzlich Validierung brauche, setze ich sie meist in __post_init__ um, also direkt nach der generierten Initialisierung. Das ist sauberer als eine halbfertige Eigenimplementierung von __init__, solange die Klasse keine Sonderlogik erzwingt.

from dataclasses import dataclass

@dataclass
class PortRange:
    start: int
    end: int

    def __post_init__(self) -> None:
        if self.start > self.end:
            raise ValueError("start muss kleiner oder gleich end sein")

Ich nutze Dataclasses besonders gern für Konfigurationsobjekte, DTOs und kleine Domänenmodelle. Wenn die Klasse aber viele Zustände, externe Abhängigkeiten oder komplexe Erzeugungsregeln hat, ist ein manueller Konstruktor oft klarer. Der Punkt ist nicht, möglichst wenig Code zu schreiben, sondern die richtige Art von Code zu schreiben.

Vererbung richtig initialisieren

Sobald Klassen voneinander erben, wird Initialisierung schnell empfindlich. Eine Basisklasse kann Eigenschaften setzen, Gültigkeitsregeln durchsetzen oder interne Strukturen vorbereiten. Wenn die abgeleitete Klasse das überspringt, fehlen später Teile des Zustands, und der Fehler taucht meist an einer ganz anderen Stelle auf.

class BaseClient:
    def __init__(self, timeout: float = 5.0) -> None:
        self.timeout = timeout

class ApiClient(BaseClient):
    def __init__(self, base_url: str, timeout: float = 5.0) -> None:
        super().__init__(timeout=timeout)
        self.base_url = base_url

Ich setze super() hier bewusst früh ein, damit die Basisklasse ihren Anteil zuverlässig erledigt. In einfachen Hierarchien ist das Pflicht. In komplexeren Vererbungsstrukturen mit mehreren Basen ist es noch wichtiger, weil Python dann über die MRO, also die Method Resolution Order, die Initialisierungskette koordiniert.

Ein Sonderfall sind unveränderliche Typen wie int, str oder tuple. Dort spielt __new__ eine größere Rolle, weil der endgültige Wert schon beim Erzeugen feststeht. Wer solche Klassen erweitert, muss die Trennung zwischen Objektbau und Initialisierung wirklich verstanden haben. Genau an dieser Stelle zeigt sich, warum der Python-Konstruktionsbegriff oft vereinfacht dargestellt wird.

Wann eine Factory besser ist als der Konstruktor

Es gibt Situationen, in denen ich absichtlich keinen komplizierten Konstruktor baue, obwohl es technisch möglich wäre. Wenn die Erzeugung von Bedingungen abhängt, mehrere Varianten möglich sind oder ein Objekt aus unterschiedlichen Quellen kommen kann, ist eine Factory-Funktion meist die klarere Lösung.

class HttpClient:
    def __init__(self, base_url: str) -> None:
        self.base_url = base_url

class MockClient:
    def __init__(self, base_url: str) -> None:
        self.base_url = base_url

def build_client(kind: str, base_url: str):
    if kind == "http":
        return HttpClient(base_url)
    if kind == "mock":
        return MockClient(base_url)
    raise ValueError(f"Unbekannter Client-Typ: {kind}")

Das ist dann sinnvoll, wenn die Entscheidung nicht mehr zum eigentlichen Objekt gehört, sondern zur Bauphase. Ich trenne diese Ebenen gern, weil der Konstruktor dadurch vorhersehbar bleibt. Die Factory entscheidet, welche Instanz entsteht, die Klasse selbst kümmert sich nur darum, wie ihr Zustand aussieht.

Das hilft auch bei Tests und bei späteren Erweiterungen. Wenn morgen eine dritte Variante dazukommt, ändere ich nicht jede Klasse einzeln, sondern erweitere die Erzeugungslogik an einer Stelle. Für Infrastrukturcode, API-Clients oder Parser ist das oft robuster als eine immer kompliziertere Initialisierung im Objekt selbst.

Worauf ich bei Python-Klassen in Projekten achte

Für mich läuft eine gute Initialisierung in Python auf ein paar harte Regeln hinaus: klare Verantwortung, wenig Nebeneffekte, saubere Defaults. Wenn eine Klasse nur Daten trägt, bevorzuge ich meist eine Dataclass. Wenn sie Verhalten kapselt, halte ich den Konstruktor schlank und validiere nur das Nötigste. Und wenn die Erzeugung selbst Entscheidungen treffen muss, ziehe ich eine Factory vor.

  • Ich setze im Konstruktor nur Zustände, die direkt zum gültigen Startzustand gehören.
  • Ich verwende keine geteilten mutablen Defaults.
  • Ich rufe in Vererbungsketten bewusst super().__init__() auf.
  • Ich vermeide I/O und Netzwerkzugriffe im Konstruktor, wenn sie nicht zwingend nötig sind.
  • Ich prüfe bei Datenklassen, ob @dataclass die bessere Wartungsform ist.

Wenn eine Klasse sich schwer initialisieren lässt, ist das oft kein Detailproblem, sondern ein Designsignal. Dann ist sie wahrscheinlich zu groß, zu unklar oder übernimmt zu viele Rollen. Genau hier lohnt es sich, Konstruktor, Factory und Dataclass nicht als Konkurrenz zu sehen, sondern als Werkzeuge für unterschiedliche Aufgaben.

Häufig gestellte Fragen

`__new__` ist für die Erzeugung des Objekts zuständig und wird vor `__init__` aufgerufen. Es gibt die neue Instanz zurück. `__init__` initialisiert diese bereits erzeugte Instanz, setzt deren Attribute und darf keinen Wert außer `None` zurückgeben. Im Alltag wird meist nur `__init__` benötigt.

Verwenden Sie `@dataclass` für Klassen, die hauptsächlich Daten speichern und wenig komplexe Logik benötigen. Es reduziert Boilerplate-Code für `__init__`, `__repr__` und andere Methoden, macht Klassen lesbarer und ist ideal für Konfigurationsobjekte oder DTOs.

Mutable Standardwerte (z.B. leere Listen oder Dictionaries) werden nur einmal erstellt, wenn die Klasse definiert wird. Alle Instanzen, die den Standardwert verwenden, teilen sich dann dasselbe Objekt. Änderungen an einer Instanz wirken sich unerwartet auf andere aus. Nutzen Sie `None` als Platzhalter und initialisieren Sie das Objekt im Konstruktor.

Ja, der Konstruktor sollte schlank sein und primär die Instanz in einen gültigen Anfangszustand versetzen. Netzwerkzugriffe, Dateisystemoperationen oder aufwendige Berechnungen machen die Klasse schwer testbar und unvorhersehbar. Lagern Sie solche Operationen in separate Methoden wie `connect()` oder `load()` aus.

Artikel bewerten

Bewertung: 0.00 Stimmenanzahl: 0

Tags:

konstruktor python python __init__ vs __new__ python konstruktor best practices python dataclass initialisierung python objekt erzeugung python vererbung konstruktor

Beitrag teilen

Thilo Arndt

Thilo Arndt

Mein Name ist Thilo Arndt und ich bringe 10 Jahre Erfahrung in den Bereichen IT-Infrastruktur, Web-Technologien und Sicherheit mit. Mein Interesse an diesen Themen begann bereits in meiner Jugend, als ich die Funktionsweise von Computern und Netzwerken erkunden wollte. Diese Neugier hat sich zu einer Leidenschaft entwickelt, die mich dazu motiviert, komplexe technische Konzepte verständlich zu machen und aktuelle Trends in der Branche zu verfolgen. In meinen Beiträgen konzentriere ich mich darauf, nützliche und präzise Informationen zu liefern, die sowohl für Fachleute als auch für Interessierte zugänglich sind. Ich lege großen Wert darauf, Quellen zu überprüfen und Informationen zu vergleichen, um sicherzustellen, dass ich meinen Lesern die bestmöglichen Einblicke gebe. Dabei ist es mir wichtig, schwierige Themen zu vereinfachen und klar zu strukturieren, damit jeder die Herausforderungen und Chancen der digitalen Welt besser verstehen kann.

Kommentar schreiben