htmpl
Type-safe HTML templating for Python 3.14+ using PEP 750 template strings.
from htmpl import html
from htmpl.elements import div, h1, p, ul, li
async def greeting(name: str, items: list[str]):
return div(
h1(f"Hello, {name}!"),
t"<p>You have <strong>{len(items)}</strong> items:</p>",
ul([li(item) for item in items]),
class_="card",
)
Why htmpl?
- Type-safe — Components are functions with typed parameters. Your IDE catches errors.
- No new syntax — It's just Python. No
{% %}or{{ }}to learn. - Async-native — Coroutines resolve automatically at render time. No await spam.
- Composable — Mix element factories and t-strings freely.
- XSS-safe — All interpolations escaped by default.
- Fast — Cache components with
@cached,@cached_lru, or@cached_ttl.
Installation
pip install htmpl # Core only
pip install htmpl[all] # Everything
Requires Python 3.14+.
Quick Start
Elements
Build HTML with function calls:
from htmpl.elements import div, h1, p, a, ul, li, button
# Simple element
div("Hello") # <div>Hello</div>
# With attributes
div("Hello", class_="greeting", id="main") # <div class="greeting" id="main">Hello</div>
# Nested
div(
h1("Title"),
p("Some text"),
a("Click me", href="/page"),
)
# Lists flatten automatically
ul([li(item) for item in ["one", "two", "three"]])
T-strings
Use Python 3.14 template strings for inline HTML:
from htmpl import html
name = "World"
await html(t"<h1>Hello, {name}!</h1>")
Values are escaped automatically:
user_input = "<script>alert('xss')</script>"
await html(t"<p>{user_input}</p>")
# <p><script>alert('xss')</script></p>
Mix Both
Elements and t-strings compose freely:
def UserCard(user: User):
return article(
t"<header>{user.name}</header>",
p(user.bio),
div(
[Badge(role) for role in user.roles],
class_="badges",
),
)
Async Components
Async functions just work—coroutines resolve at render time:
async def UserProfile(user_id: int):
user = await get_user(user_id) # DB call
posts = await get_posts(user_id)
return section(
h1(user.name),
PostList(posts), # Can be sync or async
)
# No awaits needed when composing
def Dashboard(user_id: int):
return div(
UserProfile(user_id), # Coroutine, resolved at render
Sidebar(),
)
Conditional Rendering
Return None to render nothing:
def AdminBadge(user: User):
if not user.is_admin:
return None
return span("Admin", class_="badge")
# Renders badge only for admins
div(
h1(user.name),
AdminBadge(user), # None disappears cleanly
)
Caching
from htmpl import cached, cached_lru, cached_ttl
@cached # Forever
async def Footer():
return footer(t"<p>© 2025</p>")
@cached_lru(maxsize=100) # LRU eviction
async def UserBadge(role: str):
return span(role, class_="badge")
@cached_ttl(seconds=60) # Expires after 60s
async def GlobalStats():
stats = await fetch_stats()
return div(t"<strong>{stats.users}</strong> users")
FastAPI Integration
from fastapi import FastAPI
from htmpl.fastapi import Router
from htmpl.elements import section, h1, p
app = FastAPI()
router = Router()
@router.get("/")
async def home():
return section(h1("Welcome"), p("Just return Elements."))
@router.get("/user/{name}")
async def user(name: str):
return section(h1(f"Hello, {name}!"))
app.include_router(router)
The Router automatically converts Element, Fragment, SafeHTML, and Template returns to HTML responses.
HTMX Integration
from htmpl.htmx import HX, SearchInput, LazyLoad
# HX attribute builder
hx = HX(post="/api/save", target="#result", swap="innerHTML")
button("Save", **{str(k): v for k, v in hx})
# Or use built-in patterns
SearchInput("q", src="/search", target="#results", debounce=300)
LazyLoad("/api/content", placeholder=div("Loading...", aria_busy="true"))
Forms
Render Pydantic models as forms with automatic HTML5 validation:
from pydantic import BaseModel, Field, EmailStr
from htmpl.fastapi import Router
from htmpl.elements import section, h1
router = Router()
class LoginSchema(BaseModel):
email: EmailStr
password: str = Field(min_length=8)
@router.form("/login", LoginSchema, submit_text="Sign In")
async def login(data: LoginSchema):
# Only called if validation passes
user = await authenticate(data.email, data.password)
return section(h1(f"Welcome, {user.name}!"))
The @router.form decorator:
- GET /login → Renders the form
- POST /login → Validates, re-renders with errors or calls your handler
Generates proper HTML5 validation attributes:
<input type="email" name="email" required />
<input type="password" name="password" required minlength="8" />
Custom Form Layouts
Use the template parameter for full control:
from htmpl.elements import article, h2, form, div, button
def login_template(renderer, values, errors):
return article(
h2("Sign In"),
form(
renderer.inline("email", "password", values=values, errors=errors),
button("Sign In", type="submit"),
action="/login",
),
)
@router.form("/login", LoginSchema, template=login_template)
async def login(data: LoginSchema):
user = await authenticate(data.email, data.password)
return section(h1(f"Welcome, {user.name}!"))
Or use FormRenderer directly for maximum flexibility:
from htmpl.forms import FormRenderer
renderer = FormRenderer(SignupSchema)
form(
renderer.inline("first_name", "last_name", values=values),
renderer.group("Contact", "email", "phone", values=values, errors=errors),
# Manual control
div(
renderer.label_for("password"),
renderer.input("password", class_="custom"),
renderer.error_for("password", errors),
),
button("Submit", type="submit"),
action="/submit",
)
Components Library
Built-in Pico CSS components:
from htmpl import Document, Page, Nav, Card, Form, Field, Button, Alert, Modal, Table, Grid
Page(
"My App",
nav=Nav("Brand", [("Home", "/"), ("About", "/about")]),
children=section(
Card(
p("Card content"),
title="Card Title",
),
),
)
API Reference
Core
| Function | Description |
|---|---|
html(template) |
Process a t-string into SafeHTML |
raw(str) |
Mark string as safe (no escaping) |
attr(name, value) |
Build a safe HTML attribute |
SafeHTML |
Wrapper for pre-escaped content |
Elements
All standard HTML elements as functions:
from htmpl.elements import (
# Layout
div, span, section, article, header, footer, nav, main, aside,
# Text
h1, h2, h3, h4, h5, h6, p, a, strong, em, code, pre,
# Lists
ul, ol, li,
# Tables
table, thead, tbody, tr, th, td,
# Forms
form, label, input_, button, select, option, textarea,
# Media
img, video, audio,
# Other
br, hr, fragment,
)
Attributes use _ suffix for Python keywords: class_, for_, type_.
Forms
| Method | Description |
|---|---|
render() |
Full form with all fields |
render_field(name) |
Single field with label |
input(name) |
Just the input element |
label_for(name) |
Just the label |
error_for(name, errors) |
Error message if present |
fields(*names) |
Multiple fields as list |
inline(*names) |
Fields in grid row |
group(title, *names) |
Fields in fieldset |
Comparison
| Feature | htmpl | Jinja | React |
|---|---|---|---|
| Type safety | ✅ | ❌ | ✅ (TSX) |
| Python-native | ✅ | ❌ | ❌ |
| Async support | ✅ | ❌ | ❌ |
| No build step | ✅ | ✅ | ❌ |
| IDE support | ✅ | Limited | ✅ |
| Learning curve | Low | Medium | High |
License
MIT
Metadata
Release files for htmpl 0.4.3
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| htmpl-0.4.3.tar.gz | 76.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| htmpl-0.4.3-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 97.1 kB
Release files / htmpl-0.4.3.tar.gz
| Download URL | htmpl-0.4.3.tar.gz |
|---|---|
| Size | 76.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
a7819d2d929b163b406f17fc5012e6cd97f1b7200391b26e8a6c07362f5666b5
|
|
BLAKE2b-256 checksum How to use checksums |
695502cdef2693ba31e8ea228f907e181cb1946a5b7530bf6b6f508c56ed74ba
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.9.16 {"installer":{"name":"uv","version":"0.9.16","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / htmpl-0.4.3-py3-none-any.whl
| Download URL | htmpl-0.4.3-py3-none-any.whl |
|---|---|
| Size | 20.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
12e12a35bbf53807721a53fcc3b8208dc87ab807d71c75583fa681046650c7a3
|
|
BLAKE2b-256 checksum How to use checksums |
05d7bd595db5dcfc78618e39c258134f32ca5d293786c34dfe94edf3362ef9f0
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.9.16 {"installer":{"name":"uv","version":"0.9.16","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|