Skip to content

4. Type hints, dataclasses & Pydantic

Intermediate · 9 min read

Type hints document what a function expects and returns; editors and tools like mypy use them to catch mistakes. Dataclasses and Pydantic turn those hints into real data objects.

4.1 Type hints

def score_chunks(query: str, chunks: list[str], top_k: int = 3) -> list[tuple[str, float]]:
    """Return (chunk, score) pairs for the best `top_k` chunks."""
    words = set(query.lower().split())
    scored = [(c, len(words & set(c.lower().split())) / len(words)) for c in chunks]
    return sorted(scored, key=lambda pair: pair[1], reverse=True)[:top_k]

print(score_chunks("refund policy", ["refund within 30 days", "shipping policy", "refund policy"], top_k=1))
# → [('refund policy', 1.0)]

Hints are not enforced at run time — they're documentation that tools can check.

4.2 Optional values and unions

def get_model(name: str | None = None) -> str:   # `str | None` = "a string or None" (3.10+)
    return name or "gpt-4o-mini"                  # fall back to a default

print(get_model())                                # → gpt-4o-mini

On older Python versions you'll see the same thing written Optional[str] (from typing).

4.3 Dataclasses

from dataclasses import dataclass, field

@dataclass
class Chunk:
    text: str
    source: str
    score: float = 0.0                          # default value
    tags: list[str] = field(default_factory=list)   # safe mutable default

c = Chunk("Refunds within 30 days.", "policy.md", 0.91)
print(c)              # → Chunk(text='Refunds within 30 days.', source='policy.md', score=0.91, tags=[])
print(c.source)       # → policy.md

@dataclass writes __init__, __repr__ and __eq__ for you. Add frozen=True to make objects read-only.

4.4 Pydantic: validated data

Pydantic (pip install pydantic) checks and converts data at run time — ideal for API payloads and LLM output.

from pydantic import BaseModel, Field, ValidationError

class Ticket(BaseModel):
    category: str
    priority: int = Field(ge=1, le=5)          # must be between 1 and 5
    summary: str

# Pretend this JSON came back from an LLM asked to classify a support email.
raw = '{"category": "billing", "priority": "2", "summary": "Charged twice"}'
ticket = Ticket.model_validate_json(raw)      # parse + validate in one step
print(ticket.priority + 1)                    # → 3   "2" was converted to the int 2

try:
    Ticket.model_validate_json('{"category": "bug", "priority": 9, "summary": "x"}')
except ValidationError as err:
    print("invalid:", err.error_count(), "error")   # → invalid: 1 error

Why it matters for GenAI

"Return JSON matching this schema" is one of the most common LLM tasks. Define the schema as a Pydantic model, validate the reply, and retry with the error message if it fails — many SDKs accept the Pydantic model directly for structured outputs.

Practice

  • Create a dataclass Message with role and content, then a function to_api(messages: list[Message]) -> list[dict].
Answer
from dataclasses import dataclass, asdict

@dataclass
class Message:
    role: str
    content: str

def to_api(messages: list[Message]) -> list[dict]:
    return [asdict(m) for m in messages]

print(to_api([Message("user", "Hi")]))   # → [{'role': 'user', 'content': 'Hi'}]