From fast-html¶
fast-html is a small pure-Python library that assembles HTML5 from the inside
out. Each tag is a function that takes its children first (a single node or a list) and its attributes as trailing
keyword arguments, with the usual underscore mangling (class_ to class, data_i to data-i). A tag yields
its markup lazily as a generator of string fragments, and render joins those fragments into the final string. It
escapes nothing and produces a string, not a tree, so it is a generation-only tool: composition happens in Python and
the result is markup you hand off to a response or a file.
turbohtml covers that ground with turbohtml.build.E, a terse builder over Element:
E.<tag>(attrs, *children), where a leading mapping is the attributes and each child is a node or a string that
becomes text. The difference is the result type. E hands back a real turbohtml tree, so the same call that builds
the markup also leaves an element you can query, edit, and re-serialize, and the markup it produces serializes by
exactly the rules that parse it back.
turbohtml vs fast-html¶
Dimension |
turbohtml |
fast-html |
|---|---|---|
Scope |
Full HTML parse, query, edit, and serialize engine, with the |
HTML generation only: tag functions that render to a string |
Feature breadth |
Build, parse, CSS |
Tag functions, keyword-mangled attributes, lazy fragment rendering |
Performance |
Builds in turbohtml’s arena and serializes in C, about twice as fast on the corpus below |
Pure-Python generator that yields and joins string fragments |
Typing |
Typed public API and shipped stubs ( |
Untyped |
Dependencies |
Ships the C extension, no Python dependencies |
Pure Python, zero dependencies |
Maintenance |
Actively developed alongside the turbohtml serializer |
Small, stable, single-maintainer |
Feature overlap¶
The construction path ports 1:1:
Build an element with attributes and children:
render(div([h1("Title"), p("body")], class_="card"))maps toE.div({"class": "card"}, E.h1("Title"), E.p("body")).serialize(). Children are plain arguments, with no list wrapper.Nest elements by passing built nodes as children, to any depth.
Turn a string argument into a text child:
li("text", ...)maps toE.li({...}, "text").Set a valueless boolean attribute (
disabled) by mapping its name toNone.
What turbohtml adds¶
A real tree, not a string. The result is an ordinary
Element, so the whole edit and query surface (append,extend,find,select,serialize,to_markdown) stays available; the builder only saves the construction boilerplate.Escaping.
Ebuilds text nodes, soE.div("<b>")serializes as<b>instead of emitting the markup verbatim.Round-tripping. The markup
Egenerates serializes by the same rules that parse it back, so a built fragment and a parsed one behave identically.Splicing into parsed documents. Because a built node is a real tree, you can
appendit under a document you parsed from existing HTML, not just render it in isolation.A native-C serialize path, about twice as fast as fast-html on the corpus below.
Lazy, streaming output.
serialize_iter()yields the markup in boundedstrchunks, so a very large page streams to a socket or file without ever materializing the whole string – the same shape as a fast-html tag’s fragment generator.''.join(node.serialize_iter())equalsnode.serialize().A typed surface with shipped stubs.
What fast-html has that turbohtml does not¶
A dependency-free pure-Python install. fast-html runs anywhere CPython does with no compiled extension; turbohtml ships a C extension, so it needs a wheel for the platform or a build toolchain. No equivalent if a pure-Python install is a hard requirement.
Keyword-argument attribute syntax (
class_="card"). turbohtml takes a leading mapping instead. Minor: pass a dict, writing the real attribute name as the key.
Performance¶
E assembles the fragment in turbohtml’s arena and serializes it in C; fast-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 |
291 µs (2.5x) |
1k rows |
1.41 ms |
2.93 ms (2.1x) |
10k rows |
14.8 ms |
30.2 ms (2.1x) |
E is about twice as fast as fast-html, 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 tag imports for the single builder. fast-html takes children first and attributes last; turbohtml leads with the attributes:
fast-html |
turbohtml |
|---|---|
|
|
|
|
|
|
a non-identifier tag (a custom element) |
|
a class list |
a list value that joins on a space: |
E("tag", ...) is the call form for a tag that is not a Python identifier, 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>
Before and after, reusing the same card:
# fast-html
from fast_html import div, h1, p, render
render(div([h1("Title"), p("body")], class_="card")) # a string, in Python
# turbohtml
from turbohtml.build import E
E.div({"class": "card"}, E.h1("Title"), E.p("body")).serialize() # a tree, serialized in C
Gotchas and pitfalls¶
Fragment, not document.
Ebuilds a fragment: 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.Attribute names. fast-html mangles keyword arguments (
class_toclass,data_itodata-i);Etakes a plain mapping, so write the real attribute name as the dict key, with no underscore convention to remember.Escaping changes the output. fast-html never escapes:
render(div("<b>"))emits the<b>markup verbatim.Ebuilds text nodes, so the same call serializes as<b>. Markup that used to pass through as a string must become child elements.Consumption. A fast-html tag is a generator, consumed once by the
renderthat joins it; anElementis a tree you can serialize as many times as you like.Argument order. A mapping argument to
Esets attributes and must come first; a mapping passed as a later child raisesTypeErrorrather than being rendered.