← Home

128. TypedDict And Dataclasses

Type-safe API models with TypedDict and dataclasses

128. TypedDict And Dataclasses

Try it

Raw exercise data

schema_version: '1.0'
id: 128-typed-api-models
concept: TypedDict and dataclasses
se_concept: Type-safe API models with TypedDict and dataclasses
difficulty: medium

worlds:
  ram_manager:
    scenario: |
      πŸ—οΈ Aryan asks the LLM to return structured JSON recommendations.
      His TypedDict definition has wrong field types, his dataclass
      is missing the decorator, and his JSON parser crashes when
      the LLM includes extra fields.

      πŸ’‘ **Fun fact:** `TypedDict` was added in Python 3.8 via PEP 589 and `@dataclass` in Python 3.7 via PEP 557. They solve different problems: `TypedDict` types a plain dict you receive from external sources (APIs, JSON files), while `@dataclass` generates a real class with `__init__`, `__repr__`, and `__eq__`. The third-party `pydantic` library, with over 200 million monthly downloads, extends dataclasses with runtime validation β€” it powers FastAPI, the most popular modern Python web framework. Without `pydantic`, TypedDict annotations are only enforced by static type checkers like `mypy`, not at runtime.

      ⚠️ **Watch out:** A class with type annotations but no `@dataclass` decorator is just a plain class. Python will NOT generate `__init__` for you. `ProcessRecommendation(process_name='chrome', action='kill', reason='idle', risk='low')` raises `TypeError: ProcessRecommendation() takes no arguments`. The type annotations become class-level variables that are never used. This is a silent bug β€” the class definition itself doesn't error, only instantiation does.

      πŸ€” **Think about it:** LLMs are not JSON compilers β€” they often add helpful prose before or after JSON output: "Here are my recommendations:\n[{...}]\nPlease review carefully." `json.loads()` on that string raises `JSONDecodeError`. The solution uses `re.search(r'\[.*?\]', text, re.DOTALL)` to find the array. What does `re.DOTALL` do and why is it needed here? What would happen if the LLM returned two JSON arrays in one response?
  api_integration:
    scenario: |
      πŸ“¦ A service models third-party API responses with TypedDict for
      IDE autocomplete and runtime safety. The type annotations are
      wrong, causing silent bugs that only appear in production.

broken_code: |
  import json
  import re
  from typing import TypedDict

  # TypedDict for the raw API response shape
  class LlmContentBlock(TypedDict):
      type: str
      # πŸ’‘ : hint : -010 xp the LLM API returns 'text' as a plain string, not a list β€” change the type annotation from list to str so the type checker and IDE give correct guidance
      # BUG 1: 'text' field should be str, not list
      text: list

  class LlmApiResponse(TypedDict):
      id: str
      content: list[LlmContentBlock]
      # πŸ’‘ : hint : -010 xp stop_reason is a string value like 'end_turn' or 'max_tokens', not an integer β€” change the type annotation from int to str
      # BUG 2: stop_reason is a str, not int
      stop_reason: int
      model: str

  # Dataclass for our domain model
  # πŸ’‘ : hint : -010 xp without the @dataclass decorator, Python will not generate __init__ for this class β€” add @dataclass above the class definition so ProcessRecommendation(**item) works
  # BUG 3: Missing @dataclass decorator β€” this is just a plain class,
  # __init__ is never generated, instantiation fails
  class ProcessRecommendation:
      process_name: str
      action: str    # 'kill', 'restart', or 'ignore'
      reason: str
      risk: str      # 'low', 'medium', or 'high'

  def extract_text(response: LlmApiResponse) -> str:
      """Extract the text content from an LLM API response dict."""
      return response['content'][0]['text']

  def parse_recommendations(raw_json: str) -> list[ProcessRecommendation]:
      """Parse a JSON string containing LLM recommendations."""
      # πŸ’‘ : hint : -010 xp LLMs often wrap JSON arrays in prose text like "Here are my recommendations: [...]" β€” use re.search(r'\[.*?\]', raw_json, re.DOTALL) to extract the JSON array before calling json.loads()
      # BUG 4: Tries to parse the entire string as JSON directly β€”
      # fails when the LLM wraps JSON in prose ("Here are my recommendations: [...]")
      items = json.loads(raw_json)
      return [ProcessRecommendation(**item) for item in items]

solution_code: |
  import json
  import re
  from typing import TypedDict
  from dataclasses import dataclass

  # TypedDict for the raw API response shape
  class LlmContentBlock(TypedDict):
      type: str
      text: str   # text is a string, not a list

  class LlmApiResponse(TypedDict):
      id: str
      content: list[LlmContentBlock]
      stop_reason: str   # e.g. 'end_turn', 'max_tokens'
      model: str

  # Dataclass for our domain model
  @dataclass
  class ProcessRecommendation:
      process_name: str
      action: str    # 'kill', 'restart', or 'ignore'
      reason: str
      risk: str      # 'low', 'medium', or 'high'

  def extract_text(response: LlmApiResponse) -> str:
      """Extract the text content from an LLM API response dict."""
      return response['content'][0]['text']

  def parse_recommendations(raw_json: str) -> list[ProcessRecommendation]:
      """Parse a JSON string containing LLM recommendations.

      Robust against LLM prose wrapping the JSON array.
      """
      # Extract the JSON array even if the LLM added surrounding text
      match = re.search(r'\[.*?\]', raw_json, re.DOTALL)
      if not match:
          return []
      items = json.loads(match.group())
      return [ProcessRecommendation(**item) for item in items]

setup: |
  import json
  import re
  from typing import TypedDict
  from dataclasses import dataclass

tests: |
  def test_extract_text_from_response():
      response = {
          'id': 'msg_123',
          'content': [{'type': 'text', 'text': 'Kill Chrome to free 1.1 GB'}],
          'stop_reason': 'end_turn',
          'model': 'claude-3-haiku',
      }
      result = extract_text(response)
      assert result == 'Kill Chrome to free 1.1 GB'

  def test_process_recommendation_is_dataclass():
      """ProcessRecommendation must be a dataclass β€” needs @dataclass."""
      rec = ProcessRecommendation(
          process_name='chrome',
          action='kill',
          reason='Using 1.1 GB with no active tabs',
          risk='low',
      )
      assert rec.process_name == 'chrome'
      assert rec.action == 'kill'
      assert rec.risk == 'low'

  def test_parse_recommendations_clean_json():
      raw = '[{"process_name": "chrome", "action": "kill", "reason": "idle", "risk": "low"}]'
      recs = parse_recommendations(raw)
      assert len(recs) == 1
      assert recs[0].process_name == 'chrome'
      assert recs[0].action == 'kill'

  def test_parse_recommendations_with_prose():
      """LLM often wraps JSON in explanatory text β€” must still parse."""
      raw = '''Based on your RAM snapshot, here are my recommendations:
  [{"process_name": "chrome", "action": "restart", "reason": "memory leak", "risk": "medium"}]
  Please review carefully before taking action.'''
      recs = parse_recommendations(raw)
      assert len(recs) == 1, (
          'Must extract JSON array even when surrounded by LLM prose text. '
          'Use re.search to find the [...] block.'
      )
      assert recs[0].process_name == 'chrome'

  def test_parse_recommendations_empty_returns_empty_list():
      assert parse_recommendations('No JSON here') == []
      assert parse_recommendations('') == []

  def test_recommendation_has_all_fields():
      rec = ProcessRecommendation(
          process_name='idea',
          action='ignore',
          reason='Active IDE in use',
          risk='high',
      )
      assert hasattr(rec, 'process_name')
      assert hasattr(rec, 'action')
      assert hasattr(rec, 'reason')
      assert hasattr(rec, 'risk')

se_concept_detail: |
  ## Type-Safe API Models

  ### TypedDict β€” for dicts you don't control (API responses)

  ```python
  from typing import TypedDict

  class ApiResponse(TypedDict):
      id: str
      status: str
      data: list[dict]

TypedDict tells your type checker and IDE the shape of a dict. It does NOT validate at runtime β€” use it for documentation + autocomplete.

def process(resp: ApiResponse) -> str:
    return resp['status']  # IDE knows 'status' is a str

dataclass β€” for your own domain objects

from dataclasses import dataclass

@dataclass
class Recommendation:
    process_name: str
    action: str
    risk: str = 'unknown'   # default value

@dataclass auto-generates:

  • __init__(self, process_name, action, risk='unknown')
  • __repr__ (pretty printing)
  • __eq__ (equality comparison)

Parsing JSON from LLM responses

LLMs sometimes wrap JSON in prose. Be robust:

import re, json

def extract_json_array(text: str) -> list:
    match = re.search(r'\[.*?\]', text, re.DOTALL)
    if not match:
        return []
    return json.loads(match.group())

When to use which

WhatUse
Typing a dict from an external APITypedDict
Your own domain/data objects@dataclass
Validation at runtimedataclass + post_init
Full validation + serializationPydantic (third-party)

learning_objectives:

  • Use TypedDict to annotate external API response shapes
  • Use @dataclass to define domain objects with auto-generated init/repr/eq
  • Extract JSON arrays from LLM responses that include prose
  • Understand the difference between TypedDict (dict subtype) and dataclass (class)

key_concepts:

  • TypedDict β€” typed dict for external data
  • β€˜@dataclass’ β€” auto-generated init/repr/eq
  • re.search(r’[.*?]’) β€” robust JSON array extraction
  • re.DOTALL β€” match newlines in regex
  • json.loads() β€” parse JSON string

resources:

  • title: TypedDict β€” Python Docs url: https://docs.python.org/3/library/typing.html#typing.TypedDict type: official
  • title: Python dataclasses β€” Official Docs url: https://docs.python.org/3/library/dataclasses.html type: official
  • title: Pydantic (runtime validation) url: https://docs.pydantic.dev/latest/ type: library
  • title: ArjanCodes β€” TypedDict vs Dataclasses vs Pydantic url: https://www.youtube.com/watch?v=Mf0KPct55aA type: video
  • title: Real Python β€” Python Data Classes url: https://realpython.com/python-data-classes/ type: tutorial

tasks:

  • test: test_extract_text_from_response label: extract_text reads content from response dict
  • test: test_process_recommendation_is_dataclass label: ProcessRecommendation can be instantiated
  • test: test_parse_recommendations_clean_json label: parse_recommendations parses clean JSON
  • test: test_parse_recommendations_with_prose label: parse_recommendations handles LLM prose wrapping
  • test: test_parse_recommendations_empty_returns_empty_list label: parse_recommendations returns empty list on no JSON
  • test: test_recommendation_has_all_fields label: ProcessRecommendation has all required fields