Focused practice · 8 min read

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 &amp; 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 &amp; 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

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.

Open practice

Keep reading: