########################### From the standard library ########################### Python's standard library ships HTML primitives in the :mod:`python:html` package. :func:`python:html.escape` and :func:`python:html.unescape` handle entity encoding and decoding, :mod:`python:html.entities` exposes the reference tables, and :class:`python:html.parser.HTMLParser` is a SAX-style tokenizer you subclass and drive with ``handle_*`` callbacks. These are the zero-dependency, always-available building blocks that ship with CPython; countless scripts, templating helpers, and scrapers reach for them because they are already installed. The scope stops at tokenizing and entity work: ``html.parser`` does not build a document tree, does not implement WHATWG error recovery, and is explicitly documented as not fully HTML5-conformant. turbohtml covers that same ground and extends past it. :func:`turbohtml.escape` and :func:`turbohtml.unescape` match the stdlib functions byte for byte, :func:`turbohtml.tokenize` and :class:`turbohtml.Tokenizer` replace the callback tokenizer, and :class:`turbohtml.migration.stdlib.HTMLParser` keeps your existing ``handle_*`` subclass working unchanged. Everything runs over a WHATWG-conformant C core that also builds a full parse tree, which ``html.parser`` has no equivalent for. ********************* turbohtml vs stdlib ********************* .. list-table:: :header-rows: 1 :widths: 20 40 40 - - Dimension - turbohtml - stdlib (``html``) - - Scope - Escape/unescape, tokenizer, and full WHATWG tree construction - Escape/unescape, entity tables, and a tokenizer only (no tree) - - Feature breadth - Tokens, tree, selectors, serialization, plus the callback shim - ``escape``/``unescape``, ``html.entities`` tables, ``HTMLParser`` callbacks - - Performance - SIMD scanning, several times faster on escape/unescape - Pure-Python entity scan and tokenizer - - Typing - Fully type annotated across the public surface - Annotated in typeshed stubs, not conformant behavior - - Dependencies - Compiled C extension (wheels), installed from PyPI - Built into CPython, zero install - - Maintenance - Actively developed, tracks the WHATWG spec - Stable CPython module, ``html.parser`` frozen as non-conformant Feature overlap =============== The shared surface ports one-to-one: - :func:`python:html.escape` → :func:`turbohtml.escape`, same signature and output. - :func:`python:html.unescape` → :func:`turbohtml.unescape`, same signature and output. - :class:`python:html.parser.HTMLParser` subclasses → :class:`turbohtml.migration.stdlib.HTMLParser`, same ``handle_*`` callbacks and ``feed``/``close``/``reset``/``getpos`` methods. What turbohtml adds =================== - WHATWG-conformant tokenizing and tree construction via :func:`turbohtml.parse` and :func:`turbohtml.parse_fragment`; ``html.parser`` tokenizes but never builds a tree and is documented as not HTML5-conformant. - A token stream you drive yourself through :func:`turbohtml.tokenize` and :class:`turbohtml.Tokenizer`, instead of inverting control into callbacks. - Verbatim source capture per token (``capture_source=True`` → ``token.source``) and unresolved reference tokens (``resolve_references=False`` → ``TokenType.CHARACTER_REFERENCE``). - SIMD-accelerated escape/unescape scanning. What stdlib has that turbohtml does not ======================================= - ``html.parser`` and the ``html`` functions are built into CPython with no install step. turbohtml ships a compiled extension from PyPI; in environments that cannot install wheels or build C, the stdlib remains the only option. - :mod:`python:html.entities` exposes the raw reference tables (``name2codepoint``, ``codepoint2name``, ``html5``) as public data. turbohtml resolves references through ``escape``/``unescape`` and the tokenizer rather than exposing the dicts; if you consume those tables directly, keep importing ``html.entities``. Performance =========== .. bench-table:: :file: bench/stdlib.json :func:`turbohtml.escape` and :func:`turbohtml.unescape` reproduce the standard-library functions byte for byte, so they are drop-ins, but scan with SIMD and run several times faster. **************** How to migrate **************** Swap the imports and, if you subclass the parser, swap the base class: .. list-table:: :header-rows: 1 :widths: 50 50 - - stdlib call - turbohtml call - - ``html.escape(s)`` - ``turbohtml.escape(s)`` - - ``html.unescape(s)`` - ``turbohtml.unescape(s)`` - - ``class P(html.parser.HTMLParser)`` - ``class P(turbohtml.migration.stdlib.HTMLParser)`` - - ``handle_starttag(tag, attrs)`` - ``token.type is TokenType.START_TAG`` → ``token.tag``, ``token.attrs`` - - ``handle_startendtag(tag, attrs)`` - ``TokenType.START_TAG`` with ``token.self_closing`` - - ``handle_endtag(tag)`` - ``TokenType.END_TAG`` → ``token.tag`` - - ``handle_data(data)`` - ``TokenType.TEXT`` → ``token.data`` - - ``handle_comment(data)`` - ``TokenType.COMMENT`` → ``token.data`` - - ``handle_decl(decl)`` - ``TokenType.DOCTYPE`` → ``token.name`` - - ``handle_entityref``/``handle_charref`` - ``tokenize(..., resolve_references=False)`` → ``TokenType.CHARACTER_REFERENCE``, else resolved in ``token.data`` - - ``get_starttag_text()`` - ``tokenize(..., capture_source=True)`` → ``token.source`` Escape and unescape are literal drop-ins: .. testcode:: import html from turbohtml import escape, unescape print(escape('') == html.escape('')) print(unescape("café 🎉") == html.unescape("café 🎉")) .. testoutput:: True True To keep an existing :class:`python:html.parser.HTMLParser` subclass, swap its base class for :class:`turbohtml.migration.stdlib.HTMLParser`: the same ``handle_*`` callbacks and ``feed``/``close`` methods run over the WHATWG-conformant tokenizer. Or drop the subclass and take the token stream from :func:`turbohtml.tokenize` (or :meth:`turbohtml.Tokenizer.feed` for incremental input), or skip tokens entirely and :func:`turbohtml.parse` straight to a tree. All three are WHATWG-conformant, unlike ``html.parser``. The :doc:`/how-to/index` guide has a worked port. ``HTMLParser`` is a SAX-style callback API; turbohtml gives you the events as a token stream you drive yourself, which inverts the control flow. Each ``handle_*`` override becomes a branch on :attr:`Token.type `: .. testcode:: import turbohtml from turbohtml import TokenType events = [] for token in turbohtml.tokenize('

