Skill v1.0.1
currentAutomated scan100/100+3 new
version: "1.0.1" name: fastapi-chameleon description: > Adds integration of the Chameleon template language to FastAPI. Use when writing Python code that uses the fastapi_chameleon package. license: MIT compatibility: Requires Python >=3.10.
FastAPI Chameleon
Adds integration of the Chameleon template language to FastAPI.
Installation
pip install fastapi-chameleon
When to use what
| Need | Use | |
|---|---|---|
| Render a Chameleon template from a FastAPI view (sync or async) | @fastapi_chameleon.template('home/index.pt') on the view function | |
| Return a friendly HTML 404 page instead of FastAPI's JSON default | fastapi_chameleon.not_found() | |
| Render any error template with any status code (optionally with data) | fastapi_chameleon.generic_error(template_file, status_code, template_data={...}) | |
| Build a rendered Response with a custom status code or mimetype | fastapi_chameleon.response(template_file, mimetype=..., status_code=..., **model) | |
| Redirect (or return JSON) from a decorated view | return a fastapi.Response subclass — the template is skipped | |
| Get rendered HTML as a plain string | fastapi_chameleon.engine.render(template_file, **model) |
API overview
Setup
Point the library at your Chameleon templates folder once at app startup.
global_init: Initialize the Chameleon template engine for your app
Rendering views
Decorate FastAPI view functions to render templates, or build a response directly.
template: Decorate a FastAPI view to render its return value through a Chameleon templateresponse: Render a template and return it as a FastAPI response directly
Error responses
Short-circuit a view to render a friendly error page with the right status code.
not_found: Short-circuit the current view and render a friendly 404 pagegeneric_error: Short-circuit the current view and render an error page with a custom status code
Exceptions
Exception types raised by the library (the error helpers raise these internally).
exceptions.FastAPIChameleonExceptionexceptions.FastAPIChameleonNotFoundExceptionexceptions.FastAPIChameleonGenericException
Gotchas
- global_init() is one-shot by default: with cache_init=True, later calls are silently ignored. Pass cache_init=False or call fastapi_chameleon.engine.clear() to re-initialize.
- The bare @template form resolves the template file name once, at decoration time — if global_init() hasn't run yet, the folder silently defaults to 'templates' relative to the CWD and a later global_init() won't re-resolve it.
- Decorated views must return a dict (the template model) or a fastapi.Response; any other return type raises FastAPIChameleonException at request time.
- The decorator keyword is mimetype (not content_type), and @template has no status_code parameter — use response() when you need a non-200 status.
- Error pages from not_found()/generic_error() always render as text/html at their own status; the decorator's mimetype does not apply.
- not_found() renders the 404 template with an empty model; only generic_error() can pass template_data into the error template.
- not_found()/generic_error() only work below a @template-decorated view — from an undecorated route or middleware the exception goes unhandled and FastAPI returns a 500.
Best practices
- Call global_init(template_folder, auto_reload=dev_mode) exactly once at app startup, before view modules are imported/registered.
- Put the route decorator outermost: @app.get(...) above @fastapi_chameleon.template(...), which sits directly on the view function.
- Return plain dicts from views; return a fastapi.Response only for redirects and other pass-through cases.
- Enable auto_reload only during development so templates stay cached in production.
- Use fastapi_chameleon.response() where the decorator doesn't fit — custom status codes, non-HTML mimetypes, or rendering outside a view.
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().
# main.pyfrom pathlib import Pathimport fastapiimport fastapi_chameleonapp = fastapi.FastAPI()# Init once at startup, BEFORE any decorated view is defined.templates = Path(__file__).resolve().parent / 'templates'fastapi_chameleon.global_init(str(templates), auto_reload=True) # auto_reload for dev only@app.get('/')@fastapi_chameleon.template('home/index.pt') # route decorator OUTERMOST, @template just above the functiondef index():return {'title': 'Home', 'items': ['a', 'b', 'c']} # dict == the template model
<!-- templates/home/index.pt --><!DOCTYPE html><html lang="en"><body><h1>${title}</h1><ul><li tal:repeat="item items">${item}</li></ul></body></html>
The decorator is signature-transparent (ParamSpec overloads + functools.wraps), so FastAPI dependency injection — Request, path/query params, Depends(...) — keeps working on decorated views, sync or async.
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.
<!-- Interpolation: any Python expression inside ${ } --><h1>Hello, ${user.name.title()}!</h1><p>You have ${len(items)} item(s).</p><!-- Loop --><li tal:repeat="item items">${item.name} — ${item.price}</li><!-- The repeat variable exposes index/number/even/odd/first/last --><li tal:repeat="item items" tal:attributes="class 'odd' if repeat.item.odd else 'even'">${repeat.item.number}. ${item}</li><!-- Condition (element is omitted entirely when falsy) --><div tal:condition="user">Welcome back, ${user.name}.</div><div tal:condition="not user">Please sign in.</div><!-- Set element content / replace whole element --><span tal:content="message">placeholder shown only in a browser preview</span><span tal:replace="formatted_date">2024-01-01</span><!-- Set attributes (semicolon-separated); escaping is automatic --><a tal:attributes="href item.url; class item.css_class">${item.name}</a><!-- Define a reusable local variable --><span tal:define="total sum(i.price for i in items)">Total: ${total}</span>
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 %}.
<!-- templates/shared/layout.pt --><html metal:define-macro="layout"><head><title>${title}</title></head><body><main metal:define-slot="content">default content</main></body></html>
<!-- templates/home/index.pt --><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/├── main.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/
Redirects and pass-through responses
Return any fastapi.Response subclass from a decorated view and the template is skipped entirely — the idiomatic shape for POST/redirect/GET flows:
@router.post('/account/login')@fastapi_chameleon.template('account/login.pt')async def login(request: fastapi.Request):user = await try_login(request)if user:return fastapi.responses.RedirectResponse('/account', status_code=302)return {'error': 'Invalid login'} # falls through: re-render the form with an error
Friendly error pages from anywhere below a view
not_found() and generic_error() are exception-based control flow: they always raise, the @template decorator catches, so they work from deep in a service or data layer — as long as a decorated view is above them on the call stack. FastAPI's default 404 is JSON; this is how you get an HTML error page instead.
@router.get('/catalog/item/{item_id}')@fastapi_chameleon.template('catalog/item.pt')async def item(item_id: int):item = service.get_item_by_id(item_id) # service may call not_found() itselfif not item:fastapi_chameleon.not_found() # renders errors/404.pt at 404, empty modelreturn item.dict()# Any status code, with data for the error template:fastapi_chameleon.generic_error('errors/500.pt', 500, template_data={'detail': 'Something went sideways.'})
not_found() renders its template with an empty model; only generic_error() can pass template_data through. Both render as text/html regardless of the decorator's mimetype.
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/fastapi-chameleon/reference/template.html is also available at https://mkennedy.codes/docs/fastapi-chameleon/reference/template.md. Prefer the .md form when reading these docs programmatically.
Resources
- Full documentation
- llms.txt — Indexed API reference for LLMs
- llms-full.txt — Comprehensive documentation for LLMs
- Source code