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):
pip install leptrisThe 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:
cmake -B build -S . -DLEPTRIS_BUILD_SHARED=ON
cmake --build build --target leptris_shared
export LEPTRIS_LIB_PATH=$PWD/build/src/libleptris.dylibQuick start — the lxml shape
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
| member | notes |
|---|---|
fromstring / XML / parse / tostring / c14n | module-level, lxml names; parse takes paths and file-likes (no URLs) and returns a Document context manager. |
Element.tag / .text / .tail | ElementTree model; tag uses {uri}local Clark notation; adjacent text and CDATA runs merge, as lxml’s default parser does. |
Element.attrib / .get / .keys / .items | attrib is a read-only Mapping — the parser is read-only. |
elem[i], len(elem), iteration, slices | child indexing, never attribute lookup. |
getparent / getnext / getprevious | navigation; sibling accessors are single FFI calls. |
iter / iterdescendants / itertext | lxml iteration protocol. |
find / findall / findtext | accepts 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 / sourceline | the element’s own namespace binding, and its 1-based source line (lxml parity, since 1.5.0). |
Document.getroot / write / parse_file / process_xinclude | document lifecycle, file output, XInclude splice. |
leptris.sax | one-shot SAX parse and a streaming push parser — constant memory, events emitted as chunks arrive. |
Namespaces, variables, canonical XML, SAX
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.StreamingParserremains for callback-style streaming.- Smart strings — XPath string and attribute results are plain
str. nsmap— usenamespace/prefixandxpath(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
- leptris-py — source, tests, and the benchmark matrix.
- Benchmarks — the per-language race.
- Internals & FFI — the ABI contract the cdef mirrors.
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.