Skip to content

Python-Dataclasses: Ein vollständiger Leitfaden zum @dataclass-Decorator

Updated on

Das Schreiben von Python-Klassen bringt oft wiederholten Boilerplate-Code mit sich. Du definierst __init__, um Attribute zu initialisieren, __repr__ für eine gut lesbare Ausgabe, __eq__ für Vergleiche und manchmal __hash__ für Hashbarkeit. Diese manuelle Implementierung wird schnell mühsam bei Klassen, die primär Daten halten – insbesondere beim Umgang mit Konfigurationsobjekten, API-Antworten oder Datenbank-Records.

Python 3.7 führte Dataclasses über PEP 557 ein und automatisiert damit diesen Boilerplate, ohne die Flexibilität normaler Klassen zu verlieren. Der @dataclass-Decorator generiert anhand von Type Annotations automatisch spezielle Methoden und reduziert Code von Dutzenden Zeilen auf nur wenige. Dieser Guide zeigt, wie du Dataclasses für saubereren, besser wartbaren Python-Code nutzt.

📚

Warum es Dataclasses gibt: Das Boilerplate-Problem lösen

Klassische Python-Klassen erfordern explizite Methodendefinitionen für häufige Operationen. Betrachte diese Standardklasse zum Speichern von User-Daten:

class User:
    def __init__(self, name, email, age):
        self.name = name
        self.email = email
        self.age = age
 
    def __repr__(self):
        return f"User(name={self.name!r}, email={self.email!r}, age={self.age!r})"
 
    def __eq__(self, other):
        if not isinstance(other, User):
            return NotImplemented
        return (self.name, self.email, self.age) == (other.name, other.email, other.age)

Mit Dataclasses reduziert sich das auf:

from dataclasses import dataclass
 
@dataclass
class User:
    name: str
    email: str
    age: int

Der Decorator generiert __init__, __repr__ und __eq__ automatisch aus den Type Annotations. Das eliminiert über 15 Zeilen Boilerplate – bei identischer Funktionalität.

Grundlegende @dataclass-Syntax

Die einfachste Dataclass braucht nur Type Annotations für die Fields:

from dataclasses import dataclass
 
@dataclass
class Product:
    name: str
    price: float
    quantity: int
 
product = Product("Laptop", 999.99, 5)
print(product)  # Product(name='Laptop', price=999.99, quantity=5)
 
product2 = Product("Laptop", 999.99, 5)
print(product == product2)  # True

Der Decorator akzeptiert Parameter zur Anpassung des Verhaltens:

@dataclass(
    init=True,       # Generate __init__ (default: True)
    repr=True,       # Generate __repr__ (default: True)
    eq=True,         # Generate __eq__ (default: True)
    order=False,     # Generate comparison methods (default: False)
    frozen=False,    # Make instances immutable (default: False)
    unsafe_hash=False  # Generate __hash__ (default: False)
)
class Config:
    host: str
    port: int

Field-Typen und Default Values

Dataclasses unterstützen Default Values für Fields. Fields ohne Default müssen vor Fields mit Default stehen:

from dataclasses import dataclass
 
@dataclass
class Server:
    host: str
    port: int = 8080
    protocol: str = "http"
 
server1 = Server("localhost")
print(server1)  # Server(host='localhost', port=8080, protocol='http')
 
server2 = Server("api.example.com", 443, "https")
print(server2)  # Server(host='api.example.com', port=443, protocol='https')

Für mutable Default Values wie Listen oder Dictionaries verwende default_factory, um geteilte Referenzen zu vermeiden:

from dataclasses import dataclass, field
 
# WRONG - all instances share the same list
@dataclass
class WrongConfig:
    tags: list = []  # Raises error in Python 3.10+
 
# CORRECT - each instance gets a new list
@dataclass
class CorrectConfig:
    tags: list = field(default_factory=list)
    metadata: dict = field(default_factory=dict)
 
config1 = CorrectConfig()
config2 = CorrectConfig()
 
config1.tags.append("production")
print(config1.tags)  # ['production']
print(config2.tags)  # [] - separate list

Die field()-Funktion: Erweiterte Field-Konfiguration

Die Funktion field() gibt dir granulare Kontrolle über einzelne Fields:

from dataclasses import dataclass, field
from typing import List
 
@dataclass
class Employee:
    name: str
    employee_id: int
    salary: float = field(repr=False)  # Hide salary in repr
    skills: List[str] = field(default_factory=list)
    _internal_id: str = field(init=False, repr=False)  # Not in __init__
    performance_score: float = field(default=0.0, compare=False)  # Exclude from comparison
 
    def __post_init__(self):
        self._internal_id = f"EMP_{self.employee_id:06d}"
 
