Python HTMLParser Exercises: Links, Text, and Attributes
A parser is easier to trust when you build it in small handlers. These exercises add one responsibility at a time: collect tags, read attributes, capture text, then combine them into a record.
Start with a tag collector
Override handle_starttag and append the tag name. Keep the class small enough that you can read the state in one screen. A parser with one job is easier to test than a parser that extracts everything at once.
from html.parser import HTMLParser
class Tags(HTMLParser):
def __init__(self):
super().__init__()
self.names = []
def handle_starttag(self, tag, attrs):
self.names.append(tag)
p = Tags()
p.feed("<article><h1>Title</h1><p>Text</p></article>")
print(p.names)One handler, one job
Read attributes without crashing
attrs is a list of pairs, and some values may be None. Convert to a dictionary, then use get. A missing href is normal in navigation markup, so skip it instead of appending None.
from html.parser import HTMLParser
class Links(HTMLParser):
def __init__(self):
super().__init__()
self.links = []
def handle_starttag(self, tag, attrs):
values = dict(attrs)
if tag == "a" and values.get("href"):
self.links.append(values["href"])
p = Links()
p.feed('<a href="/one">One</a><a>No link</a><a href="/two">Two</a>')
print(p.links)Skip the missing value
Capture text between tags
handle_data receives text pieces, not a finished sentence. Strip each piece, ignore empty strings, and decide how you want to join them. If whitespace matters, preserve it deliberately rather than relying on the browser.
from html.parser import HTMLParser
class Text(HTMLParser):
def __init__(self):
super().__init__()
self.parts = []
def handle_data(self, data):
text = data.strip()
if text:
self.parts.append(text)
p = Text()
p.feed("<h1>Title</h1><p>First sentence.</p>")
print(" ".join(p.parts))Text nodes, cleaned
Decode entities, keep the source
Character references such as & are decoded by HTMLParser. If you store extracted text, keep the original URL and a short source label. The parser gives you structure; provenance lets a human check the result.
from html import unescape
raw = "Tom & Jerry"
clean = unescape(raw)
print(clean)
print(raw)Decode and retain
Combine handlers into a record
When one class collects both links and text, keep the state explicit and reset it for each page. Do not reuse a parser instance across documents unless you deliberately clear every list; stale state is a common source of duplicate records.
from html.parser import HTMLParser
class Page(HTMLParser):
def __init__(self):
super().__init__()
self.links = []
self.text = []
def handle_starttag(self, tag, attrs):
href = dict(attrs).get("href")
if tag == "a" and href:
self.links.append(href)
def handle_data(self, data):
text = data.strip()
if text:
self.text.append(text)
p = Page()
p.feed('<h1>Docs</h1><a href="/docs">Read</a>')
print(p.links)
print(p.text)Two lists, one pass
Your practice checklist
- Call super().__init__ before adding parser state.
- Use dict(attrs).get(name) instead of assuming every attribute exists.
- Strip text nodes and skip empty strings.
- Create a fresh parser for each document.
- Keep the source URL beside extracted text.
Common questions
Is HTMLParser a full browser?
No. It is a tolerant tokenizer for Python code. It does not run JavaScript or build a complete DOM.
Why not use a regex?
Regular expressions do not understand nesting or quoted attributes. A parser follows the document structure and is easier to maintain.
Can I use BeautifulSoup instead?
Yes, if you can install a third-party package. The standard HTMLParser is enough for simple, well-structured pages and has no dependency.
Practice HTML parsing
12 questions on tags, attributes, text nodes, entities, and parser state.
Keep reading: