Skip to content
v0.1.0latest release21 skills2 categories3 sub-categories22 tested scripts68 reference docsContact us

python-dataclass-converter

Convert boilerplate Python classes, dicts, tuples and JSON payloads into dataclasses, or into pydantic v2 BaseModel, TypedDict, NamedTuple, attrs or msgspec when that fits better. Covers every @dataclass option (frozen, slots, kw_only, order, unsafe_hash, match_args, weakref_slot), field() (default_factory, init=False, repr, compare, hash, metadata), __post_init__, InitVar, KW_ONLY, inheritance and default ordering, replace(), asdict deep-copy pitfalls, hashing/equality, serialization and validation. AI focus: LLM request/response and chat Message types, RAG chunks with embeddings (numpy eq gotcha), eval records, configs, typed OpenAI/Anthropic JSON. Use when the user asks to convert a class or dict to a dataclass, remove __init__/__repr__/__eq__ boilerplate, model a JSON/API response with types, pick dataclass vs pydantic vs TypedDict, or fix dataclass errors (mutable default, non-default argument follows default argument, FrozenInstanceError, unhashable type).

Programming / Pythonv1.0.0Updated OCT 20 20265 references1 script

Turn hand-written data-holding classes, loose dicts, positional tuples and raw JSON payloads into precise, typed data containers. The default target is @dataclass for internal objects; pydantic v2 is chosen at trust boundaries that need validation, TypedDict when the data must stay a plain dict, NamedTuple for small immutable records that are unpacked, attrs or msgspec when converters or decode speed matter. Every conversion must preserve behaviour (constructor signature, equality, hashing, repr secrecy) or state exactly what changed.

Trigger Conditions

Activate this skill when the user:

  • Asks to "convert this class to a dataclass", "remove the init boilerplate", "make this a dataclass / pydantic model / TypedDict"
  • Pastes a JSON sample or API response (OpenAI, Anthropic, a vector DB, a webhook) and wants typed classes for it
  • Passes dicts or tuples around (msg["role"], row[2]) and wants named, typed fields
  • Hits ValueError: mutable default <class 'list'> for field ... is not allowed, TypeError: non-default argument 'x' follows default argument, FrozenInstanceError, TypeError: unhashable type, or The truth value of an array with more than one element is ambiguous from a dataclass __eq__
  • Asks about field(), __post_init__, InitVar, KW_ONLY, slots=True, frozen=True, replace(), asdict()
  • Designs config objects, LLM request/response types, chat messages, RAG chunks, eval records

Workflow

text
1. INVENTORY  -> list the classes/dicts/JSON to convert; run scripts/to_dataclass.py class|json
2. CLASSIFY   -> pick the container per object (decision table below): boundary vs internal
3. CONVERT    -> fields + defaults + options; move logic to __post_init__; drop redundant dunders
4. PRESERVE   -> check constructor signature, eq/hash identity semantics, repr secrets, ordering
5. SERIALIZE  -> decide to/from JSON path (asdict, from_dict, pydantic, msgspec)
6. VERIFY     -> construct, compare, hash, round-trip JSON; run tests and type checker

Choosing the Container

SituationUseWhy
Internal domain object, trusted inputs (Chunk, EvalResult, RunState)@dataclassstdlib, zero deps, fast, explicit
Immutable config / cache key / dict key@dataclass(frozen=True) (+ slots=True 3.10+)hashable, safe to share across threads
Untrusted input: HTTP body, LLM JSON output, env, YAML filespydantic v2 BaseModelvalidation, coercion, error messages, JSON schema
Tool/function-calling schema for an LLMpydantic BaseModel + model_json_schema()schema generation
Must remain a dict (passed to json.dumps, SDK messages=[...])TypedDictstatic typing only, zero runtime cost
Tiny immutable record you unpack (score, idx = hit)typing.NamedTupletuple semantics, positional
Converters/validators without pydanticattrs @definefield(converter=...), validators
Hot decode path (millions of JSON lines, streaming events)msgspec.Structvery fast decode + validation

Rule of thumb: validate at the edge (pydantic), compute in the core (dataclasses). Convert once at the boundary, then pass dataclasses around. Full matrix: choosing-a-data-container.md.

Conversion Rules

1. Fields come from __init__ parameters, in order

python
# Before
class RetrievedChunk:
    def __init__(self, chunk_id: str, text: str, score: float = 0.0, metadata: dict | None = None):
        self.chunk_id = chunk_id
        self.text = text
        self.score = score
        self.metadata = metadata if metadata is not None else {}

# After
from dataclasses import dataclass, field

@dataclass
class RetrievedChunk:
    chunk_id: str
    text: str
    score: float = 0.0
    metadata: dict = field(default_factory=dict)
  • Keep parameter order: positional call sites (RetrievedChunk("c1", "text")) must keep working.
  • Keep annotations; infer from defaults when missing; use Any and flag it rather than guess.
  • x: T = None without Optional -> write Optional[T] / T | None (3.10+).
  • The None sentinel + or [] idiom becomes default_factory. Behaviour change to mention: passing None explicitly now stores None.

2. Mutable defaults always use default_factory

Original defaultDataclass
tags=[] (shared-list bug)tags: list[str] = field(default_factory=list)
meta={}field(default_factory=dict)
stop=["\n\n"]field(default_factory=lambda: ["\n\n"])
self._cache = {} in __init___cache: dict = field(default_factory=dict, init=False, repr=False, compare=False)
self._lock = threading.Lock()_lock: threading.Lock = field(default_factory=threading.Lock, init=False, repr=False, compare=False)

The decorator rejects unhashable defaults (list, dict, set, and since 3.11 any instance whose class sets __hash__ = None) with ValueError. Defaults that are other mutable objects (a Tokenizer() instance) are silently shared: use a factory.

3. Logic goes into __post_init__; construction-only params become InitVar

python
import os
from dataclasses import InitVar, dataclass, field

@dataclass
class LLMClient:
    model: str
    api_key_env: InitVar[str] = "OPENAI_API_KEY"
    timeout: float = 30.0
    api_key: str = field(init=False, repr=False)       # never printed in logs

    def __post_init__(self, api_key_env: str) -> None:
        if self.timeout <= 0:
            raise ValueError("timeout must be positive")
        self.api_key = os.environ.get(api_key_env, "")
  • Validation (if ...: raise), normalisation (self.model = self.model.strip()) and derived attributes all move to __post_init__; parameter names become self.<name>.
  • A parameter that is used but never stored becomes InitVar[T]; it is passed to __post_init__ in field order and is excluded from fields(), asdict() and repr.
  • Derived attributes are field(init=False); give them a default if they are only set conditionally, otherwise repr() raises AttributeError.
  • Subclasses must call super().__post_init__(...) themselves.

4. Frozen classes write through object.__setattr__

python
@dataclass(frozen=True, slots=True)
class PromptTemplate:
    template: str
    variables: tuple[str, ...] = ()

    def __post_init__(self) -> None:
        if not self.variables:
            import string
            names = tuple(n for _, n, _, _ in string.Formatter().parse(self.template) if n)
            object.__setattr__(self, "variables", names)

Methods that mutated self must return a new instance: dataclasses.replace(cfg, temperature=0.0) (or copy.replace 3.13+). Store tuples / frozenset / MappingProxyType in frozen classes; a frozen class holding a list is only shallowly immutable and unhashable.

5. Drop dunders the decorator generates, but keep semantics

Hand-writtenReplace withWatch out
__init__ that only stores paramsfieldsrename self._x = x breaks keyword callers: keep a manual __init__ or a property
__repr__ listing fieldsrepr=True (default)fields it hid (API keys, embeddings) -> field(repr=False)
__eq__ comparing a tuple of fieldseq=True (default)fields it ignored -> field(compare=False); dataclass requires the same class, not isinstance
__hash__ over fieldsfrozen=True (preferred) or unsafe_hash=Truefields not hashed -> field(hash=False)
__lt__ + total_ordering on a field tuple in field orderorder=Trueordering is by field order; otherwise keep the method
__slots__ = (...)slots=True (3.10+)manual __slots__ conflicts with defaults
no __eq__ at alleq=False if identity semantics are neededeq=True sets __hash__ = None: objects stop working as dict keys / set members

6. Arrays and tensors never take part in eq/hash

python
import numpy as np
from dataclasses import dataclass, field

