Cello v1.1.0 — MiniJinja Template Engine¶
Release Date: April 11, 2026 License: MIT Python: 3.12+
Overview¶
Cello v1.1.0 introduces first-class Jinja2-compatible template rendering via the MiniJinja Rust crate — written by Armin Ronacher, the original author of Jinja2. Because rendering runs entirely in the Rust extension, there is zero Python overhead on the template render path.
Templates are an optional, opt-in feature: attach the engine to your app with a single app.enable_templates() call and render with app.render(). Nothing changes for applications that don't use templates.
This release is fully backwards-compatible — all v1.0.1 code works without modification.
What's New¶
MiniJinjaEngine — Rust-backed Jinja2 engine¶
A new MiniJinjaEngine class is exposed at the top level of cello. It can be used standalone (independent of App) or via app.enable_templates().
from cello import MiniJinjaEngine
engine = MiniJinjaEngine(template_dir="templates", auto_escape=True)
# Render from a file
html = engine.render("index.html", {"title": "Home", "items": [1, 2, 3]})
# Render an inline string
text = engine.render_string("Hello, {{ name }}!", {"name": "World"})
# Global variables (available in every template rendered by this engine)
engine.add_global("app_name", "My Site")
engine.add_globals({"version": "1.1.0", "year": 2026})
App.enable_templates() — optional middleware attachment¶
app = App()
app.enable_templates(
template_dir="templates", # default
auto_escape=True, # default — XSS-safe for .html/.htm/.xml
globals={"site_name": "My App"}, # optional
)
Returns the configured MiniJinjaEngine instance if you need direct access. Raises RuntimeError if called more than once on the same App.
App.render() and App.render_string()¶
@app.get("/page")
def page(request):
html = app.render("page.html", {"user": "Alice"})
return Response.html(html)
@app.get("/snippet")
def snippet(request):
text = app.render_string("Hi {{ name }}!", {"name": "Bob"})
return Response.text(text)
Both methods raise RuntimeError if enable_templates() has not been called.
Template Syntax¶
All standard Jinja2 syntax is supported:
| Feature | Syntax |
|---|---|
| Variable | {{ name }} |
| Attribute / index | {{ user.email }} · {{ items[0] }} |
| Filter | {{ name \| upper }} · {{ items \| length }} |
| If / elif / else | {% if condition %}…{% elif x %}…{% else %}…{% endif %} |
| For loop | {% for item in list %}…{% endfor %} |
| Loop variables | {{ loop.index }} · {{ loop.first }} · {{ loop.last }} |
| Template inheritance | {% extends "base.html" %} |
| Block | {% block name %}…{% endblock %} |
| Include | {% include "nav.html" %} |
| Macro | {% macro btn(text) %}<button>{{ text }}</button>{% endmacro %} |
| Import | {% from "macros/ui.html" import btn, card %} |
| Set variable | {% set x = 42 %} |
| Comment | {# this is ignored #} |
| Raw block | {% raw %}{{ not rendered }}{% endraw %} |
| JSON filter | {{ data \| tojson }} |
Auto-Escaping¶
When auto_escape=True (the default), variables rendered with {{ }} in .html, .htm, and .xml templates are HTML-escaped automatically:
engine.render_string("{{ content }}", {"content": "<script>alert(1)</script>"})
# → "<script>alert(1)</script>"
Plain-text templates (.txt, .csv, .md, etc.) are never auto-escaped.
To render trusted HTML inside a template, use the safe filter:
Setting auto_escape=False disables escaping entirely — use this only for plain-text engines (e.g., email text templates).
Python Value Conversion¶
Python values are converted to their Jinja2 equivalents via serde_json as an intermediary:
| Python | Jinja2 |
|---|---|
str | string |
int, float | number |
bool | boolean |
None | null / undefined |
list, tuple | sequence |
dict | object |
object with __dict__ | object (public attrs, _-prefixed excluded) |
| anything else | stringified |
New Examples¶
| File | Port | Topic |
|---|---|---|
examples/minijinja_basic.py | 8080 | Simple setup, variables, filters, loops |
examples/minijinja_advanced.py | 8081 | Template inheritance, globals, standalone engine |
examples/minijinja_blog.py | 8082 | Full blog with pagination, 404, tag filter |
examples/minijinja_forms.py | 8083 | Form validation with sticky values and error display |
examples/minijinja_macros.py | 8084 | Reusable UI component library via macros |
examples/minijinja_emails.py | 8085 | HTML + plain-text email templates, standalone engine |
Tests¶
47 new Python tests in tests/test_minijinja.py:
- Import and
__all__checks MiniJinjaEnginestandalone: create, render_string, render from file, template inheritance, includes, subdirectory templates- Filters:
upper,lower,length,tojson, if/elif/else - Globals:
add_global,add_globals, context override priority App.enable_templatesguard rails (double-call, render-without-enable)App.renderandApp.render_string- Auto-escape on/off for
.htmland.txt - Complex Python types: list-of-dicts, nested lists, tuples, large integers, zero/empty string
- Version check:
cello.__version__ == "1.1.0"
Dependencies Added¶
# Cargo.toml
minijinja = { version = "2", features = ["loader", "builtins", "json"] }
# [dev-dependencies]
tempfile = "3"
No new Python-side dependencies.
Migration from v1.0.1¶
No changes required. Templates are opt-in.
To start using templates:
import cello
assert cello.__version__ == "1.1.0"
app = App()
app.enable_templates("templates") # that's it
Full Changelog¶
See changelog.md for all changes.