typedout
Reliable structured output from OpenAI and Anthropic, with a provider interface for others. Define a schema, get back a validated object — with tolerant JSON repair, error-aware retries, and an offline mock provider. No API key needed to try it.
from pydantic import BaseModel
from typedout import TypedOut, MockProvider
class Person(BaseModel):
name: str
age: int
email: str
llm = TypedOut(MockProvider(script=["invalid", "valid"]))
person = llm.extract(Person, "Ada Lovelace, 36, ada@example.com")
print(person) # name='Ada Lovelace' age=36 email='ada@example.com'
print(llm.last_usage) # 2 req · 352 in + 19 out = 371 tok · $0.000000
Why
Getting valid, typed data out of an LLM is deceptively hard. Models wrap JSON in
```json fences, add a "Sure, here you go" preamble, emit Python's
True/None, leave trailing commas, use single quotes, or simply get cut off
mid-object at the token limit. typedout handles all of that behind one small API:
- Tolerant JSON repair — a single-pass, string-aware scanner turns almost-JSON into strict JSON (no
eval, no network). - Schema validation — pydantic models or raw JSON Schema dicts.
- Error-aware retries — when validation fails, the concrete errors are fed back to the model and it tries again.
- Provider-agnostic — Anthropic, OpenAI, or your own; a deterministic
MockProvideris included so tests and demos run fully offline. - Extras — partial-object streaming, token/cost tracking, and an
@extractdecorator that turns any function into a typed extractor.
Install
pip install typedout-py # core (pydantic only)
pip install "typedout-py[anthropic]" # + Anthropic SDK
pip install "typedout-py[openai]" # + OpenAI SDK
Install typedout-py, import typedout. The distribution name carries a -py
suffix; the package you import does not:
from typedout import TypedOut
Requires Python 3.10+. The only hard dependency is pydantic>=2.
Renamed from
structllmon 4 Sep 2026. The distribution namestructllmis held on PyPI by a different project doing the same thing — "Universal Python library for Structured Outputs with any LLM provider" — so this package could never have been published under it, and anyone searching for that name would have found the other library. The GitHub URL of the old name still redirects here.And why the
-pysuffix. PyPI also refuses the bare nametypedout: it normalises away-,_and.before comparing, sotypedoutcollides with an unrelated project calledtyped-out. Renaming the library a third time would have cost more than it is worth, so the distribution istypedout-pyand the import staystypedout— the same split asbeautifulsoup4/bs4orpillow/PIL.
Usage
1. Extract a typed object
from pydantic import BaseModel, Field
from typedout import TypedOut
from typedout import AnthropicProvider # or OpenAIProvider, or your own
class Invoice(BaseModel):
number: str
total: float = Field(ge=0)
currency: str
llm = TypedOut(AnthropicProvider()) # defaults to claude-opus-5
invoice = llm.extract(Invoice, "Invoice INV-2043, total 1,299.00 EUR, due in 30 days")
# -> Invoice(number='INV-2043', total=1299.0, currency='EUR')
The default is claude-opus-5 — the strongest model, chosen because the
retry loop feeds validation errors back to the model, and a model that gets it
right first time is often cheaper per successful extraction than a cheaper one
that needs three attempts. It is not the cheapest per token: at the time of
writing it is $5 / $25 per million input / output tokens against $2 / $10 for
claude-sonnet-5. Pass model= to choose another, and read llm.last_usage
for what a call actually cost.
The schema is injected into the prompt automatically. If the model's reply doesn't
parse or doesn't validate, typedout repairs it, and — if it still fails —
re-prompts with the exact errors (up to max_retries, default 2).
2. The repair engine, standalone
repair_json is useful on its own. These are real outputs from the library:
from typedout import repair_json
repair_json("```json\n{'name': 'Ada', 'age': 36,}\n```")
# -> {"name":"Ada","age":36}
repair_json('{"name": "Ada", "age": 36, "email": "ada@exampl') # truncated
# -> {"name":"Ada","age":36,"email":"ada@exampl"}
repair_json("{name: 'Ada', active: True, mgr: None}") # JS/Python-isms
# -> {"name":"Ada","active":true,"mgr":null}
repair_json('Sure! Here you go:\n{"score": 9.5}\nHope that helps!') # prose around it
# -> {"score":9.5}
| Fixes | Example in → out |
|---|---|
| Markdown fences | ```json {...} ``` → {...} |
| Surrounding prose | Here you go: {...} thanks! → {...} |
| Trailing commas | [1, 2, 3,] → [1, 2, 3] |
| Single quotes | {'a': 'b'} → {"a": "b"} |
| Unquoted keys | {a: 1} → {"a": 1} |
| Python literals | True / False / None → true / false / null |
// and /* */ comments |
stripped |
| Truncated output | {"a": [1, 2 → {"a": [1, 2]} |
3. @extract — a typed extractor in three lines
from typedout import extract, MockProvider
@extract(Person, provider=MockProvider(script=["valid"]))
def parse_person(text: str) -> Person:
return f"Extract the person described here:\n{text}"
parse_person("Ada Lovelace, 36, ada@example.com") # -> Person(...)
The function just builds the prompt; the decorator runs extraction and returns the validated object of the declared type.
4. Stream a partial object as it fills in
llm = TypedOut(MockProvider(script=["valid"], chunk_size=8))
for partial in llm.stream(Person, "Ada Lovelace, 36, ada@example.com"):
print(partial)
print(repr(llm.last_result)) # fully validated Person
Real output — the object materialises field by field, even from a half-received stream:
{}
{'name': 'Ada'}
{'name': 'Ada Lovelac'}
{'name': 'Ada Lovelace'}
{'name': 'Ada Lovelace', 'age': 36}
{'name': 'Ada Lovelace', 'age': 36, 'email': 'ada'}
{'name': 'Ada Lovelace', 'age': 36, 'email': 'ada@example'}
{'name': 'Ada Lovelace', 'age': 36, 'email': 'ada@example.com'}
Person(name='Ada Lovelace', age=36, email='ada@example.com')
AnthropicProviderandOpenAIProviderdo not yet implement token streaming; with themstream()yields a single snapshot containing the full object. OverrideProvider.stream()in your own provider to get progressive snapshots.
5. Track tokens and cost
llm = TypedOut(OpenAIProvider(model="gpt-4o-mini"), model="gpt-4o-mini")
llm.extract(Invoice, "...")
print(llm.last_usage) # this call, incl. retries
print(llm.total_usage) # running total for the session
# 1 req · 166 in + 15 out = 181 tok · $0.000034
Prices are configurable (register_price(...)) and default to illustrative values —
nothing here bills you or calls the network.
6. Raw JSON Schema, no pydantic
Pass a plain dict and get a validated dict back, checked by the built-in lite
validator (type, required, enum, bounds, anyOf, $ref, …):
schema = {
"type": "object",
"properties": {"id": {"type": "integer"}, "priority": {"enum": ["low", "high"]}},
"required": ["id"],
}
llm.extract(schema, "ticket 7, high priority") # -> {"id": 7, "priority": "high"}
How it works
prompt ─▶ Provider.complete ─▶ repair(text) ─▶ validate(schema) ─▶ typed object
▲ │
└──────── re-prompt with errors ◀─────┘ (up to max_retries)
- Prompt — the JSON Schema is embedded in a system prompt asking for a single JSON object.
- Repair —
repair.pyscans the reply character by character. Strings are re-encoded throughjson.dumps(so their contents are never corrupted); unbalanced braces are closed; it stops at the first complete top-level value, ignoring trailing prose. - Validate — pydantic (typed instance) or
jsonschema_lite(dict). - Retry — on failure the assistant's bad answer plus a precise correction ("field
age: input should be a valid integer") are appended, and the model tries again.
Every layer is independent: use repair_json alone, swap the provider, or drive the
whole thing offline with MockProvider — which can synthesise schema-valid answers
(it honours type, enum, numeric bounds, string minLength/maxLength and the
common formats; a pattern is not synthesised) or deliberately produce fenced,
loose, truncated, or invalid replies to exercise the repair and retry paths.
Testing
pip install -e ".[dev]"
pytest # 100 tests, fully offline
python examples/quickstart.py
The suite covers the repair scanner (fences, comments, truncation, unicode, nested mess), the JSON Schema validator, the retry loop, streaming, cost math, and the Anthropic/OpenAI payload mapping (via injected fake clients — no SDKs, no keys).
Part of the ferinazumaDEV ecosystem
typedout is one of a set of small, focused tools by ferinazumaDEV for building reliably with LLMs — here, turning free-form model output into schema-validated, machine-readable data you can trust. Related projects:
- The GEO Handbook — the open reference on getting content cited by AI answer engines (ChatGPT, Perplexity, Google AI Overviews, Gemini, Copilot).
- notebooklm-kb-system — a token-efficient "second brain" for AI agents: local memory, NotebookLM notebooks, and knowledge routing.
- politeclient — a polite, bulletproof HTTP client for Python: retries with backoff, per-host rate-limiting, and caching — handy for talking to LLM provider APIs.
- scaffld — scaffold fully-wired Python projects (tests, CI, pre-commit, license) from templates.
- Hub & writing: zentimes.es.
By ferinazumaDEV.
License
MIT — see LICENSE.
Built by Fernando Aporta Franco (@ferinazumaDEV).
Metadata
Release files for typedout-py 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| typedout_py-0.1.0.tar.gz | 38.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| typedout_py-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 70.1 kB
Release files / typedout_py-0.1.0.tar.gz
| Download URL | typedout_py-0.1.0.tar.gz |
|---|---|
| Size | 38.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
eb291215669302ab5afb5e90783f250fe1c0ca3590a3e2ae266d9777832db96d
|
|
BLAKE2b-256 checksum How to use checksums |
004b6ea0bc13040fd77fa4357ad7874ca63134d04db48139abedc05e9524c071
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.11.2
|
Release files / typedout_py-0.1.0-py3-none-any.whl
| Download URL | typedout_py-0.1.0-py3-none-any.whl |
|---|---|
| Size | 31.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
98cf3605b932d9d96adf8348a1b639b3af19be65f6cd35abd4d633db193304f8
|
|
BLAKE2b-256 checksum How to use checksums |
c5d81555f7ac4ee7b2669f8d2242ad5000f048a8a0aba4a90ee6651609c959d4
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.11.2
|