- Python 96.7%
- Just 3.3%
| .github/workflows | ||
| jinja_typed_template | ||
| tests | ||
| .gitignore | ||
| justfile | ||
| LICENSE | ||
| pyproject.toml | ||
| README.md | ||
| uv.lock | ||
jinja-typed-template
Type-safe Jinja2 templates with compile-time validation — keep your Python code and Jinja templates in sync, fail fast on mismatches, and never ship a broken template again.
Installation
pip install jinja-typed-template
For Flask support:
pip install jinja-typed-template[flask]
Quick Start
1. Annotate your template
Add a header comment declaring every variable the template uses, along with its type:
{#
name: str
age: int
items: list[str]
#}
<h1>Hello {{ name }}!</h1>
<p>You are {{ age }} years old.</p>
<ul>
{% for item in items %}
<li>{{ item }}</li>
{% endfor %}
</ul>
2. Define a typed template class
Subclass TypedTemplate, set the template name, and declare fields with type hints:
from jinja_typed_template import TypedTemplate, env_context
from jinja2 import Environment, FileSystemLoader
class ProfileTemplate(TypedTemplate):
__template_name__ = "profile.html"
name: str
age: int
items: list[str]
3. Render safely
Wrap usage in env_context to bind a Jinja2 environment, then instantiate and render:
env = Environment(loader=FileSystemLoader("templates"))
with env_context(env):
t = ProfileTemplate(name="Alice", age=30, items=["apples", "bananas"])
print(t.render())
4. Flask setup (optional)
If you're using Flask, initialize the extension on your app — then TypedTemplate works in any route without manual env_context:
from flask import Flask
from jinja_typed_template.flask import TypedTemplateExtension
app = Flask(__name__)
typed_templates = TypedTemplateExtension()
typed_templates.init_app(app)
Now use TypedTemplate directly in a route:
from jinja_typed_template import TypedTemplate
class ProfileTemplate(TypedTemplate):
__template_name__ = "profile.html"
name: str
age: int
@app.route("/profile/<name>")
def profile(name: str):
t = ProfileTemplate(name=name, age=30)
return t.render()
What Gets Validated
Every TypedTemplate instance is validated at construction time (__post_init__). Three checks run automatically:
| Check | Catches |
|---|---|
| Type checking | T(name=123) when name: str — raises TypeError |
| Variable matching | Missing or extra fields vs. what the template actually uses — raises ValueError |
| Header comment consistency | Header declares name: int but class says name: str — raises TypeError |
This means you catch mismatches the moment you create the object, not when a user hits the page.
API Reference
TypedTemplate
Base class. Subclass it, set __template_name__, and declare typed fields.
| Member | Description |
|---|---|
render() |
Render the template to a string |
context_dict |
Dict of all field names → values |
copy_updating(**kwargs) |
Return a new instance with some fields replaced (instances are frozen/immutable) |
env_context(env)
Context manager that binds a Jinja2 Environment for the current thread/context. Required before creating or rendering any TypedTemplate.
from jinja_typed_template import env_context
with env_context(env):
t = MyTemplate(...)
TypedTemplateExtension (Flask)
See the Flask setup in Quick Start above.
Header Comment Format
Place a Jinja2 comment at the very top of the template:
{#
variable_name: TypeName
another_var: list[str]
optional_var: str = "default"
#}
variable_namemust match a field on yourTypedTemplatesubclass.TypeNameuses a short-form representation (str,int,list[str],dict[str, int], etc.) and must match the Python type hint.- Default values after
=are stripped — they're documentation only.
License
MIT