XPath in XSLT and in code
How XPath drives XSLT select and match attributes, and the same expressions in Python, Java, JavaScript and the shell.
XPath inside a stylesheet
In XSLT almost every attribute is an XPath expression. select chooses nodes to process, test makes a decision, and match declares which template handles a node.
<xsl:template match="/catalog">
<table>
<xsl:apply-templates select="book[price > 20]">
<xsl:sort select="price" data-type="number" order="descending"/>
</xsl:apply-templates>
</table>
</xsl:template>
<xsl:template match="book">
<tr>
<td><xsl:value-of select="title"/></td>
<td><xsl:value-of select="format-number(price, '#0.00')"/></td>
</tr>
</xsl:template>selecttakes a path;testtakes a boolean expression;matchtakes a pattern.- Inside a template body, the context node is the matched node, so paths are relative to it.
- Markup like
>must be escaped inside attribute values in the stylesheet. - XPath 2.0 adds sequences,
for, and typed comparisons inside the same attribute syntax.
💡
Expressions written in
match patterns are a restricted subset of XPath. Predicates with function calls such as matches() are allowed in select but are not portable in match patterns.In Python and Java
from lxml import etree
tree = etree.parse("catalog.xml")
ns = {"s": "http://example.com/shop"}
books = tree.xpath("//s:book[s:price > 20]", namespaces=ns)
for b in books:
print(b.get("id"), b.xpath("string(s:title)", namespaces=ns))
# lxml returns smart strings; cast explicitly when you need plain values
print(float(books[0].xpath("number(s:price)", namespaces=ns)))import javax.xml.xpath.*;
import org.w3c.dom.*;
XPath xp = XPathFactory.newInstance().newXPath();
NodeList nodes = (NodeList) xp.evaluate("//book[price > 20]",
document, XPathConstants.NODESET);
System.out.println(xp.evaluate("count(//book)", document));
// namespaces must be resolved through a context, as prefixes are local
xp.setNamespaceContext(new SimpleNamespaceContext("s", "http://example.com/shop"));In the browser and on the command line
// every browser ships an XPath 1.0 engine for XML documents
const parser = new DOMParser();
const doc = parser.parseFromString(xmlText, "application/xml");
if (doc.querySelector("parsererror")) throw new Error("not well-formed XML");
const result = doc.evaluate(
"//book[price > 20]/title",
doc, null,
XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null
);
const titles = [];
for (let i = 0; i < result.snapshotLength; i++) {
titles.push(result.snapshotItem(i).textContent);
}- The same API works on HTML documents in Chrome and Firefox, but not in every engine — prefer
querySelectorfor HTML. - Always check for a parser error node before evaluating: a parse failure otherwise looks like an empty result.
xmllint --xpathis the fastest way to rehearse an expression against a file.- On the command line, namespace prefixes are declared inline on the XPath expression itself.
xmllint --xpath '//book[price > 20]/title/text()' catalog.xml; echo
xmllint --xpath 'string(sum(//book/price))' catalog.xml; echo
# xmllint cannot bind a prefix directly; use local-name() or the interactive shell
xmllint --xpath '//*[local-name()="book"]/*[local-name()="title"]/text()' catalog.xml; echo
printf 'setns s=http://example.com/shop\nxpath //s:book/s:title\nexit\n' | xmllint --shell catalog.xmlFAQ
Is XPath still worth learning?
Yes, in the places it still runs: XSLT, XML tooling, XSD assertions, Schematron, Selenium locators, and configuration systems. The mental model of paths and predicates transfers to CSS selectors and JSONPath.
Why does the browser return an empty set instead of throwing?
XPath follows node-set semantics: no matches is a valid empty result. Detect the failure at the parse stage and print
count() when debugging.Related
Predicates and functions Templates and apply-templates
Last refreshed 2026-09-18.