What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.

Use Beautiful Soup’s string= filter. Call soup.find_all(string="Exact text") when you need matching text nodes, or add a tag name—such as soup.find_all("a", string="Exact text")—when you need tags whose .string matches. For partial text, pass a compiled regular expression. The distinction matters: the first form returns strings, while the second returns tags.

Start with the result you actually need

“Find an element by text” can mean two different operations. A text node is the string object inside the document. An element is the surrounding tag, such as an <a>, <button> or <h2>. Beautiful Soup uses the same string= filter for both, but the presence of a tag name changes what is returned.

Need Code Returned value
Exact text node soup.find_all(string="Elsie") Matching string objects
Tag whose .string is exact soup.find_all("a", string="Elsie") Matching <a> tags
Text containing a pattern soup.find_all(string=re.compile("Dormouse")) String objects that satisfy the regular expression
Element identified structurally soup.select("article h2") or soup.find_all("h2", class_="title") Tags selected by markup structure or attributes

The current parameter name is string. Beautiful Soup documentation says it was introduced in version 4.4.0; older releases used text.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Exact text matching

Find the text node

Pass only string= when the text itself is what you want:

from bs4 import BeautifulSoup

html = '<p>Hello <b>world</b></p><a>Elsie</a>'
soup = BeautifulSoup(html, "html.parser")

matches = soup.find_all(string="Elsie")
print(matches)
# ['Elsie']

Each item in matches is a text string, not its parent element. This is useful when you want to inspect, replace or count the exact text node.

Find a tag whose string is exact

Put the tag name first when you need the element:

links = soup.find_all("a", string="Elsie")

for link in links:
    print(link.name, link.get("href"), link.string)
# a None Elsie

This asks Beautiful Soup for <a> tags whose .string matches Elsie. Replace "a" with the element you expect, such as "button", "h2" or "li".

Partial text and regular expressions

A literal string is appropriate for an exact value. For text that contains a word, varies by suffix, or follows a pattern, pass a compiled regular expression:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
import re
from bs4 import BeautifulSoup

html = '<p>The Dormouse was here.</p><p>Another animal.</p>'
soup = BeautifulSoup(html, "html.parser")

matches = soup.find_all(string=re.compile("Dormouse"))
for value in matches:
    print(value)
# The Dormouse was here.

The regular-expression filter uses search behavior, so the pattern can match a substring instead of requiring the whole string to be identical. You can make the expression stricter yourself when that is important:

exact = soup.find_all(string=re.compile(r"^The Dormouse was here.$"))
starts_with = soup.find_all(string=re.compile(r"^The Dormouse"))

Remember that these calls still return strings. To obtain the containing tag, use the matched string’s parent or perform a tag search with a suitable structure, depending on how the markup is built.

What string= matches

It matches a string or a tag’s .string

With no tag name, Beautiful Soup tests individual text strings. With a tag name, it tests that tag’s .string property. The property is a single string only when the tag has one direct string value. Consider this markup:

html = '<p>Hello <b>world</b></p>'
soup = BeautifulSoup(html, "html.parser")

print(soup.find_all(string="Hello "))
# ['Hello ']
print(soup.find_all("p", string="Hello world"))
# []

The paragraph contains a separate text node and a nested <b> tag, so its .string is not the combined phrase Hello world. In nested markup, select the reliable structural element first and then inspect normalized descendant text:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
paragraph = soup.find("p")
if paragraph and paragraph.get_text(" ", strip=True) == "Hello world":
    print("The paragraph contains the complete phrase")

Do not assume that string= automatically searches the result of get_text(), or that it normalizes whitespace. Exact matching is sensitive to the actual string values in the parsed tree. If spacing or line breaks are uncertain, inspect the value with repr() or normalize it after selecting the relevant node.

Other filters accepted by string=

The filter can be a literal string, regular expression, list, callable or True. These forms are useful when a single exact value is not enough.

Several permitted values

allowed = soup.find_all(string=["Elsie", "Lacie"])

This returns text nodes equal to one of the listed values.

A callable for custom logic

def starts_with_hello(value):
    return value is not None and value.strip().startswith("Hello")

matches = soup.find_all(string=starts_with_hello)

The callable receives a candidate string. Return a truthy value for strings you want to keep. Guard against None when your function may be used with values that are not ordinary text.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Any available string

all_strings = soup.find_all(string=True)

Use this when you need to examine every text node yourself. It is often the simplest diagnostic when a supposedly exact match is failing because of whitespace, punctuation or nested markup.

