Skip to main content

typedout

Python License: MIT tests

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 MockProvider is included so tests and demos run fully offline.
  • Extras — partial-object streaming, token/cost tracking, and an @extract decorator 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 structllm on 4 Sep 2026. The distribution name structllm is 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 -py suffix. PyPI also refuses the bare name typedout: it normalises away -, _ and . before comparing, so typedout collides with an unrelated project called typed-out. Renaming the library a third time would have cost more than it is worth, so the distribution is typedout-py and the import stays typedout — the same split as beautifulsoup4/bs4 or pillow/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')

AnthropicProvider and OpenAIProvider do not yet implement token streaming; with them stream() yields a single snapshot containing the full object. Override Provider.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)
  1. Prompt — the JSON Schema is embedded in a system prompt asking for a single JSON object.
  2. Repair — repair.py scans the reply character by character. Strings are re-encoded through json.dumps (so their contents are never corrupted); unbalanced braces are closed; it stops at the first complete top-level value, ignoring trailing prose.
  3. Validate — pydantic (typed instance) or jsonschema_lite (dict).
  4. 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)

Source distribution for typedout-py 0.1.0
File Size Uploaded
typedout_py-0.1.0.tar.gz 38.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for typedout-py 0.1.0
File Interpreter ABI Platform
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

Release history Release notifications | RSS feed

0.1.3

2 release files

0.1.2

2 release files

0.1.1

2 release files

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page