Escape Text for HTML Output
Render untrusted text as visible HTML text without treating its angle brackets as markup.
Example check: Worked and guided code samples are checked against CPython 3.13. Practice uses Pyodide 0.29.5 (Python 3.13) in your browser. How we check lessons
Video lesson: Escape Text for HTML Output
The preview is stored on this site. YouTube loads only when you press Play. Watch on YouTube
Jump to a chapter
Read the video transcript
Select a timestamp to watch that moment on YouTube.
- 00:00
Suppose a learner enters the label angle bracket b angle bracket bold question mark closing b. Our page should display exactly that text. If we interpolate the raw label into an HTML string, the browser may instead interpret those brackets as markup and show only bold question mark in bold type. That is a change of meaning caused by the output context. Today we will compare the raw and escaped strings, then write a small renderer that keeps its tag and class fixed while treating the supplied label as text. The Python browser console prints these strings so we can inspect them; it does not render the strings as live HTML.
- 00:42
That is useful because we can see the exact character references before a browser turns them back into the visible characters. We will use the standard library html.escape function rather than a series of hand-written replacements. We will also test ampersands, quotes, empty labels, and ordinary labels. Finally we will draw a clear boundary around the technique: HTML text escaping does not validate a URL destination and does not sanitize arbitrary user-supplied markup. First, watch the raw string and decide which part is fixed code and which part is variable data. The console has not executed any markup.
- 01:24
It only prints a string. But if this exact string is inserted into an HTML document as markup, the inner b element may be interpreted as a tag rather than displayed literally. The second line is a simple probe: the raw opening tag sequence still appears inside the generated markup. The fixed span is supposed to be markup; the label is supposed to be data. Those two roles should not be confused. A broader example could contain an ampersand or a quote, and each special character can matter differently depending on whether text is placed in an element body or a quoted attribute. Our function will keep the class attribute fixed, so we only insert the label into
- 02:06
the span's body. That gives us one clear output context. We should escape the variable label just before inserting it into HTML output, leaving the original input string available for other work such as search or storage. A common mistake is to strip angle brackets or write multiple replace calls. That can discard legitimate text or double-escape a newly inserted ampersand. Python already has a function for this exact HTML representation. The angle brackets become character references, the ampersand becomes amp, and the double quote becomes quot.
- 02:46
The apostrophe in the last line becomes a numeric character reference. With quote=True, html.escape covers both quote characters as well as ampersands and angle brackets. When these references are later interpreted in an HTML text node, the person sees the original label characters rather than a new tag. This is encoding for an output context, not deleting content. Notice that the original label still exists in our Python variable. We did not need to mutate it at input time. If we later place a value into a different context, such as a JavaScript string, CSS property, or URL, we need rules for that context instead.
- 03:31
Likewise, escaping a URL's characters does not decide whether a link points somewhere acceptable. For this lesson, we simplify the interface: there is no variable tag name, no variable attribute name, and no variable URL. Only the text between fixed span tags changes. That makes the rendering function small enough to reason about and test directly. Let us apply the escape at that precise boundary. The span and its class are fixed strings chosen by the program. Only the label varies, and that label is converted with html.escape before entering the returned markup. The first line shows the angle brackets and ampersand as references; the second
- 04:14
shows the quote inside the label is encoded. Compare this with the broken run: there is no raw less-than b tag supplied by the input between the fixed span tags. We can reason about the function's behavior without running an HTML renderer because the returned strings contain exactly the references we expect. You might notice that quote escaping is more than an element text node strictly needs. That is acceptable here and reinforces how quote=True behaves, but it does not make the function a universal sanitizer. If callers could choose an attribute value, a tag name, a URL, or a script,
- 04:54
each would need additional design. A production web framework usually provides automatic context-aware escaping through its template engine; using that is safer than manually concatenating markup throughout an application. For this focused Python exercise, the function's contract is intentionally narrow and fully testable in the console. Now we should check the less dramatic inputs that often reveal accidental special cases. An empty label produces an empty fixed span rather than an error or a different tag. Ordinary text stays readable. The apostrophe gets a character reference because we explicitly requested quote
- 05:35
escaping. These are small assertions, but they help prove that the function does not remove user text or change the surrounding markup based on the value. There is one mistake to avoid after this: do not call html.unescape on the result just before inserting it into a page. That would turn the references back into syntax characters at the moment we most need the escaping. Also remember that this function is for HTML text within our fixed element. It does not decide whether a user- controlled href points to a safe site, and it does not turn arbitrary input HTML into sanitized HTML.
- 06:18
If a larger application needs to accept formatted markup, use a dedicated sanitizer with a well-defined allowlist and your framework's escaping facilities. On the lesson page, try the checked task, then replace the label with your own combination of ampersands, quotes, and brackets. Before you press run, predict the exact references you expect to see. That habit of naming the output context before inserting data is the reusable skill here.
Understand the concept
When text is inserted into HTML, characters such as < and & can change how the browser interprets the document. If a title is meant to be displayed literally, escape it at the point where it enters an HTML text node. Python's html.escape converts &, <, and > into character references. With quote=True it also converts the two quote characters, which helps when the result is placed inside a quoted attribute.
The exercise builds one simple tag with a fixed class attribute and a variable label. The label is escaped before insertion: '<Ada & Grace>' becomes visible text inside the span, rather than a new tag. The output is a string that we inspect and print; the browser console does not render it as live HTML. Escaping at output keeps the original label intact for searching or other processing.
Do not use html.unescape as a safety step before output. It turns character references back into characters that may become markup if inserted into an HTML renderer. Also avoid a hand-written sequence of replace calls: replacing < before & can escape the new ampersand twice, while replacing only < leaves other special characters untreated. Use the standard library function for this HTML context.
html.escape is not a full HTML sanitizer and it does not decide whether a URL is safe. A user-controlled href needs a separate policy for allowed schemes and destinations. JavaScript, CSS, and URL contexts each have different rules. For larger web applications, use the framework's context-aware autoescaping and keep generated HTML templates separate from stored user data. This lesson's function deliberately has no variable tag name or URL.
- Escape data when inserting it into HTML output
- html.escape handles ampersands and angle brackets
- quote=True also escapes single and double quotes
- Escaping text is not equivalent to validating a URL or sanitizing arbitrary HTML
See it step by step
Read the code, predict the output, then compare it with the result.
01. Keep angle brackets visible as text
from html import escape
print(escape("<Ada & Grace>"))<Ada & Grace>The returned string contains character references. When safely inserted into an HTML text node, they display as the original characters.
02. Escape quotes for a quoted attribute
from html import escape
label = 'Ada "the first"'
print(escape(label, quote=True))Ada "the first"quote=True includes quote characters, which matter when data is placed within a quoted attribute.
03. Inspect output without rendering it
from html import escape
name = "<b>bold?</b>"
markup = f"<p>{escape(name)}</p>"
print(markup)<p><b>bold?</b></p>Printing markup as text lets us verify that the variable content will display literally rather than becoming a bold element.
A closer look
Follow the reasoning, inspect each result, then try the suggested changes in the console below.
See why raw interpolation changes the meaning of HTML
A template may intend to display the literal text <b>bold?</b>. If it inserts that string as raw HTML, the angle brackets could be interpreted as a bold element instead. The issue is not that the original text should be discarded; it should be represented safely in the output context. Python's html.escape returns character references for ampersand and angle brackets. With quote=True it handles quotation marks too.
The console prints strings rather than rendering a web page, which gives us a direct inspection step. Compare the raw markup string with the escaped markup string. In the second line, the variable label remains text when inserted into an HTML text node. The fixed <p> tags are supplied by the program, while the variable label has been encoded. This is an output-boundary decision, not a mutation of stored user data.
from html import escape
label = "<b>bold?</b>"
print(f"<p>{label}</p>")
print(f"<p>{escape(label, quote=True)}</p>")<p><b>bold?</b></p>
<p><b>bold?</b></p>- Identify the fixed tags and the variable label in both lines.
- Observe the escaped angle brackets in the second line before any browser renders it.
Keep the element fixed and escape only its content
render_tag takes a label and returns a span with a fixed class. The caller does not choose the tag name, attribute name, or attribute value, so the function has one simple variable HTML text context. Escape the label immediately before interpolation. html.escape with quote=True is slightly broader than this text node requires, and it also illustrates how quotes would be represented if a quoted attribute were needed later.
Test ampersand and angle brackets together. Replacing one special character by hand can produce double escaping or leave another character unhandled. The standard library function expresses the intended operation directly. Keep the original string unchanged for other uses, such as a search index. Do not call html.unescape on the escaped value before adding it to markup; that would reverse the protection and restore characters the HTML parser can interpret as syntax.
from html import escape
def render_tag(label):
escaped_label = escape(label, quote=True)
return f'<span class="tag">{escaped_label}</span>'
print(render_tag("<Ada & Grace>"))
print(render_tag('A "quote"'))<span class="tag"><Ada & Grace></span>
<span class="tag">A "quote"</span>- Verify the variable text has escaped ampersand and angle brackets.
- Verify quote=True also handles the quote inside the label.
Verify empty text and understand the context limit
An empty label still produces a well-formed fixed span, and ordinary text remains readable. These edge cases matter because a function that attempts to strip markup with a regular expression might accidentally remove text or return an inconsistent structure. Here the output element stays the same, while only the data segment changes. Print both cases, then compare the returned string with the precise expected form.
html.escape is not a general HTML sanitizer. It does not inspect or authorize an href destination, remove dangerous attributes from arbitrary HTML, or encode text for JavaScript, CSS, or URL syntax. Those contexts have separate rules. In a full web framework, use its context-aware autoescaping template system for ordinary HTML output and a separate URL policy when users can influence navigation. This narrow example is safe because it has a fixed tag and fixed attribute and inserts escaped text into the element body.
from html import escape
def render_tag(label):
return f'<span class="tag">{escape(label, quote=True)}</span>'
print(render_tag(""))
print(render_tag("Python"))
print(render_tag("It's useful"))<span class="tag"></span>
<span class="tag">Python</span>
<span class="tag">It's useful</span>- Check that empty content does not alter the fixed span.
- Observe the encoded apostrophe without mistaking escaping for a URL safety policy.
Try it in Python
Edit the example and run it. Python starts in your browser the first time you click Run.
Python console
Ready to runNeed input()? Add one value per line
Your output appears here.
Need a hint?
Build escaped_label = escape(label, quote=True), then return f'<span class="tag">{escaped_label}</span>'. The import is already provided. Check ampersand as well as angle brackets.
Complete the task and select Check task to verify your code.
Quick quiz
Three questions. You can change your answers and try again.
Typical mistakes
Everyone meets these errors. See what causes them and how to fix them.
Interpolating raw text into markup
label = '<b>Hi</b>'
print(f'<span>{label}</span>')from html import escape
label = '<b>Hi</b>'
print(f'<span>{escape(label, quote=True)}</span>')What happens: The label can be interpreted as markup instead of displayed literally.
Escape the variable text at the HTML output boundary.
Undoing the escape before output
from html import unescape
print(unescape('<b>Hi</b>'))from html import escape
label = '<b>Hi</b>'
print(escape(label, quote=True))What happens: Character references become angle brackets again before insertion.
Keep the text escaped for HTML output; unescape is for decoding input when that is actually required.