Text matching versus CSS selectors

Text is useful when the visible wording is the most stable identifier. It is fragile when a site changes labels, localizes content, adds whitespace or renders different copy for different users. If the HTML has a dependable ID, class, data attribute or hierarchy, a structural query is usually easier to maintain:

heading = soup.select_one("article[data-id='42'] h2")
buttons = soup.find_all("button", class_="primary")

Beautiful Soup’s CSS selector support is implemented through Soup Sieve. The documentation notes that lxml is faster when CSS selectors are all you need, so consider lxml when selector-only processing and throughput are the priority. Keep string= when the requirement is genuinely content-based, such as locating a notice whose wording is known but whose classes are not.

A complete, defensive example

The following script demonstrates the three common outcomes: exact text nodes, exact tags and a partial match. It also handles the case where nothing was found.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
import re
from bs4 import BeautifulSoup

html = """
<nav>
  <a href='/about'>About</a>
  <a href='/contact'>Contact us</a>
</nav>
<main>
  <h2>Contact us</h2>
  <p>Send questions to the support team.</p>
</main>
"""

soup = BeautifulSoup(html, "html.parser")

# 1. Exact text nodes (returns strings)
contact_text = soup.find_all(string="Contact us")
print("Text nodes:", contact_text)

# 2. Exact anchor tags (returns tags)
contact_links = soup.find_all("a", string="Contact us")
for link in contact_links:
    print("Link:", link.get("href"))

# 3. Any text containing 'support' (returns strings)
support_text = soup.find_all(string=re.compile("support", re.IGNORECASE))
print("Support matches:", support_text)

if not contact_links:
    print("No matching link: inspect spelling, whitespace and nesting.")

When the same words appear in several places, the tag argument narrows the result. If you need one item, take the first result only after checking that the list is non-empty; otherwise, iterate over all matches so duplicate labels are not silently discarded.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Troubleshooting no-match and wrong-result problems

Symptom Likely cause Fix
find_all(string="...") returns an empty list The text differs by capitalization, punctuation, whitespace or a line break. Print candidate values with repr(), then use the exact value or a regular expression that reflects the variation.
A text search finds a value but not the expected element You used the string-only form, which returns text nodes. Add the tag name, or use the matched string’s parent when that is the element you need.
A tag search returns nothing even though the browser displays the phrase The phrase is assembled from nested tags, so the tag’s .string is not the complete visible text. Select the containing structure first and compare tag.get_text(...) after your own normalization.
A regex matches more items than expected Regular-expression matching searches for the pattern within each string. Anchor the expression with ^ and $, escape punctuation, or add a tag/attribute constraint.
A selector works in one environment but behaves differently in another Your installed Beautiful Soup version or parser may differ from the current documentation examples. Check the installed version, parser choice and actual parsed tree before changing the filter.
The result is structurally unstable The query relies on display text that changes, while a class, ID or data attribute is stable. Prefer select() or find_all() with structural attributes, and reserve text matching for content-driven cases.

Version and parser considerations

Use string= in current code. The Beautiful Soup guide identifies 4.4.0 as the release where that name was introduced; code written for earlier releases may use the former text argument. The behavior you observe can also depend on the Beautiful Soup version and parser installed in your environment, so test against the same setup that will run in production.

For reliable automation, keep a small fixture containing the markup patterns you expect: a plain text tag, a tag with nested markup, extra whitespace and duplicate labels. Assert both the number of matches and the returned object type. That catches accidental changes from a string query to a tag query before they affect downstream code.

Or skip the browser setup

Beautiful Soup parses HTML you already have; it does not fetch pages or render JavaScript. If your goal is to obtain a clean page image while investigating a site, ScreenshotNeo provides a single website-screenshot API call. It accepts cookie and consent banners before capture and removes more than 60 known consent platforms, newsletter popups and chat widgets; each step can be turned off. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads and cache hits are not billed, and the response identifies the page verdict and billing status in X-Page-Verdict and X-Billed headers.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Use the API documentation at https://screenshotneo.com/docs/ for the complete option list. A basic request for a WebP image is:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

The equivalent Python request is:

import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)

And in 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}`);

ScreenshotNeo also exposes an MCP server with take_screenshot, get_page_info and capture_pdf tools for Claude, Cursor and other MCP clients. Every feature is available on every plan: 1,000 screenshots per month are free with no card, and paid plans start at $5 for 3,000 screenshots. If that fits your workflow, sign up for the free ScreenshotNeo plan.

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.