Skip to content

HTML in Python

The ht factory builds HTML Element trees in Python. Use it for fragments, email bodies, or HTML you want to return without a template file. The package root exports ht, Element, and Document.

from starlette_templates import ht, Element, Document

Building elements

Call ht.<tag>(...) with children as positional arguments and attributes as keyword arguments:

card = ht.div(
    ht.h1("Hello World"),
    ht.p("Built in Python."),
    id="card",
    classes=["container", "content"],
    style={"color": "red"},
)
  • A positional argument becomes a child. It can be a string, a number, another element, or a callable that returns one.
  • classes= accepts a string or a list. It renders into the class attribute.
  • style= accepts a dict. It renders into an inline style string.
  • Every other keyword argument becomes an attribute. An underscore maps to a hyphen where needed, so http_equiv becomes http-equiv.

Rendering

An Element renders to a string in four equivalent ways:

card.render()            # returns Markup
card.to_string()         # returns str
str(card)                # returns str
ht.render_element(card)  # returns str; also accepts strings, numbers, None, and callables

Element implements _repr_html_, so an element also displays as rendered HTML in a Jupyter notebook.

Documents and responses

Document is an Element subclass. It renders a full HTML document, and it builds the <html>, <head>, <title>, and <body> elements for you. The <head> includes charset and viewport meta tags.

doc = Document(
    ht.h1("Welcome"),
    ht.p("This is a full page."),
    page_title="Home",
)

A Document is also an ASGI application, so you can return it directly from a Starlette route. It renders itself as an HTMLResponse with the status_code and headers you pass:

from starlette.applications import Starlette
from starlette.routing import Route

from starlette_templates import ht, Document

async def homepage(request):
    return Document(
        ht.h1("Welcome"),
        page_title="Home",
        status_code=200,
    )

app = Starlette(routes=[Route("/", homepage)])

Composing with pipe

Element.pipe() applies a function to an element and returns the result, so you can build reusable transforms:

def add_prefix_classes(element: Element, prefix: str) -> Element:
    element.add_classes(f"{prefix}-box")
    return element

ht.div("Hello world").pipe(add_prefix_classes, "card")