Just In Time Implementation: declare a typed function or method by its interface and let an LLM write, validate, and cache a real implementation.
Project description
jiti
Interface-first Python. Declare the interfaces, wire them into a call graph, run the program — an LLM writes the implementations the first time each one is called, and the result is real, committable code that you keep.
The idea
You decide what: typed signatures, docstrings, and tests. You decide how the pieces
fit: which function calls which. Then you run, and bodies appear just in time, get
validated against ruff + ty + your tests, and land as plain Python under .jiti/. Every
call after that is plain dispatch — no model, no API key, no network. When you're ready,
jiti merge folds the generated code back into your source and removes the decorator.
You can stop using jiti at any time and keep everything it wrote.
A tiny example
from jiti import jiti
@jiti
def slugify(text: str) -> str:
"""Convert text to a URL-safe slug."""
...
The first call to slugify("Hello, World!") runs an agent that inspects the real
arguments, explores your repo, drafts code, runs ruff + ty + any tests you've gated on it,
and writes the result to a file beside your source. Every call after that runs that file.
Wiring a graph
Interface-first pays off when interfaces compose. You write the orchestration in plain Python — that's your code, and that's where the graph lives. jiti writes the leaves.
from jiti import jiti
@jiti
def satisfies(version: str, spec: str) -> bool:
"""True if `version` satisfies `spec`. Specs: exact, '>=', '>', '<=', '<', '~', '^'."""
...
@jiti
def sort_versions(versions: list[str]) -> list[str]:
"""Return the version strings sorted ascending by semver precedence."""
...
# Your code — plain Python — composing the jiti pieces:
def latest_matching(versions: list[str], spec: str) -> str | None:
"""Return the highest-precedence version satisfying `spec`, or None."""
candidates = [v for v in versions if satisfies(v, spec)]
return sort_versions(candidates)[-1] if candidates else None
latest_matching is yours — no decorator, no magic, just a function. The first call to
latest_matching(["1.0.0", "2.0.0", "2.1.3"], "^2.0.0") runs your code, which calls
satisfies and sort_versions, which jiti generates on demand (and which may themselves
need other stubs along the way — generation cascades). Every call after that is plain
dispatch.
The full runnable version (with a Version dataclass, more stubs, and a method) lives in
examples/semver/.
Install
pip install jiti # or: uv add jiti
Needs Python 3.13+. Generation uses LiteLLM and defaults to Claude Sonnet 4.6; set
ANTHROPIC_API_KEY for the default model, or set JITI_MODEL to any LiteLLM model id
and provide that provider's API key. Running already-generated code needs nothing — no
key, no network.
ANTHROPIC_API_KEY=... python your_script.py
OPENAI_API_KEY=... JITI_MODEL=openai/<model-id> python your_script.py
Stubs
A stub is a function with a docstring and a placeholder body: ..., pass, or
raise NotImplementedError. A real body is an error — @jiti means "write this for me."
A comment in the stub becomes a hint:
@jiti
def parse_money(raw: str) -> Decimal:
"""Parse a currency string like '$1,234.56' into a Decimal."""
# strip the symbols and separators, then Decimal()
...
Methods and async def functions work the same way — decorate the targets you want
generated, use self freely on methods, and await async targets normally (see
Version.bump in examples/semver/core.py).
Strict type checkers flag an empty body with a non-
Nonereturn (empty-body). That's your checker reacting to the stub, not jiti. Disable that rule or useraise NotImplementedError.
Test-driven generation
State a function's definition of done from your test file with @jiti.required_for(target).
Tests import the real code, so the reference is type-checked — and running pytest is
the loop: generation happens to make your tests pass, red → green.
# tests/test_money.py
from app.money import parse_money
from jiti import jiti
@jiti.required_for(parse_money) # real body → your gate test, run as-is
def test_parses_symbols():
assert parse_money("$1,234.56") == Decimal("1234.56")
@jiti.required_for(parse_money) # empty body → jiti writes this test from the interface
def test_rejects_garbage() -> None:
"""parse_money raises ValueError on '' and 'not money'."""
...
An empty-bodied stub is a jiti-test: written before the implementation exists, so it
can only see the interface and can't couple to internals. jiti writes it, commits it
under .jiti/tests/, and gates the implementation on it. Both are ordinary test_*
functions your own pytest run executes.
Graduating off jiti
Interface-first is a development mode you can leave. jiti merge folds the generated
implementation back into your source, replaces the stub, removes @jiti, and cleans up
the mirror:
jiti status # what's generated, what you've hand-edited
jiti merge app.text.slugify # inline one function into its source
jiti merge --all # graduate the entire project
After merge --all, you have plain Python, no jiti dependency required. See
docs/reference.md for the full CLI and configuration surface.
A few things worth knowing
- The code is yours. Edit a generated body and jiti runs it as-is — it tracks a hash and won't clobber your edits. Change a stub's signature, docstring, or gates and it regenerates; if you'd hand-edited that section, it surfaces a conflict instead.
- git is yours. jiti only writes files into
.jiti/. Commit it (so production runs cached code with no key) or gitignore it. jiti never runs git. - Concurrency. Running generated code is fully safe — it's plain dispatch. Generating does no locking, so warm the cache once single-threaded, then parallelize. Writes are atomic, so a reader never sees a half-written file.
Where to go next
examples/semver/— a runnable interface-first walkthrough: stubs, a graph, tests, and themergegraduation.docs/reference.md— everyEngineknob, env var, and CLI flag.CHANGELOG.md— release history.
Development
uv sync
uv run pre-commit install
The gate, exactly what CI runs — ruff-format, ruff, ty, then pytest (no API key; uses a fake client):
uv run pre-commit run --all-files
uv run pytest
Status
Today, jiti supports sync and async free functions and methods, lazy agentic generation
with cascading across the call graph, in-process validation (ruff + ty + pytest),
test-driven generation via @jiti.required_for (works on free functions and methods),
a score-gated refactor pass, common decorator stacks (@staticmethod, @classmethod,
@property, and functools wrappers), the edit/conflict lifecycle, and the jiti CLI
(status / merge / test / clear). Anthropic, OpenAI, Gemini, and other model
families are available through LiteLLM.
Not yet: a pytest plugin, whole-class generation, generation locking, and dependency-aware invalidation.
Inspiration
This video is what inspired me to make this package. I then found this package, a joke implementation of the idea from 2 years ago. But now we've gotten to the point where LLMs are good enough that maybe, just maybe, this direction is worth entertaining.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file jiti-1.3.0.tar.gz.
File metadata
- Download URL: jiti-1.3.0.tar.gz
- Upload date:
- Size: 51.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
fd86dd5ebdd0aa93a582e44d23deffb2daed180336aa954b53f54d621b30b9c1
|
|
| MD5 |
b19034929ab8fa2a056be5bd7f74e940
|
|
| BLAKE2b-256 |
4bf873a86ad9928c5e519710341c2aa14cea4d480e14977863769ed4b1fa4332
|
Provenance
The following attestation bundles were made for jiti-1.3.0.tar.gz:
Publisher:
release.yml on RyanSaxe/jiti
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
jiti-1.3.0.tar.gz -
Subject digest:
fd86dd5ebdd0aa93a582e44d23deffb2daed180336aa954b53f54d621b30b9c1 - Sigstore transparency entry: 1749001797
- Sigstore integration time:
-
Permalink:
RyanSaxe/jiti@f630cb61e2599434067b461d1c0daa5fa383d30b -
Branch / Tag:
refs/heads/main - Owner: https://github.com/RyanSaxe
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@f630cb61e2599434067b461d1c0daa5fa383d30b -
Trigger Event:
push
-
Statement type:
File details
Details for the file jiti-1.3.0-py3-none-any.whl.
File metadata
- Download URL: jiti-1.3.0-py3-none-any.whl
- Upload date:
- Size: 63.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c742a3650e9ca7fa2b50fb9484b6c2051d73a28c95588eb6e94e2748094f8071
|
|
| MD5 |
561d570458ff5b80240981f40ab8453e
|
|
| BLAKE2b-256 |
8d04ee0b7c2b35795b8ad979abdb3f00eb6d115b650389cae88835f0bce6a0c0
|
Provenance
The following attestation bundles were made for jiti-1.3.0-py3-none-any.whl:
Publisher:
release.yml on RyanSaxe/jiti
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
jiti-1.3.0-py3-none-any.whl -
Subject digest:
c742a3650e9ca7fa2b50fb9484b6c2051d73a28c95588eb6e94e2748094f8071 - Sigstore transparency entry: 1749001881
- Sigstore integration time:
-
Permalink:
RyanSaxe/jiti@f630cb61e2599434067b461d1c0daa5fa383d30b -
Branch / Tag:
refs/heads/main - Owner: https://github.com/RyanSaxe
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@f630cb61e2599434067b461d1c0daa5fa383d30b -
Trigger Event:
push
-
Statement type: