| name | chameleon-flask |
| description | Adds integration of the Chameleon template language to Flask and Quart. Use when writing Python code that uses the chameleon_flask package.
|
| license | MIT |
| compatibility | Requires Python >=3.10. |
chameleon-flask
Adds integration of the Chameleon template language to Flask and Quart.
Installation
pip install chameleon-flask
When to use what
| Need | Use |
|---|
| Render a Chameleon template from a Flask/Quart view | @chameleon_flask.template('home/index.pt') on the view function |
| Return a friendly 404 page from a view | chameleon_flask.not_found() |
| Build a rendered Response outside a decorated view | chameleon_flask.response(template_file, **model) |
| Get rendered HTML as a plain string | chameleon_flask.engine.render(template_file, **model) |
| Use Alpine.js/Vue shorthand attributes in templates | global_init(..., restricted_namespace=False) |
API overview
Configuration
Set up the Chameleon template engine once at app startup.
global_init: Initialize the Chameleon template engine
engine.clear
Decorating views
Render templates from Flask/Quart view functions (sync or async) and return friendly 404s.
template: Decorate a Flask or Quart view method to render a Chameleon template
not_found: Abort the current view and render a friendly 404 page
Direct rendering
Render a template to a Response or raw HTML without the decorator.
response: Render a Chameleon template directly to a flask.Response
engine.render
Exceptions
Errors raised by the engine. Importable from chameleon_flask directly or from chameleon_flask.exceptions.
FlaskChameleonException: Base exception for all chameleon-flask errors
FlaskChameleonNotFoundException: Raised by not_found() to signal that a view should render a 404 page
Gotchas
- global_init() is one-shot by default: with cache_init=True, later calls are silently ignored. Pass cache_init=False or call chameleon_flask.engine.clear() to re-initialize.
- The bare @template form resolves the template file name once, at decoration time — call global_init() before defining views that rely on it.
- Decorated views must return a dict (the template model) or a Flask/Quart Response; any other return type raises FlaskChameleonException at request time.
- Chameleon's restricted namespace (the default) rejects Alpine.js/Vue shorthand attributes like @click, :class, and x-data. Pass restricted_namespace=False to global_init() to allow them.
- The 404 path from not_found() always renders with text/html and status 404; the view's own content_type and status_code do not apply.
- The decorator keyword is content_type, not mimetype (renamed in an earlier release).
Best practices
- Call global_init(template_folder, auto_reload=dev_mode) exactly once at app startup, before any views are defined.
- Return plain dicts from views; return a Response only for redirects and other pass-through cases.
- Enable auto_reload only during development so templates stay cached in production.
- Use chameleon_flask.response() inside error handlers and other spots where the decorator doesn't fit.
End-to-end wiring
A complete, minimal app — engine init, a decorated view, and the template it renders. The @template path is always relative to the folder passed to global_init().
from pathlib import Path
import flask
import chameleon_flask
app = flask.Flask(__name__)
templates = Path(__file__).resolve().parent / 'templates'
chameleon_flask.global_init(str(templates), auto_reload=True)
@app.get('/')
@chameleon_flask.template('home/index.pt')
def index():
return {'title': 'Home', 'items': ['a', 'b', 'c']}
<!DOCTYPE html>
<html lang="en">
<body>
<h1>${title}</h1>
<ul>
<li tal:repeat="item items">${item}</li>
</ul>
</body>
</html>
Chameleon template syntax (this is TAL, not Jinja)
Chameleon templates are valid XML/HTML where directives live in tal:, metal:, and i18n: attributes. There is no {% ... %} or {{ ... }} — do not use Jinja/Django syntax. Interpolation uses ${ ... } and may contain arbitrary Python expressions.
<h1>Hello, ${user.name.title()}!</h1>
<p>You have ${len(items)} item(s).</p>
<li tal:repeat="item items">${item.name} — ${item.price}</li>
<li tal:repeat="item items" tal:attributes="class 'odd' if repeat.item.odd else 'even'">
${repeat.item.number}. ${item}
</li>
<div tal:condition="user">Welcome back, ${user.name}.</div>
<div tal:condition="not user">Please sign in.</div>
<span tal:content="message">placeholder shown only in a browser preview</span>
<span tal:replace="formatted_date">2024-01-01</span>
< =>${item.name}
Total: ${total}
Escaping is on by default (${expr} is HTML-escaped). Use structure: to emit already-safe HTML without escaping: <div tal:content="structure: raw_html"></div>.
Shared layouts with METAL macros
METAL is how Chameleon does template inheritance / partials — the equivalent of Jinja's {% extends %}/{% block %}.
<html metal:define-macro="layout">
<head><title>${title}</title></head>
<body>
<main metal:define-slot="content">default content</main>
</body>
</html>
<div metal:use-macro="load: ../shared/layout.pt">
<div metal:fill-slot="content">
<h1>${title}</h1>
</div>
</div>
Template resolution & project layout
With an explicit path (@template('catalog/item.pt')) the string is resolved relative to the global_init() folder. With the bare form (@template or @template()) the path is derived once at decoration time as {last segment of module}/{function_name}.html, falling back to .pt if the .html file does not exist on disk.
my_app/
├── app.py # global_init() here, before views are imported/defined
├── views/
│ └── home.py # def index(...) -> bare @template looks for home/index.html|.pt
├── templates/
│ ├── home/index.pt
│ ├── errors/404.pt # default target of not_found()
│ └── shared/layout.pt # METAL macros
└── static/
Flask and Quart
The same decorator API works for both frameworks and for both sync and async views — async is detected automatically, so no separate import or flag is needed. The library never imports Quart; it recognizes Quart responses through the shared werkzeug response base class.
@app.get('/')
@chameleon_flask.template('home/index.pt')
async def index():
return {'items': await load_items()}
Alpine.js / Vue shorthand in templates
restricted_namespace=True (the default) makes Chameleon reject non-TAL/METAL/i18n namespaced attributes, which includes Alpine.js/Vue shorthand like @click, :class, and x-data. Initialize with restricted_namespace=False to allow them, then use the shorthand normally in templates.
chameleon_flask.global_init(str(templates), restricted_namespace=False)
<div x-data="{ open: false }">
<button @click="open = !open">Toggle</button>
<div :class="{ hidden: !open }">Content</div>
</div>
Fetching the docs as Markdown
Every page on the documentation site has a plain-Markdown twin: swap the .html extension for .md to get token-efficient source without the site chrome. For example https://mkennedy.codes/docs/chameleon-flask/reference/template.html is also available at https://mkennedy.codes/docs/chameleon-flask/reference/template.md. Prefer the .md form when reading these docs programmatically.
Resources