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 &gt; 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>
  • select takes a path; test takes a boolean expression; match takes a pattern.
  • Inside a template body, the context node is the matched node, so paths are relative to it.
  • Markup like &gt; 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 querySelector for HTML.
  • Always check for a parser error node before evaluating: a parse failure otherwise looks like an empty result.
  • xmllint --xpath is 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.xml

FAQ

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.

Predicates and functions Templates and apply-templates

Last refreshed 2026-09-18.