Call an LLM like a typed Python function.
The docstring is the prompt, the parameters are the inputs, and the return type is what you get back, checked. Runs on Claude, OpenAI, a local model, or your Claude Code or Codex login.
import thunc
@thunc.function
def urgency(ticket: str) -> int:
"""Rate how urgent this ticket is,
from 1 (can wait) to 5 (customer is blocked)."""
...
urgency("I was charged twice!")
# -> 4 # a checked int
Your function is the prompt.
No prompt templates, no JSON schemas, no parsing code. Write the signature you want and call it.
The instructions the model follows. Or pass instructions= built in code.
The inputs, sent separately from the instructions as data, which blunts prompt injection.
What you get back, parsed and checked: bool, Literal, lists, dataclasses and more.
Dataclasses in, dataclasses out.
Return types nest, so messy text becomes a list[Item] you can use straight away. This is a real run.
Never a silent bad value.
- Every answer is validated against your return type. Dataclasses come back as real instances.
- Wrong answers get fixed. The problem is sent back to the model and it tries again.
- Loud when it can't. If the answer still doesn't fit, you get a
ThuncError. - Your own checks with
ensure=lambda n: 1 <= n <= 5, retried the same way. - Cache what should be stable.
cache=Trueasks the model once per input. - Zero dependencies. Standard library only; provider SDKs are optional extras.
Agents that hand back a typed answer.
Give an agent a folder and permissions. It lists, searches, reads, edits and runs commands, then finishes with a checked value of your return type.
fixer = thunc.Agent(
"fixer",
workdir=".",
permissions=["write:src/**", "run:pytest"],
)
@fixer.task
def fix_failing_tests() -> bool:
"""Run the tests and fix any problems that they surface."""
...Runs on what you already have.
Choose per program, per function or per call. Mix them: a fast judgment model for decisions, a frontier model for writing.
backend="claude-code"Your local login. No API key, no SDK.
backend="codex"Your local login. No API key, no SDK.
thunc[anthropic]Native tool calls and prompt caching for agents.
thunc[openai]The Responses API, with native tool calls.
OPENAI_BASE_URLTested with LM Studio and gpt-oss-20b.
backend="jev"Yes/no, labels and ratings in about 0.3 s.
Prompts built in code.
When the prompt comes from a variable, a config file or a loop, call thunc.call with it. You still get typed, checked results, and thunc.map runs many at once.
text = thunc.call(f"Translate into {lang}.", {"note": note})
score = thunc.call(rubric, {"answer": answer}, returns=int)
scores = thunc.map(urgency, tickets, workers=8)Durable agents with Temporal
Recorded model turns, recoverable tool effects, reconnectable runs and explicit resolution of uncertain commands. Local calls keep their dependency-free runtime.