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

CSS selectors in Python query a parsed HTML (or XML) tree; the selector string does not download or parse a page. For most beginners, parse text with Beautiful Soup and call select() for every match or select_one() for the first match. If your project already uses lxml or needs XPath, use lxml.cssselect.CSSSelector. Python’s built-in html.parser can parse markup through callbacks, but it does not provide a CSS-selector query method.

What a CSS selector does in Python

A browser applies CSS selectors to a document tree. Python follows the same conceptual order:

  1. Obtain HTML text from a file, response, or other source.
  2. Parse that text into a document representation.
  3. Run a selector against the representation.
  4. Read text, attributes, or nested elements from the matches.

A selector cannot find content that is not present in the parsed markup. Interactive browser DOM, JavaScript-generated content, access controls, and a site’s extraction rules are separate concerns from selector syntax.

Beautiful Soup: the shortest path to CSS selectors

Install and parse HTML

Install Beautiful Soup with pip:

python -m pip install beautifulsoup4

Beautiful Soup’s current documentation says its CSS-selector implementation is Soup Sieve, installed along with Beautiful Soup through pip. The following complete example uses only an HTML string:

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

html = """
<main>
  <article class="story" data-kind="guide">
    <h2>Selectors</h2>
    <a href="/learn">Read more</a>
  </article>
</main>
"""

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

articles = soup.select("article.story[data-kind='guide']")
heading = soup.select_one("article.story h2")

print([article.get_text(" ", strip=True) for article in articles])
print(heading.get_text(strip=True) if heading else "No heading found")

select() returns a list of matching Tag objects, including an empty list when nothing matches. select_one() returns the first match or None. Check for None before reading a one-off result.

Common selector forms

Selector Meaning Example
article Elements by tag name soup.select("article")
.story Elements containing a class soup.select(".story")
#main The element with an ID soup.select_one("#main")
article.story Tag and class together soup.select("article.story")
[data-kind='guide'] Exact attribute value soup.select("[data-kind='guide']")
a[href] Elements possessing an attribute soup.select("a[href]")
[href^='/docs'] Attribute starts with text soup.select("a[href^='/docs']")
[href$='.pdf'] Attribute ends with text soup.select("a[href$='.pdf']")
[class*='card'] Attribute contains text soup.select("[class*='card']")
main article Descendant at any depth soup.select("main article")
main > article Direct child only soup.select("main > article")
li:nth-of-type(2) Second li among its siblings soup.select_one("li:nth-of-type(2)")

CSS classes are matched as class tokens, so .story matches an element whose class attribute includes story. For an attribute that may be absent, use tag.get("href") rather than indexing tag["href"].

Extract text and attributes safely

for link in soup.select("main article a[href]"):
    label = link.get_text(" ", strip=True)
    href = link.get("href")
    print(label, href)

get_text(" ", strip=True) joins nested text with spaces and removes surrounding whitespace. Keep the Tag object when you need to make a second query inside that element:

for card in soup.select("article.story"):
    title = card.select_one("h2")
    if title:
        print(title.get_text(" ", strip=True))

The .css interface and versions

Beautiful Soup documentation records Soup Sieve integration beginning in Beautiful Soup 4.7.0 and the .css property arriving in 4.12.0. Confirm the version installed in your project before relying on version-specific APIs. The familiar, explicit select() and select_one() methods work well across current code examples.

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.

Using CSS selectors with lxml

Install and translate a selector

lxml includes a CSS-selector convenience API that translates a selector into an XPath 1.0 expression for lxml’s XPath engine. Install the relevant packages:

python -m pip install lxml cssselect
from lxml import html
from lxml.cssselect import CSSSelector

markup = """
<main>
  <article class="story" data-kind="guide">
    <h2>Selectors</h2>
    <a href="/learn">Read more</a>
  </article>
</main>
"""

tree = html.fromstring(markup)
select_articles = CSSSelector("article.story[data-kind='guide']")
articles = select_articles(tree)

for article in articles:
    heading = article.cssselect("h2")
    print(" ".join(article.itertext()).strip())
    if heading:
        print(heading[0].text_content().strip())

You can also call lxml’s cssselect() method directly on an element. The separate cssselect translator project documents CSS3-to-XPath 1.0 translation; this means selector support is defined by the translator and XPath engine, not by a browser’s complete, ever-changing selector set.

