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
findallfor element results,findtextfor one string,findfor a single element orNone. - There is no namespace prefix support: write
{urn:example:catalog}bookwith 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()"))| Need | ElementTree | lxml |
|---|---|---|
| Attribute predicates | Yes | Yes |
| Text comparison | No | Yes |
| Axes beyond child | No | Yes |
| Namespace prefixes | {uri}name | namespaces= map |
| Functions such as count | No | Yes |
| CSS selectors | No | Via cssselect |
| Custom extension functions | No | Yes |
💡
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.
Related
XPath from the shell: xmllint and xmlstarlet XPath in Java and .NET
Last refreshed 2026-09-18.