templane-python
Python implementation of Templane — typed template contracts for Jinja2.
Templane adds compile-time schema validation to your templates. Define what data your template expects in a small .schema.yaml file next to your .jinja, and Templane catches missing fields, typos, and wrong types before Jinja renders, not at 2am in production.
- Conformance: 40/40 fixtures across the Templane protocol · 75 unit tests
- Engine binding: Jinja2 (
jinja_templane) - Also ships: breaking-change detector, schema hash
- Runtime: Python 3.12+
- License: Apache 2.0
Install
pip install templane-python
# or with uv:
uv add templane-python
Quick start
Create email.jinja (plain Jinja2 — not modified by Templane):
Hi {{ user.name }}! Your order #{{ order_id }} total is ${{ amount }}.
Create email.schema.yaml next to it (declares the data contract):
body: ./email.jinja
engine: jinja
user:
type: object
required: true
fields:
name: { type: string, required: true }
order_id:
type: string
required: true
amount:
type: number
required: true
Use from Python:
from jinja_templane import TemplaneEnvironment, TemplaneTemplateError
env = TemplaneEnvironment("templates")
tmpl = env.get_template("email.schema.yaml")
try:
output = tmpl.render(
user={"name": "Alice"},
order_id="INV-042",
amount=99.00,
)
print(output)
# → "Hi Alice! Your order #INV-042 total is $99.0."
except TemplaneTemplateError as exc:
for err in exc.errors:
print(f"[{err.code}] {err.field}: {err.message}")
Validation errors — caught before rendering
# Missing field + wrong type both trip at once
try:
tmpl.render(
# user missing entirely
order_id=42, # wrong type
amount="free", # wrong type
)
except TemplaneTemplateError as exc:
for err in exc.errors:
print(f"[{err.code}] {err.field}")
# [missing_required_field] user
# [type_mismatch] order_id
# [type_mismatch] amount
All errors are collected — never short-circuits at the first.
Breaking-change detection
Detect schema evolution issues before they break downstream data:
from templane_core.schema_parser import parse as parse_schema
from templane_core.models import typed_schema_from_dict
from templane_core.breaking_change import detect
def load(yaml_str: str, name: str):
result = parse_schema(yaml_str, name)
return typed_schema_from_dict(result["schema"])
old = load(open("schema-v1.yaml").read(), "v1")
new = load(open("schema-v2.yaml").read(), "v2")
changes = detect(old, new)
for c in changes:
print(f"[{c.category}] {c.field_path}: {c.old} → {c.new}")
# Four categories:
# removed_field — schema had the field; now doesn't
# required_change — optional → required
# type_change — field type changed
# enum_value_removed — an enum value was removed
Safe changes (new optional field, added enum value, required → optional) are NOT reported.
API
jinja_templane — the engine binding
from jinja_templane import (
TemplaneEnvironment,
TemplaneTemplate,
TemplaneTemplateError,
)
# Like jinja2.Environment, but get_template returns a TemplaneTemplate
env = TemplaneEnvironment(search_path: str | Path)
env.get_template(name: str) -> TemplaneTemplate
# Handles both .schema.yaml (with body: reference) and legacy inline-body .templane files
tmpl.render(**data) -> str # raises TemplaneTemplateError on validation failure
tmpl.schema # the parsed TypedSchema
templane_core — the protocol primitives
from templane_core.schema_parser import parse, load_from_path
from templane_core.type_checker import check
from templane_core.ir_generator import generate
from templane_core.breaking_change import detect
from templane_core.hash import schema_hash
# parse(yaml_str, schema_id) → dict with {schema, body?, body_path?, engine?, error?}
# load_from_path(path) → same shape, with body resolved from disk when sidecar
# check(schema, data) → list[TypeCheckError]
# generate(ast, data, schema_id, template_id) → TIRResult
# detect(old_schema, new_schema) → list[BreakingChange]
# schema_hash(schema) → "sha256:..." stable across equivalent schemas
Why Templane
Templates are untyped contracts. They accept a bag of values, look up names by string, and render something — even when the data has a typo, a missing field, or a wrong type. The failure is silent: the render succeeds, the customer gets a broken email, and you find out four days later.
Templane fixes this at the boundary. A schema next to your template declares what the template expects; the binding refuses to render when the data doesn't match. See the main README for the full pitch.
Adoption pattern
You don't migrate templates. Your existing .jinja files stay as-is. You drop one .schema.yaml beside each one:
templates/
welcome.jinja ← untouched
welcome.schema.yaml ← NEW
invoice.jinja ← untouched
invoice.schema.yaml ← NEW
Your code switches from jinja2.Environment to jinja_templane.TemplaneEnvironment. That's the migration.
Examples
Six worked examples under the repo's templane-python/examples/: hello, validation errors, nested objects and lists, Jinja features (filters, if/for), breaking-change detection, and a full password-reset email demo.
Building from source
git clone https://github.com/ereshzealous/Templane.git
cd Templane/templane-python
uv sync --extra dev
.venv/bin/pytest # 75 tests
Links
- Repo: https://github.com/ereshzealous/Templane
- Full spec: SPEC.md
- Architecture & cross-language conformance: main README
- Issues: GitHub Issues
- PyPI:
templane-python
License
Apache License 2.0
Release files for templane-python 0.2.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 | |
|---|---|---|---|
| templane_python-0.2.0.tar.gz | 36.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| templane_python-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 54.1 kB
Release files / templane_python-0.2.0.tar.gz
| Download URL | templane_python-0.2.0.tar.gz |
|---|---|
| Size | 36.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
767f2f102470e9fa0bbbbff9242348562dc1f3e2f3666839af1b314cb995b92f
|
|
BLAKE2b-256 checksum How to use checksums |
decd4034e8f7ecfa3f7738c0f37fb44d1d5b552694ba3173471d99ae46aa829c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
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 Apr 27, 2026.
Transparency logRelease files / templane_python-0.2.0-py3-none-any.whl
| Download URL | templane_python-0.2.0-py3-none-any.whl |
|---|---|
| Size | 18.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
855916c682db6b7c338c76d19846858004f76a37c838b6d1d25079416a92d5cd
|
|
BLAKE2b-256 checksum How to use checksums |
d38e8089c1a730f5b0cad6cbd7ff0b14a1d5007e67686ba2066ca611bb202dd8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
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 Apr 27, 2026.
Transparency log