html

escape() makes text safe to drop into an HTML page or a quoted attribute; unescape() decodes named and numeric references exactly the way a browser does. Everything else lives in the html.parser and html.entities submodules.

TextPython 3.2+ (escape), 3.4+ (unescape)Live demo
Import
import html
from html import escape, unescape
Public API
escape(s, quote=True), unescape(s) — that is the whole of html.__all__
Submodules
html.parser (HTMLParser, a tag-event parser) and html.entities (the name tables) — imported separately
Escapes
& < > always; " and ' too unless quote=False. Nothing else — non-ASCII text is left as it is
Decodes
All 2231 HTML5 named references (html.entities.html5), decimal and hex numeric references, with the HTML5 error-recovery rules

Demo

Live evaluation
Make text safe to put inside HTML. Try text that is already escaped: escaping is not idempotent.
Try:
Inputs
textstrany text
Code
import html
html.escape('<script>alert("hi")</script>')
Result
'&lt;script&gt;alert(&quot;hi&quot;)&lt;/script&gt;'

escape() replaces & first, so text that already contains a reference gets a second layer: "Fish & Chips" written as a reference comes out with its & escaped again. unescape() follows the HTML5 rules: a few legacy names such as amp and not work without the semicolon (so "&notit;" becomes "¬it;"), but most names, like hearts, need it; references 128-159 are read as Windows-1252, so &#150; is an en dash and &#153; is ™; and &#0; becomes U+FFFD (�).

Members

Common patterns

Insert user text into a page
Escape every untrusted value at the point where it enters HTML.
import html
row = f"<td>{html.escape(comment)}</td>"
Build an attribute value
With the default quote=True, " and ' are escaped too, so the value cannot close the attribute.
import html
link = f'<a href="{html.escape(url)}" title="{html.escape(title)}">'
Plain text out of an HTML snippet
Decode references after the tags have been removed (by a parser, not a regex).
import html
text = html.unescape(fragment_without_tags)
Parse tags with the standard library
html.parser.HTMLParser calls your methods for each start tag, end tag and run of text.
from html.parser import HTMLParser

class Links(HTMLParser):
    def handle_starttag(self, tag, attrs):
        if tag == "a":
            print(dict(attrs).get("href"))

Links().feed(page)

Examples

1. Apostrophes become a hex reference
import html html.escape("It's")
Returns
'It&#x27;s'
2. quote=False leaves quotes alone
import html html.escape("It's \"ok\"", quote=False)
Returns
'It\'s "ok"'
3. Named references
import html html.unescape('&copy; 2026 caf&eacute;')
Returns
'© 2026 café'
4. Decimal and hex references
import html html.unescape('&#60;b&#62; &#x1F600;')
Returns
'<b> 😀'
5. Round trip
import html s = '<b>"Tom" & \'Jerry\'</b>' html.unescape(html.escape(s)) == s
Returns
True
6. Non-ASCII text is not touched
import html html.escape('naïve 😀')
Returns
'naïve 😀'
7. Only str is accepted
import html html.escape(42)
Returns
AttributeError: 'int' object has no attribute 'replace'

Pitfalls

1. Escaping twice
escape() does not know that its input is already escaped: every & is escaped again, and the page shows the reference text instead of the character.
escape(escape(s))
import html
html.escape(html.escape('Fish & Chips'))
'Fish &amp;amp; Chips'
escape once, at output
import html
html.escape('Fish & Chips')
'Fish &amp; Chips'
2. quote=False inside an attribute
Without quote escaping, a " in the value ends the attribute and the rest becomes markup.
quote=False
import html
v = 'x" onmouseover="alert(1)'
f'<a title="{html.escape(v, quote=False)}">'
'<a title="x" onmouseover="alert(1)">'
default quote=True
import html
v = 'x" onmouseover="alert(1)'
f'<a title="{html.escape(v)}">'
'<a title="x&quot; onmouseover=&quot;alert(1)">'
3. Expecting unescape to strip tags
unescape() only decodes references. Tags stay — and decoding can even create new ones out of escaped text.
unescape only
import html
html.unescape('&lt;b&gt;bold&lt;/b&gt;')
'<b>bold</b>'
parse, then use the text
from html.parser import HTMLParser
class Text(HTMLParser):
    def __init__(self):
        super().__init__()
        self.parts = []
    def handle_data(self, data):
        self.parts.append(data)
p = Text()
p.feed('<b>bold</b> &amp; plain')
''.join(p.parts)
'bold & plain'

When to use

Use it
  • Generating HTML by hand (f-strings, small reports, emails) with values you do not control
  • Decoding references in scraped text, RSS titles or API fields that arrive HTML-encoded
Reach for something else
  • Full pages → a template engine with auto-escaping (Jinja2, Django templates)
  • Sanitizing HTML you want to keep partly (allow <b>, drop <script>) → a dedicated sanitizer library
  • URLs in query strings → urllib.parse.quote; JSON in a <script> → json.dumps plus escaping of </

Notes

CPython impl
Lib/html/__init__.py — escape is five str.replace calls; unescape is one regex substitution over the html.entities.html5 table
html.entities
html5 (2231 names → text, e.g. "amp;" → "&"), name2codepoint and codepoint2name (the 252 HTML 4 entities), entitydefs
html.parser
HTMLParser — a forgiving, event-based parser: subclass it and override handle_starttag, handle_endtag, handle_data
Not a sanitizer
escape() makes text inert in element content and quoted attributes. It does not make text safe inside <script>, <style>, unquoted attributes or javascript: URLs

FAQ

import html, then html.escape(text). It replaces &, < and > with the named references amp, lt and gt (each written between & and ;), and with the default quote=True also " (quot) and ' (the hex reference &#x27;). Do it once, at the moment the text goes into the page.