When lxml is the better fit

  • Use lxml when the project already relies on its tree, XPath, or serialization APIs.
  • Use it when you want to switch between CSS selectors and XPath for complex structural queries.
  • Beautiful Soup’s documentation recommends lxml for a selector-only workflow and describes it as faster; that is qualitative project guidance, not a benchmark for every workload.

Choose based on parser behavior for malformed markup, supported selector features, dependencies, and whether your codebase already uses XPath.

What Python’s standard library provides

Python’s html.parser documentation describes an HTMLParser instance that is fed HTML and calls handler methods for start tags, end tags, text, comments, and other markup. A minimal parser looks like this:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
from html.parser import HTMLParser

class LinkParser(HTMLParser):
    def __init__(self):
        super().__init__()
        self.links = []

    def handle_starttag(self, tag, attrs):
        if tag == "a":
            self.links.append(dict(attrs).get("href"))

parser = LinkParser()
parser.feed('<a href="/learn">Read more</a>')
print(parser.links)

This callback approach is useful for a small, custom event stream, but there is no built-in select() or CSS query method. Build or use a tree and a selector library if CSS syntax is your requirement.

Debugging selectors that return nothing

Inspect the markup you actually parsed

Print a small fragment or serialize the tree. A selector that worked in browser developer tools may target a live DOM node created by JavaScript, while your Python input contains only the original response. Verify that the expected tag, class, attribute, and nesting exist in html before changing the selector.

Check class, attribute, and combinator details

  • .card matches a class token; [class='card'] requires the entire class attribute to equal exactly card.
  • A space means any descendant; > means an immediate child. Extra wrapper elements can therefore break a child selector.
  • Attribute values may use single or double quotes inside the selector string. Keep Python’s outer string quotes distinct or escape them.
  • select_one() returning None is a normal no-match result, not an exception.

Confirm implementation support

Selector support varies by implementation and installed version. Check the Beautiful Soup documentation or the cssselect documentation instead of assuming every browser selector is available.

Fetching, rendering, and policy boundaries

Selector code begins after you have HTML text. How that text was obtained—reading a file, making an HTTP request, or rendering a browser page—changes what can be selected. A server response may not contain content inserted after page load. Keep network authentication, JavaScript rendering, robots or terms-of-service requirements, and rate limits as separate decisions; the selector APIs themselves do not establish permission to extract a site or guarantee that a response equals an interactive browser DOM.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Performance, reliability, and maintainability

  • Parse once and run several selectors against the same tree rather than reparsing identical text.
  • Start with a narrow structural selector such as main article h2, then broaden it only when the markup requires.
  • Prefer stable attributes such as documented data attributes over auto-generated class names.
  • Use select_one() when you need one result; it communicates intent and avoids processing a full result list.
  • Handle missing elements and attributes explicitly so a markup change produces a controlled result.
  • Pin and review dependency versions when selector behavior matters; Beautiful Soup’s Soup Sieve integration and .css property have documented version milestones.
  • For lxml, retain the compiled CSSSelector object when applying the same query repeatedly.

Or skip the browser setup

If your real goal is to obtain a clean screenshot of a page before inspecting it, ScreenshotNeo provides a website screenshot API and MCP server. 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 or CAPTCHAs, 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.

One GET request returns PNG, JPEG, WebP, or PDF. See the ScreenshotNeo documentation for all options.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
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)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

It also offers an MCP server for Claude, Cursor, and other MCP clients, with take_screenshot, get_page_info, and capture_pdf tools. Every plan includes features such as full-page and element capture, device and retina settings, custom CSS or JavaScript, waits, request blocking, cookies and headers, geolocation, caching, signed links, asynchronous webhooks, bulk capture of up to 100 URLs per call, and a usage API.

The Free plan includes 1,000 screenshots per month with no card. Paid plans start at $5 for 3,000 shots; yearly billing gives two months free. Create a free ScreenshotNeo account.

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

Frequently Asked Questions

Can I use a CSS selector without Beautiful Soup or lxml?

Not as a built-in tree query. Python’s html.parser reports parser events through callbacks; add a selector-capable library or implement your own tree and query layer.

Why does a selector work in DevTools but not in Python?

Your Python input may differ from the browser’s live DOM, or the installed library may not support that selector feature. Inspect the parsed markup and consult the implementation’s documentation.

Should I learn XPath as well as CSS selectors?

If you use lxml, yes: CSSSelector translates CSS to XPath 1.0, and XPath can express relationships that are awkward in CSS. Beautiful Soup users can stay with CSS selectors and its tree navigation APIs.

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.

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