emp = Employee("Alice", 12345, 85000.0, ["Python", "SQL"])
print(emp)  # Employee(name='Alice', employee_id=12345, skills=['Python', 'SQL'], performance_score=0.0)
print(emp._internal_id)  # EMP_012345

Wichtige field()-Parameter:

ParameterTypeBeschreibung
defaultAnyDefault Value für das Field
default_factoryCallableFunktion ohne Argumente, die einen Default Value zurückgibt
initboolField in __init__ aufnehmen (default: True)
reprboolField in __repr__ aufnehmen (default: True)
compareboolField in Vergleichsmethoden berücksichtigen (default: True)
hashboolField in __hash__ berücksichtigen (default: None)
metadatadictBeliebige Metadaten (vom dataclasses-Modul nicht verwendet)
kw_onlyboolField als keyword-only markieren (Python 3.10+)

Der Parameter metadata speichert beliebige Informationen, die über fields() abrufbar sind:

from dataclasses import dataclass, field, fields
 
@dataclass
class APIRequest:
    endpoint: str = field(metadata={"description": "API endpoint path"})
    method: str = field(default="GET", metadata={"choices": ["GET", "POST", "PUT", "DELETE"]})
 
for f in fields(APIRequest):
    print(f"{f.name}: {f.metadata}")
# endpoint: {'description': 'API endpoint path'}
# method: {'choices': ['GET', 'POST', 'PUT', 'DELETE']}

Type Annotations mit Dataclasses

Dataclasses basieren auf Type Annotations, erzwingen sie aber nicht zur Laufzeit. Für komplexe Typen nutze das typing-Modul:

from dataclasses import dataclass
from typing import List, Dict, Optional, Union, Tuple
from datetime import datetime
 
@dataclass
class DataAnalysisJob:
    job_id: str
    dataset_path: str
    columns: List[str]
    filters: Dict[str, Union[str, int, float]]
    output_format: str = "csv"
    created_at: datetime = field(default_factory=datetime.now)
    completed_at: Optional[datetime] = None
    error_message: Optional[str] = None
    results: Optional[Dict[str, Tuple[float, float]]] = None
 
job = DataAnalysisJob(
    job_id="job_001",
    dataset_path="/data/sales.csv",
    columns=["date", "revenue", "region"],
    filters={"year": 2026, "region": "US"}
)

Für Type Checking zur Laufzeit kannst du Bibliotheken wie pydantic integrieren oder in __post_init__ validieren.

frozen=True: Immutable Dataclasses erstellen

Setze frozen=True, um Instanzen nach der Erstellung unveränderlich zu machen – ähnlich wie named tuples:

from dataclasses import dataclass
 
@dataclass(frozen=True)
class Point:
    x: float
    y: float
 
    def distance_from_origin(self):
        return (self.x**2 + self.y**2) ** 0.5
 
point = Point(3.0, 4.0)
print(point.distance_from_origin())  # 5.0
 
# Attempting to modify raises FrozenInstanceError
try:
    point.x = 5.0
except AttributeError as e:
    print(f"Error: {e}")  # Error: cannot assign to field 'x'

Frozen Dataclasses sind standardmäßig hashable, wenn alle Fields hashable sind – damit kannst du sie in Sets und als Dictionary-Keys verwenden:

@dataclass(frozen=True)
class Coordinate:
    latitude: float
    longitude: float
 
locations = {
    Coordinate(40.7128, -74.0060): "New York",
    Coordinate(51.5074, -0.1278): "London"
}
 
print(locations[Coordinate(40.7128, -74.0060)])  # New York

post_init-Methode: Validierung und berechnete Fields

Die Methode __post_init__ wird nach __init__ ausgeführt und ermöglicht Validierung sowie die Initialisierung berechneter Fields:

from dataclasses import dataclass, field
from datetime import datetime
 
@dataclass
class BankAccount:
    account_number: str
    balance: float
    created_at: datetime = field(default_factory=datetime.now)
    account_type: str = field(init=False)
 
    def __post_init__(self):
        if self.balance < 0:
            raise ValueError("Initial balance cannot be negative")
 
        # Compute account_type based on balance
        if self.balance >= 100000:
            self.account_type = "Premium"
        elif self.balance >= 10000:
            self.account_type = "Gold"
        else:
            self.account_type = "Standard"
 
