Skip to content
Python Web Scraping

How to Parse HTML in Python

Pim

Pim · Scrappey Research

June 8, 2026 5 min read

How to Parse HTML in Python — conceptual illustration
On this page

To parse HTML in Python you load the markup into a parser that turns it into a navigable tree, then select the elements you want with CSS selectors or XPath. The most popular parser is BeautifulSoup for its forgiving, beginner-friendly API; lxml is the fast workhorse with full XPath support; and selectolax is the fastest option for high-volume parsing. The standard library also ships html.parser, but a dedicated library is almost always the better choice.

Quick facts

Easiest to learnBeautifulSoup (beautifulsoup4) — forgiving, readable API
Fastest with XPathlxml — C-backed, supports CSS and XPath
Fastest overallselectolax — Modest/Lexbor engine, ideal at scale
No install neededhtml.parser (stdlib) — basic, slower, less robust
SelectorsCSS selectors (all) or XPath (lxml, parsel)

HTML is a tree

Before parsing, it helps to see HTML as what it is: a nested tree of elements. <html> contains <body>, which contains <div>s, which contain <p>s and <a>s. A parser reads the raw markup string and builds this tree (the DOM) in memory, so instead of hunting through text with fragile string operations or regex, you navigate: "find every <a> inside the element with class product."

You select nodes in that tree two ways: CSS selectors (div.product > a) which every library below supports, or XPath (//div[@class="product"]/a) which lxml and parsel support and which can do things CSS cannot, like selecting by visible text or walking back up to a parent. See our XPath for web scraping guide for the full syntax.

Do not parse HTML with regular expressions. HTML is not a regular language; nested tags, optional attributes, and broken markup will break any regex eventually. Use a real parser.

BeautifulSoup — the beginner-friendly default

BeautifulSoup wraps a parser (use lxml as the backend for speed) in a famously forgiving API. Install with pip install beautifulsoup4 lxml.

from bs4 import BeautifulSoup

html = '''
<div class="product">
  <h2>Wireless Mouse</h2>
  <span class="price">$24.99</span>
  <a href="/p/123">details</a>
</div>
'''

soup = BeautifulSoup(html, 'lxml')

# By tag, by class, by CSS selector:
print(soup.h2.text)                       # Wireless Mouse
print(soup.find('span', class_='price').text)   # $24.99
print(soup.select_one('.product a')['href'])    # /p/123

# Loop over many elements:
for a in soup.select('a[href]'):
    print(a['href'], a.text.strip())

Key methods: find() / find_all() for tag-and-attribute matching, select() / select_one() for CSS selectors, .text for inner text, and ['attr'] for attributes. It tolerates broken, real-world HTML gracefully, which is why it is the most common starting point.

lxml — fast parsing with XPath

lxml is a C-backed library that is both fast and the standard way to use XPath in Python. Install with pip install lxml.

from lxml import html as lxml_html

doc = lxml_html.fromstring(html)   # same markup as above

# CSS selectors:
print(doc.cssselect('.product h2')[0].text_content())   # Wireless Mouse

# XPath — selecting by attribute, text, or relationship:
print(doc.xpath('//span[@class="price"]/text()')[0])     # $24.99
print(doc.xpath('//a/@href')[0])                          # /p/123

# XPath can select by visible text (CSS cannot):
link = doc.xpath('//a[contains(text(), "details")]/@href')
print(link)   # ['/p/123']

Reach for lxml when you need XPath’s power (selecting by text, axes like parent:: / following-sibling::) or when BeautifulSoup is too slow. Many people use both: BeautifulSoup with features='lxml' gives the friendly API on top of the fast parser.

selectolax — the fastest parser

When you are parsing millions of pages, parser speed matters. selectolax wraps the C-based Modest/Lexbor engine and is typically several times faster than lxml and an order of magnitude faster than BeautifulSoup. Install with pip install selectolax.

from selectolax.parser import HTMLParser

tree = HTMLParser(html)   # same markup as above

print(tree.css_first('h2').text())                 # Wireless Mouse
print(tree.css_first('.price').text())             # $24.99
print(tree.css_first('.product a').attributes['href'])   # /p/123

for node in tree.css('a[href]'):
    print(node.attributes['href'], node.text(strip=True))

The API is CSS-selector based (css() / css_first()). It does not support XPath, but for the common case of CSS-selecting at high volume it is the fastest option in Python. Most competitor guides never mention it — it is the easiest performance win at scale.

Which parser should you use?

ParserSpeedSelectorsBest forInstall
BeautifulSoupSlowestCSS, find()Learning, readable code, messy HTMLbeautifulsoup4
lxmlFastCSS + XPathXPath power, speedlxml
selectolaxFastestCSSHigh-volume parsingselectolax
html.parserSlowvia BeautifulSoupZero dependenciesstdlib
parselFastCSS + XPathScrapy projectsparsel

Rule of thumb: start with BeautifulSoup (on the lxml backend) for readability, switch to lxml directly when you need XPath, and to selectolax when speed at scale matters.

One caveat that no parser fixes: you can only parse HTML you actually received. If the page is JavaScript-rendered, the data will not be in the HTML at all, and if the site blocks you, you will be parsing a CAPTCHA or 403 page. A scraping API like Scrappey returns the fully rendered HTML so your parser always has real markup to work with.

Code example

python
# Get real, rendered HTML first -- then parse it with any library.
import requests
from bs4 import BeautifulSoup

resp = requests.post(
    'https://publisher.scrappey.com/api/v1?key=YOUR_API_KEY',
    json={'cmd': 'request.get', 'url': 'https://example.com/products'},
    timeout=120,
)
html = resp.json()['solution']['response']

soup = BeautifulSoup(html, 'lxml')
for card in soup.select('.product'):
    name = card.select_one('h2').get_text(strip=True)
    price = card.select_one('.price').get_text(strip=True)
    print(name, price)

Related terms

Concept map

Concept map

How How to Parse HTML in Python (2026 Guide) connects

The terms most directly tied to this one. Hover a node to see its neighbours, click to preview, drag to rearrange.

0 terms · 0 connections
You are here · Python Web Scraping
Building map…

Frequently asked questions

What is the best way to parse HTML in Python?

For most people, BeautifulSoup with the lxml backend is the best starting point — it has a forgiving, readable API and handles messy real-world HTML well. Use lxml directly when you need XPath, and selectolax when you need maximum speed for high-volume parsing. Avoid parsing HTML with regular expressions; HTML is not a regular language and regex breaks on nested or malformed markup.

Is lxml faster than BeautifulSoup?

Yes. lxml is C-backed and significantly faster than BeautifulSoup’s default parser. In fact, BeautifulSoup can use lxml as its backend (BeautifulSoup(html, "lxml")), giving you the friendly API on top of the fast parser. For the absolute fastest parsing, selectolax (Modest/Lexbor engine) is typically several times faster than lxml.

Should I use CSS selectors or XPath?

CSS selectors are shorter and supported by every parser, so use them for most selecting. Switch to XPath when you need something CSS cannot do: selecting an element by its visible text, walking up to a parent, or selecting previous siblings. lxml and parsel support XPath; BeautifulSoup and selectolax are CSS-only.

Why is my parsed data empty even though I see it in the browser?

Almost always because the page is JavaScript-rendered: the data is injected by JavaScript after the page loads, so it is not in the HTML that requests downloaded. Parsing cannot recover data that was never in the markup. Render the page with Playwright or Selenium, call the underlying JSON API, or use a scraping API that returns the fully rendered HTML.

Last updated: 2026-06-08