T tomlkit·org
Inspect Formatter Validator Lint Stats Keys Query Convert TOMLJSON JSONTOML TOMLYAML YAMLTOML INITOML TOMLINI .envTOML TOML.env TOMLTS TOMLCSV CSVTOML TOMLXML TOML.properties .propertiesTOML Transform Sort keys Flatten Unflatten Redact Minify Generate Go struct Rust struct Python types JSON Schema Compare Diff Merge

TOML to Python types

updated 20 August 2026

Paste a config and get the Python types that describe it — a TypedDict for type checking a tomllib.load() result, @dataclasses for plain objects, or pydantic models when you want validation at load time.

PEP 604 unions, modern syntax

What this tool does

It infers a type for every key and prints Python classes in the style you choose. Tables become classes, arrays of tables become list[Class], and scalars map to str, int, float, bool, or datetime. Classes are printed children-first so every reference is already defined, and from __future__ import annotations is included so forward references are never a problem.

The intent it closes: "I load this config with tomllib and get a bare dict — I want the type checker to know what is in it." A TypedDict gives you exactly that with no runtime cost. Pydantic gives you validation instead, which is the better choice when the config comes from users.

Optional fields are detected across the entries of an array of tables: a key missing from some entries becomes NotRequired[T] in a TypedDict, or T | None = None in a dataclass or model. Dataclass fields are ordered so defaults never precede required fields, which would be a syntax error.

When you'd reach for it

  • Type-check a tomllib result. Annotate the loaded dict with the generated TypedDict and mypy or pyright will check every key access.
  • Validate a config at startup. Generate pydantic models and let them raise on a bad value instead of failing three functions later.
  • Read pyproject.toml. Types for [project], [tool.*], and the override arrays, generated from your own file.
  • Document the config shape. The generated classes are the most compact accurate description of a config file.
  • Move a config into a settings object. Dataclasses give you attribute access instead of dictionary lookups.

How it works

Inference, ordering, then printing in one of three styles.

1. Infer types

Values are classified as string, bool, int, float, datetime, list, or nested table. Arrays of tables are merged: the class gets the union of all entries' keys, and any key missing from an entry is marked optional. Integers and floats in one array unify to float; anything genuinely mixed becomes Any.

2. Order the classes

Declarations are emitted children-first, so a class is always defined before the class that references it. from __future__ import annotations is added as well, which makes annotations lazy and keeps the file valid even if you reorder it later.

3. Print in your chosen style

TypedDict emits class X(TypedDict) with NotRequired[...] for optional keys. dataclass emits @dataclass classes with required fields first and = None defaults on the optional ones. pydantic emits BaseModel subclasses in the same shape. Imports — datetime, Any, TypedDict, NotRequired, dataclass, BaseModel — are added only when the output actually uses them.

Options reference

Style

TypedDict is the zero-cost option: it describes the dict tomllib.load() already returns, with no conversion step and no runtime dependency. dataclass gives you attribute access and a constructor, but you have to build the objects yourself. pydantic validates and coerces at construction time — the right choice when a wrong value should be an error rather than a surprise later.

Root class

The name of the outermost class. Config is conventional; PyProject, Settings, or the service name may read better.

Datetimes

datetime matches what tomllib actually returns for TOML date-times, so it is the accurate annotation. Choose str if you would rather keep the value opaque or avoid the import.

Example

Input:

[[tool.mypy.overrides]]
module = "legacy.*"
ignore_missing_imports = true

[[tool.mypy.overrides]]
module = "vendor.*"
ignore_missing_imports = true
follow_imports = "skip"

Output (TypedDict style, trimmed):

from __future__ import annotations

from typing import NotRequired, TypedDict


class Override(TypedDict):
    module: str
    ignore_missing_imports: bool
    follow_imports: NotRequired[str]


class Mypy(TypedDict):
    overrides: list[Override]

follow_imports is NotRequired because only the second override sets it. In dataclass or pydantic style the same field becomes follow_imports: str | None = None and is moved below the required fields.

Limits and notes

  • Keys that are not identifiers get renamed. requires-python becomes requires_python with a comment recording the original key. For a TypedDict that is a real mismatch — use pydantic with an alias, or the functional TypedDict("X", {...}) form, when keys contain dashes.
  • One sample, one type. A field that is integral in your file is annotated int. Widen it by hand where the value can be fractional.
  • Empty containers become Any. There is nothing to infer from an empty array or table; add a representative entry.
  • No validators are generated. Pydantic models get types, not constraints — ranges, regexes, and enums are yours to add.

FAQ

Which style should I use with tomllib?

TypedDict. tomllib.load() returns a plain dict, and a TypedDict annotation lets mypy or pyright check every key you touch without converting anything at runtime.

Why children-first ordering?

So each class is defined before it is used, which keeps the file valid even without lazy annotations. The from __future__ import annotations line makes it robust to reordering too.

How are dashed keys like requires-python handled?

They are converted to snake_case with a trailing comment naming the original key. For a dict-shaped type that mapping is not automatic — reach for pydantic with alias=, or use the functional TypedDict syntax, when dashed keys matter.

Does it generate __init__ or parsing code?

No, only the type declarations. For TypedDict none is needed. For dataclasses and models you construct them from the loaded dict yourself — usually a single Config(**data) for a flat config, or a small builder for nested ones.

Is my config sent anywhere?

No. Everything runs in the browser tab.