PackageTrack
Sign in Get early access

turbohtml

A fast, fully typed HTML toolkit for Python, powered by a C-accelerated core.

1.6.1 tox-dev/turbohtml

What this package is like to depend on

Last release 12 days ago

15 Aug 2026

Ships on a steady schedule

a new release about every 3 weeks

Nearly every release is documented

notes for 15 of 15 stable releases

Nothing withdrawn

no release was ever pulled

2 months old

15 releases ยท first in 2026

15 releases in the last 12 months

see the full history below

Release timeline

15 releases ยท Jun 2026 to Aug 2026
2026
Less More Withdrawn

Releases

latest 15
  1. 1.6.1 15 Aug 2026
    Release notes

    What's Changed

    Full Changelog: 1.6.0...1.6.1

    Open source โ†’
    Release notes
    • Parse a document, or feed a first stream chunk, that opens with a carriage return without touching the not yet allocated input buffer; UndefinedBehaviorSanitizer reported the empty append as a zero offset applied to a null pointer. (752)

    v1.6.0 (2026-08-11)

    Open source โ†’
  2. 1.6.0 11 Aug 2026
    Release notes

    What's Changed

    Full Changelog: 1.5.1...1.6.0

    Open source โ†’
    Release notes
    • Compile reusable XSLT stylesheet state during ~turbohtml.transform.Transform construction. (713)

    • Add allow_imports and import_root controls for XSLT import filesystem access. (714)

    Open source โ†’
  3. 1.5.1 27 Jul 2026
    Release notes

    What's Changed

    Full Changelog: 1.5.0...1.5.1

    Open source โ†’
    Release notes
    • Match tag and attribute names case-sensitively on a turbohtml.parse_xml document: an uppercase name like Y reads through ~turbohtml.Element.attrs instead of raising KeyError, CSS selectors tell [Attr] from [attr] and Child from child, and pickle.loads/copy.deepcopy round-trip the tree without folding its names to the HTML parser's lowercase. (695)

    • Normalize line endings in a turbohtml.parse_xml document per XML 1.0 ยง2.11 across attribute values, CDATA sections, comments, and processing instructions: a literal CRLF folds to a single LF (one space in an attribute value) rather than two, matching the text-content path and every other conformant XML parser. (696)

    v1.5.0 (2026-07-21)

    Open source โ†’
  4. 1.5.0 21 Jul 2026
    Release notes

    What's Changed

    Full Changelog: 1.4.0...1.5.0

    Open source โ†’
    Release notes
    • Speed the C core across the read, query, validation, and extraction paths. Measured against the 1.4.0 release on a fixed workload, an id-selective XPath lookup runs about 260x faster, overlapping text search over 100x faster, Unicode normalization about 6x, microdata itemref resolution about 5x, Relax NG compilation about 4x, and CSS minification, conformance checking, attribute rewriting, date extraction, and computed-style resolution roughly 2x to 3x. Linear table scans give way to hashed or indexed lookups for tag atoms, XPath id tokens, microdata itemrefs, the Relax NG and XSD global tables, the encoding labels, and the entity names (659, 672, 671, 657, 656, 661, 660, 651, 654). The substring and literal-regex searches, the transform sort, and the CSS rule merge now bound their worst case instead of scanning to the end (665, 664, 666, 652, 662). The tokenizer name buffers, the table grid rows, and the DOM arena grow by doubling so a large document amortizes its allocation (669, 668, 667). The GB18030 decoder, the NFC normalizer, the attribute rewriter, and the IDNA mapper drop the branchy table walks that a common input never needed (674, 673, 649, 670, 650). Stylesheets cache their parse, list filtering batches, the language detector merges its trigram profiles, and date extraction trims its scan (653, 663, 648, 658).

      Serialize documents faster. The clean-run scan that hunts the next character needing an escape steps four UCS-4 code points per SIMD probe rather than two, and since the tree stores text as UCS-4 this scan runs on every serialization. The serialize benchmark improves by about 24%, and the gain carries into the strip and sanitize operations that serialize their result (676). IDNA host normalization rejects a non-composing mark pair with one comparison ahead of the composition-table search, saving the probes a run of unaccented text would otherwise spend (680, 679). (676)

    • Number a run of siblings in one pass. xsl:number computes a level's number as the count of preceding siblings that match the count criteria, which rescanned the whole run for each sibling and cost time quadratic in the run length; number_counts was 10.13% of all instructions executed by the instruction-dense transform workload, the largest single entry ahead of every CPython symbol. The engine now carries the previous answer forward, since a node's number is its previous sibling's number plus whether that sibling counted, and holds the criteria alongside it so a run numbered under different criteria does not reuse it. Callgrind measures 5.9% fewer instructions over that workload (687). (687)

    • Dispatch the html.parser adapter's tokens from C. turbohtml.migration.stdlib.HTMLParser used to loop in Python over the token stream, building a token object and reading six of its fields for every token; the tokenizer now calls the handle_* methods itself, binding them once per feed and passing the strings straight through. Under Callgrind that removes 30.8% of the instructions the whole workload executes and 46.7% of its indirect branches, which is what the interpreter spends on dispatch. The adapter now leads lxml's native target parser by 1.7 to 2.7 times where it previously trailed it, and html.parser by 7.5 to 11.2 times. (689)

    • Move the read path's per-item loops into C. turbohtml.saxparse.sax_parse built a tuple, a typed record, and an isinstance chain for every event; the tokenizer now binds the six handler methods once and drives them off the tree walk, for 43.7% fewer instructions with instruction-cache misses down 71% on the WHATWG specification. URL cleaning (turbohtml.extract.clean_url, ~turbohtml.extract.normalize_url, ~turbohtml.extract.extract_links), the ~turbohtml.extract.dates <meta> stage, turbohtml.query.escape_identifier, and turbohtml.clean.linkify's scheme classification each moved their loops to C as well, for 3.5% to 56.8% fewer instructions on those operations. Differential runs against the retired Python across 1,484,640, 809,481, 360,282, 302,762, 38,590, and 4,828 inputs found one divergence: the URL resolver matches %2e in either case as the WHATWG URL path state specifies, which no public call reaches because percent-encoding uppercases escape hex first. ~turbohtml.saxparse.iter_events keeps its typed records, since those are its public product.

      Other hot paths keep their Python entry and shed work. XSD validation resolves each schema element's qname once at compile time into a binary-searched table for 17.2% fewer instructions, built for XSD only after the eager cache slowed RELAX NG compilation by 8.15%. The link walk mints element wrappers on first use (14.8% fewer) and skips the URL join that already returns an absolute reference (67.8% fewer); the <meta> encoding prescan jumps inert bytes with memchr (8.67% fewer on ASCII); the gb18030 decoder maps astral pointers by arithmetic (13.4% fewer); and CSS property dispatch gates on the first byte (9.3% fewer on bootstrap.css). The migration and benchmark tables also stopped over-claiming: a caveat marks only the rows of the operation it describes, a shared measurement shows in both columns, XPath labels render as inline code, and the CSS-stripper note gives the measured 1-3% size difference. (692)

    Open source โ†’
  5. 1.4.0 11 Jul 2026
    Release notes

    What's Changed

    Full Changelog: 1.3.1...1.4.0

    Open source โ†’
    Release notes
    • Add capture_attributes=False for tag-and-text token streams, and reduce DOM allocation for large documents. (640)

    Open source โ†’
  6. 1.3.1 10 Jul 2026
    Release notes

    What's Changed

    • ๐Ÿ”ง chore(meta): enrich PyPI links and issue templates by @gaborbernat in #633

    Full Changelog: 1.3.0...1.3.1

    Open source โ†’
  7. 1.3.0 10 Jul 2026
    Release notes

    What's Changed

    • โœจ feat(pypy): run the C core on PyPy, publish 3.10 + 3.11 wheels by @gaborbernat in #623
    • โšก perf(encoding): inline the decoder and copy ASCII runs past it by @gaborbernat in #624
    • ๐Ÿ› fix(detect): stream the encoding detector by @gaborbernat in #628
    • ๐Ÿ› fix(encoding, tokenizer): honor a late , and report every parse error by @gaborbernat in #626
    • ๐Ÿ› fix(stream): report the parse errors a streamed document raises by @gaborbernat in #631
    • ๐Ÿ› fix(detect): restore chardetng's TLD hint by @gaborbernat in #632

    Full Changelog: 1.2.0...1.3.0

    Open source โ†’
    Release notes
    • Run the C core on PyPy 3.10 and 3.11 through cpyext, with the same conformance and byte-identical output as CPython; see /explanation/interpreters for what cpyext costs and for the three behaviors that do not carry over. (623)

    • Decode the legacy encodings 1.2x to 1.5x faster by inlining each WHATWG decoder into its specialized loop, and stop reallocating turbohtml.IncrementalParser's decode scratch on every ~turbohtml.IncrementalParser.feed. (624)

    Open source โ†’
  8. 1.2.0 09 Jul 2026
    Release notes

    What's Changed

    Full Changelog: 1.1.1...1.2.0

    Open source โ†’
    Release notes
    • turbohtml.detect.detect reports windows-1252, not ascii, for pure-ASCII input; ~turbohtml.detect.EncodingMatch gained a trailing codec field; and ~turbohtml.IncrementalParser takes WHATWG labels, so latin-1 raises where iso-8859-1 works. (622)

    Open source โ†’
  9. 1.1.1 08 Jul 2026
    Release notes

    What's Changed

    • ๐Ÿ”ง chore: correct package metadata and widen discoverability by @gaborbernat in #616

    Full Changelog: 1.1.0...1.1.1

    Open source โ†’
  10. 1.1.0 08 Jul 2026
    Release notes

    What's Changed

    • โœจ feat(bench): name why each empty benchmark cell is blank by @gaborbernat in #526
    • โœจ feat(convert): expose CSS selector specificity by @gaborbernat in #556
    • โœจ feat(sanitize): add SAFE_FOR_TEMPLATES marker stripping by @gaborbernat in #555
    • โœจ feat(clean): report what the sanitizer dropped by @gaborbernat in #557
    • โœจ feat(query): add XPath 2.0 string functions by @gaborbernat in #558
    • โœจ feat(detect): add Unicode normalization (UAX #15) by @gaborbernat in #560
    • โœจ feat(extract): parse RSS and Atom feeds by @gaborbernat in #562
    • โœจ feat(clean): per-property inline-style value allowlist by @gaborbernat in #561
    • โœจ feat(parse): add scripting flag for noscript by @gaborbernat in #559
    • โœจ feat(sanitize): rename tags during sanitize by @gaborbernat in #563
    • โœจ feat(serialize): add XML/XHTML output syntax by @gaborbernat in #564
    • โœจ feat(parser): per-attribute and tag source locations by @gaborbernat in #566
    • โœจ feat(dom): add TreeWalker and NodeIterator by @gaborbernat in #567
    • โœจ feat(sanitize): add DOM-clobbering isolation by @gaborbernat in #568
    • โœจ feat(parser): add strict XML parsing mode by @gaborbernat in #569
    • โœจ feat(dom): add DOM Range and StaticRange by @gaborbernat in #570
    • โœจ feat(cssom): add CSSOM cascade and computed style by @gaborbernat in #571
    • โœจ feat(sanitize): custom elements + content profiles by @gaborbernat in #572
    • โœจ feat(serialize): add XML canonicalization (c14n) by @gaborbernat in #574
    • โœจ feat(dom): add Shadow DOM tree model by @gaborbernat in #573
    • โœจ feat(parser): add DOM-less SAX event parsing by @gaborbernat in #575
    • โœจ feat(transform): add XSLT 1.0 processor by @gaborbernat in #576
    • โœจ feat(parser): declarative Shadow DOM (<template shadowrootmode>) by @gaborbernat in #577
    • โœจ feat(dom): add synchronous MutationObserver by @gaborbernat in #578
    • โœ… test(cssom): validate cascade against jsdom by @gaborbernat in #582
    • ๐Ÿ‘ท ci: add dedicated conformance job by @gaborbernat in #585
    • โœ… test: error on missing submodule instead of silent skip by @gaborbernat in #586
    • โœ… test(detect): validate normalize against NormalizationTest.txt by @gaborbernat in #579
    • ๐Ÿ”ง chore: drop stray conformance changelog by @gaborbernat in #592
    • โœ… test(dom): validate DOM APIs against jsdom by @gaborbernat in #581
    • โœ… test(xslt): validate XSLT 1.0 against libxslt REC suite by @gaborbernat in #590
    • โœจ feat(cssom): expand border and outline shorthands by @gaborbernat in #593
    • โœจ feat(validate): XSD and RELAX NG schema validation by @gaborbernat in #589
    • โœ… test(xml): validate parse_xml against the W3C XML Conformance Suite by @gaborbernat in #588
    • โœ… test(c14n): validate canonicalize against libxml2's c14n corpus by @gaborbernat in #580
    • ๐Ÿ‘ท fix(tools): route HTTP through httpx2 with tenacity retry by @gaborbernat in #596
    • โœจ feat(rewrite): DOM-less streaming HTML rewriter (lol-html style) by @gaborbernat in #591
    • โœ… test(xpath): validate fn:* functions against W3C QT3 suite by @gaborbernat in #587
    • โœ… test(clean): validate sanitizer against DOMPurify suite by @gaborbernat in #583
    • โœจ feat(xslt): complete the XSLT 1.0 feature set by @gaborbernat in #595
    • โœจ feat(xml): enforce Namespaces 1.0 well-formedness by @gaborbernat in #594
    • โœ… test(dom): validate source locations against parse5 by @gaborbernat in #584
    • ๐Ÿ“ docs(migration): order guides by downloads, add npm/crates shields by @gaborbernat in #597
    • โœจ feat(serialize): lossless byte-preserving to_source by @gaborbernat in #600
    • โœจ feat(validate): HTML5 conformance checker with a severity model by @gaborbernat in #599
    • โœจ feat(treebuild): retarget the parser at a custom tree by @gaborbernat in #601
    • โœจ feat(clean): add XML/XHTML output to the sanitizer by @gaborbernat in #598
    • ๐Ÿ“ docs(migration): slot html5validator by download rank by @gaborbernat in #602
    • โšก perf(c14n): drop redundant xlink ancestor walk by @gaborbernat in #603
    • โšก perf(cssom): binary-search the property table by @gaborbernat in #604
    • โšก perf(xslt): classify instructions once per node by @gaborbernat in #605
    • ๐Ÿ“Š feat(bench): stress the perf-optimized paths by @gaborbernat in #607
    • โ™ป๏ธ refactor(c): split serialize headers, tidy layout by @gaborbernat in #606
    • โ™ป๏ธ refactor(_c): dissolve features into clean + extract by @gaborbernat in #608
    • โ™ป๏ธ refactor(_c): consolidate CSS under css/ by @gaborbernat in #609
    • โ™ป๏ธ refactor(_c): lift JS minifier to js/ by @gaborbernat in #610
    • ๐Ÿ“ docs: re-taxonomize reference by task by @gaborbernat in #611
    • โœจ feat(bench): in-process rewrite peer with peak memory by @gaborbernat in #612
    • โ™ป๏ธ refactor: group private modules under public packages by @gaborbernat in #613
    • ๐Ÿ“ docs: generate CLI reference from the argparse parser by @gaborbernat in #614
    • ๐Ÿ“ docs: nest nav, add tutorials & workflows, generate llms.txt by @gaborbernat in #615

    Full Changelog: 1.0.0...1.1.0

    Open source โ†’
    Release notes
    • ~turbohtml.clean.Policy gained strip_template_markers: with it on, sanitizing collapses template-engine expressions ({{ }}, ${ }, <% %>) in kept text and attribute values to a single space, so the output cannot re-inject when a template engine renders it. This matches DOMPurify's SAFE_FOR_TEMPLATES. (527)

    • turbohtml.clean.sanitize_report (and turbohtml.clean.Sanitizer.sanitize_report) sanitize a fragment and return what the policy dropped alongside the cleaned HTML: one turbohtml.clean.Removed record per removed element or stripped attribute, in walk order. This matches DOMPurify's DOMPurify.removed. (528)

    • turbohtml.convert.css_specificity returns the (a, b, c) specificity of each selector in a comma-separated list, per CSS Selectors Level 4 ยง17, the value cssselect exposes as Selector.specificity(). It weighs the parsed selector in one C pass, with :is()/:not()/:has() taking their most specific argument and :where() contributing zero. (529)

    • turbohtml.extract.feed normalizes an RSS 2.0, Atom 1.0, or RDF/RSS-1.0 document into one frozen, typed ~turbohtml.extract.Feed of ~turbohtml.extract.Entry records, the feedparser.parse entry point over turbohtml.Document.feed. It detects the format from the root element and maps each dialect's spelling of a field -- the entry title, link, id, updated/published, summary/content, and author -- onto one shape in a single C walk of the parsed tree, over 12x faster than feedparser on a 30-item feed. (530)

    • turbohtml.clean.Policy gains transform_tags: a map that renames elements while sanitizing, sanitize-html's transformTags. Map a source tag to a string to rename it, or to a turbohtml.clean.Transform to rename it and add attributes (sanitize-html's simpleTransform). The rename runs before the allowlist in the same C walk, so the renamed element is re-checked from scratch -- a transform decides an element's name but never its safety: mapping a tag to script still drops it, and an added attribute is scrubbed like the element's own. (531)

    • A new turbohtml.saxparse module adds a DOM-less, event-driven parse. turbohtml.saxparse.sax_parse drives a document through the WHATWG tree builder and fires a callback on a turbohtml.saxparse.SaxHandler subclass for each construct it builds -- a start tag, an end tag, a run of text, a comment, the doctype, and a <?...> processing instruction -- while turbohtml.saxparse.iter_events yields the same stream as typed records. The events reflect the fully spec-correct tree (implied html/head/body, foster parenting, the adoption agency), so unlike html.parser.HTMLParser you see the tree the parser built; no per-node Python object is created and nothing is retained after the parse, so a one-pass extraction never builds a document-sized object graph. The tokenization, tree construction, and walk all run in C. (532)

    • turbohtml.clean.Policy gains allowed_styles, a per-element, per-property value allowlist for the style attribute keyed {tag: {property: [pattern, ...]}} with "*" matching every tag. A declaration survives only when its value matches one of the property's patterns, porting sanitize-html's allowedStyles. It narrows css_properties by value and never weakens the baseline that drops expression() and disallowed-scheme url(). (533)

    • ~turbohtml.clean.Policy gained isolate_named_props: with it on, sanitizing prefixes every kept id and name value with user-content-, moving it out of the property namespace so it cannot shadow a built-in document or form property through named access (DOM clobbering, where <input name="attributes"> makes form.attributes resolve to the input and <img name="body"> hides document.body). An already-prefixed value is left alone, so re-sanitizing is a fixpoint. This matches DOMPurify's SANITIZE_NAMED_PROPS. (534)

    • ~turbohtml.Html gained xml: with xml=True, ~turbohtml.Node.serialize, ~turbohtml.Node.encode, and ~turbohtml.Node.serialize_iter emit XML/XHTML instead of HTML -- the equivalent of lxml's tostring(method="xml"). Every empty element self-closes (<br/>), foreign SVG and MathML subtrees carry their namespace declarations, and text and attribute values follow the XML escaping rules, with no HTML void-element or raw-text special casing. It composes with sort_attributes and an ~turbohtml.Indent layout. (535)

    • ~turbohtml.clean.Policy gained a predicate-based custom-element allowance and split content profiles, porting DOMPurify's CUSTOM_ELEMENT_HANDLING and USE_PROFILES. custom_element_check keeps an unlisted hyphenated custom element (my-widget, x-card) when a caller-supplied matcher admits its name, custom_attribute_check extends the same idea to that element's attributes, and allow_customized_builtins keeps an is attribute whose value names a custom element. allow_html, allow_svg, and allow_mathml gate each namespace independently, so a policy can keep SVG but drop MathML, or the reverse. All of it runs in the one C sanitize walk, and the non-configurable safety baseline -- on* handlers, javascript: URLs, unsafe tags -- still applies to whatever a matcher keeps. (536)

    • turbohtml.transform adds a full XSLT 1.0 processor, the job lxml's etree.XSLT does. turbohtml.transform.Transform compiles a stylesheet (parsed with turbohtml.parse_xml) and applies it to source documents, and turbohtml.transform.transform does both in one call. The whole transform runs in the C extension, reusing turbohtml's XPath 1.0 engine for every match pattern and select expression. It covers the entire XSLT 1.0 instruction set: templates with match/name/mode/priority, apply-templates with sort and with-param, call-template, for-each, if, choose, value-of, copy/copy-of, element/ attribute/text, variable/param, multi-level number, key with the key() function, strip-space/preserve-space, attribute-set with use-attribute-sets, namespace-alias, fallback, simplified literal-result-element stylesheets, xsl:import with import precedence (resolved against a base_url), cdata-section-elements, and the xml/html/text output methods (html auto-selected for a null-namespace html root, with <meta> injection). Validated against libxslt's XSLT 1.0 Recommendation corpus at 76 of 79 cases byte-for-byte; the three remaining need a locale-collation, DTD, or XPath-namespace-axis layer turbohtml does not carry. (537)

    • turbohtml.Node.canonicalize serializes a subtree to Canonical XML (c14n), the byte-exact form an XML signature signs. A turbohtml.Canonical config selects the algorithm: Canonical XML 1.0 or 1.1, the exclusive variant that renders only the namespaces a subtree visibly uses, the with-comments variant, and an inclusive_ns_prefixes prefix list for exclusive mode. Attributes are reordered (namespace declarations first, then by namespace URI and local name), redundant namespace declarations are dropped, empty elements are written as start-end pairs, and character references are normalized, matching lxml's tostring(method="c14n") byte-for-byte over the same infoset. (538)

    • turbohtml.validate.XMLSchema and turbohtml.validate.RelaxNG validate a document parsed with turbohtml.parse_xml against an XSD 1.0 or RELAX NG schema, mirroring lxml's etree.XMLSchema / etree.RelaxNG. A schema compiles once in the C core and each ~turbohtml.validate.XMLSchema.validate returns a ~turbohtml.validate.ValidationResult -- a valid flag plus one ~turbohtml.validate.ValidationError per violation, each with the document-order path that located it. XSD covers the element/attribute declarations, the sequence/choice/all content models with minOccurs/maxOccurs, references, complex/simple types with extension, the built-in datatypes, and the constraining facets; RELAX NG covers the full XML-syntax pattern set (including interleave) through the derivative algorithm. (539)

    • turbohtml.parse_xml parses a document under XML 1.0 well-formedness instead of the WHATWG HTML tree builder, returning the same navigable ~turbohtml.Document. Names stay case-sensitive, <x/> self-closes any element, CDATA sections and processing instructions become ~turbohtml.CData and ~turbohtml.ProcessingInstruction nodes, only the five predefined entities and numeric references resolve, and a namespace prefix must be declared with xmlns. Names follow the exact XML 1.0 NameStartChar /NameChar productions, and the Namespaces in XML 1.0 well-formedness constraints hold in full: the reserved xml and xmlns prefixes and their namespace names cannot be rebound, a prefix declaration cannot be empty, a processing-instruction target carries no colon, and no two attributes share an expanded name. There is no HTML recovery: the first well-formedness violation -- a mismatched or unclosed tag, an undeclared prefix, an undefined entity, a duplicate attribute -- raises ~turbohtml.HTMLParseError. This is the equivalent of lxml.etree.fromstring / etree.XMLParser over turbohtml's dependency-free, fully typed node API. (540)

    • Added an HTML5 authoring-conformance checker with a severity model. turbohtml.conformance.check walks a parsed document and returns a ~turbohtml.conformance.ConformanceReport -- a valid verdict plus every ~turbohtml.conformance.ConformanceMessage, each carrying a stable code, a severity ("error", "warning", or "info"), a human-readable message, and a source line and column. It flags the document-conformance requirements the parser does not raise as a ~turbohtml.ParseError: a missing img alt, obsolete presentational elements and attributes, duplicate ids, invalid or redundant ARIA roles, empty headings, a section without a heading, and a document with no title or lang. The document is valid exactly when nothing is an error, so warnings and info notes never change the verdict. The whole walk runs in the C core against the WHATWG authoring rules and WAI-ARIA 1.2, the model the Nu Html Checker (validator.nu) uses; ~turbohtml.conformance.check_html parses a markup string first. (541)

    • ~turbohtml.Node.xpath gained the string subset of XPath 2.0: ends-with, string-join(seq, sep), lower-case and upper-case (Unicode case mapping), and the regex matches(input, pattern[, flags]) and replace(input, pattern, repl[, flags]) spellings, where replace reads $1-style group references and rewrites every match. They dispatch in the compiled-C engine alongside the XPath 1.0 core and the EXSLT namespaces, so an expression ported from elementpath, lxml, or htmlquery that leans on them runs without registration. (542)

    • turbohtml.detect.normalize returns text in a Unicode normalization form (UAX #15) -- NFC, NFD, NFKC, or NFKD -- the C successor to python:unicodedata.normalize, and turbohtml.detect.is_normalized tests membership. Both run over tables generated from the interpreter's own unicodedata, so they agree with it exactly, and a quick check returns already-normalized text without allocating. (543)

    • turbohtml.rewrite.rewrite transforms HTML in a single streaming pass without building a tree, the model Cloudflare's lol-html popularized. It runs the WHATWG tokenizer over the input while tracking only the open-element stack, hands each element a CSS selector matches -- and, on request, each run of text, each comment, and the doctype -- to a Python handler that edits it in place (set or remove an attribute, insert markup before, after, or around it, replace its inner content, unwrap it, or drop it), and emits the result incrementally. Working memory stays proportional to the open-element depth, not the document size, so a multi-megabyte page rewrites in a fixed footprint, and an untouched construct is reproduced verbatim. Because the pass never looks ahead, the matchable selector subset is the one decidable from an element and its ancestors -- type, universal, id, class, and attribute selectors, the descendant and child combinators, :root, and :is()/:where()/:not() over that subset; a sibling combinator, a positional or structural pseudo-class, or :has() raises ~turbohtml.SelectorSyntaxError. (544)

    • A new turbohtml.treebuild module retargets the parser at a tree of your own. turbohtml.treebuild.parse_into runs the WHATWG tree builder and drives a builder object -- a create_* method per node kind plus an append that links a child under its parent -- to construct the tree directly, returning whatever the builder made its document root. No navigable turbohtml.Node is materialized and the tree is walked only once, so an index, a diff tree, or another library's nodes is populated straight from the parse rather than by a second descent. Each element carries its namespace URI and its attributes as (name, value) pairs, a <template>'s content is appended under the template handle, and a bogus <?...> construct reaches a distinct create_pi. This is Rust html5ever's TreeSink and Node parse5's TreeAdapter in turbohtml shape; the tree construction, the walk, and the string extraction all run in C. (545)

    • turbohtml.cssom runs the CSS Object Model cascade: ~turbohtml.cssom.computed_style resolves the getComputedStyle of an element by collecting every <style> sheet plus the inline style along its ancestor chain, matching the native selector engine, ordering the declarations by origin importance, the style attribute, specificity, and source order, then applying inheritance, shorthand expansion, and each property's initial value -- all in the C core under the per-tree critical section. Alongside it, ~turbohtml.cssom.StyleSheet, ~turbohtml.cssom.RuleList, ~turbohtml.cssom.StyleRule, and ~turbohtml.cssom.StyleDeclaration are the read-only, turbohtml-native spelling of the CSSOM CSSStyleSheet / CSSRuleList / CSSStyleRule / CSSStyleDeclaration interfaces. The returned value is the computed value, not the used value: turbohtml runs no layout, so lengths and percentages come back as written, the same boundary jsdom and cssstyle draw. Shorthand expansion covers the distributive families (margin, padding, border-width/style/color, overflow) and the <line-width> || <line-style> || <color> shorthands border, each border-<side>, and outline, whose components resolve in any order and reset every longhand they cover. (546)

    • turbohtml.Node.to_source losslessly serializes a tree back to HTML, re-emitting the verbatim source bytes of every element and text run a parse left untouched and reserializing only the parts a mutation changed. Parse with source_locations=True and an unedited round trip reproduces the input byte for byte -- author quoting, tag-name case, character-reference spelling, and insignificant whitespace intact -- for markup that parsed without implied elements or content reordering; after an edit only the changed node's markup is rewritten while every untouched sibling and subtree keeps its original span. It is the tree-based counterpart to the streaming turbohtml.rewrite.rewrite, the model Cloudflare's lol-html popularized. (547)

    • turbohtml.parse, turbohtml.parse_fragment, and ~turbohtml.IncrementalParser gained a source_locations flag (default False) that records the granular source spans parse5 exposes as sourceCodeLocationInfo. With it on, each element's ~turbohtml.Node.source_location returns a ~turbohtml.SourceLocation giving the ~turbohtml.SourceSpan of its start tag, its end tag (None when the source never closed it), and each attribute's whole name="value", every span carrying start/end line, column, and code-point offset so source[start_offset:end_offset] slices the construct out. The tokenizer stamps the spans in C as it runs and the tree builder hangs the record off each element, so the feature is zero-overhead when off; it implies positions when on, keeping ~turbohtml.Node.source_line and ~turbohtml.Node.position available beside the spans. (548)

    • Added declarative Shadow DOM to the parser. When the tree builder meets a <template> carrying a shadowrootmode of open or closed on a valid shadow host, it attaches a shadow root to the template's parent and parses the template's content into it, reusing the Shadow DOM tree model -- the template element never joins the light tree. shadowrootdelegatesfocus and shadowrootclonable set the matching flags, readable as the new ~turbohtml.ShadowRoot.delegates_focus and ~turbohtml.ShadowRoot.clonable properties. Following the WHATWG per-document flag, turbohtml.parse honors declarative shadow roots by default (a browser navigation) while turbohtml.parse_fragment does not (an innerHTML assignment); the new allow_declarative_shadow_roots argument flips either default, matching setHTMLUnsafe when turned on for a fragment. (549)

    • Added the DOM Living Standard traversal objects turbohtml.TreeWalker and turbohtml.NodeIterator, with the turbohtml.NodeFilter constants for the what_to_show bitmask and the filter verdicts. A TreeWalker is a movable cursor over a subtree -- parent_node, first_child, last_child, next_sibling, previous_sibling, next_node, previous_node -- while a NodeIterator is the flat, filtered forward/backward view and iterates directly in a for loop. Both take a what_to_show node-type mask and an optional filter callback returning FILTER_ACCEPT, FILTER_REJECT, or FILTER_SKIP; reject drops a node and its whole subtree while skip drops only the node, so a TreeWalker prunes where a NodeIterator (having no subtree) treats the two alike. The state machine and the what_to_show test run in the C core; the filter is the one callback into Python. This ports traversal code written against the browser DOM or jsdom. (550)

    • turbohtml.parse and turbohtml.parse_fragment gained a scripting flag (default False). With it on, turbohtml sets the WHATWG scripting flag: <noscript> becomes a raw-text element, so its content is one text run rather than parsed markup and serializes back unescaped, reproducing the tree a scripting browser builds. The flag is a property of the parsed tree, so the serializer and inner_html stay consistent with how it was parsed. parse5 and html5ever default this on for browser fidelity; turbohtml keeps it off so <noscript> fallback content stays navigable. (551)

    • Added the DOM Living Standard ~turbohtml.Range and ~turbohtml.StaticRange types. A Range holds two boundary points -- each a (container, offset) pair -- and carries the full boundary API (~turbohtml.Range.set_start/~turbohtml.Range.set_end and their _before/_after variants, ~turbohtml.Range.select_node, ~turbohtml.Range.select_node_contents, ~turbohtml.Range.collapse), the derived ~turbohtml.Range.collapsed and ~turbohtml.Range.common_ancestor_container properties, the comparisons (~turbohtml.Range.compare_boundary_points, ~turbohtml.Range.compare_point, ~turbohtml.Range.is_point_in_range, ~turbohtml.Range.intersects_node), and the content operations (~turbohtml.Range.clone_contents, ~turbohtml.Range.extract_contents, ~turbohtml.Range.delete_contents, ~turbohtml.Range.insert_node, ~turbohtml.Range.surround_contents, ~turbohtml.Range.clone_range), each following the WHATWG boundary-point ordering and extract/clone/delete algorithms in C under the per-tree critical section. StaticRange is the immutable four-value snapshot. Offsets index code points in character data and children elsewhere, so a Python string's own indexing lines up with a text-node offset. (552)

    • Added the DOM Living Standard Shadow DOM tree model. ~turbohtml.Element.attach_shadow attaches an open or closed shadow tree and returns a ~turbohtml.ShadowRoot -- a document-fragment-like root held off the light tree, so it never appears among the host's children or in its serialization -- reachable through ~turbohtml.Element.shadow_root (None for a closed root) and carrying ~turbohtml.ShadowRoot.mode, ~turbohtml.ShadowRoot.host, ~turbohtml.ShadowRoot.set_inner_html, and ~turbohtml.ShadowRoot.append. <slot> elements assign the host's children by name (the unnamed default slot takes the rest): ~turbohtml.Element.assigned_nodes and ~turbohtml.Element.assigned_elements read what a slot received, with a flatten option that falls back to a slot's own children and expands nested shadow slots, and ~turbohtml.Node.assigned_slot gives the slot a child landed in. ~turbohtml.Node.flattened_children returns the composed tree with every slot replaced by its assigned nodes. The assignment and flattening algorithms run in C under the per-tree critical section and are computed on demand, so they always reflect the current tree. (553)

    • Added ~turbohtml.MutationObserver, a synchronous take on the DOM MutationObserver for recording tree edits. Register a node with ~turbohtml.MutationObserver.observe and the DOM options (child_list, attributes, character_data, subtree, attribute_old_value, character_data_old_value, attribute_filter); every change made through the mutation API queues a ~turbohtml.MutationRecord carrying the added and removed nodes, the surrounding siblings, and the attribute name and old value when asked, following the WHATWG "queue a mutation record" algorithm in C under the per-tree critical section. Because turbohtml has no event loop, delivery is synchronous rather than microtask-scheduled: ~turbohtml.MutationObserver.take_records returns and clears the queued batch, and ~turbohtml.MutationObserver.deliver drains it and calls the observer's callback. ~turbohtml.MutationObserver.disconnect stops observing and discards pending records. (554)

    • ~turbohtml.clean.Policy gained xml: with it on, the sanitizer serializes the cleaned tree as well-formed XML/XHTML instead of HTML. Every kept empty element self-closes (<br/>), foreign SVG and MathML subtrees declare their namespace, text and attribute values follow the XML escaping rules, and a kept comment, a control character outside XML's Char production, or an attribute name XML cannot hold is neutralized, so the output always reparses through turbohtml.parse_xml. The walk and the safety baseline are unchanged, so an XML-mode policy is exactly as safe as its HTML-mode twin. This clones DOMPurify's PARSER_MEDIA_TYPE: 'application/xhtml+xml' and replaces the brittle .replace("<br>", "<br/>") a bleach-based cleaner needs to feed a strict XHTML consumer such as Reportlab's RML. turbohtml.Node.inner_xml exposes the same children-only XML serialization for any node. (565)

    • The rewrite benchmark now runs against a fair in-process peer. lxml and BeautifulSoup do the same edits -- rel=nofollow on every link, loading=lazy on every image, every comment dropped -- through the parse, mutate, and serialize round trip that turbohtml.rewrite.rewrite skips, and the table reports each party's peak resident memory beside throughput, so the tree the streaming rewriter never builds shows up as memory it never holds. The lol-html migration guide carries the numbers. (612)

    v1.0.0 (2026-07-05)

    The 1.0 release finishes the native-C port, settles one canonical public API, and closes the feature gap against the libraries turbohtml replaces. The notes below fold the whole 0.4.0 to 1.0.0 span into one overview; the anchor issues point at the epics behind each theme.

    Open source โ†’
  11. 1.0.0 06 Jul 2026
    Release notes
    • Give the public surface one name per concept. CSS matching folds from turbohtml.match into turbohtml.query; the sanitizer, linkifier, and every minifier gather under turbohtml.clean; a malformed selector raises one turbohtml.SelectorSyntaxError from every parse path; the two Detector classes split into turbohtml.detect.EncodingDetector and turbohtml.clean.LinkDetector; and each surface with more than six arguments takes one frozen options config. (478)

    • Select a serialization mode with a single layout argument in place of indent, so serialize(indent=2) becomes serialize(layout=Indent(2)) and ~turbohtml.Minify selects minified output. (171)

    • Report a valueless attribute (<x a>) as the empty string rather than None in turbohtml.Element.attrs, matching the WHATWG tokenizer and the DOM. (87)

    Open source โ†’
  12. 0.4.0 16 Jun 2026
    Release notes
    • Build and edit the tree, not just read it: construct ~turbohtml.Element, ~turbohtml.Text, and ~turbohtml.Comment nodes and rearrange them with the full set of insert, wrap, extract, and normalize methods, with ~turbohtml.Element.attrs and .text/.data as live setters. copy, deepcopy, and pickle duplicate a subtree - by gaborbernat. (19)

    • Round out the node model: ~turbohtml.ProcessingInstruction and ~turbohtml.CData join the hierarchy, ~turbohtml.Doctype exposes its ~turbohtml.Doctype.public_id and ~turbohtml.Doctype.system_id, and every node type supports structural pattern matching - by gaborbernat. (22)

    Open source โ†’
  13. 0.3.0 16 Jun 2026
    Release notes
    • Query any node with CSS through ~turbohtml.Node.select and ~turbohtml.Node.select_one, a native matcher covering type, universal, #id, .class, and attribute selectors (all operators plus the case-sensitivity flag) across the descendant, child, adjacent, and sibling combinators, returning comma groups in document order. An invalid selector raises ValueError - by gaborbernat. (14)

    • Search with a richer ~turbohtml.Node.find and ~turbohtml.Node.find_all filter grammar: match the tag and attributes by string, regex, bool, callable, or list (including class_ and the attrs mapping), and choose the search direction with the axis keyword. find_all takes a limit and returns a list - by gaborbernat. (15)

    • Test a node against a selector with ~turbohtml.Node.matches and ~turbohtml.Node.closest: matches() reports whether the node satisfies a CSS selector in context, and closest() returns the nearest matching ancestor (or the node itself), or None - by gaborbernat. (16)

    • Walk the tree by axis with new iterators: ~turbohtml.Node.next_siblings, ~turbohtml.Node.previous_siblings, document-order ~turbohtml.Node.following and ~turbohtml.Node.preceding, plus the ~turbohtml.Node.strings and ~turbohtml.Node.stripped_strings text iterators - by gaborbernat. (17)

    • Read HTML token-list attributes (class, rel, headers, sizes, sandbox, and the rest) as a list[str] in turbohtml.Element.attrs, split on ASCII whitespace; other attributes stay strings and valueless ones stay None - by gaborbernat. (18)

    • Control serialization on any node: ~turbohtml.Node.inner_html returns the children, while ~turbohtml.Node.serialize and ~turbohtml.Node.encode take a formatter (the ~turbohtml.Formatter enum picks the escape policy) and an indent for pretty output. The default stays WHATWG-conformant HTML - by gaborbernat. (20)

    • Parse bytes directly: turbohtml.parse sniffs the encoding with the WHATWG algorithm (BOM, encoding argument, <meta> charset, then windows-1252), decodes with U+FFFD replacement, and reports the result in ~turbohtml.Document.encoding - by gaborbernat. (21)

    v0.2.0 (2026-06-11)

    Open source โ†’
  14. 0.2.0 11 Jun 2026
    Release notes
    • Tokenize HTML directly with a WHATWG-conformant tokenizer: turbohtml.tokenize for whole strings, the streaming turbohtml.Tokenizer, and the turbohtml.Token / turbohtml.TokenType types, validated against the html5lib-tests tokenizer conformance suite. (6)

    • Run turbohtml.escape and turbohtml.unescape faster: vectorized scanning and bulk copying speed up both calls, with unescaping of real escaped HTML about three times faster than the general lookup path. The benchmark now uses pyperf over multi-MiB real documents - by gaborbernat. (7)

    v0.1.1 (2026-06-09)

    Open source โ†’
  15. 0.1.1 09 Jun 2026
    Release notes
    • Install reliably from PyPI again: publishing each wheel in its own job keeps PEP 740 attestations within the Sigstore identity's lifetime, fixing the sigstore.oidc.ExpiredIdentity failure that blocked the first upload - by gaborbernat. (4)

    v0.1.0 (2026-06-09)

    Open source โ†’

Every package, every release, already written down.

The archive is open and free. Watching your own project is what we are building next.

Browse the archive