From simple-html

simple-html latest releasesimple-html supported Pythonssimple-html licensesimple-html monthly downloadssimple-html total downloadssimple-html GitHub starssimple-html last commit

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; E is one builder over that tree

HTML generation and escaping only; no parser, no queryable tree

Feature breadth

Build, find/select, mutate, to_markdown, re-parse

Build and render a string; the result is inert once produced

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

E.<tag> resolves dynamically via __getattr__; the tree API is fully typed

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; E shares the parser’s escape and serialize rules

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 E calls.

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() and select().

  • 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. E has no SafeString; 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

simple-html

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:

simple-html

turbohtml

render(div({"class": "card"}, h1("Title")))

E.div({"class": "card"}, E.h1("Title")) .serialize(); same shape, serialize() replaces render

li({"class": "item", "data-i": "1"}, "text")

E.li({"class": "item", "data-i": "1"}, "text"), unchanged

SafeString("<b>hi</b>") for pre-escaped markup

build the markup as child elements instead: E.b("hi")

del_({}, "removed") (keyword-shadowing tag)

E("del", {}, "removed") via the call form

DOCTYPE_HTML5 + render(...) for a full page

turbohtml.build.document(body=[...]), which emits the doctype and shell

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

  • E builds a fragment, not a document: there is no implicit <html>/<head>/<body> wrapper and no doctype, where simple-html ships a DOCTYPE_HTML5 constant to prepend. Serialize the element you built, or reach for turbohtml.build.document() when you need the full page shell.

  • Both escape string children by default; simple-html’s SafeString escape 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 form E("del", ...) for those tags.

  • A mistyped tag on E silently builds that element rather than erroring, since E.<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.