account = BankAccount("ACC123456", 50000.0)
print(account.account_type)  # Gold

Für Fields mit init=False, die von anderen Fields abhängen, nutze __post_init__:

from dataclasses import dataclass, field
 
@dataclass
class Rectangle:
    width: float
    height: float
    area: float = field(init=False)
    perimeter: float = field(init=False)
 
    def __post_init__(self):
        self.area = self.width * self.height
        self.perimeter = 2 * (self.width + self.height)
 
rect = Rectangle(5.0, 3.0)
print(f"Area: {rect.area}, Perimeter: {rect.perimeter}")  # Area: 15.0, Perimeter: 16.0

Vererbung mit Dataclasses

Dataclasses unterstützen Vererbung inklusive automatischem Zusammenführen der Fields:

from dataclasses import dataclass
 
@dataclass
class Animal:
    name: str
    age: int
 
@dataclass
class Dog(Animal):
    breed: str
    is_good_boy: bool = True
 
dog = Dog("Buddy", 5, "Golden Retriever")
print(dog)  # Dog(name='Buddy', age=5, breed='Golden Retriever', is_good_boy=True)

Subklassen erben die Parent-Fields und können neue hinzufügen. Fields ohne Defaults dürfen nicht nach Fields mit Defaults kommen – auch nicht über Vererbungsgrenzen hinweg:

from dataclasses import dataclass
 
@dataclass
class BaseConfig:
    environment: str = "production"
 
# ERROR: Non-default field 'api_key' cannot follow default field 'environment'
# @dataclass
# class APIConfig(BaseConfig):
#     api_key: str
 
# CORRECT: Use default or rearrange fields
@dataclass
class APIConfig(BaseConfig):
    api_key: str = ""  # Provide default
    timeout: int = 30

Python 3.10+ führte kw_only ein, um das zu lösen:

from dataclasses import dataclass
 
@dataclass
class BaseConfig:
    environment: str = "production"
 
@dataclass(kw_only=True)
class APIConfig(BaseConfig):
    api_key: str  # Must be passed as keyword argument
    timeout: int = 30
 
config = APIConfig(api_key="secret_key_123")  # OK
# config = APIConfig("secret_key_123")  # TypeError

slots=True: Speichereffizienz (Python 3.10+)

Python 3.10 ergänzte slots=True, um __slots__ zu definieren und den Memory-Overhead zu reduzieren:

from dataclasses import dataclass
import sys
 
@dataclass
class RegularUser:
    username: str
    email: str
    age: int
 
@dataclass(slots=True)
class SlottedUser:
    username: str
    email: str
    age: int
 
regular = RegularUser("john", "john@example.com", 30)
slotted = SlottedUser("jane", "jane@example.com", 28)
 
print(f"Regular: {sys.getsizeof(regular.__dict__)} bytes")  # ~104 bytes
print(f"Slotted: {sys.getsizeof(slotted)} bytes")          # ~64 bytes

Slotted Dataclasses bringen 30–40% Memory-Einsparung und schnelleren Attributzugriff, verzichten aber auf dynamisches Hinzufügen von Attributen:

regular.new_attribute = "allowed"  # OK
# slotted.new_attribute = "error"  # AttributeError

kw_only=True: Keyword-only Fields (Python 3.10+)

Erzwinge, dass alle Fields keyword-only sind, um Instanziierung klarer zu machen:

from dataclasses import dataclass
 
@dataclass(kw_only=True)
class DatabaseConnection:
    host: str
    port: int
    username: str
    password: str
    database: str = "default"
 
# Must use keyword arguments
conn = DatabaseConnection(
    host="localhost",
    port=5432,
    username="admin",
    password="secret"
)
 
# Positional arguments raise TypeError
# conn = DatabaseConnection("localhost", 5432, "admin", "secret")

Kombiniere kw_only mit Kontrolle pro Field:

from dataclasses import dataclass, field
 
@dataclass
class MixedArgs:
    required_positional: str
    optional_positional: int = 0
    required_keyword: str = field(kw_only=True)
    optional_keyword: bool = field(default=False, kw_only=True)
 
obj = MixedArgs("value", 10, required_keyword="kw_value")

Vergleich: dataclass vs Alternativen

