M-Bus Gateway
← Tilbage til blog
· Python· dataclasses· Pydantic· FastAPI· SQLModel· backend· typing

Python dataclasses vs. Pydantic — hvornår bruges hvad?

Sammenligning af Python dataclasses og Pydantic v2: validering, performance, JSON-serialisering, FastAPI integration, SQLModel og hvornår dataclasses er det rigtige valg.

Af M-Bus Gateway

Pydantic v2 er standardvalget for FastAPI-projekter, men Python dataclasses er hurtigere og enklere til intern kode. Her er den klare sondring.


Side-by-side sammenligning

# Python dataclass — intern kode, ingen validering:
from dataclasses import dataclass, field
from datetime import datetime

@dataclass
class GatewayStatus:
    gateway_id: str
    last_seen: datetime
    rssi_dbm: int | None = None
    firmware_version: str = "unknown"
    readings_today: int = field(default=0)


# Pydantic v2 — API I/O, validering, serialisering:
from pydantic import BaseModel, Field, field_validator
from datetime import datetime

class GatewayStatusOut(BaseModel):
    gateway_id: str
    last_seen: datetime
    rssi_dbm: int | None = None
    firmware_version: str = "unknown"
    readings_today: int = Field(default=0, ge=0)

    @field_validator("firmware_version")
    @classmethod
    def validate_semver(cls, v: str) -> str:
        if v != "unknown" and not v.startswith(("v", "0.", "1.", "2.")):
            raise ValueError("Ugyldig firmware-version")
        return v

Performance — hvornår dataclasses vinder

# Benchmark: Opret 100.000 objekter

import timeit

# dataclass: ~45ms
timeit.timeit(
    "GatewayStatus('GW-001', datetime.now(), -87, '1.2.3', 42)",
    number=100_000
)

# Pydantic BaseModel: ~180ms (4× langsommere)
timeit.timeit(
    "GatewayStatusOut(gateway_id='GW-001', last_seen=datetime.now())",
    number=100_000
)

# Pydantic med model_validate (fra dict): ~220ms
timeit.timeit(
    "GatewayStatusOut.model_validate(data)",
    number=100_000
)

# Konklusion:
# Dataclasses: Intern behandling af mange objekter (readings, parsing)
# Pydantic: API input/output (validering vigtigere end speed)

# Platform-eksempel:
# wmbusmeters JSON → GatewayReading dataclass (RAM buffer, 10.000/dag)
# API response → GatewayStatusOut Pydantic (100 kald/dag)

Hvornår bruges hvad — beslutningsmatrix

# ✅ Brug dataclass:
# - Intern datastruktur (ingen ekstern I/O)
# - Høj-frekvens oprettelse (parsing-loops, message buffers)
# - Simpel container uden valideringsbehov
# - Arver fra eksisterende dataclass-hierarki

@dataclass
class MbusReading:             # Intern RAM-buffer
    meter_id: str
    timestamp: datetime
    value: float
    unit: str

@dataclass
class PayloadChunk:            # MQTT send-buffer
    readings: list[MbusReading]
    gateway_id: str
    sequence_no: int


# ✅ Brug Pydantic BaseModel:
# - FastAPI request/response schemas
# - Konfiguration der læses fra miljøvariabler
# - JSON-deserialisering fra ekstern kilde
# - Validering kritisk for korrekthed

class MeterCreate(BaseModel):  # API input
    gateway_id: uuid.UUID
    meter_type: MeterType
    manufacturer: str = Field(min_length=1, max_length=100)
    wmbus_id: str = Field(pattern=r"^[0-9a-fA-F]{8}$")

class Settings(BaseSettings):  # Konfiguration
    database_url: str
    stripe_secret_key: SecretStr
    mqtt_host: str = "localhost"


# ✅ Brug SQLModel (= Pydantic + SQLAlchemy):
# - Database-tabeller (table=True)
# - Deler schema med API-modeller

class Property(SQLModel, table=True):
    id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
    name: str
    tenant_id: uuid.UUID

Serialisering — dataclass vs. Pydantic

# dataclass: Ingen built-in JSON (brug dataclasses.asdict):
from dataclasses import asdict, astuple
import json

reading = MbusReading("12345678", datetime.now(), 1234.5, "kWh")
d = asdict(reading)
json_str = json.dumps(d, default=str)   # default=str til datetime

# Pydantic: Direkte JSON med fuld kontrol:
status = GatewayStatusOut(gateway_id="GW-001", last_seen=datetime.now())
json_str = status.model_dump_json()
# '{"gateway_id":"GW-001","last_seen":"2025-01-15T06:00:00Z",...}'

# Pydantic serialization aliaser (API naming conventions):
class ReadingOut(BaseModel):
    model_config = ConfigDict(populate_by_name=True)

    meter_id: uuid.UUID = Field(alias="meterId")    # camelCase i API
    reading_value: float = Field(alias="value")
    
    # Frontend ser: {"meterId": "...", "value": 1234.5}
    # Backend kode: reading.meter_id, reading.reading_value

dataclasses med __post_init__ validering

# Når du vil have let validering i dataclass:

from dataclasses import dataclass, field

@dataclass
class MqttConfig:
    host: str
    port: int = 8883
    client_id: str = ""

    def __post_init__(self):
        if not (1 <= self.port <= 65535):
            raise ValueError(f"Ugyldig port: {self.port}")
        if not self.client_id:
            self.client_id = f"mbus-{uuid.uuid4().hex[:8]}"
        if not self.host:
            raise ValueError("MQTT host er påkrævet")

# Men vær opmærksom:
# __post_init__ køres KUN ved __init__ (ikke ved setattr)
# Pydantic validerer ved hvert field-assignment (med model_config)

TypedDict til MQTT-payloads

# TypedDict: Typehinting af dicts uden runtime overhead
from typing import TypedDict, NotRequired

class GatewayHeartbeat(TypedDict):
    gateway_id: str
    timestamp: str            # ISO 8601
    firmware: str
    rssi_dbm: NotRequired[int]

# Bruges til MQTT JSON-parsing (Pydantic ville være overkill her):
def parse_heartbeat(raw: dict) -> GatewayHeartbeat:
    return GatewayHeartbeat(
        gateway_id=raw["gateway_id"],
        timestamp=raw["timestamp"],
        firmware=raw.get("firmware", "unknown"),
        rssi_dbm=raw.get("rssi_dbm"),
    )

# mypy validerer TypedDict-adgang statisk (ingen runtime-check)

Konklusion

Brug dataclass til intern kode med høj-frekvens oprettelse (MQTT parsing, RAM-buffere, batch-processing) og Pydantic BaseModel til alle FastAPI request/response schemas og konfiguration. SQLModel kombinerer begge verdener til database-tabeller. TypedDict er ideel til MQTT JSON-payload type-hints uden runtime overhead. Prioritér Pydantic, når validering og JSON-serialisering er vigtige — dataclasses, når performance i loops er kritisk.

Se Pydantic v2 guide eller FastAPI dependency injection guide.