Hi & bye

'): if token.type is TokenType.START_TAG: events.append(("start", token.tag, token.attrs)) elif token.type is TokenType.TEXT: events.append(("data", token.data)) elif token.type is TokenType.END_TAG: events.append(("end", token.tag)) print(events) .. testoutput:: [('start', 'p', [('class', 'x')]), ('data', 'Hi & bye'), ('end', 'p')] ********************** Gotchas and pitfalls ********************** - The token stream inverts ``html.parser``'s callback control flow: you loop over tokens and branch on :attr:`Token.type ` instead of overriding ``handle_*`` (unless you subclass :class:`turbohtml.migration.stdlib.HTMLParser`, which keeps the callbacks). - By default ``token.data`` already holds decoded text (the equivalent of ``convert_charrefs=True``). To recover the split stream ``convert_charrefs=False`` gives, pass ``resolve_references=False`` and handle ``TokenType.CHARACTER_REFERENCE`` tokens, whose ``token.source`` is the verbatim reference and ``token.data`` its resolved value. On :class:`turbohtml.migration.stdlib.HTMLParser` the ``convert_charrefs`` argument is accepted for signature compatibility but ignored; references are always resolved. - The verbatim start-tag text ``get_starttag_text()`` returns is ``token.source`` once you pass ``capture_source=True``; it is not captured by default. - ``html.parser`` is documented as not fully HTML5-conformant, so tricky recovery cases (malformed tags, misnested elements, foreign content) can tokenize differently. turbohtml follows the WHATWG spec, so output may diverge from a legacy ``html.parser`` run on the same broken input; the turbohtml result is the conformant one. - If your code imports the reference tables from :mod:`python:html.entities` directly, keep that import: turbohtml does not re-export ``name2codepoint``/``codepoint2name``/``html5``.