Skip to main content

templore

A from-scratch, zero-dependency Jinja2-like template engine in pure Python. Written to actually understand how a real template engine works end to end: lexing → expression parsing → AST → tree-walking rendering → inheritance.

No pip install of anything — the whole engine is standard-library only.

from templore import Environment, DictLoader

env = Environment(loader=DictLoader({"hi": "Hello {{ name|upper }}!"}))
print(env.get_template("hi").render(name="ada"))     # Hello ADA!

print(Environment().from_string("{{ 1 + 2 * 3 }}").render())  # 7

Features

Category What's supported
Output {{ expr }} with attribute (a.b), index (a[0]), slices (a[1:3], a[::-1]), calls (f(x))
Operators + - * / // % ~, == != < > <= >=, and or not, in / not in
Literals ints, floats, strings, true/false/none, lists [..], dicts {..}
Comprehensions [x*2 for x in xs if x], {k: v for k, v in pairs} (scoped, both backends)
Control flow {% if/elif/else %}, {% for %} (+ {% else %}, tuple unpacking)
Inline conditional {{ a if cond else b }} (else optional)
Loop variable loop.index/index0/first/last/revindex/length/previtem/nextitem/cycle()
Assignment {% set x = expr %}, attribute/item targets ({% set ns.x = ... %}), block set ({% set x %}...{% endset %}, optionally filtered)
Mutable state namespace(...) for values that survive across loop iterations
Side effects {% do list.append(x) %}
Scoping {% with x = a, y = b %}...{% endwith %}
Macros {% macro f(a, b=default) %}...{% endmacro %}, called {{ f(x) }} (closes over scope)
Call blocks {% call f(x) %}body{% endcall %} — macro renders the body via caller()
Imports {% import 'lib' as ns %}, {% from 'lib' import a, b as c %} (reuse macros)
Filters | pipeline with args & chaining: {{ x|round(2)|string }} — 40+ built-ins
Filter blocks {% filter upper %}...{% endfilter %}
Tests {% if x is defined %}, is not none, is even, is divisibleby(3), is sameas(y), …
Comments {# ... #}
Line statements opt-in # for x in xs / ## comment via Environment(line_statement_prefix=...)
Raw {% raw %}...{% endraw %} (emit template syntax literally)
Whitespace control {%- ... -%} / {{- ... -}}
Inheritance {% extends %}, {% block %} (multi-level), {{ super() }}
Includes {% include %} (shares the caller's context)
Autoescaping opt-in HTML escaping with |safe / |escape and Markup; {% autoescape true/false %} block toggle
Undefined lenient (renders "") or strict mode
Error locations runtime errors carry the template name + line number (both backends)
Custom delimiters swap {{ }} / {% %} / {# #} for anything via Environment(delimiters=...)
Two backends tree-walking interpreter (default) or Environment(compiled=True) codegen (~2.5x faster)

1.0.2 security fixes

  • tojson escapes HTML-sensitive characters while preserving valid JSON, so untrusted values cannot terminate a surrounding HTML script element.
  • FileSystemLoader rejects absolute paths, parent traversal and Windows path syntax, and resolves links before enforcing each configured template root.

Use forward-slash relative template names. Template source itself must still be trusted; the loader is not a sandbox for executing untrusted templates. JSON inside double-quoted HTML attributes still needs attribute escaping; prefer a script element or a single-quoted attribute for tojson output.

Install / run

python -m pip install templore
python -m pytest -q           # from the source distribution

# render a template file from the command line:
python -m templore render page.html --data ctx.json
python -m templore render page.html --var name=Ada --compiled -o out.html

API

Environment(loader=None, autoescape=False, undefined="lenient")
    .from_string(source, name=None) -> Template
    .get_template(name) -> Template          # via loader, cached

Template.render(context=None, **kwargs) -> str

Loaders: DictLoader({name: source}), FileSystemLoader(path_or_paths).

CLI

python -m templore render TEMPLATE [--data FILE.json] [--var K=V ...]
                                   [--compiled] [--autoescape] [-o OUT]

The template is loaded via a FileSystemLoader rooted at its own directory, so extends / include / import resolve relative to it. --var values are strings and override keys from --data.

How it works

The pipeline lives in templore/ as five small stages:

source ──Lexer──▶ tokens ──Parser──▶ AST ──Template.render──▶ output
                              │
                     ExpressionParser (recursive descent w/ precedence)
  • lexer.py — splits raw source on the {{ }} / {% %} / {# #} delimiters (applying - whitespace trimming), then tokenizes the expression text inside each tag.
  • expressions.py — a recursive-descent parser that turns expression tokens into Expr nodes, honoring operator precedence (or < and < not < compare < add < mul < unary < filter < postfix < primary).
  • parser.py — assembles statement nodes (for/if/set/block/…), recursing through nested bodies until it meets the matching end tag.
  • nodes.py — the AST. Expression nodes evaluate(ctx, env); statement nodes render(ctx, env). A plain tree-walking interpreter — no codegen.
  • environment.py — configuration, the template cache, and the inheritance engine. It flattens the extends chain into per-block "chains" so that overriding and super() fall out naturally.

Runtime bits (runtime.py): a scoped Context, the lenient Undefined sentinel, the Namespace for imports, and the Markup safe-string used by autoescaping. Filters and tests live in filters.py.

The compiled backend (compiler.py)

Environment(compiled=True) swaps the tree-walker for a code generator: the AST is turned into Python source for a root(ctx, env) function, exec'd once, and cached on the template. Expressions become inlined Python expressions and {% for %} / {% if %} become native loops/branches — no per-node dispatch at render time. Inspect it with template.compiled_root.__source__.

env = Environment(compiled=True)
t = env.from_string("{% for x in xs %}{{ x|upper }}{% endfor %}")
print(t.compiled_root.__source__)   # the generated Python

Templates using extends / block / macro / import / call are left to the interpreter (the compiler returns None and rendering falls back), so turning compiled=True on is always safe — output is identical either way, which the test suite verifies case-by-case.

License

MIT

Metadata

Release files for templore 1.0.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for templore 1.0.2
File Size Uploaded Provenance Actions
templore-1.0.2.tar.gz 43.6 kB Without provenance Details for templore-1.0.2.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for templore 1.0.2
File Interpreter ABI Platform Actions
templore-1.0.2-py3-none-any.whl Python 3 none any Details for templore-1.0.2-py3-none-any.whl

Total release size: 76.7 kB

Release files / templore-1.0.2.tar.gz

Download URL templore-1.0.2.tar.gz
Size 43.6 kB
Tags Source
SHA-256 checksum
How to use checksums
d91c457198b5247d78ffc050c0fb17dca201357853b69a778eabe32611b26904
BLAKE2b-256 checksum
How to use checksums
3e2f5d8b1473e007197d6a2ef46ca7fccd079bec4489a01abd4989b803373529
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.12.9

Release files / templore-1.0.2-py3-none-any.whl

Download URL templore-1.0.2-py3-none-any.whl
Size 33.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
c1c4ef6b3b9bdb3c5ce2fa30cd69e4a4f1a9d36e459f4aa072e63af08bb4276d
BLAKE2b-256 checksum
How to use checksums
606d0dc33f1b95d81eb3e05fda376dd451f7559fece3e01653af431078c8d435
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.12.9

Security for templore 1.0.2

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.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page