pyxsd compared with lxml

lxml is a general-purpose XML and HTML toolkit with Python APIs backed by libxml2 and libxslt. It provides tree editing, XPath, validation, and stylesheet execution. pyxsd focuses on XSD-driven Python data: it compiles schema declarations into classes and binds documents to instances of those classes.

Choose pyxsd when the schema should define your application objects and their validation, extensions, and exports. Keep lxml when the main job is general tree processing, HTML, or an existing lxml-based pipeline. You can also use the libraries at different stages of an application. pyxsd does not depend on lxml or use its validator internally.

At a glance

Task or property

pyxsd

lxml

Primary model

XSD-derived Python classes and a bound Document

etree elements and trees; objectify adds object-style data access

XSD validation

XSD 1.1 default and explicit 1.0 mode; see Supported features

etree.XMLSchema uses libxml2’s XSD 1.0 implementation; no XSD 1.1

Typed values

XSD datatype classes and schema-derived restrictions

etree text/attributes are strings; objectify provides typed data elements

Application methods

Mixins selected from schema declarations at compile time

Custom element classes and class-lookup schemes

Queries

find, findall, and xpath return original bound nodes for element results

ElementPath, XPath 1.0, compiled XPath, and Python extension functions

Validation findings

Report attached to the document, stable codes, phase tags, optional occurrence locations

Validation booleans or exceptions; structured error logs with source/error details

Python data output

Dict/JSON export; optional XSD-derived Pydantic and Arrow/Parquet projections

Application-defined extraction from etree or objectify

Large instance documents

Full bound instance tree

iterparse, pull/feed parsers, and target callbacks

Other document tools

XSD validation and Python callable transforms

HTML APIs, DTD/Relax NG/ISO Schematron validation, XSLT 1.0, canonical XML

Installation

Pure-Python base; optional extras may contain compiled packages

Compiled extension and native libraries; binary wheels available

These are interface and scope differences, not throughput or memory measurements. The separate pyxsd compared with libxml2 probe concerns one native validator build, not current lxml releases. See lxml’s standards overview for its supported XML languages.

Where pyxsd helps

From a schema to application data

In lxml.etree, schema validation checks a tree but does not turn its text into schema-derived Python values. With the trusted input files from Quickstart and lxml installed (python -m pip install lxml), a validation-and-extraction step looks like this:

from lxml import etree

validator = etree.XMLSchema(file="inventory.xsd")
tree = etree.parse("inventory.xml")
validator.assertValid(tree)
ns = {"i": "urn:inventory"}
quantity = int(tree.findtext("i:item/i:quantity", namespaces=ns))

With pyxsd, the binding step already applies the schema’s declared datatype:

from pyxsd import ParseModes, Schema

schema = Schema.compile("inventory.xsd", mode=ParseModes.NAMESPACED)
schema.require_valid()
document = schema.parse("inventory.xml")
document.require_valid()
item = document.find("i:item", namespaces={"i": "urn:inventory"})
quantity = item.quantity

Both produce a quantity equal to 4. In pyxsd, quantity uses the XSD positiveInteger datatype, and the same approach applies across the schema’s declarations. You avoid writing a separate text-to-value mapping for each field. Runtime class generation needs no source-generation step; optional Type stubs for generated binding classes describe the classes for static tools.

Add behavior where the schema defines it

pyxsd turns XSD type extension into Python inheritance and lets you attach mixins to schema-declared types. Application methods then live on the bound objects, and a __pyxsd_validate__ hook contributes findings to their document’s report. Selection can use a type, namespace, declaration path, or predicate. See Mixin extensions.

lxml also supports custom element classes. Its class lookup mechanisms associate Python classes with XML elements; they do not compile an XSD type hierarchy into application classes for you.

Carry the schema into downstream tools

pyxsd’s optional integrations derive Pydantic models and Arrow record schemas from XSD, including before any XML records are present. Record projections can write Parquet and include ancestor/root context in selected columns. This is useful when the same schema must drive both XML checks and the declared shape of application output. See Optional Pydantic and Arrow integrations for supported shapes and fidelity limits.

The report also stays with the bound Document: queries select the objects your functions consume, occurrence locations help identify findings, and revalidate() returns a freshly checked tree after edits. Lax retention can preserve more data for repair while leaving validation findings visible. See Validation and Parse modes and binding policies.

How lxml.objectify differs

lxml.objectify provides attribute-style child access, typed data elements, and normal Python operations on values. It can also parse with an XMLSchema validator.

Objectify chooses data element classes using Python type annotations, xsi:type hints, and text-value inference. Structural elements use a tree class. Passing a schema validates the XML; it does not generate classes from each XSD complex type or use declared simple types as a general binding map. pyxsd instead builds its classes from the compiled schema and uses those declarations when binding. Prefer it when schema-derived types, inheritance, and compile-time extensions should define the object model.

Where lxml is a better fit

  • General XML and HTML processing. lxml has HTML parsing and manipulation, comment and processing-instruction nodes, and canonical serialization. pyxsd’s bound tree targets schema-defined data and does not preserve every source construct. Its mixed text can survive writing, but structured exports have narrower limits; see Data model.

  • Incremental processing. lxml’s parser APIs include iterparse, pull/feed parsing, and target callbacks. Releasing processed elements can reduce retained memory; iterparse alone does not guarantee bounded use. pyxsd builds a full instance tree and allows only one parse at a time per compiled Schema.

  • Other XML languages and existing assets. lxml supports DTD, Relax NG, and ISO Schematron validation, plus XPath 1.0 and XSLT 1.0 with Python extensions. pyxsd’s validation language is XSD and its transforms are Python callables. See lxml’s validation and query/stylesheet documentation for those APIs.

  • Native-library integration. Keep lxml when your application depends on its element APIs, custom resolvers, catalogs, or other libraries that consume lxml trees. pyxsd’s bound nodes are a different object model, not drop-in etree elements.

Use both with an explicit boundary

An application can use lxml for general document work and pyxsd for the XSD-driven data stage. Serialize XML at that boundary, then compile/parse with pyxsd through a supported byte stream or file input. This is an application pipeline, not a built-in lxml adapter: parsing again costs time and memory, and bound output is not a byte-for-byte copy of its source.

Apply input protections at each stage. lxml exposes parser options, resolvers and catalogs, and stylesheet access controls; capabilities and defaults depend on its version and native build. pyxsd has opt-in parser profiles, structural limits, and resource policies described in XML security and untrusted input. Neither a valid document nor a local-only loader establishes a security boundary by itself.

Native engine evidence

The page below records the scope and results of a dated libxml2 C-API probe. lxml may bundle a different libxml2 version and adds its own parsing, resource, and error-handling behavior, so those results must not be read as lxml benchmark or conformance results.

See also