XPath cheat sheet

A scannable XPath reference: 26 short snippets across 12 topics, each linking back to the lesson it came from.

At a glance

TopicWhat it covers
Location paths and axesXPath addresses an XML document as a tree of nodes. An expression is evaluated against a context node, and the resultlesson
Predicates and functionsA predicate in square brackets keeps only the nodes for which the expression is true. Inside a predicate, the contextlesson
XPath in XSLT and in codeIn XSLT almost every attribute is an XPath expression. select chooses nodes to process, test makes a decision, andlesson
The XPath data model: nodes, order and valuesAn XPath expression is evaluated against a tree, not against text. The tree is built by the parser and normalisedlesson
Namespaces and the default namespace trapAn unprefixed name in XPath matches a node in no namespace. When the document declares a default namespace, everylesson
Selecting attributes, text and special nodesThe example above is the single most common scraping bug in XPath: text()="Overview" only compares the first text nodelesson
XPath from the shell: xmllint and xmlstarletTwo command-line tools turn XPath into something you can pipe, diff and run in a build script, including on namespacedlesson
XPath in the browser: document.evaluateThe DOM XPath API is older than querySelector but still the only way to walk upwards and to match by text, and itslesson
XPath 1.0, 2.0 and 3.1: what changedThe version you are allowed to use is decided by your engine, not your preferences, and the jump from 1.0 changes whatlesson
Robust XPath for scraping and testingEvery positional step is a promise that nothing will ever be inserted before it. Live pages break that promiselesson
XPath performance: cost, indexes and //Descendant search is the expensive part of XPath. Knowing which constructs force a full scan tells you where to spendlesson
CSS selectors, JSONPath and jq as alternativesXPath is not always the right tool: CSS is faster and simpler for downward-only selection, and JSON needs an entirelylesson

Quick snippets

Location paths and axes

The data model behind a path

<catalog>
  <book id="bk-101"><title>Alpha</title><price>39.95</price></book>
  <book id="bk-102"><title>Beta</title><price>12.00</price></book>
</catalog>

Axes worth knowing