FeaturedataclassnamedtupleTypedDictPydanticattrs
MutabilityMutable (default)ImmutableN/A (dict subclass)MutableConfigurable
Type validationAnnotations onlyNoAnnotations onlyRuntime validationRuntime validation
Default valuesYesYesNoYesYes
MethodsFull class supportLimitedNoFull class supportFull class support
InheritanceYesNoLimitedYesYes
Memory overheadModerateLowLowHigherModerate
Slots supportYes (3.10+)NoNoYesYes
PerformanceFastFastestFastSlower (validation)Fast
Built-inYes (3.7+)YesYes (3.8+)NoNo

Wähle Dataclasses für:

  • Standard-Python-Projekte ohne Dependencies
  • Einfache Data Container mit Type Hints
  • Wenn Flexibilität zwischen frozen/mutable gebraucht wird
  • Vererbungs-Hierarchien

Wähle Pydantic für:

  • Validierung von API Request/Response
  • Konfigurationsmanagement mit strikter Validierung
  • JSON-Schema-Generierung

Wähle namedtuple für:

  • Leichtgewichtige immutable Container
  • Maximale Speichereffizienz
  • Python <3.7 Kompatibilität

Konvertierung zu/aus Dictionaries

Dataclasses bieten asdict() und astuple() zur Serialisierung:

from dataclasses import dataclass, asdict, astuple
 
@dataclass
class Config:
    host: str
    port: int
    ssl_enabled: bool = True
 
config = Config("api.example.com", 443)
 
# Convert to dictionary
config_dict = asdict(config)
print(config_dict)  # {'host': 'api.example.com', 'port': 443, 'ssl_enabled': True}
 
# Convert to tuple
config_tuple = astuple(config)
print(config_tuple)  # ('api.example.com', 443, True)

Für verschachtelte Dataclasses:

from dataclasses import dataclass, asdict
 
@dataclass
class Address:
    street: str
    city: str
    zipcode: str
 
@dataclass
class Person:
    name: str
    address: Address
 
person = Person("Alice", Address("123 Main St", "Springfield", "12345"))
person_dict = asdict(person)
print(person_dict)
# {'name': 'Alice', 'address': {'street': '123 Main St', 'city': 'Springfield', 'zipcode': '12345'}}

Dataclasses mit JSON-Serialisierung

Dataclasses unterstützen JSON-Serialisierung nicht nativ, aber die Integration ist unkompliziert:

import json
from dataclasses import dataclass, asdict
from datetime import datetime
 
@dataclass
class Event:
    name: str
    timestamp: datetime
    attendees: int
 
    def to_json(self):
        data = asdict(self)
        # Custom serialization for datetime
        data['timestamp'] = self.timestamp.isoformat()
        return json.dumps(data)
 
    @classmethod
    def from_json(cls, json_str):
        data = json.loads(json_str)
        data['timestamp'] = datetime.fromisoformat(data['timestamp'])
        return cls(**data)
 
event = Event("Python Conference", datetime.now(), 500)
json_str = event.to_json()
print(json_str)
 
restored = Event.from_json(json_str)
print(restored)

Für komplexere Szenarien nutze die Bibliothek dataclasses-json oder Pydantic.

Patterns aus der Praxis

Konfigurationsobjekte

from dataclasses import dataclass, field
from typing import List
 
@dataclass
class AppConfig:
    app_name: str
    version: str
    debug: bool = False
    allowed_hosts: List[str] = field(default_factory=lambda: ["localhost"])
    database_url: str = "sqlite:///app.db"
    cache_timeout: int = 300
 
    def __post_init__(self):
        if self.debug:
            print(f"Running {self.app_name} v{self.version} in DEBUG mode")
 
config = AppConfig("DataAnalyzer", "2.1.0", debug=True)

API-Response-Modelle

from dataclasses import dataclass
from typing import List, Optional
from datetime import datetime
 
@dataclass
class APIResponse:
    status: str
    data: Optional[List[dict]] = None
    error_message: Optional[str] = None
    timestamp: datetime = field(default_factory=datetime.now)
 
    @property
    def is_success(self):
        return self.status == "success"
 
response = APIResponse("success", data=[{"id": 1, "name": "Dataset A"}])
print(response.is_success)  # True

Datenbank-Records mit PyGWalker-Integration

from dataclasses import dataclass, asdict
from typing import List
import pandas as pd
 
@dataclass
class SalesRecord:
    date: str
    product: str
    revenue: float
    region: str
    quantity: int
 
# Create sample data
records = [
    SalesRecord("2026-01-01", "Laptop", 1299.99, "US", 5),
    SalesRecord("2026-01-02", "Mouse", 29.99, "EU", 50),
    SalesRecord("2026-01-03", "Keyboard", 89.99, "US", 20),
]
 
