From htbuilder¶
htbuilder assembles HTML in Python from the other direction than a parser: each
element takes its attributes as keyword arguments in one call and its children in a second (div(_class="card")(...),
with _class mangled to class and data_i to data-i), and stringifying the result renders it. Tag
callables (div, span, img, and the rest) are generated on import, and a set of helpers builds attribute
values: classes() and styles() assemble class and style strings, and CSS unit functions (px, em,
rem, percent, rgba) format lengths and colors. It ships from the author of Streamlit and is used mostly to
compose small HTML snippets inside Streamlit components. It is pure Python and renders to a string, not a tree.
turbohtml covers that ground with the terse turbohtml.build.E builder, a thin front end over
Element. Every E.<tag>(...) call builds a real turbohtml tree in the arena and serializes it in
C, so the same call that emits the markup also leaves a node you can query, edit, and re-serialize.
turbohtml vs htbuilder¶
Dimension |
turbohtml |
htbuilder |
|---|---|---|
Scope |
HTML builder ( |
HTML string builder for composing snippets, mostly inside Streamlit components |
Feature breadth |
Builds a real |
Two-call element form, keyword-argument mangling, and |
Performance |
Native C build and serialize, about 1.5x faster on the corpus below |
Pure-Python string assembly |
Typing |
Untyped |
|
Dependencies |
None (ships the C extension) |
Pure Python |
Maintenance |
Actively developed alongside the turbohtml serializer |
Maintained, small, Streamlit-scoped |
Feature overlap¶
The construction surface ports 1:1:
Build an element with attributes and children:
div(_class="card")(h1("Title"))maps toE.div({"class": "card"}, E.h1("Title")).Hyphenated and
data-attributes: htbuilder’sli(_class="item", data_i="1")("text")maps toE.li({"class": "item", "data-i": "1"}, "text").A class token list: htbuilder’s
classes("card", "lg")maps to a space-joined list value,{"class": ["card", "lg"]}.Rendering to a string: htbuilder stringifies the root; turbohtml calls
serialize().
What turbohtml adds¶
A real tree, not a string.
Ehands back anElement, soappend,find,select,serialize, andto_markdownall work on the result; htbuilder gives you a value whose only real operation isstr().Spec-correct text escaping.
EbuildsTextnodes, soE.div("<b>")serializes as<b>. htbuilder escapes nothing, sostr(div("<b>"))emits the<b>markup verbatim.Plain attribute names, no mangling to remember.
Etakes a mapping, soclassanddata-iare written as the literal dict keys rather than the_class/data_iunderscore convention.Native-C build and serialize, so the fragment is assembled in the arena and written out in C rather than joined in Python.
The rest of turbohtml: the same tree you built can be re-parsed, converted with
to_markdown(), or edited in place, none of which htbuilder attempts.
What htbuilder has that turbohtml does not¶
CSS value helpers. htbuilder’s
styles(padding=px(3), color=rgba(...))and thepx/em/rem/percentunit functions build astylestring for you.Ehas no equivalent; write thestylevalue as a plain string ({"style": "padding:3px"}).classes()/fonts()/params()builder helpers.Ecoversclasseswith a list value ({"class": ["card", "lg"]}); thefontsandparamshelpers have no direct counterpart, so build those attribute values yourself.Generated tag callables you can import by name (
from htbuilder import div, span).Enamespaces every tag under one object instead:E.div,E.span, orE("div", ...)for a non-identifier tag.
Performance¶
E assembles the fragment in turbohtml’s arena and serializes it in C; htbuilder stays in Python. The same <ul>
of rows – a class, a data attribute, and a text child apiece – built both ways:
build a list |
turbohtml |
|
|---|---|---|
100 rows |
117 µs |
223 µs (2.0x) |
1k rows |
1.41 ms |
2.32 ms (1.7x) |
10k rows |
14.8 ms |
27.6 ms (1.9x) |
E is about one and a half times faster than htbuilder, 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¶
htbuilder splits an element across two calls, attributes first and children second; turbohtml passes both to one call:
# htbuilder
from htbuilder import div, h1, li
# turbohtml
from turbohtml.build import E
turbohtml |
|
|---|---|
|
|
|
|
|
a list value that joins on a space: |
|
a plain string value: |
a non-identifier tag name |
|
|
Attributes are a leading mapping and children follow in the same call:
from turbohtml.build import E
card = E.div({"class": "card"}, E.h1("Title"), E.p("body"))
print(card.serialize())
<div class="card"><h1>Title</h1><p>body</p></div>
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.htbuilder mangles keyword arguments (
_classtoclass,data_itodata-i);Etakes a plain mapping, so write the real attribute name as the dict key – no underscore convention to remember.htbuilder renders lazily, escaping nothing:
str(div("<b>"))emits the<b>markup verbatim.Ebuilds text nodes, so the same call serializes as<b>; markup belongs in child elements, not strings.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"))).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.