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 (~2x faster) |
Install / run
python examples/demo.py # guided tour
python examples/benchmark.py # interpreter vs compiled
python -m pytest -q # 251 tests
# 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 intoExprnodes, 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 nodesevaluate(ctx, env); statement nodesrender(ctx, env). A plain tree-walking interpreter — no codegen.environment.py— configuration, the template cache, and the inheritance engine. It flattens theextendschain into per-block "chains" so that overriding andsuper()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.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 | Provenance | Actions |
|---|---|---|---|---|
| templore-1.0.0.tar.gz | 41.1 kB | Without provenance | Details for templore-1.0.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| templore-1.0.0-py3-none-any.whl | Python 3 | none | any | Details for templore-1.0.0-py3-none-any.whl |
Total release size: 73.3 kB
Release files / templore-1.0.0.tar.gz
| Download URL | templore-1.0.0.tar.gz |
|---|---|
| Size | 41.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
200a5fbdca56c72db6d81b5c04f5814f5e725322ac77bc9c4795035cf2b97595
|
|
BLAKE2b-256 checksum How to use checksums |
fb08e894fa2d036def36909782c865d713de08e77b6d5332f06b6b64861623c7
|
| 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.0-py3-none-any.whl
| Download URL | templore-1.0.0-py3-none-any.whl |
|---|---|
| Size | 32.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
15b22f648ed17a6d64b2b416a69ad9d98351bddfebebf26b11cc8d2c5c5541ef
|
|
BLAKE2b-256 checksum How to use checksums |
a8d4ef932997881949765c79174d4fd33705356ad864cea9916c908f682f453a
|
| 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.0
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.