Troubleshooting

The things that trip up a first turbohtml session, each as a symptom and a fix. turbohtml is not a drop-in for BeautifulSoup or lxml, so a habit from one of those is the usual cause.

A node indexes its children

turbohtml models text as real child nodes, the WHATWG DOM shape, where lxml uses text/tail and BeautifulSoup uses .string. So node[i] indexes the children, and a text run is one of them. For the text, call text:

import turbohtml

para = turbohtml.parse("<p>Hello <b>world</b></p>").find("p")
print(len(para), type(para[0]).__name__)
print(para.text)
2 Text
Hello world

Attributes live in attrs

An element is not a mapping of its attributes. Reach an attribute through attrs (or attr() for one with a default), never node["href"]:

link = turbohtml.parse('<a href="/x">y</a>').find("a")
print(link.attrs["href"])
print(link.attr("title"))
/x
None

find can return None

find() and select_one() return None when nothing matches, so reaching an attribute straight off the result raises AttributeError on a page that happens to lack the element. Guard it:

doc = turbohtml.parse("<p>x</p>")
heading = doc.select_one("h1")
print(heading.text if heading is not None else "no heading")
no heading

An invalid selector raises

A malformed CSS selector raises SelectorSyntaxError, and a malformed XPath raises ValueError. Catch the specific type when the selector comes from user input:

from turbohtml import SelectorSyntaxError

try:
    doc.select("p!!!")
except SelectorSyntaxError:
    print("bad selector")
bad selector

Encoding detection is opt-in

turbohtml.parse() treats a str as already decoded and never sniffs it. When the bytes came off a socket or a file with an unknown encoding, pass the bytes so the parser runs the WHATWG sniff, or call turbohtml.detect.detect() to learn the encoding first. Decoding a stream yourself with the wrong codec before handing turbohtml a str is the usual cause of mojibake. See Handle character encodings.

Streaming rewrite cannot look ahead

turbohtml.rewrite.rewrite() runs in one forward pass and never buffers the document, so a selector that needs content it has not reached – a sibling combinator, :has(), :nth-child() – raises SelectorSyntaxError rather than silently matching nothing. When you need one of those, parse a tree with turbohtml.parse() and edit it instead. See The streaming rewrite model.

parse, parse_fragment, parse_xml differ

The three entry points build different trees. turbohtml.parse() builds a full document, inserting the implied <html>, <head>, and <body>. turbohtml.parse_fragment() parses in a fragment context, so no wrapper elements appear. turbohtml.parse_xml() applies XML rules, where tag case is significant and every element must close. Reaching for find("body") on a fragment, or feeding XML to the HTML parser, gives a tree you did not expect.

Sanitize needs a policy

turbohtml.clean.sanitize() keeps only what its Policy allows on top of a non-overridable safety baseline. An empty or too-narrow policy strips more than you meant; Policy.relaxed() is the starting point for user content. transform_tags renames a tag rather than dropping it. See Sanitize untrusted HTML.

Foreign content changes matching

Inside SVG and MathML, tag and attribute names are case-sensitive and namespaced (<clipPath>, viewBox), unlike the ASCII-folded HTML around them. A selector written in lowercase will miss a camel-cased foreign element, and XML parsed with turbohtml.parse_xml() keeps case throughout. Match foreign elements with their exact casing.

Concurrency safety is per-object

Free-threading safety holds per tree and per Tokenizer: two threads on one tree are safe, but feeding one tokenizer, or driving one incremental parse, from several threads at once needs your own synchronization. Give each thread its own tokenizer, or lock around the shared one. See Free-threading.

It is not a bs4 or lxml drop-in

There is no .string, no text/tail, and no soup(...) callable; one concept carries one name. Code written to BeautifulSoup’s or lxml’s contract needs a translation, not a rename. The Migrating to turbohtml guides map each library’s surface onto turbohtml’s.

For anything the guides do not cover, the Reference has the exact signatures, the Explanation carries the reasoning, and the issue tracker takes a report.