mrkeyoor.com_
Sun 20 Sept 11:43 UTC
PyPIDataupdated 20 Sept 2026

lxml review

lxml exposes libxml2 and libxslt through Python APIs compatible with much of ElementTree. It parses and builds XML or damaged HTML, evaluates XPath 1.0, runs XSLT 1.0, validates DTD, Relax NG, and XML Schema documents, and streams large trees through iterparse. The stable 6.1 line changed entity resolution defaults for iterparse and ETCompatXMLParser after an XXE report, updated packaged libxslt builds, and added safer HTML link attributes. Version 6.1.2 repairs missing source-distribution build files and minor error paths; version 7 remains a prerelease and is not the stable PyPI version.

Verdict

lxml remains the right tool for serious XML, full XPath, XSLT, validation, and streamed parsing. Use the standard library for small trusted documents, and treat wheel availability plus parser security flags as part of deployment design.

We installed it

Lab card: what happened when we installed lxmlScreenshot of lxml documentation
Install✓ · 0.3s1 package on disk · 12 MB
Importimport lxml in 0.02s · compiled extensions · requires Python >=3.8
Known vulns0(pip-audit)

Answers from our run

Does lxml install cleanly?

Yes. In a fresh container with an empty cache, pip install lxml finished in 0.3s, leaving 1 package and 12 MB on disk. pip-audit reported no known vulnerabilities.

What does lxml need to run?

Python >=3.8, and a platform wheel with compiled extensions. In our run import lxml succeeded in 0.02s.

lxml or beautifulsoup4: which should you use?

beautifulsoup4: Choose it for an approachable HTML traversal API and use lxml underneath only as the parser. lxml remains the right tool for serious XML, full XPath, XSLT, validation, and streamed parsing.

When should you not use lxml?

Input is small, trusted, and only uses basic XML traversal; xml.etree.ElementTree avoids a compiled dependency and shares the core tree vocabulary

API stability5/5The etree tree, parser, XPath, XSLT, validation, and iterparse APIs have decades of compatibility and deliberately resemble ElementTree where possible. Stable 6.1 changes a risky entity default and adds a chunk-size option without reorganizing everyday parsing. Version 7 is visibly marked beta, so users can stay on 6.1.2 instead of receiving an unannounced stable-line rewrite.
Docs4/5lxml.de has a substantial tutorial, API index, parsing and validation guides, FAQ, performance notes, compatibility material, and security guidance. Examples cover namespace maps, iterparse cleanup, custom resolvers, XPath extensions, XSLT access control, and element classes. Navigation and presentation feel older than newer documentation sites, and some security facts are easiest to understand from the changelog.
Maintenance4/5GitHub shows an unarchived repository pushed on August 22, 2026, with 12 open issues and pull requests. Stable 6.1.2 shipped August 18, while 7.0 beta work is active. Recent stable releases address XXE defaults, packaged libxslt vulnerabilities, HTML cleaner link attributes, and source builds. The small long-running maintainer base remains a concentration risk for a critical compiled package.
Ecosystem5/5The supplied figure is 96,743,633 weekly downloads and GitHub reports 3,050 stars. Linux distributions package lxml, and Beautiful Soup, pandas HTML readers, Scrapy selectors, XML integration tools, and document pipelines commonly use it. Binary wheels make common CPython installs simple. Consumers on unusual targets still inherit the native libxml2 and libxslt build boundary.

Use it if

  • Python code needs full XPath 1.0 or XSLT instead of the small ElementPath subset in the standard library
  • Large XML feeds must be processed incrementally while cleared elements keep memory bounded
  • Malformed HTML needs a forgiving parser plus XPath or CSS selection at high volume
  • DTD, Relax NG, or XML Schema validation must run locally against a parsed document
Skip it if

Setup reality

Our fresh Python 3.12 install of lxml 6.1.2 completed in 0.3 seconds. One package used 12 MB, and pip-audit found no known vulnerabilities. The package metadata lists four direct dependency entries, requires Python 3.8 or newer, carries BSD-3-Clause, and includes compiled shared objects. It did not ship py.typed. Importing lxml worked in 0.02 seconds. The clean install used a compatible wheel, so no compiler or system headers were needed.

When a wheel is unavailable, pip builds the C extension. That path needs a compiler, Python development files, and libxml2 and libxslt development headers that match what the build can consume. Alpine, unusual architectures, prerelease Python, and constrained build images are the common places to hit it. Build wheels in a controlled image instead of compiling on every deployment. Version 6.1.2 specifically restores files missing from the source distribution, so source builders should avoid 6.1.1.

Parser flags are security decisions. Keep no_network enabled, leave huge_tree disabled, avoid loading DTDs, and set resolve_entities to false when entity substitution is unnecessary. In 6.1.0 the iterparse and ETCompatXMLParser defaults were corrected to resolve internal entities only after CVE-2026-41066. That reduces one exposure and does not make every XML feature safe. External schemas, XSLT document access, custom resolvers, decompression, giant text nodes, and deep trees require their own controls.

Namespaces cause more application bugs than installation. XPath has no implicit default namespace, so bind the document URI to a prefix in the namespaces argument. Element tags use the {uri}local form. Mixed XML content is split between element.text and child.tail, which makes naive text assembly lose ordering. In streaming code, elem.clear() alone leaves prior siblings attached; delete processed siblings from the parent if file size is large. CSS selection requires the separate cssselect package.

Patterns

Query a parsed XML document parse-xml-xpath

from lxml import etree

tree = etree.parse('catalog.xml')
titles = tree.xpath("//book[@lang='en']/title/text()")
for title in titles:
    print(title)

xpath may return elements, strings, numbers, or booleans depending on the expression.

Bind a prefix for XPath query-default-namespace

from lxml import etree

tree = etree.parse('feed.xml')
ns = {'atom': 'http://www.w3.org/2005/Atom'}
titles = tree.xpath('//atom:entry/atom:title/text()', namespaces=ns)

XPath does not inherit the document's default namespace. Assign any prefix and map it to the URI.

Disable entity and network access parse-untrusted-xml

from lxml import etree

parser = etree.XMLParser(
    resolve_entities=False,
    load_dtd=False,
    no_network=True,
    huge_tree=False,
)
root = etree.fromstring(untrusted_bytes, parser=parser)

Keep input-size and time limits outside the parser too. Do not enable huge_tree merely to accept hostile documents.

Release processed nodes during iterparse stream-large-xml

from lxml import etree

for _event, element in etree.iterparse(
    'events.xml',
    events=('end',),
    tag='{urn:events}event',
    resolve_entities=False,
    no_network=True,
):
    process(element)
    element.clear()
    while element.getprevious() is not None:
        del element.getparent()[0]

Clearing content and deleting earlier siblings prevents the parent tree from retaining the whole file.

Extract links from HTML parse-broken-html

from lxml import html

document = html.fromstring(page_bytes)
links = [
    (node.text_content().strip(), node.get('href'))
    for node in document.xpath('//a[@href]')
]

Pass bytes when possible so declarations and meta tags can guide encoding detection. The HTML parser repairs malformed markup.

Create and serialize elements build-xml-document

from lxml import etree

root = etree.Element('order', id='42')
item = etree.SubElement(root, 'item', sku='INK-1')
item.text = 'Black ink'

output = etree.tostring(
    root, encoding='UTF-8', xml_declaration=True, pretty_print=True
)

An explicit encoding returns bytes. Use encoding='unicode' when a Python string is required.

Collect XML Schema failures validate-xsd

from lxml import etree

schema = etree.XMLSchema(etree.parse('order.xsd'))
document = etree.parse('order.xml')

if not schema.validate(document):
    for error in schema.error_log:
        print(error.line, error.column, error.message)

Use assertValid when the first validation failure should raise. Schema imports can involve external resources and need resolver policy.

Apply an XSLT 1.0 stylesheet run-xslt

from lxml import etree

stylesheet = etree.XSLT(etree.parse('report.xsl'))
result = stylesheet(
    etree.parse('report.xml'),
    title=etree.XSLT.strparam('Monthly report'),
)
print(str(result))

libxslt implements XSLT 1.0. Use access controls when an untrusted stylesheet could read files or network resources.

Delete nodes through their parent remove-tree-elements

tree = etree.parse('config.xml')
root = tree.getroot()

for debug in root.findall('.//debug'):
    debug.getparent().remove(debug)

tree.write('config.xml', encoding='UTF-8', xml_declaration=True)

lxml elements expose getparent(), unlike standard ElementTree elements. Writing recreates formatting around changed content.

Remove scripts before collecting text extract-visible-text

document = html.fromstring(page_bytes)
for node in document.xpath('//script | //style | //template'):
    parent = node.getparent()
    if parent is not None:
        parent.remove(node)

text = ' '.join(document.text_content().split())

text_content includes all descendant text. Remove non-content nodes before normalization.

Alternatives

PackageRegistryPick it when
beautifulsoup4PyPIChoose it for an approachable HTML traversal API and use lxml underneath only as the parser
defusedxmlPyPIChoose it for hardened wrappers around common XML parsers when untrusted input is the main concern
selectolaxPyPIChoose it for fast HTML parsing and CSS selection without XML Schema or XSLT features

More data guides

numpy · fsspec · pandas · sqlalchemy · pyarrow · s3fs · the whole shelf →

How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.