@dataclass
class Embedded:
    doc_id: str
    vector: np.ndarray = field(compare=False, repr=False)

    def same_vector(self, other: "Embedded", atol: float = 1e-6) -> bool:
        return self.vector.shape == other.vector.shape and bool(np.allclose(self.vector, other.vector, atol=atol))

Generated __eq__ compares field values like tuples; array == array returns an array and bool(array) raises ValueError: The truth value of an array ... is ambiguous (it "works" only when both sides are the same object). The same applies to torch.Tensor, pandas objects and anything whose == is element-wise.

7. Inheritance and default ordering

python
@dataclass
class BaseRequest:
    model: str
    temperature: float = 0.7

@dataclass(kw_only=True)            # 3.10+: avoids "non-default argument follows default argument"
class ChatRequest(BaseRequest):
    messages: list["Message"]
    stream: bool = False

Fields are collected base-first. A subclass field without a default after a base field with a default raises TypeError unless it is keyword-only (kw_only=True, field(kw_only=True) or after _: KW_ONLY). On 3.9, give the subclass field a default or use composition. Frozen and non-frozen dataclasses cannot inherit from each other.

Option Cheat-Sheet

OptionDefaultEffectVersion
initTruegenerate __init__3.7
reprTruegenerate __repr__3.7
eqTruegenerate __eq__ (same class, field-wise)3.7
orderFalse__lt__/__le__/__gt__/__ge__ by field tuple3.7
unsafe_hashFalseforce __hash__ even if mutable3.7
frozenFalseassignment raises FrozenInstanceError3.7
match_argsTrue__match_args__ for match positional patterns3.10
kw_onlyFalseall fields keyword-only3.10
slotsFalsecreate __slots__, no __dict__3.10
weakref_slotFalseadd __weakref__ slot (needs slots=True)3.11

field() parameters: default, default_factory, init, repr, hash, compare, metadata, kw_only (3.10), doc (3.14). Full semantics, hash table and error catalogue: dataclass-reference.md.

asdict / astuple / replace Pitfalls

  • asdict() recurses into dataclasses, lists, tuples and dicts and copy.deepcopys everything else: a field holding a 100 MB np.ndarray is copied; a field holding a client, lock or open file raises TypeError: cannot pickle .... Use fields() + getattr for a shallow dict, or mark the field and skip it.
  • asdict() keeps Enum, datetime, Path, Decimal objects as-is: json.dumps then fails. Pass default= or write a to_json_dict().
  • replace() calls __init__ again: __post_init__ re-runs, init=False fields are recomputed (not copied), InitVars without defaults must be passed.
  • fields() excludes ClassVar and InitVar pseudo-fields.

Serialization patterns (json, nested, enums, datetimes, numpy) and validation strategies: serialization-and-validation.md.

AI-Engineering Patterns (summary)

python
from dataclasses import dataclass, field
from enum import Enum
from typing import Any, Literal, Optional

class Role(str, Enum):
    SYSTEM = "system"
    USER = "user"
    ASSISTANT = "assistant"
    TOOL = "tool"

@dataclass(frozen=True, slots=True)
class ToolCall:
    id: str
    name: str
    arguments: str          # raw JSON string from the model; parse+validate at the edge

@dataclass(slots=True)
class Message:
    role: Role
    content: Optional[str] = None
    tool_calls: list[ToolCall] = field(default_factory=list)
    tool_call_id: Optional[str] = None

    def to_api(self) -> dict[str, Any]:
        d: dict[str, Any] = {"role": self.role.value, "content": self.content}
        if self.tool_calls:
            d["tool_calls"] = [{"id": t.id, "type": "function",
                                "function": {"name": t.name, "arguments": t.arguments}} for t in self.tool_calls]
        if self.tool_call_id:
            d["tool_call_id"] = self.tool_call_id
        return d

@dataclass(frozen=True)
class GenerationConfig:
    model: str
    temperature: float = 0.0
    max_tokens: int = 1024
    stop: tuple[str, ...] = ()
    response_format: Literal["text", "json_object"] = "text"
  • Configs: frozen dataclass, built once from YAML/env (validated with pydantic or a from_env classmethod), hashed into cache keys and run names.
  • Requests/responses: dataclass with to_api() / from_api(); pydantic when parsing model output that may be malformed.
  • Chunks/documents: embedding field with compare=False, repr=False; identity by chunk_id.
  • Eval records: order=True or sorted(key=...); asdict() straight into a DataFrame / JSONL.

