Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.
Pass a tag-level predicate to find_all() and return the opposite of has_attr(). For example, soup.find_all('a', lambda tag: not tag.has_attr('target')) returns every link that does not have a target attribute. A tag-level function is important because it receives the complete tag, so it can test both presence and absence safely.
The basic pattern
Define a function that receives a BeautifulSoup Tag and returns True only when the attribute is missing. Then pass that function as the second argument to find_all().
from bs4 import BeautifulSoup
html = '''
No target
Has target
Also no target
'''
soup = BeautifulSoup(html, 'html.parser')
def lacks_target(tag):
return not tag.has_attr('target')
matches = soup.find_all('a', lacks_target)
for link in matches:
print(link.get('href'))
The output is /one and /three. Replace 'a' with the element name you need, such as 'img', 'div' or 'input'.
Do these 3 things before closing this tab:
1Fix the driver behind crashes, sound loss and screen glitches2Clear out junk files and repair common Windows errors3Scan for outdated or missing drivers - takes under a minuteSearch every tag type
Omit the name argument when the predicate itself determines which tags qualify:
#1 Best Overall
matches = soup.find_all(lacks_target)
This searches all descendants and returns a list-like ResultSet. If nothing matches, the result is an empty collection rather than an exception.
Why the predicate must receive the whole tag
BeautifulSoup supports callables in two different places, and they receive different inputs:
| Form | Callable receives | Use it for |
|---|---|---|
soup.find_all(predicate) |
The complete Tag |
Checking whether attributes exist, combining several attributes, inspecting tag names or reading other tag data |
soup.find_all(href=predicate) |
The value of href |
Testing an existing attribute value, such as whether a URL contains a substring |
An attribute-specific function cannot reliably answer “does this tag lack target?” because it is given an attribute value, not the tag object. Use a tag-level predicate whenever the condition depends on attribute presence or on more than one property.
PC Slower Than It Used to Be?
A free scan shows the junk files, broken settings and background clutter dragging Windows down - then fixes them in one click.Free scan · Windows 10 & 11Outdated Drivers Are Slowing You Down
One free scan finds every outdated or missing driver and matches the right update for your exact hardware.Free scan · exact hardware matchUseful absence and presence predicates
Require that an attribute is absent
def without_data_id(tag):
return not tag.has_attr('data-id')
items = soup.find_all('li', without_data_id)
has_attr() tests whether the attribute exists, even if its value is an empty string. The not operator therefore expresses absence directly.
Require one attribute but exclude another
The same shape handles compound rules. This example finds links with a class but no id:
def has_class_but_no_id(tag):
return tag.has_attr('class') and not tag.has_attr('id')
matches = soup.find_all('a', has_class_but_no_id)
Both conditions must be true. Add more clauses with and when a scraper has stricter requirements.
Match a tag name inside the function
When you omit the name argument, check tag.name to avoid matching unrelated elements:
Quick wins for a faster PC:
Repair Windows errors before they cause bigger problemsFix Now →Scan for outdated or missing drivers - takes under a minuteDriver Scan →def image_without_alt(tag):
return tag.name == 'img' and not tag.has_attr('alt')
missing_alt = soup.find_all(image_without_alt)
Use a reusable predicate factory
If the attribute name is configurable, return a function that closes over it:
def missing_attribute(attribute_name):
def predicate(tag):
return not tag.has_attr(attribute_name)
return predicate
without_role = soup.find_all(missing_attribute('role'))
without_title = soup.find_all(missing_attribute('title'))
This keeps the matching logic in one place while allowing different audits.
Read optional attributes without exceptions
Finding a tag without an attribute is only half the job; you may then need to inspect another attribute. Avoid subscription such as tag['target'] when the key might not exist. Subscription raises KeyError for a missing attribute.
for link in soup.find_all('a'):
target = link.get('target')
href = link.get('href', '')
print(href, target)
get() returns None by default, or a fallback you provide:
label = link.get('aria-label', '(no label)')
Use has_attr() when you need to distinguish “missing” from “present with an empty value.” Use get() when you simply need a safe value.
Rank #3
A complete, runnable audit script
The following program reads a local file, reports links lacking target, and separately reports links that have a class but no id. It does not assume either optional attribute exists.
from pathlib import Path
from bs4 import BeautifulSoup
html = Path('page.html').read_text(encoding='utf-8')
soup = BeautifulSoup(html, 'html.parser')
def lacks_target(tag):
return tag.name == 'a' and not tag.has_attr('target')
def class_without_id(tag):
return tag.name == 'a' and tag.has_attr('class') and not tag.has_attr('id')
for link in soup.find_all(lacks_target):
print('Missing target:', link.get('href', '(no href)'))
for link in soup.find_all(class_without_id):
classes = ' '.join(link.get('class', []))
print('Class but no id:', classes, link.get('href', '(no href)'))
Install BeautifulSoup with the package that provides the bs4 import, then run the script from the directory containing page.html. Change the parser to one installed in your environment if your input requires it.
find_all(), find() and search scope
Get every match
find_all() traverses descendants and returns all matching tags. Iterating over the result is safe even when there are no matches:
for tag in soup.find_all('a', lacks_target):
process(tag)
Get only the first match
Use find() when the first qualifying element is sufficient. It returns a single Tag or None:
first = soup.find('a', lacks_target)
if first is not None:
print(first.get('href'))
Limit the search to a subtree
Locate a container first, then search only inside it. This reduces accidental matches elsewhere in the document:
main = soup.find('main')
links = main.find_all('a', lacks_target) if main else []
You can also pass recursive=False to search only direct children of a tag:
direct_children = soup.find_all('li', lacks_target, recursive=False)
Use that option when nested descendants should not count.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
CSS selectors as an alternative
BeautifulSoup delegates CSS selection to SoupSieve. A selector such as a:not([target]) expresses the same rule concisely:
links = soup.select('a:not([target])')
Documentation describes support for most CSS4 selectors from BeautifulSoup 4.7.0 onward, through SoupSieve. Check the versions installed in your environment before relying on a newer selector. The callable form is clearer when you need compound Python logic, custom validation, or compatibility with older setups.
When CSS is the better choice
- Use
select()for a short, declarative selector that your team already understands. - Use a callable when you must combine several tests, normalize values, or run Python code against the entire tag.
- Keep a callable for rules that are difficult to express or review as a CSS selector.
Common mistakes and fixes
Indexing a missing attribute
Symptom: KeyError at tag['target'].
Cause: The tag does not contain that attribute.
Fix: Test with has_attr(), or read with get().
Using an attribute-level callable for absence
Symptom: A predicate written as href=predicate cannot inspect the tag or behaves unexpectedly.
Cause: Attribute filters receive the attribute value only.
Fix: Pass the predicate directly to find_all() so it receives the complete tag.
Recommended Free Tools
Matching the wrong element types
Symptom: Results include tags you did not intend to audit.
Cause: Calling soup.find_all(predicate) searches every tag.
Fix: Supply the tag name, or check tag.name inside the function.
Best Value
Confusing an empty value with absence
Symptom: A tag such as <div data-id=""> is treated as if the attribute were missing.
Cause: Testing the value rather than the attribute key.
Fix: Use tag.has_attr('data-id') for presence; use tag.get('data-id') when the value itself matters.
Unexpectedly broad results
Symptom: A valid-looking predicate returns too many matches.
Cause: The search starts at the document root and includes nested widgets, templates or unrelated sections.
Fix: Search from a known container, or use recursive=False for direct children.
Performance and reliability considerations
For ordinary documents, a single find_all() traversal is straightforward and predictable. Keep the predicate cheap: check the tag name and attributes first, and avoid expensive parsing or network work inside it. If you need several reports, consider one pass that classifies tags rather than repeatedly scanning the entire document.
Malformed HTML can produce a tree different from the source text. Choose a parser appropriate to your input and test predicates against representative pages, including tags with empty attributes, duplicated attributes, nested elements and missing containers. Treat a zero-result collection as a normal outcome; decide in your application whether that means “nothing to report” or a validation failure.
Or skip the browser setup
If your broader workflow needs rendered page images rather than parsed HTML, ScreenshotNeo provides a website screenshot API. It accepts consent banners before capture and removes more than 60 known consent platforms, newsletter popups and chat widgets; each step can be disabled. Bot checks, 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.
Use the API documentation at https://screenshotneo.com/docs/ for authentication and options. A single GET request is enough:
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,
)
r.raise_for_status()
open('shot.webp', 'wb').write(r.content)
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}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const fs = await import('node:fs/promises');
await fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));
ScreenshotNeo also offers an MCP server with take_screenshot, get_page_info and capture_pdf tools for Claude, Cursor and other MCP clients. One thousand screenshots per month are free with no card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account to get started.
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 →Frequently Asked Questions
Are HTML attribute names case-sensitive in this test?
BeautifulSoup normalizes parsed HTML into its tag and attribute model, but behavior can differ for XML parsing. Use the parser appropriate to your document type and test a sample containing the casing you expect.
Can I combine a missing-attribute test with a value check?
Yes. Return one Boolean expression from the tag-level predicate, checking each required condition with and and excluding conditions with not.
What should I do when the parent container is missing?
Check the result of find() before calling find_all() on it; a missing parent is represented by None.
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.

