LEPTRIS

Python — the leptris package

The Python binding wraps libleptris and mirrors lxml: parse, query, and serialize code written against lxml's read path ports with an import change. Version 1.9.156.0 beats lxml on every operation in the matrix — five cp39-abi3 wheels on PyPI make pip install the whole setup.

Setup

The package publishes to PyPI as leptris (Python 3.9+): five cp39-abi3 wheels — macOS arm64/x86_64, manylinux x86_64/aarch64, Windows — plus the sdist. Wheels ship the C accelerator compiled; the accelerator is a required component — sdist installs without a toolchain fail loudly instead of degrading (the pure-Python fallback was removed in 1.9.0):

bash
pip install leptris

The pinned libleptris shared library must be on the loader path — if it is not, build it from source (seegetting started) and pointLEPTRIS_LIB_PATH — which must name the libraryfile — at it:

bash
cmake -B build -S . -DLEPTRIS_BUILD_SHARED=ON
cmake --build build --target leptris_shared
export LEPTRIS_LIB_PATH=$PWD/build/src/libleptris.dylib

Quick start — the lxml shape

python
from leptris import fromstring, tostring

root = fromstring("<library><book id='1' lang='en'>Ulysses</book></library>")

root.tag                    # "library"
root[0].get("id")           # "1"
root[0].attrib              # {"id": "1", "lang": "en"}
root[0].text                # "Ulysses"

root.xpath("count(//book)")             # 1.0
[b.text for b in root.findall("book")]  # ["Ulysses"]

tostring(root[0], encoding="unicode")   # "<book id='1' lang='en'>Ulysses</book>"

The surface

membernotes
fromstring / XML / parse / tostring / c14nmodule-level, lxml names; parse takes paths and file-likes (no URLs) and returns a Document context manager.
Element.tag / .text / .tailElementTree model; tag uses {uri}local Clark notation; adjacent text and CDATA runs merge, as lxml’s default parser does.
Element.attrib / .get / .keys / .itemsattrib is a read-only Mapping — the parser is read-only.
elem[i], len(elem), iteration, sliceschild indexing, never attribute lookup.
getparent / getnext / getpreviousnavigation; sibling accessors are single FFI calls.
iter / iterdescendants / itertextlxml iteration protocol.
find / findall / findtextaccepts full XPath 1.0 — a superset of ElementPath — including {uri}local names.
xpath(expr, namespaces=…, variables=…)W3C XPath 1.0; pre-bound namespace prefixes, and variables — a leptris extension.
namespace / prefix / sourcelinethe element’s own namespace binding, and its 1-based source line (lxml parity, since 1.5.0).
Document.getroot / write / parse_file / process_xincludedocument lifecycle, file output, XInclude splice.
leptris.saxone-shot SAX parse and a streaming push parser — constant memory, events emitted as chunks arrive.

Namespaces, variables, canonical XML, SAX

python
root.xpath("//x:item", namespaces={"x": "urn:ex"})
root.xpath("//book[@id=$id]", variables={"id": "2"})
c14n(root, exclusive=True)

from leptris import sax
sax.parse(xml, handler)                        # one-shot
with sax.StreamingParser(handler) as parser:   # push, constant memory
    parser.feed(chunk, final=last)

Migrating from lxml

Most read-path code needs only the import change. What is deliberately absent, and why:

  • Tree building (etree.Element, SubElement, append, set) — this binding is read-only today (the core has partial mutation upstream — content setters, set_root, append_child — not surfaced yet); build trees elsewhere.
  • iterparse — shipped: leptris.iterparse(source) yields ("end", element) for each completed top-level child, memory bounded by the largest subtree. Two engine limitations are tracked upstream (leptris/leptris#563): it currently runs ~2× slower than lxml’s, and truncated input ends iteration silently instead of raising. leptris.sax.StreamingParser remains for callback-style streaming.
  • Smart strings — XPath string and attribute results are plain str.
  • nsmap — use namespace / prefix and xpath(namespaces=…).
  • Compiled XPath objects, parser options — both landed in libleptris 1.4.0 (thread-safe compiled handles, scoped per-parse options); binding surfacing follows.
  • Undeclared XPath prefixes evaluate to an empty nodeset instead of raising.

Memory model

Errors

LeptrisError is the base; ParseError andXPathError subclass it. Since 1.5.0, XPath errors prefer the document-scopedleptris_document_last_error — immune to concurrent operations on other documents. leptris.libleptris_version()reports the loaded library at runtime.

Benchmarks

Since 1.6.1 — and through 1.9.121.0, with wider margins — leptrisbeats lxml on every operation in the matrix (parse 4.6×, ID predicates 19×, serialize 2.3×; traversal, the last holdout, is now 2× ahead of lxml — the stdlib’s ElementTree keeps the single traversal crown). The matrix ships inbenchmarks/matrix.py and runs in CI; the accelerator story and the full scoreboard:the benchmarks page.

Where next

This is the curated guide. The repositories are canonical: when this page and the repo disagree, the repo wins. Full documentation lives with the source.