XPath in Python: lxml and ElementTree

ElementTree's subset, lxml's complete XPath 1.0 with extensions, and the namespace and return-type differences between the two.

What ElementTree can and cannot do

import xml.etree.ElementTree as ET

root = ET.fromstring("<r><a id='1'>x</a><a id='2'>y</a></r>")

print(root.findall("a"))                     # direct children only
print(root.findall(".//a[@id='2']"))         # works: attributes and predicates
print(root.findtext(".//a"))                 # first match, text as str

# These raise SyntaxError: unsupported XPath syntax
for bad in ["//a[text()='x']", "a/following-sibling::a", "count(.//a)", "//a[last()]"]:
    try:
        root.findall(bad)
    except SyntaxError as e:
        print("unsupported:", bad)

ElementTree supports a deliberately small subset: child paths, //, .., ., attribute predicates with =, and index predicates like [1] or [last()]. Anything with a function call, an axis, or a predicate combining two conditions is out.

  • Use findall for element results, findtext for one string, find for a single element or None.
  • There is no namespace prefix support: write {urn:example:catalog}book with the full URI in braces.
  • Use iter() for a fast descendant scan when you do not need predicates.

lxml: full XPath 1.0

from lxml import etree

ns = {"c": "urn:example:catalog"}
tree = etree.parse("catalog.xml")

# Element results
books = tree.xpath("//c:book", namespaces=ns)

# Scalar results, because the expression is a function call
total = tree.xpath("count(//c:book)", namespaces=ns)         # float
first = tree.xpath("string(//c:book[1]/@id)", namespaces=ns) # str
any_missing = tree.xpath("boolean(//c:book[not(@id)])", namespaces=ns)

# Smart strings keep a reference to their origin element
title = tree.xpath("string((//c:book)[1]/title)", namespaces=ns)
print(title, title.getparent() is not None)

# XPath on HTML that is not well formed
frag = etree.fromstring("<div class='x'>Hi<br>there</div>", etree.HTMLParser())
print(frag.xpath("//div[@class='x']//text()"))
NeedElementTreelxml
Attribute predicatesYesYes
Text comparisonNoYes
Axes beyond childNoYes
Namespace prefixes{uri}namenamespaces= map
Functions such as countNoYes
CSS selectorsNoVia cssselect
Custom extension functionsNoYes
💡
lxml is strict about input. Pass untrusted documents through etree.XMLParser(resolve_entities=False, no_network=True, huge_tree=False) to avoid entity expansion and SSRF, especially when the document came from a feed or an upload.

FAQ

Why does //div return nothing in lxml?
Almost always a default namespace on the document. Declare it in the namespaces map and use a prefix in the expression; unprefixed names only match no-namespace nodes.
Can I query with an XPath 2.0 expression?
No. lxml implements XPath 1.0 plus a small set of extension functions. Use a Saxon/C binding or ElementPath when you need 2.0 or later.

XPath from the shell: xmllint and xmlstarlet XPath in Java and .NET

Last refreshed 2026-09-18.