# Convert to DataFrame for visualization with PyGWalker
df = pd.DataFrame([asdict(r) for r in records])
 
# Use PyGWalker for interactive data exploration
# import pygwalker as pyg
# walker = pyg.walk(df)
# This creates a Tableau-like interface to visualize your dataclass-based data

Dataclasses eignen sich hervorragend, um Daten vor der Visualisierung zu strukturieren. PyGWalker konvertiert DataFrames in interaktive visuelle Oberflächen und macht Dataclass-basierte Data-Analysis-Workflows dadurch nahtlos.

Performance-Benchmarks vs reguläre Klassen

import timeit
from dataclasses import dataclass
 
# Regular class
class RegularClass:
    def __init__(self, x, y, z):
        self.x = x
        self.y = y
        self.z = z
 
    def __repr__(self):
        return f"RegularClass(x={self.x}, y={self.y}, z={self.z})"
 
    def __eq__(self, other):
        return (self.x, self.y, self.z) == (other.x, other.y, other.z)
 
@dataclass
class DataClass:
    x: int
    y: int
    z: int
 
# Benchmark instantiation
regular_time = timeit.timeit(lambda: RegularClass(1, 2, 3), number=1000000)
dataclass_time = timeit.timeit(lambda: DataClass(1, 2, 3), number=1000000)
 
print(f"Regular class: {regular_time:.4f}s")
print(f"Dataclass: {dataclass_time:.4f}s")
# Dataclasses are typically 5-10% slower due to decorator overhead
# but provide significantly cleaner code

Mit slots=True (Python 3.10+) erreichen Dataclasses die Performance regulärer Klassen oder übertreffen sie sogar, während der Speicherverbrauch um 30–40% sinkt.

Advanced Patterns: Benutzerdefinierte Field-Reihenfolge

from dataclasses import dataclass, field
 
def sort_by_priority(items):
    return sorted(items, key=lambda x: x.priority, reverse=True)
 
@dataclass(order=True)
class Task:
    priority: int
    name: str = field(compare=False)
    description: str = field(compare=False)
 
tasks = [
    Task(3, "Review PR", "Code review for feature X"),
    Task(1, "Write docs", "Documentation update"),
    Task(5, "Fix bug", "Critical production issue"),
]
 
sorted_tasks = sorted(tasks)
for task in sorted_tasks:
    print(f"Priority {task.priority}: {task.name}")
# Priority 1: Write docs
# Priority 3: Review PR
# Priority 5: Fix bug

Best Practices und typische Stolperfallen

  1. Nutze immer default_factory für mutable Defaults: Weise niemals direkt [] oder {} zu
  2. Type Hints sind erforderlich: Dataclasses verlassen sich auf Annotations, nicht auf Werte
  3. Die Field-Reihenfolge ist wichtig: Fields ohne Default vor Fields mit Default
  4. frozen=True für immutable Daten: Gut für hashbare Objekte und Thread-Safety
  5. __post_init__ sparsam einsetzen: Zu viel Logik konterkariert die Einfachheit
  6. slots=True bei großen Datasets in Betracht ziehen: Deutliche Memory-Einsparungen in Python 3.10+
  7. In __post_init__ validieren: Dataclasses erzwingen Types nicht zur Laufzeit

FAQ

Fazit

Python Dataclasses eliminieren Boilerplate-Code, während sie die volle Power von Klassen beibehalten. Der @dataclass-Decorator generiert automatisch Initialisierungs-, Repräsentations- und Vergleichsmethoden und reduziert damit Entwicklungszeit und Wartungsaufwand. Von Konfigurationsobjekten bis hin zu API-Modellen und Datenbank-Records bieten Dataclasses einen sauberen, type-annotierten Ansatz für datenhaltende Klassen.

Zu den wichtigsten Vorteilen gehören die automatische Methodengenerierung, anpassbares Field-Verhalten über field(), Immutability mit frozen=True, Validierung über __post_init__ und Speichereffizienz mit slots=True. Während Alternativen wie namedtuples und Pydantic für spezifische Use Cases sinnvoll sind, treffen Dataclasses für die meisten Python-Projekte eine sehr gute Balance zwischen Einfachheit und Funktionalität.

Für Data-Analysis-Workflows ermöglicht die Kombination von Dataclasses mit Tools wie PyGWalker leistungsfähige Pipelines, in denen strukturierte Datenmodelle direkt in interaktive Visualisierungen fließen – und so alles von Data Ingestion bis zur Insight-Generierung schlanker machen.

📚