From simple-html¶
simple-html is a pure-Python library for generating HTML from the
inside out: each element is a tag object you call with a leading attribute mapping and then its children
(div({"class": "card"}, h1("Title"))), and render walks the resulting tuples into a string. It carries no
dependencies, is fully type-checked, and is scoped deliberately narrow – it builds and escapes markup and nothing else,
which makes it a common choice for server-rendered views and email templates where a full DOM would be overkill.
turbohtml covers the same construction ground with the terse turbohtml.build.E builder, which keeps
simple-html’s call shape (attributes first, children after) but hands back a real Element tree
instead of a string. That one difference in result type is the whole reason to move: the same call that builds your
markup also leaves something you can query, edit, and re-serialize by exactly the rules that parse it back.
turbohtml vs simple-html¶
Dimension |
turbohtml |
simple-html |
|---|---|---|
Scope |
Full WHATWG parse, query, edit, and serialize; |
HTML generation and escaping only; no parser, no queryable tree |
Feature breadth |
Build, |
Build and |
Performance |
Assembles in a C arena, serializes in C; ~3x slower than simple-html on raw string generation |
Concatenates tuples straight into a string, the faster path for one-shot rendering |
Typing |
|
Every tag is an explicitly typed symbol, so a mistyped tag name fails at type-check time |
Dependencies |
Requires the compiled C extension |
Zero dependencies, pure Python |
Maintenance |
Active; |
Small, stable, single-purpose |
Feature overlap¶
The construction surface ports 1:1 – both put attributes first and children after:
div({"class": "card"}, h1("Title"))->E.div({"class": "card"}, E.h1("Title")).A string child becomes escaped text in both.
A leading mapping sets attributes; everything after is a child in order.
Nested calls compose the same way, so an existing simple-html view tree maps node for node onto
Ecalls.
What turbohtml adds¶
E returns a real Element, so the entire tree surface is available on what you just built:
Query the fragment you constructed with
find()andselect().Mutate it with
append(),extend(), and the rest of the edit API.Convert it with
to_markdown().Serialize by the same C rules that parse HTML back, so round-tripping generated markup is exact.
A list-valued attribute joins on a space, so
{"class": ["card", "lg"]}reads as a token list naturally.
What simple-html has that turbohtml does not¶
Per-tag static typing. simple-html exposes each tag as its own typed symbol, so a typo like
dvi(...)is a type-check error.E.dvi(...)resolves through__getattr__and would build a literal<dvi>element; the builder cannot catch an unknown tag name statically. No equivalent – rely on runtime output or tests.A pre-escaped-markup escape hatch. simple-html’s
SafeString("<b>hi</b>")injects trusted raw markup.Ehas noSafeString; express the markup as child elements (E.b("hi")) or parse the string into nodes first.Zero dependencies / pure Python. simple-html installs with no build step. turbohtml requires its compiled C extension, which matters on locked-down or exotic targets.
Performance¶
E assembles the fragment in turbohtml’s arena and serializes it in C; simple-html 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 |
44.6 µs (0.4x) |
1k rows |
1.41 ms |
435 µs (0.4x) |
10k rows |
14.8 ms |
4.89 ms (0.4x) |
simple-html renders about three times faster than E on this microbenchmark – it concatenates tuples straight into a
string – but 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 the render step. simple-html and turbohtml share the call shape, so most calls port unchanged:
# before
from simple_html import div, h1, render
# after
from turbohtml.build import E
The API mapping:
turbohtml |
|
|---|---|
|
|
|
|
|
build the markup as child elements instead: |
|
|
|
|
Before and after:
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>
Where simple-html prepends DOCTYPE_HTML5 to a rendered page, turbohtml.build.document() emits the doctype and
the <html>/<head>/<body> shell around the content you pass, returning a Document:
from turbohtml.build import E, document
page = document(title="Report", lang="en", body=[E.h1("Sales"), E.p("Up 4%")])
print(page.serialize())
<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><title>Report</title></head><body><h1>Sales</h1><p>Up 4%</p></body></html>
Gotchas and pitfalls¶
Ebuilds a fragment, not a document: there is no implicit<html>/<head>/<body>wrapper and no doctype, where simple-html ships aDOCTYPE_HTML5constant to prepend. Serialize the element you built, or reach forturbohtml.build.document()when you need the full page shell.Both escape string children by default; simple-html’s
SafeStringescape hatch has no equivalent, so express raw markup as child elements (or parse it into nodes first).simple-html suffixes tags that shadow keywords (
del_);E.del_would build a literal<del_>, so use the call formE("del", ...)for those tags.A mistyped tag on
Esilently builds that element rather than erroring, sinceE.<tag>resolves dynamically; simple-html would flag the same typo at type-check time. Cover generated markup with a test.A mapping argument is always read as attributes and must come first; passing one after a child raises
TypeError.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.