Type-safe HTML in Python — no templates, no runtime surprises
Been building FastAPI apps for a while.
Every time the same thing: Jinja2 works until it doesn't.
Misspelled variable? Silent empty cell. Missing context key? Crash on render.
No mypy, no autocomplete, no safety net.
I wanted something different, so I built htmforge —
HTML entirely in Python, validated by Pydantic v2.
Before
{# users.html #}
{% for user in users %}
<td>{{ user.naem }}</td> {# typo — silent empty cell #}
<td>{{ user.role }}</td> {# wrong variant? find out at runtime #}
{% endfor %}
After
from htmforge.components import DataTable, ColumnDef, Badge, BadgeVariant
DataTable(
columns=[
ColumnDef(key="name", label="Name"),
ColumnDef(key="role", label="Role"),
],
dict_rows=[
{
"name": user["name"],
"role": Badge(
text=user["role"].title(),
variant=BadgeVariant.DANGER
if user["role"] == "admin"
else BadgeVariant.SUCCESS,
),
}
for user in users
],
)
Typo in "name"? Pydantic catches it on startup.
Wrong BadgeVariant? mypy catches it.
XSS? Escaped automatically — no | safe to misuse.
HTMX is typed too
from htmforge.htmx import HxSwap, HxTarget
from htmforge.elements import button
button(
"Delete",
hx_delete=f"/users/{user_id}",
hx_swap=HxSwap.OUTER_HTML,
hx_target=HxTarget.CLOSEST_TR,
)
No more "outerHTML" string guessing. Full autocomplete.
Stack
- Pydantic v2 — prop validation on construction and assignment
- MarkupSafe — XSS protection, automatic, no opt-out
- 20+ components — DataTable, Form, Modal, Toast, Tabs, Accordion...
- Framework adapters — FastAPI, Flask, Django built in
- 240 tests, mypy strict, ruff clean
Looking for contributors
The project is early but functional. Good first issues are documented
in CONTRIBUTING.md — from small things like adding a lang attribute
to Page, to rendering improvements in DataTable.
If this approach interests you:
- 👉 github.com/mondi04/htmforge
- 👉
pip install htmforge
Top comments (0)