/catalog/book                      absolute: books that are children of the root
//book/title                        any title element anywhere
catalog/book/@id                   attribute axis, abbreviated with @
//book[1]                           first book child of each parent
(//book)[1]                         the first book in the document - note the brackets
//book/ancestor::catalog            the catalog that encloses each book
//title/following-sibling::price    the price that comes right after a title
//book[@id = 'bk-102']/title/text()  the text node of a matching title
//comment()                          every comment node

Node tests and multiple results

# try expressions from the command line before putting them in code
xmllint --xpath 'count(//book)' catalog.xml; echo
xmllint --xpath 'string(//book[1]/title)' catalog.xml; echo
xmllint --xpath '//book/@id' catalog.xml; echo

Full lesson: Location paths and axes →

Predicates and functions

Predicates filter a node-set

//book[price > 20]                      value test
//book[@id]                             has the attribute
//book[not(@id)]                        lacks the attribute
//book[position() = 1]                  same as //book[1], per parent
//book[last()]                          the last book child of each parent
//book[position() <= 3]                 the first three
//book[title = 'Alpha']                 exact text comparison
//book[contains(title, 'lph')]          substring test
//book[starts-with(@id, 'bk-10')]       prefix test
//book[price > 20 and @available='true']  boolean combination

The functions you will actually use

count(//book)                                  how many books
sum(//book/price)                              total price as a number
round(sum(//book/price) * 100) / 100           two-decimal total
normalize-space(//book[1]/title)               trim and collapse spaces
concat(//book[1]/title, ' - ', //book[1]/price)  join into one string
substring-before(//book[1]/title, ' ')         text before the first space
translate(//book[1]/title, 'abc', 'ABC')       character-by-character map
string-length(//book[1]/title) > 4             boolean result

Full lesson: Predicates and functions →

XPath in XSLT and in code

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)))

In Python and Java

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

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

Full lesson: XPath in XSLT and in code →

The XPath data model: nodes, order and values

Document order and the context node

# Every expression is evaluated with respect to a context node.
# Inside a predicate, position() counts siblings from that context.

xmllint --xpath '//ul/li[position() = 1]' page.html   # first li of each ul
xmllint --xpath '(//ul/li)[1]' page.html              # first li in the whole doc
xmllint --xpath '//ul/li[last()]' page.html           # last li of each ul
xmllint --xpath '//ul/li[position() < 3]' page.html   # first two of each ul

Values, conversions and comparison

from lxml import etree

doc = etree.fromstring("<r><n>10</n><n>9</n><s>abc</s></r>")

# string() collapses an element to text; number() converts it
print(doc.xpath("number(//n[1])"))      # 10.0
print(doc.xpath("//n[text() > '9']"))   # text comparison, not numeric

# Compare with a number to force numeric comparison: only the 10 survives
print(doc.xpath("//n[. > 9]"))          # [<Element n>]
print(doc.xpath("count(//n)"))          # 2.0
print(doc.xpath("boolean(//missing)"))  # False

Full lesson: The XPath data model: nodes, order and values →

Namespaces and the default namespace trap

The trap

<catalog xmlns="urn:example:catalog" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <book><dc:title>Pragmatic XPath</dc:title></book>
</catalog>

The trap

xmllint --xpath 'count(//book)' catalog.xml
# XPath error: no result / returns nothing useful

xmllint --xpath 'count(//*[local-name()="book"])' catalog.xml
# 1  -- this works, because it ignores the namespace URI

Binding a prefix in each environment

from lxml import etree

ns = {"c": "urn:example:catalog", "d": "http://purl.org/dc/elements/1.1/"}
doc = etree.parse("catalog.xml")

print(doc.xpath("//c:book/d:title/text()", namespaces=ns))   # ['Pragmatic XPath']

Full lesson: Namespaces and the default namespace trap →

Selecting attributes, text and special nodes

Text, comments and processing instructions

from lxml import etree, html

doc = html.fromstring("<div><h2>Over<span>view</span></h2></div>")

# text() sees the heading and its span as separate text nodes
print(doc.xpath("//h2/text()"))            # ['Over']
print(doc.xpath("//h2//text()"))           # ['Over', 'view']
print(doc.xpath("string(//h2)"))           # 'Overview'
print(doc.xpath("//h2[text() = 'Overview']"))  # [] -- the trap
print(doc.xpath("//h2[normalize-space(.) = 'Overview']"))  # [<Element h2>]

Whitespace-only text nodes

# Indented markup produces text nodes that look empty
xmllint --xpath 'count(//ul/li/text())' page.html

# Inspect what is actually there
xmllint --xpath '//ul/li/text()' page.html

# XML parsers can drop them for you
xmllint --noblanks --xpath 'count(//ul/li/text())' page.html

Full lesson: Selecting attributes, text and special nodes →

XPath from the shell: xmllint and xmlstarlet

xmlstarlet

# select: print values, one per line (the default template for -t is -v)
xmlstarlet sel -t -m '//item' -v 'title' -n feed.xml

# select with namespace binding
xmlstarlet sel -N c=urn:example:catalog -t -v 'count(//c:book)' catalog.xml

# CSV out, useful in a report script
xmlstarlet sel -t -m '//item' \
  -v 'concat(title, ",", pubDate)' -n feed.xml

# edit in place: rename an element and set an attribute
xmlstarlet ed -L -r '//legacyName' newName -a '//book[1]' -t attr -n 'lang' -v 'en' catalog.xml

Full lesson: XPath from the shell: xmllint and xmlstarlet →

XPath in the browser: document.evaluate

Namespaces and the comparison with CSS

// XPath on an XML document needs a resolver function
const NS = { svg: "http://www.w3.org/2000/svg", xlink: "http://www.w3.org/1999/xlink" };
const resolver = (prefix) => NS[prefix] || null;

const svg = await fetch("/diagram.svg").then(r => r.text());
const xml = new DOMParser().parseFromString(svg, "image/svg+xml");

const first = xml.evaluate("//svg:rect[1]", xml, resolver,
                           XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
first.setAttribute("fill", "tomato");

Namespaces and the comparison with CSS

// Selecting the table row that contains a label, then walking up
const row = document.evaluate(
  "//td[normalize-space(.)='Total']/ancestor::tr[1]",
  document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null
).singleNodeValue;
console.log(row.querySelector("td:last-child").textContent);

Full lesson: XPath in the browser: document.evaluate →

XPath 1.0, 2.0 and 3.1: what changed

What XPath 1.0 cannot express

<!-- Locating a book whose price is a two-decimal number: not possible in 1.0,
     where every value is a string or a double and a trailing zero is lost -->

Sequences, functions and types

<!-- XPath 2.0 and later -->
for $b in //book[price > 20]
return string-join(($b/title, $b/author), ' by ')

if (count(//book) gt 10) then 'long' else 'short'

//book[matches(title, '^The\s', 'i')]
//book[upper-case(@lang) eq 'EN']
distinct-values(//book/@lang)
xs:date(//book[1]/published) lt current-date()

//book[price castable as xs:decimal]

Sequences, functions and types

# Saxon gives you XPath 3.1 on the command line for a one-off query
java -cp saxon-he.jar net.sf.saxon.Query -qs:"count(//book[matches(title,'^The')])" catalog.xml

# xmllint and xmlstarlet remain XPath 1.0 and silently differ on edge cases

Full lesson: XPath 1.0, 2.0 and 3.1: what changed →

Robust XPath for scraping and testing

Why structural paths break

# A path generated by a browser devtools "Copy XPath"
/html/body/div[3]/div[2]/table/tbody/tr[4]/td[2]

# What the same thing looks like after one wrapper div is added
/div[1]/div[3]/div[2]/table/tbody/tr[4]/td[2]   # now broken

Full lesson: Robust XPath for scraping and testing →

XPath performance: cost, indexes and //

What actually costs time

# Measure rather than guess
time xmllint --xpath 'count(//item)' big.xml

# Narrow the context node so // has less to scan
time xmllint --xpath 'count(//feed/item)' big.xml

# A path that never leaves the top level is the cheapest form
time xmllint --xpath 'count(/*/*/item)' big.xml

Making a query fast

<!-- Keys give you a hash index you control -->
<xsl:key name="byId" match="item" use="@id"/>

<xsl:template match="/">
  <!-- one key call per lookup, instead of scanning every item -->
  <xsl:value-of select="key('byId', '42')/title"/>
</xsl:template>

Full lesson: XPath performance: cost, indexes and // →

CSS selectors, JSONPath and jq as alternatives

CSS versus XPath

from lxml import html

doc = html.fromstring("<ul><li class='on'>a</li><li>b</li></ul>")

print(doc.xpath("//li[@class='on']/text()"))        # XPath
print(doc.cssselect("li.on")[0].text)               # same result, shorter

# The one thing CSS cannot do here
print(doc.xpath("//li[.='b']/preceding-sibling::li/text()"))   # ['a']

JSONPath and jq

import json
from jsonpath_ng.ext import parse

data = json.load(open("books.json"))
expr = parse("$.items[?(@.price > 20)].title")
print([m.value for m in expr.find(data)])

Full lesson: CSS selectors, JSONPath and jq as alternatives →

FAQ

Is this XPath cheat sheet free to use?
Yes. No sign-up and no tracking: the page is static, every example is on the page itself, and you can print it or save it as a one-page reference.
Where do the examples come from?
Every snippet is taken from the 12 lessons of the XPath course on this site, and each section links back to the lesson it was pulled from.
How do I go deeper than a cheat sheet?
Open the full XPath course — it carries the worked explanations, the edge cases and the exercises behind every line here.

XML XSLT SOAP RESTful APIs RSS & Atom

Last refreshed 2026-09-27.