From hyperpython¶
hyperpython assembles HTML in Python from the other direction than a
parser: each element takes keyword attributes in a call and children in a [...] subscript
(div(class_="card")[h1("Title")]), and stringifying the root renders the tree. It also carries the h("tag", attrs,
children) call form for dynamic tags, escapes string children through markupsafe, and offers Blob/safe
escape hatches for raw markup. It was built to generate fragments inside Django and other Python web stacks without a
template language.
The library is unmaintained and no longer imports on Python 3.11 or newer: its sidekick dependency crashes at import
time unless pinned to sidekick<0.7, and it pins markupsafe<2. Porting is also the unblock for current
interpreters and dependency stacks. turbohtml covers the same construction ground with the terse
turbohtml.build.E builder, but the value it hands back is a real Element in a parsed tree
rather than a render object, so the whole query, edit, and serialize surface stays available on what you just built.
turbohtml vs hyperpython¶
Dimension |
turbohtml |
hyperpython |
|---|---|---|
Scope |
Full WHATWG parser, serializer, builder, selectors, sanitizer, converters |
HTML generation only, from Python |
Feature breadth |
|
Subscript nesting, |
Performance |
Builds and serializes in C |
Pure Python |
Typing |
Fully annotated, |
Untyped |
Dependencies |
None (self-contained C extension) |
|
Maintenance |
Active |
Unmaintained; does not import on Python 3.11+ |
Feature overlap¶
The shared surface ports one-to-one:
div(class_="card")[h1("Title")]->E.div({"class": "card"}, E.h1("Title")); attributes are a leading mapping, children follow in the same call.li(class_="item", data_i="1")["text"]->E.li({"class": "item", "data-i": "1"}, "text"); write the real attribute name as the dict key, with noclass_alias ordata_-to-data-rewrite to remember.h("tag", {"class": "x"}, children)->E("tag", {"class": "x"}, *children), the call form for a dynamic or non-identifier tag.A string child becomes escaped text in both: hyperpython routes it through
markupsafe,Ebuilds aTextnode.A class token list joins on a space:
div(class_="card lg")->E.div({"class": ["card", "lg"]}).
What turbohtml adds¶
The result is an ordinary
Element, soselect(),xpath(),find(),to_markdown(), and the full edit API are available. hyperpython hands back a render object with no CSS or XPath query surface.The markup serializes by exactly the WHATWG rules that parse it back, so a fragment round-trips; hyperpython hand-renders its own string.
Eassembles the fragment in turbohtml’s arena and serializes it in C, about two and a half times faster than hyperpython.A
Minifylayout (whitespace collapse, optional-tag omission, JS/CSS minify) on top of the compact andIndentlayouts.Full type annotations with a
py.typedmarker; hyperpython is untyped.No pinned dependencies and it runs on current interpreters, where hyperpython needs
sidekick<0.7andmarkupsafe<2and stops importing on Python 3.11+.You can
turbohtml.parse()real markup and append anEfragment under it, mixing parsing and building in one tree.
What hyperpython has that turbohtml does not¶
Subscript children:
div(class_="card")[h1("Title")]reads the nesting in a[...]block.Ehas no subscript form. Workaround: pass children as positional arguments after the attribute mapping,E.div({"class": "card"}, E.h1("Title")).Keyword attribute syntax:
div(class_="card", data_i="1")manglesclass_toclassanddata_itodata-i.Etakes a plain mapping instead,{"class": "card", "data-i": "1"}, so there is no rename to memorize but also no keyword form.Blob/safeinject unescaped HTML into the tree.Ealways escapes a string into aTextnode. Workaround:turbohtml.parse()the raw markup and append the resulting nodes, since turbohtml is a real parser.Framework-oriented component helpers aimed at Django rendering. turbohtml is a standalone parser and builder with no framework integration layer. Workaround: serialize the
Efragment and hand the string to the framework.
Performance¶
E is about two and a half times faster than hyperpython. The same <ul> of rows – a class, a data attribute,
and a text child apiece – built both ways (hyperpython measured on its last importable dependency pin):
build a list |
turbohtml |
|
|---|---|---|
100 rows |
117 µs |
350 µs (3.0x) |
1k rows |
1.41 ms |
3.64 ms (2.6x) |
10k rows |
14.8 ms |
37.2 ms (2.6x) |
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 per-tag imports for the single E builder:
# hyperpython
from hyperpython import div, h1, p
# turbohtml
from turbohtml.build import E
turbohtml |
|
|---|---|
|
|
|
|
|
|
|
|
A subscript tree becomes one nested call, attributes leading and children following:
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.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"))).Eescapes strings into text nodes, soE.div("<b>")serializes as<b>. hyperpython’sBlob/safehad no escaping; to inject real markup,turbohtml.parse()it and append the nodes.Write attribute names as plain dict keys (
{"data-i": "1"}), not hyperpython’s keyword mangling (class_,data_i).turbohtml serializes compact by default. Pass
Html(layout=Indent())toserialize()for indented output.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.