Use (//p)[1]/text()[last()] for the last direct text node in the first paragraph, or (//p)[last()]/text()[last()] for the last direct text node in the document’s final paragraph. Selenium’s normal find_element API is intended to return elements, not text nodes, so evaluate the XPath in JavaScript (or read the paragraph’s childNodes) when you need the node value itself.
The XPath you need
These expressions separate two decisions: which paragraph is the context, and which text child is selected from it.
| Goal | XPath |
|---|---|
| Last direct text node in the first paragraph | (//p)[1]/text()[last()] |
| Last direct text node in the final paragraph in the document | (//p)[last()]/text()[last()] |
| Last non-whitespace direct text node in the first paragraph | (//p)[1]/text()[normalize-space()][last()] |
| Last direct text node of an already-selected paragraph | ./text()[last()] |
| Last descendant text node, including text inside nested elements | (.//text())[last()] |
The parentheses around //p matter when you mean the final paragraph across the complete result set. The expression //p[last()] applies last() within each applicable parent context, so a page with several containers can return the last paragraph in more than one container.
What each XPath step means
Selecting paragraphs
//p finds paragraph elements anywhere below the document context. (//p)[1] groups that complete node set and then chooses its first item. (//p)[last()] similarly chooses the final paragraph in document order. The grouping prevents the position predicate from being evaluated separately for each parent.
Free tools Windows power users keep installed
One-click scans. No signup required.
#1 Best Overall
Selecting direct text children
text() is the XPath child axis for text nodes. It does not include text held by nested elements. In this markup:
<p>Read <em>all</em> of this sentence.</p>
the paragraph has a text node containing “Read ”, an em element, and a text node containing “ of this sentence.” The final direct text node is the latter. Text inside em is not part of text().
Choosing the final node
[last()] is evaluated against the current step’s context size. After /text(), that context contains only the paragraph’s direct text children, so text()[last()] selects the final one.
Ignoring formatting whitespace
Indentation and line breaks can create text nodes whose value is only whitespace. Add [normalize-space()] before [last()] to filter those nodes:
The Tool Desk
Outbyte PC Repair FREERepair Windows errors before they cause bigger problemsFix Now →Outbyte Driver Updater FREEFix the driver behind crashes, sound loss and screen glitchesFind Drivers →(//p)[1]/text()[normalize-space()][last()]
normalize-space() tests whether a node has non-whitespace content; it does not merge separate DOM nodes or alter the value returned for the selected node.
Why a direct Selenium XPath lookup can fail
Selenium documents By.XPATH as an element locator (“Select the element via XPATH”). A text node is not a WebElement. Consequently, calling find_element(By.XPATH, "(//p)[1]/text()[last()]") may produce an invalid-selector error in browser WebDriver implementations. Select the paragraph element first, then run a DOM XPath evaluation in the page.
Rank #2
Python Selenium: return the text node value
This complete example opens a page, selects its first paragraph, evaluates a relative XPath, and returns either the node’s stored value or None when no matching direct text node exists.
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.chrome.options import Options
options = Options()
# options.add_argument("--headless=new") # enable in CI if required
driver = webdriver.Chrome(options=options)
try:
driver.get("https://example.com")
paragraph = driver.find_element(By.XPATH, "(//p)[1]")
last_text = driver.execute_script(
"""
const result = document.evaluate(
'./text()[last()]',
arguments[0],
null,
XPathResult.FIRST_ORDERED_NODE_TYPE,
null
).singleNodeValue;
return result ? result.nodeValue : null;
""",
paragraph,
)
print(last_text)
finally:
driver.quit()
Use './text()[normalize-space()][last()]' in the script when indentation-only nodes must be ignored. The relative ./ is important: arguments[0] is the paragraph element passed as the evaluation context.
Do these 3 things before closing this tab:
1Repair Windows errors before they cause bigger problems2Fix the driver behind crashes, sound loss and screen glitches3Clear out junk files and repair common Windows errorsSelecting the final paragraph instead
To locate the document’s last paragraph before evaluating its children, change the element lookup:
paragraph = driver.find_element(By.XPATH, "(//p)[last()]")
last_text = driver.execute_script(
"""
const result = document.evaluate(
'./text()[normalize-space()][last()]',
arguments[0], null,
XPathResult.FIRST_ORDERED_NODE_TYPE, null
).singleNodeValue;
return result ? result.nodeValue : null;
""",
paragraph,
)
Returning the complete node object
JavaScript can return the node’s value, name, or other DOM properties, but WebDriver’s JSON conversion is not a reliable way to expose a browser text-node object as a Selenium element. Return primitive data such as nodeValue and process it in Python.
Alternative: inspect childNodes directly
When the requirement is explicitly “the final direct text child,” filtering childNodes is often easier to debug than XPath:
last_text = driver.execute_script(
"""
const nodes = [...arguments[0].childNodes]
.filter(node => node.nodeType === Node.TEXT_NODE && node.nodeValue.trim());
return nodes.length ? nodes[nodes.length - 1].nodeValue : null;
""",
paragraph,
)
This preserves the node’s original whitespace around meaningful text while excluding nodes whose trimmed value is empty. If whitespace-only nodes are significant to your test, remove the node.nodeValue.trim() condition.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
Rank #3
Direct children versus all descendant text
Choose the axis based on the content you actually want:
text(): direct text-node children only; text nested inspan,em, links, or other descendants is excluded..//text(): every descendant text node below the context element, including nested inline elements.(.//text())[last()]: the final descendant text node in document order.
For example, with <p>Start <span>middle</span> finish</p>, ./text()[last()] returns the direct node containing “ finish”, while (.//text())[last()] also returns “ finish” in this case but would return nested text when the final content is inside an element. Do not substitute .//text() when the test is specifically about direct children.
Common failure modes and fixes
Invalid selector or unsupported return type
Symptom: InvalidSelectorException from find_element. Cause: the XPath result is a text node, not an element. Fix: locate the p element and call execute_script with document.evaluate.
The result is None
Cause: the paragraph has no direct text children, or every direct text child is whitespace after filtering. It may contain only nested elements. Fix: decide whether descendant text is intended and use (.//text())[last()], or handle the missing direct node as an expected case.
The “last paragraph” is not the page’s last paragraph
Cause: //p[last()] is evaluated per parent context. Fix: use (//p)[last()] for one document-wide final paragraph, then inspect that element.
Unexpected blank or newline text
Cause: HTML formatting created whitespace-only text nodes. Fix: use text()[normalize-space()][last()] or the equivalent childNodes filter.
Text appears missing after a nested element
Cause: text() deliberately excludes descendants. Fix: use (.//text())[last()] if the final text may be inside an inline element, or keep the direct-child XPath if nested content must not count.
Dynamic content has not loaded
Cause: the paragraph is inserted or changed after the initial page load. Fix: wait for the paragraph element or a page-specific condition before evaluating its children. Re-fetch the element after a navigation or DOM replacement because a previously stored Selenium element can become stale.
Recommended Free Tools
Reliable test design
Wait for the element, not an arbitrary sleep
Use an explicit wait for the paragraph’s presence or visibility, then evaluate the node. A fixed delay can be too short on a slow run and waste time on a fast one.
from selenium.webdriver.support.ui import WebDriverWait
paragraph = WebDriverWait(driver, 15).until(
lambda d: d.find_element(By.XPATH, "(//p)[1]")
)
Compare normalized values deliberately
If layout whitespace is irrelevant, normalize in Python:
expected = "Final sentence"
actual = " ".join((last_text or "").split())
assert actual == expected
If exact source whitespace is part of the requirement, compare last_text without normalization and document why.
Account for DOM structure changes
Templates may add introductory paragraphs, cookie notices, or hidden content. A positional selector such as (//p)[1] is correct only when document order is stable. Prefer a semantic container, for example (//article//p)[1], when the test targets article content. Scope the paragraph lookup first, then apply ./text() relative to that element.
Best Value
Or skip the browser setup
If your goal is a clean capture rather than DOM-node testing, ScreenshotNeo returns a screenshot or PDF from one request. It accepts cookie and consent banners before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each cleanup step can be disabled. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies the result with X-Page-Verdict and X-Billed headers. Its MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients.
Using the API requires no Selenium driver:
curl -G "https://api.screenshotneo.com/v1/shot"
-d access_key=YOUR_API_KEY
--data-urlencode url=https://stripe.com
-o shot.webp
See the ScreenshotNeo documentation for output formats and options. The same endpoint supports full-page lazy-image loading, CSS-selector element capture, device and viewport settings, retina scale, PDF paper and page controls, custom CSS and JavaScript, clicks, selector waits, delay or network-idle waits, request and resource blocking, headers, cookies, user agents, authorization, timezone, geolocation, transparent backgrounds, resizing, chosen-TTL caching, signed links, asynchronous webhooks, bulk capture of up to 100 URLs per call, a usage API, and an OpenAPI specification. Common screenshot-API parameter names also work when migrating.
For Python:
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
For Node.js:
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = Buffer.from(await res.arrayBuffer());
require('fs').writeFileSync('shot.webp', data);
Every feature is included on every plan: 1,000 screenshots per month are free with no card; paid plans start at $5 for 3,000 shots, with yearly billing giving two months free. Create a free ScreenshotNeo account to try the 1,000-shot allowance.
Quick decision guide
- Need the final direct child only? Use
./text()[last()]. - Need the final direct child while ignoring formatting whitespace? Use
./text()[normalize-space()][last()]. - Need the final paragraph in the entire document? Select
(//p)[last()]first. - Need text nested in inline elements? Use
(.//text())[last()]. - Need the value in Selenium Python? Return
nodeValuethroughexecute_script, rather than locating the text node as a WebElement.
Frequently Asked Questions
Does XPath return the text node’s visible text or its source value?
The XPath result is a DOM text node. Reading its nodeValue returns the node’s stored character data, including its original surrounding whitespace; visibility and CSS rendering are separate concerns.
Quick wins for a faster PC:
Fix the driver behind crashes, sound loss and screen glitchesFind Drivers →Repair Windows errors before they cause bigger problemsFix Now →Scan for outdated or missing drivers - takes under a minuteDriver Scan →Can I use this technique with Selenium Java or JavaScript bindings?
Yes. The browser-side document.evaluate expression is the same; only the binding’s script-execution method and string syntax change.
Why might two adjacent pieces of visible text be separate nodes?
DOM edits, inline elements, entity boundaries, and template rendering can split one visual sentence into multiple text nodes. XPath positions nodes, not visually merged strings.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