Full patterns (training/inference configs, streaming deltas, Anthropic content blocks, vector store records, eval harness rows): ai-engineering-models.md.

Anti-Patterns to Fix

SmellFix
def __init__(self, tags=[])field(default_factory=list)
@dataclass on a class that is used in a set / as dict key, without frozenfrozen=True, or eq=False to keep identity
@dataclass with np.ndarray field and default eqfield(compare=False)
api_key: str printed by repr into logsfield(repr=False)
asdict(obj) on objects holding clients/lockscustom to_dict() over fields()
dict[str, Any] passed through 5 layersdataclass at the boundary, typed access inside
pydantic model for every internal struct in a hot loopdataclass internally, pydantic only at the edge
self.__dict__.update(kwargs) constructorsexplicit fields; reject unknown keys
Field type Optional[list] with default None and or [] everywherenon-optional list + default_factory
Subclass field breaks default orderingkw_only=True (3.10+) or composition

Output Format

When converting, provide:

  1. Container choice per object with one-line reason (boundary vs internal, mutability, hashing).
  2. Converted code: imports, decorator options, fields, __post_init__, kept methods.
  3. Behaviour changes: constructor signature, equality/hash semantics, repr output, explicit-None handling, version requirements (3.10 for kw_only/slots/KW_ONLY).
  4. Serialization path if the object crosses a JSON/API boundary (from_dict, model_validate, asdict).
  5. Verification snippet: construct, compare, hash, replace, JSON round-trip.

Scripts

scripts/to_dataclass.py is static analysis only (it parses, never imports or runs the code):

bash
# Convert boilerplate classes in a module; prints imports, notes with rule ids and @dataclass code
python scripts/to_dataclass.py class models.py
python scripts/to_dataclass.py class models.py --frozen --slots --kw-only
python scripts/to_dataclass.py class models.py --json --strict    # exit 1 if anything is convertible
cat models.py | python scripts/to_dataclass.py class -

# Generate nested typed classes from a JSON sample (object or list of objects)
python scripts/to_dataclass.py json response.json --name ChatCompletion                  # dataclasses + from_dict()
python scripts/to_dataclass.py json response.json --name ChatCompletion --style pydantic # BaseModel + Field(alias=)
python scripts/to_dataclass.py json events.json --name Event --style typeddict --json

