Skip to content

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>"})
# → "&lt;script&gt;alert(1)&lt;/script&gt;"

Plain-text templates (.txt, .csv, .md, etc.) are never auto-escaped.

To render trusted HTML inside a template, use the safe filter:

{{ trusted_html | safe }}

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
  • MiniJinjaEngine standalone: 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_templates guard rails (double-call, render-without-enable)
  • App.render and App.render_string
  • Auto-escape on/off for .html and .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:

pip install --upgrade cello-framework
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.