From airium¶
airium assembles HTML in Python from the other direction than a parser: you open
each element as a with a.tag(...) block on an Airium instance, call the instance for text, and let it track
indentation as you nest, then stringify it. It is a pure-Python, zero-dependency builder whose reach is generation only
– it also ships a reverse tool (python -m airium) that turns existing HTML back into airium source. It is used to
emit reports, templated pages, and snippets from Python without a template engine.
turbohtml covers the same construction ground with the terse turbohtml.build.E builder, but every call returns a
real Element in turbohtml’s C-backed tree, so the markup you generate is also a document you can
query, edit, re-serialize, or convert – not a one-way string.
turbohtml vs airium¶
Dimension |
turbohtml |
airium |
|---|---|---|
Scope |
Parse, build, query, edit, serialize, and convert one WHATWG tree |
One-way HTML generation from Python, plus an HTML-to-airium code generator |
Feature breadth |
|
Context-manager tag authoring, auto-indented pretty-print, void-tag handling, reverse codegen |
Performance |
Builds in a C arena and serializes in C (about 5x faster on the bench below) |
Pure-Python string assembly with running indentation bookkeeping |
Typing |
Ships |
Dynamic |
Dependencies |
Self-contained C extension (needs a wheel or a build) |
Pure Python, zero runtime dependencies, runs anywhere Python does |
Maintenance |
Actively developed C core with a thin Python shim |
Small, stable, narrowly-scoped project |
Feature overlap¶
The construction surface ports one-for-one:
Elements with attributes:
a.div(klass="card")becomesE.div({"class": "card"}).Nested children: airium’s
withblocks become positional children on theEcall.Text nodes:
a("text")or the_t=kwarg becomes a plain string child.data-*and boolean attributes: airium’sdata_i=(underscore to dash) and turbohtml’s{"data-i": "1"}/{"disabled": None}.Void tags close themselves in both.
Stringify:
str(a)becomesserialize().Pretty-printed output: airium indents by default; turbohtml opts in with
serialize(Html(layout=Indent(2))).
What turbohtml adds¶
The result is a real
Element, not a string, so the same call that builds the markup leaves a tree you can walk and mutate withappend,insert,remove, andextend.Query the built tree with CSS
select(),xpath(), andfind()/find_all– airium has no query surface over what it emits.Convert with
to_markdown()andto_text().Round-trip: the same tree type parses arbitrary HTML back in, so generation and parsing share one API.
The arena build and C serializer run about five times faster than airium’s Python assembly (see below).
What airium has that turbohtml does not¶
with-block authoring. airium spells nesting with context managers; turbohtml nests by passing children as arguments. No context-manager equivalent – restructure thewithbodies into nestedEcalls.HTML-to-source codegen.
python -m airium page.htmlemits airium Python that reproduces the input. turbohtml has no builder-code generator; it parses HTML into a tree, not into Python source. No equivalent.Zero-dependency, pure-Python install. airium needs no compiled extension and runs in any restricted or wheel-less environment. turbohtml ships a C extension, so it depends on a wheel or a local build.
Performance¶
build a list |
turbohtml |
|
|---|---|---|
100 rows |
117 µs |
774 µs (6.7x) |
1k rows |
1.41 ms |
7.76 ms (5.6x) |
10k rows |
14.8 ms |
77 ms (5.2x) |
E is roughly five times faster than airium, and the decisive difference is the result type: E hands back a real
Element, not a string, so the call that builds the markup also leaves a tree you can query, edit,
and re-serialize().
How to migrate¶
Swap the import and drop the Airium instance; E is a module-level singleton:
from turbohtml.build import E # was: from airium import Airium
airium tracks structure by call depth inside with blocks; turbohtml tracks it in the tree:
turbohtml |
|
|---|---|
|
|
|
|
|
|
|
a plain string child |
|
|
|
The same <ul> built both ways:
# airium
from airium import Airium
a = Airium()
with a.ul(), a.li(klass="item", **{"data-i": "1"}):
a("row")
html = str(a)
# turbohtml
from turbohtml.build import E
ul = E.ul(E.li({"class": "item", "data-i": "1"}, "row"))
html = ul.serialize()
E("tag", ...) is the call form for a tag that is not a Python identifier (a custom element, say), and a list-valued
attribute joins on a space so a class list reads naturally:
from turbohtml.build import E
print(E("my-card", {"class": ["card", "lg"]}, "hi").serialize())
<my-card class="card lg">hi</my-card>
Gotchas and pitfalls¶
Ebuilds a fragment, not a document: there is no implicit<html>/<head>/<body>wrapper and no doctype. Serialize the element you built, or append it under a parsed document when you need the full page shell. airium is the same here – you write the doctype and shell yourself.A leading mapping is always read as attributes; to start an element with literal text, pass the string first (
E.p("text", E.b("bold"))). There is no_t=/klass=kwarg convention: attributes are a mapping and use their real names ("class","data-i"), so airium’s underscore-to-dash rewriting does not apply.airium pretty-prints with newlines and indentation by default;
Eserializes compact markup. Passserialize(Html(layout=Indent(2)))for indented output, orHtml(layout=Minify())to minify.The result is an ordinary
Element, so the whole edit and query surface (append,find,select,serialize,to_markdown) is available – the builder only saves the construction boilerplate.