Class mode findings: DC001 mutable default, DC002 None-sentinel default, DC003/DC004 redundant __repr__/__eq__, DC005 __hash__ -> frozen/unsafe_hash, DC006 ordering -> order=True, DC007 inferred/missing annotation, DC008 logic moved to __post_init__, DC009 slots, DC010 ClassVar, DC011 keyword-only, DC012 positional-only, DC013 InitVar, DC014 internal state field, DC015 mutation under frozen, DC016 identity-eq/hash change, DC017 array field excluded from eq, DC018 shared call default, DC019 implicit Optional. Skips: S01 already declarative, S02 no __init__, S03 *args/**kwargs, S04 super().__init__, S05 attribute renamed, S06 parameter reassigned after storing, S07 special base (Exception, Enum, BaseModel...), S08 metaclass/__new__, S09 return/yield in __init__, S10 nothing stored. JSON mode merges types across list items, marks missing/null keys Optional, snake_cases keys (aliases kept in metadata["json_key"] or Field(alias=)) and flags ISO-8601 strings. Exit codes: 0 ok, 1 with --strict when convertible classes exist, 2 on read/parse/usage errors. Always review the output: it cannot see call sites.

Additional Resources

FileContent
dataclass-reference.mdEvery decorator option and field() parameter, __post_init__, InitVar, KW_ONLY, inheritance, hash/eq table, slots, replace/asdict/fields, error catalogue, performance
choosing-a-data-container.mddataclass vs pydantic v2 vs TypedDict vs NamedTuple vs attrs vs msgspec vs plain class, with side-by-side code
serialization-and-validation.mdJSON in/out, nested objects, enums, datetimes, numpy, aliases, YAML/env config loading, validation strategies
ai-engineering-models.mdConfigs, LLM requests/responses, chat messages, tool calls, streaming, RAG chunks with embeddings, eval records, OpenAI/Anthropic JSON to typed objects
conversion-examples.mdWorked before/after conversions, including the tricky ones the script skips
More in Python

python-beginner-concept-tutor

Teach Python concepts to beginners heading into AI engineering (data scientists, analysts, students, career switchers): plain-language definitions, analogies, minimal examples, line-by-line walkthroughs, common mistakes, check-your-understanding questions and mini exercises; Socratic hints before solutions; explaining tracebacks; mutable vs immutable, references and aliasing, is vs ==, integer division, floating point, slicing and off-by-one, indentation errors; a staged learning path from variables to calling an LLM API, embeddings and a tiny RAG. Ships a script that lists the concepts a snippet uses in learning order and traces execution step by step like Python Tutor.

python-comprehension-refactorer

Refactor Python loops into list, dict and set comprehensions and generator expressions, and unroll comprehensions that are too clever: filtering, mapping, nested-loop flattening, conditional expressions, walrus in comprehensions, dict inversion, grouping with defaultdict/Counter, lazy generators for sum/any/all/max, streaming large files, itertools (chain, islice, groupby, batched, pairwise, accumulate, product), scoping rules, side-effect anti-patterns and performance (when map/filter or numpy/pandas vectorization win). AI-engineering focus: building prompt lists, batching texts for embeddings, filtering retrieved chunks by score, id->embedding dicts, flattening tool-call results, lazy dataset streaming and token counting.

python-context-manager-writer

Write correct Python context managers and find resource-handling bugs — __enter__/__exit__ semantics and exception suppression, contextlib (contextmanager, asynccontextmanager, ExitStack, AsyncExitStack, closing, aclosing, suppress, redirect_stdout, nullcontext, chdir), async with, re-entrancy, leaked files/connections/clients/pools/locks, and AI-engineering recipes: torch.no_grad/inference_mode/autocast, eval()/train() switches, GPU cache cleanup, token and latency meters, tracing spans, temp env vars, rate-limit semaphores, httpx/aiohttp client lifecycle, FastAPI lifespan, DB/vector-store transactions, atomic checkpoint writes, seeds and agent sandboxes.

python-exception-handling-designer

Design and review Python exception handling: custom exception hierarchies with retryable flags, what to catch where, EAFP vs LBYL, chaining with raise ... from, else/finally, logger.exception, add_note, ExceptionGroup and except* with asyncio.TaskGroup, warnings, asserts vs validation, FastAPI error translation, retries with backoff and jitter, timeouts, cancellation, pytest.raises, plus LLM-app failures: 429 rate limits with retry-after, 5xx overloaded, context-length exceeded, content filters, broken streams, malformed JSON repair loops, tool-call errors fed back to the model, partial embedding batch failures, CUDA OOM batch halving, vector DB outages, circuit breakers and model fallbacks.

python-idiomatic-code-reviewer

Review Python code for non-idiomatic, unpythonic or risky patterns and rewrite it into clean modern Python: PEP 8 / PEP 20, EAFP vs LBYL, iteration idioms (enumerate, zip, comprehensions, any/all), unpacking, f-strings, pathlib, truthiness, `is None`, mutable defaults, walrus, match statements, collections / itertools / functools, logging instead of print, with-blocks and resource handling. Includes AI-engineering checks: hardcoded API keys, time.sleep or requests inside async def, missing HTTP timeouts, unbounded retry loops, model clients built at import time, prompt string concatenation, sequential awaits, eval() on LLM output. Ships a 46-rule AST reviewer with severities and suggested rewrites.

python-type-hints-annotator

Add accurate, modern type hints to Python and fix mypy / pyright (pyrefly, ty) errors: builtin generics, X | None, TypeAlias and the 3.12 type statement, TypeVar and PEP 695 generics, Protocol, Callable / ParamSpec / Concatenate, TypedDict (Required, NotRequired, ReadOnly), Literal, Final, ClassVar, overload, Annotated, TypeGuard / TypeIs, Self, Never, TypeVarTuple, async types, Iterable vs Sequence vs Mapping, TYPE_CHECKING, stubs, py.typed, gradual strictness. AI-engineering focus: typed LLM message dicts and tool schemas, pydantic vs TypedDict, numpy NDArray, torch / jaxtyping shapes, streaming generators, async clients, callbacks, generic VectorStore protocols, typed config. Ships an AST audit: coverage gate, legacy-typing rewrites, inferred types.