Skip to main content

Schema-validated Jinja2 templates — typed contracts that catch bad data before render.

Project description

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

License

Apache License 2.0

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

templane_python-0.1.0.tar.gz (36.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

templane_python-0.1.0-py3-none-any.whl (18.2 kB view details)

Uploaded Python 3

File details

Details for the file templane_python-0.1.0.tar.gz.

File metadata

  • Download URL: templane_python-0.1.0.tar.gz
  • Upload date:
  • Size: 36.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for templane_python-0.1.0.tar.gz
Algorithm Hash digest
SHA256 6e446ea6ae30d2b77d58fe0a2a5053a768907dd57d20515f3209896b7555c092
MD5 901260cbc862adcb43879acfbc2af42c
BLAKE2b-256 346fe5c1047ba11dc578465296a7ca9e8f9e3fab77098bbb348203fa812bf5a4

See more details on using hashes here.

Provenance

The following attestation bundles were made for templane_python-0.1.0.tar.gz:

Publisher: release-templane-python.yml on ereshzealous/Templane

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file templane_python-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: templane_python-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 18.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for templane_python-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 2c7c1c9f3a9f1ae9114b0f00c0394107df8ed18f61c056146c6b5797f59525a5
MD5 9c190cc7322055300b079ad483eed8d2
BLAKE2b-256 30df4a12bbae998b11cab7c847862417b0eefc31f47813581bfb1b7330df36e8

See more details on using hashes here.

Provenance

The following attestation bundles were made for templane_python-0.1.0-py3-none-any.whl:

Publisher: release-templane-python.yml on ereshzealous/Templane

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page