This page installs chameleon-rs, renders a template from a string, then sets up a folder of template files the way a web app would.
Install
uv pip install chameleon-rs
chameleon-rs supports CPython 3.12 and later, including the free-threaded build. The wheel contains the compiled engine and has no dependencies.
Render a template string
PageTemplate compiles a template from a string. Calling it renders the template, with the keyword arguments as the template’s variables:
from chameleon_rs import PageTemplate
page = PageTemplate("""
<div class="greeting">
<h1>Hello, ${name}!</h1>
<ul tal:condition="items">
<li tal:repeat="item items">${repeat.item.number}. ${item}</li>
</ul>
</div>
""")
print(page(name='World', items=['templates', 'in', 'Rust']))
<div class="greeting">
<h1>Hello, World!</h1>
<ul>
<li>1. templates</li>
<li>2. in</li>
<li>3. Rust</li>
</ul>
</div>
The template compiles once, when it’s first rendered. Every later render reuses the compiled program, so keep template objects around (module level, or in a loader) rather than building one per request.
${...} inserts a value with HTML escaping. To insert markup as-is, use structure:, or pass an object with an __html__ method:
page = PageTemplate('<p>${text}</p> <p>${structure: text}</p>')
print(page(text='<b>bold</b>'))
<p><b>bold</b></p> <p><b>bold</b></p>
Render templates from files
PageTemplateFile loads one file. PageTemplateLoader loads templates by name from one or more folders, and caches each compiled template:
Suppose your templates live in a folder like this:
templates/
├── home/
│ └── index.pt
└── shared/
└── video.pt
with home/index.pt:
<html>
<body>
<h1>${title}</h1>
<div tal:repeat="video videos">
${render_partial('shared/video.pt', video=video)}
</div>
</body>
</html>
and shared/video.pt:
<a href="/videos/${video.id}">${video.title}</a>
Configure the folder once at startup with chameleon_rs.configure(), then load templates by name:
from dataclasses import dataclass
import chameleon_rs
from chameleon_rs import PageTemplateLoader
@dataclass
class Video:
id: int
title: str
chameleon_rs.configure(template_folder=template_folder)
templates = PageTemplateLoader([template_folder])
page = templates['home/index.pt']
print(page(title='Latest videos', videos=[Video(1, 'Intro'), Video(2, 'Partials')]))
<html>
<body>
<h1>Latest videos</h1>
<div>
<a href="/videos/1">Intro</a>
</div>
<div>
<a href="/videos/2">Partials</a>
</div>
</body>
</html>
render_partial is available in every template, with nothing to register or pass in. configure() tells it where to find partials. See Partials.
In a real app, build the folder path relative to your code, so it doesn’t depend on the working directory:
from pathlib import Path
import chameleon_rs
template_folder = (Path(__file__).parent / 'templates').as_posix()
chameleon_rs.configure(template_folder=template_folder, auto_reload=dev_mode)
auto_reload=True recompiles a template when its file changes. Turn it on in development and leave it off in production.