thunc
think + function. Call an LLM like a typed Python function.
Status: beta (v0.1). Expect bugs; the API may change. Feedback and issues are welcome.
import thunc
thunc.configure(backend="claude-code")
@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
The answer is parsed into the declared type. If it doesn't fit, the model is asked again, and
after that thunc.ThuncError is raised. The library uses the standard library only and needs
Python 3.10+.
It runs on the Claude API, the OpenAI API, a local model (LM Studio, or any server that speaks the OpenAI API), or your Claude Code or Codex login.
Install
pip install thunc # standard library only
pip install "thunc[anthropic]" # adds the Claude API backend
pip install "thunc[openai]" # adds the OpenAI API backend
Try it
Clone the repo and run the examples from its root. No install is needed; the examples run through your local Claude Code login:
git clone https://github.com/Eltarras/thunc && cd thunc
python3 -m examples.hello
python3 -m examples.support_inbox
THUNC_BACKEND=codex python3 -m examples.log_triage
With an API key instead, install the SDK and pick the backend: THUNC_BACKEND=openai with
OPENAI_API_KEY, or THUNC_BACKEND=anthropic with ANTHROPIC_API_KEY.
Two ways to write a prompt
| When | ||
|---|---|---|
@thunc.function |
The prompt is fixed and should read like code | The docstring is the prompt, the parameters are the inputs, the return annotation is the type |
thunc.call(...) |
The prompt is built in code (from config, in a loop, loaded from a file) | thunc.call(f"Translate into {lang}.", {"text": note}) |
@thunc.function(instructions=some_string) combines the two: a typed, reusable function whose
prompt is generated.
Keep user data out of the instructions. Your own text can go in the instructions string. Anything from users, files or the web goes in the inputs:
@thunc.functiondoes this automatically.- With
thunc.callit's up to you. In a live test, a hostile email pasted in with an f-string tricked the model 3 out of 3 times. Passed as an input, it failed 3 out of 3 times.
API
@thunc.function |
Turns a signature + docstring into an AI-backed function. Options: instructions=, ensure=, retries=, backend=, model=. The body must be empty (...); real code raises TypeError. async def works |
thunc.call(instructions, inputs=None, *, returns=str, ensure=None, retries=2, backend=None, model=None) |
One prompt. Inputs are sent separately from the instructions |
thunc.map(func, items, workers=8) |
Runs calls in parallel, keeping the input order. Each call takes 4–8s, so this is the main speed lever |
thunc.configure(backend=, api_key=, model=, timeout=, trace=) |
Process-wide settings. trace="calls.jsonl" logs every call |
thunc.ThuncError |
Raised when no valid answer arrives after the retries |
Return types: str, bool, int, float, Literal[...], list[T], dict[str, T],
T | None, and dataclasses (built into real instances).
ensure= adds your own check, for example ensure=lambda n: 1 <= n <= 5. A failed check is
sent back to the model and retried.
Backends:
anthropicis the Claude API:configure(api_key=...)orANTHROPIC_API_KEY, pluspip install "thunc[anthropic]".openaiis the OpenAI API:configure(backend="openai", api_key=...)orOPENAI_API_KEY, pluspip install "thunc[openai]". The default model isgpt-5.5.OPENAI_BASE_URLpoints it at any server that speaks the OpenAI Responses API.claude-codeandcodexcall your local CLI login, and are meant for cheap testing.
Local models: the openai backend works with a local server through OPENAI_BASE_URL. This
has been tested with LM Studio running openai/gpt-oss-20b:
# OPENAI_BASE_URL=http://localhost:1234/v1 OPENAI_API_KEY=lm-studio (any non-empty key works)
thunc.configure(backend="openai", model="openai/gpt-oss-20b")
Small models sometimes wrap an answer, like {"rating": 5} for an int, and need the retry more
often.
The backend can also be set with THUNC_BACKEND. With none set, ANTHROPIC_API_KEY (or a
configure(api_key=...) alone) selects anthropic, and otherwise OPENAI_API_KEY selects openai.
Type checking: signatures and return types are visible to mypy and Pyright. mypy reports
empty bodies; turn that off with disable_error_code = ["empty-body"].
Examples
| hello.py | The smallest call |
| support_inbox.py | Docstring functions returning a Literal, an int with ensure=, a dataclass, and a reply; tickets processed in parallel |
| dynamic_prompts.py | Prompts built from a style guide with thunc.call, and a grading function generated from a rubric |
| log_triage.py | Plain Python and AI functions mixed, with tracing |
Code
thunc/
__init__.py public API
decorator.py @thunc.function
core.py thunc.call, thunc.map, tracing
schema.py return types: describe, parse, validate
config.py settings and backend selection
backends.py anthropic, openai, claude-code, codex
errors.py ThuncError
tests/ offline: a fake backend, never a real model
live_tests/ against a real model: hello, a yes/no decision, messy text to a dict
examples/
Limitations
- There's no caching and no record/replay yet, so repeated calls cost again.
Literalresults fromthunc.callare typed asAny.@thunc.functionhas no such gap.- Docstrings disappear under
python -OO. Useinstructions=there.
Development
python3 -m venv .venv && .venv/bin/pip install -e ".[anthropic,openai,dev]"
.venv/bin/pytest # offline tests (these run in CI)
.venv/bin/pytest live_tests # real model calls through your Claude Code login; costs quota
THUNC_BACKEND=anthropic .venv/bin/pytest live_tests # the same, through the Claude API (needs ANTHROPIC_API_KEY)
THUNC_BACKEND=openai .venv/bin/pytest live_tests # the same, through the OpenAI API (needs OPENAI_API_KEY)
.venv/bin/ruff check . && .venv/bin/mypy --strict thunc
License
Metadata
Release files for thunc 0.1.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| thunc-0.1.1.tar.gz | 21.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| thunc-0.1.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 36.9 kB
Release files / thunc-0.1.1.tar.gz
| Download URL | thunc-0.1.1.tar.gz |
|---|---|
| Size | 21.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
5b6f844fcf65f064f7299bdec3456a52e2b2c535eee68ff63ec9b1680af2b445
|
|
BLAKE2b-256 checksum How to use checksums |
83227cb1b52e7e0ddc77ba00755aa7f0671b8cf41ba67c9cdb94e295ac648264
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 3, 2026.
Transparency logRelease files / thunc-0.1.1-py3-none-any.whl
| Download URL | thunc-0.1.1-py3-none-any.whl |
|---|---|
| Size | 15.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
a997858632c10951266eff577144e1296eaf0b28b2050ff2527d767dc2a66782
|
|
BLAKE2b-256 checksum How to use checksums |
5d10e56b7796b4fb9e8dd4a7a6084f4319c8ec3f8771177132a58a34d0c7eab9
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 3, 2026.
Transparency log