The XPath data model: nodes, order and values
XPath does not see your file, it sees a tree of seven node types. Knowing what each node contains explains most surprising query results.
The seven node types
An XPath expression is evaluated against a tree, not against text. The tree is built by the parser and normalised: whitespace between tags becomes text nodes, attribute order is not significant, and every value you can select is one of exactly seven node kinds.
| Node type | Selected by | String-value |
|---|---|---|
| Root | / | Concatenated text of the whole document |
| Element | //book | All descendant text, tags stripped |
| Attribute | //book/@isbn | The attribute value |
| Text | //title/text() | The characters themselves |
| Comment | //comment() | Comment body without the markers |
| Processing instruction | //processing-instruction() | Everything after the target name |
| Namespace | //namespace::* | The namespace URI |
The consequence that trips people up: an element's string-value is the concatenation of all its descendant text, so string(//div) on a container returns everything inside, not the container's own words. Element nodes do not have a separate "own text" value.
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- Node-sets are always in document order and are never duplicated, so
//a | //ais just//a. - A leading
/means the root node; a path with no leading slash is relative to the context node. //xis shorthand for/descendant-or-self::node()/child::x, which is why it is cheap to write and expensive to run.ancestor,precedingandfollowingaxes are in reverse document order as axes, but a node-set returned from them is still sorted into document order.
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)")) # FalseXPath 1.0 has four data types: node-set, string, number and boolean. There is no separate integer, no date and no null. Predicate expressions convert their result to a boolean using these rules: an empty node-set is false, a non-empty one is true, a number is false only when it is zero or NaN, an empty string is false, and NaN compares false against everything including itself.
< and > compare as numbers when at least one side is a number, but as strings when both sides are node-sets. //n[. > 9] and //n[. > '9'] can therefore disagree, and '2' > '10' is true because it is a string comparison.FAQ
Why does count(//p) return a decimal?
Does whitespace-only text count as a text node?
Related
Location paths and axes Selecting attributes, text and special nodes
Last refreshed 2026-09-18.