Simple reuse of partial HTML page templates in the Chameleon template language for Python web frameworks. Use when writing Python code that uses the chameleon_partials package.
Simple reuse of partial HTML page templates in the Chameleon template language for Python web frameworks. Use when writing Python code that uses the chameleon_partials package.
license
MIT
compatibility
Requires Python >=3.9.
Chameleon Partials
Simple reuse of partial HTML page templates in the Chameleon template language for Python web frameworks.
Installation
pip install chameleon-partials
When to use what
Need
Use
Insert a reusable HTML fragment inside a Chameleon template
Make render_partial available in other frameworks (FastAPI, plain WSGI, ...)
return chameleon_partials.extend_model(model) from the view
Get a rendered fragment as a plain string from Python (tests, HTMX, email)
render_partial(template_file, **model).html_text
Point tests or a second setup call at a different template folder
register_extensions(folder, cache_init=False), or reset has_registered_extensions first
API overview
Setup
Register chameleon_partials with Chameleon. Call this once at application startup.
register_extensions: Register chameleon_partials with Chameleon so partials can be rendered
Rendering partials
Render partial templates and expose render_partial to your view models.
render_partial: Render a partial template to an HTML fragment
extend_model: Add render_partial to a view model so templates can call it
Exceptions
Errors raised by the library.
PartialsException: Raised when chameleon_partials is configured or used incorrectly
Gotchas
register_extensions() is one-shot by default: with cache_init=True, later calls after one successful registration are silent no-ops. Pass cache_init=False or reset chameleon_partials.has_registered_extensions to re-point it.
render_partial() paths are always relative to the registered template folder — never to the calling template — even when called from inside another partial.
PartialsException covers misuse only (rendering before registration, empty/non-directory folder, non-dict model). A missing template file raises Chameleon's ValueError, unwrapped.
render_partial() returns an HTML wrapper, not a str — Chameleon inserts it unescaped via html(); read .html_text for the raw markup. The HTML class is not exported in all.
The model name 'encoding' is reserved (render_partial passes encoding='utf-8' internally); 'translate', 'target_language', and 'repeat' have special meaning to Chameleon.
extend_model() unconditionally replaces any existing 'render_partial' key in the model; render_partial() itself only injects the key when it is missing.
Registration is process-wide module state and is not lock-guarded — call register_extensions() from single-threaded startup code, not from concurrent request handlers.
Best practices
Call register_extensions(template_folder, auto_reload=dev_mode) exactly once at application startup, before any template renders; leave auto_reload=False in production.
In Pyramid, wire render_partial globally with a BeforeRender subscriber; in other frameworks, return chameleon_partials.extend_model(model) from each view.
Keep partials as bare HTML fragments under shared/partials/ and prefix METAL layouts with an underscore (shared/_layout.pt).
Build the folder path with (Path(file).parent / 'templates').as_posix() so it is package-relative and uses forward slashes on every platform.
In test fixtures, reset chameleon_partials.has_registered_extensions = False in teardown so the next test can register its own template folder.
End-to-end wiring
Register the template folder once at startup, then call render_partial(...) from inside any Chameleon template. Template paths are always relative to the registered folder — even when called from inside another partial.
# At application startup, before any template rendersfrom pathlib import Path
import chameleon_partials
folder = (Path(__file__).parent / 'templates').as_posix()
chameleon_partials.register_extensions(folder, auto_reload=True) # auto_reload for dev only
<!-- templates/shared/partials/greeting.pt (a partial is a bare HTML fragment) --><divclass="greeting"><h2>Hello, ${ name }!</h2></div>
<!-- Any page template: inline the fragment, passing its model as keyword arguments -->
${ render_partial('shared/partials/greeting.pt', name='Michael') }
The template needs render_partial in its model. Pyramid apps wire it globally with a BeforeRender subscriber; other frameworks (FastAPI, plain WSGI, ...) call extend_model(model) on the view-model dict before rendering:
# Pyramid: discovered by config.scan(); every template gets render_partial automaticallyfrom pyramid.events import subscriber, BeforeRender
import chameleon_partials
@subscriber(BeforeRender)defadd_global(event):
event['render_partial'] = chameleon_partials.render_partial
# Any other framework: extend the view model per view
model = dict(name='Michael')
return chameleon_partials.extend_model(model)
Inside a partial, render_partial is injected into the model automatically, so partials can nest further partials with no extra wiring — nesting depth is unlimited.
Chameleon template syntax (this is TAL, not Jinja)
Chameleon templates are valid HTML/XML where directives live in tal: and metal: attributes. There is no {% ... %} or {{ ... }} — do not use Jinja/Django syntax. Interpolation uses ${ ... } and may contain arbitrary Python expressions.
<h1>${ video.title }</h1><divclass="views">${ "{:,}".format(video.views) } views</div><!-- Loop: render a partial per item --><divclass="video"tal:repeat="v videos">
${ render_partial('shared/partials/video_square.pt', video=v) }
</div><!-- Condition (element omitted entirely when falsy) --><imgtal:condition="show_avatar"src="${ user.avatar_url }" />
Shared layouts with METAL macros
Partials compose with Chameleon's METAL layout inheritance — pages fill a layout's slot and call partials inside it:
Note the asymmetry: metal:use-macro="load: ..." paths are relative to the current template file, while render_partial(...) paths are always relative to the registered template folder.
Project layout conventions
your_app/
├── __init__.py # register_extensions() here, at startup
├── views/
│ └── partials_middleware.py # BeforeRender subscriber (Pyramid)
└── templates/ # the registered folder
├── home/index.pt # page templates
└── shared/
├── _layout.pt # METAL layout (underscore prefix by convention)
└── partials/ # reusable fragments live here
└── video_square.pt
Registration semantics & testing
Registration is process-wide module state. With the default cache_init=True, calling register_extensions() again after a previous successful registration is a silent no-op — safe for repeated startup calls, but a test pointing at a different folder must pass cache_init=False or reset the flag. A registration that raised does not mark the module as registered (since 0.2.0), so it can simply be retried.
@pytest.fixturedefregistered_extension():
folder = (Path(__file__).parent / 'test_templates').as_posix()
chameleon_partials.register_extensions(folder, auto_reload=True)
yield
chameleon_partials.has_registered_extensions = False# reset for the next test
render_partial returns an HTML wrapper (its __html__() makes engines insert the markup unescaped); in tests or HTMX/email code, read the raw string from .html_text.