# MarkupSafe User Guide

MarkupSafe is the small Python library that makes it impossible — or as close
to impossible as a library can — to forget HTML escaping in templated output.
It is the foundation of Jinja2's autoescape, of Flask's response rendering,
and of any framework in the Pallets ecosystem that has to weave user input
into HTML without producing an XSS hole.

Everything in MarkupSafe orbits two names: the `escape()` function, which
turns a hostile string into a safe one, and the `Markup` class, a `str`
subclass that *remembers* it is safe and refuses to be escaped a second time.
Together they implement a single, narrow contract: a value is either
trusted-for-HTML or it is not, and the type system tells you which.

This guide is for application developers and library authors who reach for
MarkupSafe directly — for example, when building a custom template helper,
sending an HTML email, or implementing a `__html__` method on a domain model.
If you only ever interact with MarkupSafe through Jinja2 (which most Flask
users do), the Quick Start tells you why your strings already do the right
thing and you can stop reading.

---

## 1. Quick Start

```bash
pip install markupsafe
```

```python
from markupsafe import Markup, escape

user_input = '<script>alert("xss")</script>'
safe = escape(user_input)
print(safe)
# &lt;script&gt;alert(&#34;xss&#34;)&lt;/script&gt;

greeting = Markup("<h1>Hello, %s!</h1>") % user_input
print(greeting)
# <h1>Hello, &lt;script&gt;alert(&#34;xss&#34;)&lt;/script&gt;!</h1>
```

That is the entire core idea. `escape()` turned five characters
(`& < > ' "`) into HTML entities. `Markup("<h1>...")` declared a literal
template safe; the `%` operator escaped the substituted value. The fixed
HTML stayed un-escaped; the user input became inert text.

> [!TIP]
> If you are writing a Flask or Jinja2 application, you almost certainly do
> not need to call `escape()` yourself. Jinja2's autoescape calls it for you
> on every `{{ variable }}` substitution. MarkupSafe ships as a Jinja2
> dependency, so `pip install jinja2` already installed it.

### What MarkupSafe gives you

- **`escape(s)`** — the canonical "make this string safe for HTML" function.
- **`Markup`** — a `str` subclass whose every operator and method preserves
  the safety guarantee.
- **The `__html__` protocol** — any object can declare itself HTML-safe by
  implementing a single method.
- **`escape_silent(s)`** — like `escape()` but treats `None` as `""` instead
  of the literal string `"None"`.
- **`soft_str(s)`** — coerce to `str` without un-escaping a `Markup`.
- **A C accelerator** — when the build environment allows it, escaping is
  done by `_speedups.c`; otherwise the pure-Python `_native` module takes
  over transparently.

### The five characters MarkupSafe escapes

| Input | Output | Why |
|---|---|---|
| `&` | `&amp;` | First, so later replacements don't double-encode. |
| `<` | `&lt;` | Opens tags. |
| `>` | `&gt;` | Closes tags. |
| `'` | `&#39;` | Breaks single-quoted attributes. |
| `"` | `&#34;` | Breaks double-quoted attributes. |

Anything else — Unicode letters, emoji, control characters — passes through
unchanged. MarkupSafe is exclusively an HTML escaper. It does not touch URLs,
JSON, JavaScript string literals, CSS, or shell commands.

> [!IMPORTANT]
> MarkupSafe is HTML/XML-only. Use `urllib.parse.quote()` for URL components,
> `json.dumps()` for JavaScript values, `shlex.quote()` for shell arguments,
> and `markupsafe.escape()` for HTML/XML body or attribute content. Mixing
> contexts is the most common XSS bypass; do not reach for `escape()` to
> sanitize a URL.

### What to try next

- Section 2 (Installation) covers the pip-only path, the dev install, and
  how to know whether the C accelerator is active.
- Section 4 (Core Usage) walks through `escape`, `Markup`, the `__html__`
  protocol, formatting, and unescape/striptags.
- Section 5 (Common Tasks) gives short recipes for the cases that come up
  most: implementing `__html__`, building a snippet, JSON-serializing a
  Markup, and integrating with Jinja2.

---

## 2. Installation

The lowest-friction path is one line:

```bash
pip install markupsafe
```

That installs the latest release from PyPI, with prebuilt wheels for every
mainstream platform (Linux, macOS, Windows; CPython 3.10 and newer; both
x86_64 and arm64). The wheels include the compiled C accelerator, so the
"speedups" path is active out of the box on supported interpreters.

### You probably already have it

```bash
python -c "import markupsafe; print(markupsafe.__name__)"
# markupsafe
```

If you've installed Flask, Jinja2, or any other Pallets package — or any
framework that depends on Jinja2 — MarkupSafe is already on your system as
a transitive dependency. You can skip directly to Section 4.

### Verify the install

```python
from markupsafe import Markup, escape

print(escape("<b>"))            # Markup('&lt;b&gt;')
print(Markup("<b>") + "</b>")   # Markup('<b>&lt;/b&gt;')
print(type(escape("x")))        # <class 'markupsafe.Markup'>
```

If those three lines run without error, the package is functional.

> [!NOTE]
> Wheels are published for CPython only. PyPy, Jython, and GraalVM users
> get the pure-Python fallback automatically — `setup.py` detects the
> implementation and skips the C extension build entirely. Functionality
> is identical; only escape throughput differs.

### Platform-specific install

| Platform | Command | Notes |
|---|---|---|
| pip (any) | `pip install markupsafe` | Recommended. Wheels for Linux/macOS/Windows. |
| uv | `uv add markupsafe` | Project-managed installs. The repo itself uses `uv`. |
| pipx (for tools that embed it) | `pipx inject yourtool markupsafe` | Inject into a pipx-installed app. |
| conda | `conda install -c conda-forge markupsafe` | Conda-forge channel. |
| From source | `pip install .` (in repo root) | Builds the C extension if a compiler is present. |

> [!TIP]
> On a fresh container or CI image you almost never need to do anything
> special. The wheel index resolves before pip even tries to compile, and
> the install completes in under a second.

### Source / development install

To hack on MarkupSafe itself or to run its test suite, clone the repo and
use `uv` (the upstream-preferred toolchain pinned in `.readthedocs.yaml`):

```bash
git clone https://github.com/pallets/markupsafe.git
cd markupsafe
uv sync
uv run pytest
```

`uv sync` reads `pyproject.toml` and `uv.lock` to install the package
editable, plus the test and docs groups. The first `uv run pytest` after a
fresh clone will compile the C extension into the source tree.

> [!IMPORTANT]
> If you're installing from source on a system without a C compiler, the
> install does not fail. `setup.py` catches `CCompilerError`, `ExecError`,
> and `PlatformError`, prints a banner that begins with
> `WARNING: The C extension could not be compiled`, and re-runs without
> the extension. The library still works — escaping is just slower.

### Forcing the C extension to build (CI builds)

Continuous integration jobs that build wheels (`cibuildwheel`) need the
build to fail loudly if the C extension can't compile, rather than silently
falling back. `setup.py` honours one environment variable:

| Variable | Default | Effect |
|---|---|---|
| `CIBUILDWHEEL` | `0` | Set to `1` to force C-extension build with no fallback. |

```bash
CIBUILDWHEEL=1 pip install --no-binary markupsafe markupsafe
```

This is exclusively for wheel-building pipelines; ordinary users should
leave it unset.

### Uninstall

```bash
pip uninstall markupsafe
```

Be aware that almost any web framework you also have installed depends on
MarkupSafe and will refuse to import without it.

---

## 3. Configuration

MarkupSafe has no configuration. It is a single-purpose library with no
files to edit, no environment variables to read at runtime, no plugin
registry, and no global state.

The complete list of "settings" you can tune:

| Knob | Where | What it does |
|---|---|---|
| Use the C accelerator | Build-time | Controlled by whether `_speedups.c` compiled. |
| Force C build | `CIBUILDWHEEL=1` env var | Build-only switch — see Section 2. |
| Custom `__html__` | Per class | Opt your own classes into safe-rendering — see Section 4. |

> [!NOTE]
> Behaviour is identical between the C-accelerated path
> (`markupsafe._speedups._escape_inner`) and the pure-Python path
> (`markupsafe._native._escape_inner`). The only observable difference is
> throughput on long strings. There is no flag to choose between them at
> runtime; whichever was importable at process start wins.

### Selecting the implementation manually (advanced)

If you need to benchmark or to force the pure-Python path for debugging,
shadow the symbol after import:

```python
import markupsafe
from markupsafe._native import _escape_inner
markupsafe._escape_inner = _escape_inner
```

This is the technique `bench.py` uses to compare the two implementations
head-to-head. Outside of benchmarking and bug-hunting, do not do this in
production code; let the import-time selector decide.

---

## 4. Core Usage

Four concepts cover everything MarkupSafe does:

1. The `escape()` function (and `escape_silent()`).
2. The `Markup` class — a `str` that knows it's safe.
3. The `__html__` protocol — how arbitrary classes opt in.
4. `Markup.format`, `Markup.unescape`, `Markup.striptags`, and the operators.

### 4.1 `escape()` — the canonical safe-conversion

```python
from markupsafe import escape

escape("<b>bold</b>")     # Markup('&lt;b&gt;bold&lt;/b&gt;')
escape(42)                # Markup('42')
escape(None)              # Markup('None')   <-- note: literal string
```

The signature is `escape(s, /) -> Markup`. The argument can be anything;
`escape()` does three things in this order:

1. If `type(s) is str`, escape directly (the fast path — most common case).
2. Otherwise, if `s` has an `__html__` method, call it and trust the return.
3. Otherwise, call `str(s)` and escape that.

The return is always a `Markup` instance — never a plain `str` — so calling
`escape()` twice on the same value is idempotent in the sense that the
double-escape only happens once you cast back to `str`. (See `soft_str` in
4.5 for why this matters.)

> [!NOTE]
> The fast path uses `type(s) is str`, not `isinstance(s, str)`. This is
> deliberate: a `Markup` is also a `str`, but it should hit the
> `__html__` branch and return itself, not be re-escaped.

### 4.2 `escape_silent()` — `None`-tolerant escape

```python
from markupsafe import escape, escape_silent

escape(None)         # Markup('None')   <-- usually NOT what you want
escape_silent(None)  # Markup('')
escape_silent("ok")  # Markup('ok')
```

Use `escape_silent` when you're rendering a template that may receive
optional values and you want missing data to disappear rather than print
the word "None". Behaviour for non-`None` values is identical to `escape()`.

### 4.3 The `Markup` class

`Markup` is a `str` subclass. The constructor *wraps without escaping*:

```python
from markupsafe import Markup

Markup("<em>yes</em>")           # Markup('<em>yes</em>')   -- not escaped
Markup.escape("<em>yes</em>")    # Markup('&lt;em&gt;yes&lt;/em&gt;') -- escaped
```

The split is the heart of the API:

- **`Markup(value)`** says "I am asserting this is already safe."
- **`Markup.escape(value)`** (or the module-level `escape(value)`) says
  "I do not trust this; please make it safe."

> [!CAUTION]
> Calling `Markup()` directly on user input completely defeats MarkupSafe's
> protection. `Markup(request.form["name"])` produces a string that templates
> will render verbatim — including any `<script>` tags inside it. Always
> use `escape()` for untrusted input. The plain `Markup()` constructor is
> for *literal HTML you wrote* (for example, layout templates, icon
> snippets, or pre-validated content from a trusted CMS).

#### Markup arithmetic preserves safety

Once you have a `Markup`, every operator and string method either preserves
the safety guarantee or escapes any unsafe operand:

```python
Markup("<b>Hi</b>") + "<u>"            # Markup('<b>Hi</b>&lt;u&gt;')
"<u>" + Markup("<b>Hi</b>")            # Markup('&lt;u&gt;<b>Hi</b>')
Markup("<em>%s</em>") % "<x>"          # Markup('<em>&lt;x&gt;</em>')
Markup(", ").join(["<a>", "<b>"])      # Markup(', ').join(...) -> escapes both
Markup("<b>x</b>").upper()             # Markup('<B>X</B>')
Markup("<b>x</b>")[0]                  # Markup('<')   (slicing returns Markup)
```

Methods that return strings (e.g. `upper`, `lower`, `replace`, `strip`,
`split`, `partition`, `expandtabs`, `zfill`, `casefold`, `removeprefix`,
`removesuffix`, `capitalize`, `title`, `swapcase`, `ljust`, `rjust`,
`center`) return `Markup`. Methods that take a string argument (`replace`,
`ljust`, `rjust`, `center`, `join`) escape that argument before applying
it. `split`, `rsplit`, `splitlines`, `partition`, `rpartition` all return
lists or tuples of `Markup` instances.

`unescape()`, `striptags()`, and `translate()` return plain `str`,
because their job is to leave the HTML world.

#### Concatenation rules in detail

- `Markup + str` → escapes the right side, returns `Markup`.
- `str + Markup` → escapes the left side, returns `Markup`.
- `Markup + non-string` → returns `NotImplemented` (lets Python try the
  other operand's `__radd__`), then a `TypeError` if nothing handles it.
- `Markup * int` and `int * Markup` → repeat, return `Markup`.
- `Markup % value` → printf-style format with each substitution escaped
  (see 4.4).

### 4.4 Formatting: `%`, `format`, and `format_map`

Three formatting paths exist; all three escape substitutions.

```python
from markupsafe import Markup

# 1. Printf style
Markup("<a href='%s'>%s</a>") % ("/x?y=1&z=2", "<click>")
# Markup("<a href='/x?y=1&amp;z=2'>&lt;click&gt;</a>")

# 2. str.format positional
Markup("<a>{}</a>").format("<click>")
# Markup('<a>&lt;click&gt;</a>')

# 3. str.format_map mapping
Markup("<a>{name}</a>").format_map({"name": "<click>"})
# Markup('<a>&lt;click&gt;</a>')
```

`Markup.format` and `Markup.format_map` route through an internal
`EscapeFormatter` (a `string.Formatter` subclass) that calls `escape()` on
every field value. Format specifiers (`{:>10}`, `{:.2f}`, etc.) work as
they do on plain strings. If a substituted object defines `__html_format__`,
that method receives the format spec and is responsible for returning a
`Markup`-safe string.

> [!WARNING]
> Passing a non-empty format spec to a value that defines `__html__` but
> not `__html_format__` raises `ValueError`:
> `Format specifier {format_spec} given, but {type(value)} does not define
> __html_format__.` Either drop the format spec or implement
> `__html_format__` (see 5.3).

### 4.5 The `__html__` protocol

Any object can declare itself HTML-safe by implementing one method:

```python
class Link:
    def __init__(self, href, text):
        self.href = href
        self.text = text

    def __html__(self):
        # We control the markup; the text needs escaping.
        from markupsafe import escape
        return f'<a href="{escape(self.href)}">{escape(self.text)}</a>'
```

What this gets you:

- `escape(Link("/x", "<y>"))` returns the result of `Link.__html__()`
  un-modified, wrapped in `Markup`.
- `Markup(Link("/x", "<y>"))` does the same — wraps without re-escaping.
- `Markup("<div>%s</div>") % link_instance` substitutes the safe HTML.

Optionally implement `__html_format__(self, format_spec)` to support format
specs in `Markup.format` calls.

```python
class Money:
    def __init__(self, cents):
        self.cents = cents

    def __html__(self):
        return f"<span class='money'>${self.cents / 100:.2f}</span>"

    def __html_format__(self, spec):
        # spec is something like '>15' or '0.0' from {:>15} etc.
        from markupsafe import Markup
        return Markup(format(self.__html__(), spec)) if spec else Markup(self.__html__())
```

> [!TIP]
> Implementing `__html__` is the cleanest way to integrate domain objects
> with Jinja2. The template author writes `{{ link }}`; Jinja2 calls
> `escape()`; `escape()` sees `__html__`; the safe string lands in the
> output unchanged.

### 4.6 `Markup.unescape()` and `Markup.striptags()`

These two methods reverse direction — they return plain `str`.

```python
from markupsafe import Markup

Markup("Main &raquo; <em>About</em>").unescape()
# 'Main » <em>About</em>'

Markup("Main &raquo;\t<em>About</em>").striptags()
# 'Main » About'
```

`unescape()` calls `html.unescape` from the standard library — entities
become their characters, but tags are left alone.

`striptags()` removes HTML comments first (so a comment containing a tag
doesn't truncate the strip), then everything between `<` and `>`,
collapses runs of whitespace into single spaces, and finally calls
`unescape()` on the result. The output is suitable for plain-text contexts
(email previews, search indices, log lines).

### 4.7 `soft_str()`

```python
from markupsafe import escape, soft_str

value = escape("<User 1>")          # Markup('&lt;User 1&gt;')
escape(str(value))                  # Markup('&amp;lt;User 1&amp;gt;')  <-- DOUBLE ESCAPED
escape(soft_str(value))             # Markup('&lt;User 1&gt;')          <-- correct
```

`soft_str` is the answer to "I want a `str`, but please don't lose the
fact that this `Markup` was already safe." It returns the input unchanged
if it's any `str` subclass (including `Markup`), and `str(s)` otherwise.
Use it when interfacing with code that demands a literal `str` type and
might naively re-escape what it receives.

### 4.8 Summary of the public surface

| Name | Type | Purpose |
|---|---|---|
| `escape` | function | Make a value safe for HTML, return `Markup`. |
| `escape_silent` | function | Like `escape`, but `None` becomes `''`. |
| `soft_str` | function | Cast to `str` without losing `Markup` identity. |
| `Markup` | class (subclass of `str`) | Mark a literal string safe; arithmetic preserves safety. |
| `Markup.escape` | classmethod | Same as `escape()` but returns the calling subclass. |
| `Markup.unescape` | method | `Markup` → `str`, decode HTML entities. |
| `Markup.striptags` | method | `Markup` → `str`, remove tags + comments + collapse spaces. |
| `__html__` | protocol method | Any class can implement to declare itself safe. |
| `__html_format__` | protocol method | Any class can implement to support `Markup.format` specs. |

Internal names (`_native._escape_inner`, `_speedups._escape_inner`,
`_HasHTML`, `_TPEscape`, `EscapeFormatter`, `_MarkupEscapeHelper`) are
implementation detail and not covered here. They are documented in the
Admin Guide.

---

## 5. Common Tasks

Eight short recipes that cover the cases that come up most.

### 5.1 Escape a single user input

```python
from markupsafe import escape

name = request.form["name"]            # untrusted
safe_name = escape(name)               # Markup
return f"<h1>Hello, {safe_name}!</h1>" # but the surrounding string is plain str!
```

> [!WARNING]
> The example above is *almost* right. Because `safe_name` lands inside an
> f-string with a plain-`str` literal, the result is a plain `str`. That's
> fine if you return it directly to a browser as `Content-Type: text/html`,
> but it loses the safety marker — any subsequent `escape()` call will
> double-encode it. The next recipe shows the cleaner pattern.

### 5.2 Build an HTML fragment that mixes safe + unsafe content

```python
from markupsafe import Markup, escape

heading = Markup("<h1>Hello, %s!</h1>") % request.form["name"]
# Or:
heading = Markup("<h1>Hello, {}!</h1>").format(request.form["name"])
# Or:
heading = Markup("<h1>Hello, ") + request.form["name"] + Markup("!</h1>")
```

All three keep `heading` as `Markup`. Pick whichever reads best for the
template you're building. `%` formatting is the canonical choice in
Jinja2's filters.

### 5.3 Implement `__html__` on a custom class

```python
from dataclasses import dataclass
from markupsafe import Markup, escape

@dataclass
class User:
    id: int
    display_name: str

    def __html__(self):
        return Markup('<a href="/users/{0}">{1}</a>').format(self.id, self.display_name)

# Usage:
user = User(id=42, display_name="<Bobby Tables>")
escape(user)
# Markup('<a href="/users/42">&lt;Bobby Tables&gt;</a>')

# In a Jinja template:
# {{ user }}  -- renders the safe link, with display_name properly escaped
```

> [!TIP]
> Always build the inner HTML with `Markup.format` (or `%`) so the
> attribute values get escaped automatically. Hand-concatenating strings
> with f-strings inside `__html__` is a common XSS source — you have to
> remember to call `escape()` on every interpolation.

### 5.4 Convert a Markup back to plain text

```python
from markupsafe import Markup

content = Markup("Hello &amp; <em>welcome</em> to &raquo; the site!")

text = content.unescape()
# 'Hello & <em>welcome</em> to » the site!'

stripped = content.striptags()
# 'Hello & welcome to » the site!'
```

Use `unescape()` when you want HTML entities decoded but tags preserved
(rare). Use `striptags()` when you want a plain-text rendering — the
typical case is generating an email preview, an Atom summary, or a
search-index field from HTML body content.

### 5.5 Strip tags from rich input

```python
from markupsafe import Markup

raw = Markup("""
    <p>Hello <strong>World</strong>!</p>
    <!-- inline tracker pixel:
         <img src="https://evil.example/?u=1"> -->
    <p>Visit <a href="/x">here</a>.</p>
""")

print(raw.striptags())
# Hello World ! Visit here.
```

`striptags()` correctly handles HTML comments that contain tags — the
comment is removed in one pass, then the surviving tags are stripped, and
the whitespace is collapsed.

> [!NOTE]
> `striptags()` is for normalizing display, not for security. It does not
> protect against malicious HTML being re-rendered later. If you need to
> sanitize untrusted HTML for re-display, use a dedicated sanitizer such
> as `bleach` — `striptags` is a one-way reduction to plain text.

### 5.6 Integrate with Jinja2 (transparent)

In a Jinja2 environment with autoescape enabled (which is the default for
`flask.render_template` and any `Environment(autoescape=True)`):

```jinja
<!-- template.html -->
<h1>Hello, {{ name }}!</h1>
<p>{{ user_html_blob | safe }}</p>
<p>{{ user.display_name }}</p>
```

```python
# Python side — nothing to do!
return render_template("template.html",
    name="<World>",                    # auto-escaped
    user_html_blob=Markup(trusted),    # already Markup, NOT re-escaped
    user=User(id=1, display_name="<>"), # __html__ method called
)
```

What NOT to do:

```python
# WRONG — defeats autoescape entirely.
return render_template("template.html",
    name=Markup(request.form["name"]),  # XSS HOLE
)
```

```jinja
{# WRONG — `safe` says "trust this verbatim" without verification. #}
<h1>Hello, {{ name | safe }}!</h1>
```

> [!CAUTION]
> The Jinja2 `| safe` filter and the bare `Markup()` constructor do the
> same thing: they declare a value safe without escaping. Both are correct
> for trusted, programmer-authored HTML; both are an XSS bug when applied
> to user input. Treat `| safe` and `Markup(x)` as security-sensitive —
> grep for them in code review.

### 5.7 Serialize a Markup to JSON safely

```python
import json
from markupsafe import Markup

data = {"snippet": Markup("<b>bold</b>")}
json.dumps(data)
# '{"snippet": "<b>bold</b>"}'
```

Because `Markup` is a `str` subclass, `json.dumps` serializes it as a
plain JSON string. The HTML is preserved verbatim — *but* note that the
JSON output is itself plain text, and reading it back with `json.loads`
returns a plain `str`, not a `Markup`. The "this is safe HTML" tag does
not survive a round-trip through JSON.

> [!IMPORTANT]
> If you embed JSON inside an HTML `<script>` tag, the bytes still need
> JavaScript-context escaping (e.g. `</script>` inside the JSON would
> close the script element). Standard JSON encoding is *not* sufficient
> for HTML embedding. Use a dedicated escape pass — Flask's `tojson`
> filter, or hand-rolled `json.dumps(..., ensure_ascii=True)` plus
> `.replace("</", "<\\/")` — for the HTML-script case.

### 5.8 Round-trip: escape → use → unescape

```python
from markupsafe import escape

# Step 1: receive untrusted input
title = '<Important & Urgent>'

# Step 2: escape for storage or display
safe = escape(title)
# Markup('&lt;Important &amp; Urgent&gt;')

# Step 3: render safely in HTML
html = f"<h2>{safe}</h2>"  # plain str, safe to send

# Step 4: later, recover the original characters
recovered = safe.unescape()
# '<Important & Urgent>'
assert recovered == title
```

This pattern is useful when you're storing pre-escaped text and need the
original back for non-HTML contexts (logs, plaintext exports, fuzzy
search). Note that `unescape` doesn't strip tags — if the original
contained tags, they'll come back too.

---

## 6. Troubleshooting

The errors below are the ones MarkupSafe actually raises (the two
`ValueError` messages are quoted verbatim from the source) and the
behaviours users most often misdiagnose as bugs.

> [!NOTE]
> MarkupSafe has a tiny error surface. If you encounter an exception
> that's not in this list, it is almost always coming from your
> framework (Jinja2, Flask) or from your own `__html__` implementation,
> not from MarkupSafe itself. Trace the stack to the highest
> `markupsafe/*.py` frame; if there isn't one, the bug is upstream of
> the library.

### Error: `TypeError: can only concatenate str (not "int") to Markup`

**Cause:** You attempted `Markup("...") + 42`. `Markup.__add__` returns
`NotImplemented` for non-string operands, and so does `int.__radd__`, so
Python raises `TypeError`.

**Solution:** Convert explicitly.

```python
# Wrong:
Markup("count: ") + 42

# Right:
Markup("count: ") + str(42)
Markup("count: %d") % 42
Markup("count: {}").format(42)
```

**Prevention:** Treat `Markup` as a typed `str`. If a value isn't a string,
either coerce it (`str(value)`) or pass it through `%`/`format` so
MarkupSafe handles it.

### Error: `ValueError: Unsupported format specification for Markup.`

**Cause:** Using a format spec on a literal `Markup` value, e.g.
`f"{my_markup:>10}"` or `Markup("{:>10}").format(other_markup)`.
`Markup.__html_format__` rejects any non-empty format spec because there
is no general way to pad a safe HTML string while keeping it safe.

**Solution:** Cast to `str` first if you need plain text alignment, then
re-wrap if needed.

```python
m = Markup("<b>x</b>")
padded = Markup(f"{str(m):>10}")
```

**Prevention:** Apply format specs at the `str` layer, not at the
`Markup` layer.

### Error: `ValueError: Format specifier '...' given, but <class 'X'> does not define __html_format__`

**Cause:** A class with `__html__` but no `__html_format__` was used in
`Markup.format` with a format spec.

**Solution:** Either remove the spec, or implement `__html_format__`:

```python
class Money:
    def __html__(self):
        return f"<span>${self.cents/100:.2f}</span>"

    def __html_format__(self, spec):
        from markupsafe import Markup
        return Markup(format(self.__html__(), spec)) if spec else Markup(self.__html__())
```

**Prevention:** When you implement `__html__`, decide up front whether
your class will be used with format specs. If yes, implement
`__html_format__` at the same time.

### `Markup('None')` appearing in output where a value is missing

**Cause:** You called `escape(None)`. `escape()` calls `str(None)` which
produces the literal string `"None"`, then escapes it (no-op for `None`'s
characters).

**Solution:** Use `escape_silent`:

```python
from markupsafe import escape_silent
escape_silent(None)   # Markup('')
escape_silent(value)  # behaves like escape() for non-None
```

**Prevention:** Use `escape_silent` for any value that might be `None`
(optional database fields, query params, defaulted kwargs).

### Output is double-escaped (`&amp;lt;` instead of `&lt;`)

**Cause:** A `Markup` value was cast back to `str` and then re-escaped.
This happens when you write `escape(str(some_markup))` or when a layer of
your stack explicitly calls `str()` on a `Markup` before re-escaping.

> [!WARNING]
> Double-escape bugs are easy to miss in code review because the rendered
> page still looks valid — `&amp;lt;` displays as `&lt;` in the browser
> rather than as the original `<`. The user-visible symptom is "my
> entities are showing up as literal text". Always test by viewing-source
> in the browser, not by visual inspection.

**Solution:** Use `soft_str` whenever you must coerce to `str`:

```python
from markupsafe import escape, soft_str
escape(soft_str(some_markup))  # idempotent — no double escape
```

**Prevention:** Don't pre-escape data that will pass through Jinja2 or
another autoescape layer. If you must, ensure the entire pipeline keeps
`Markup` identity, and use `soft_str` at any seam where a plain `str`
is required.

### "The C extension could not be compiled, speedups are not enabled."

**Cause:** Installing from source on a system without a working C
compiler. `setup.py` caught the build failure and re-ran without the
extension. The library still works.

**Solution:** No action needed if you're a user. To get the speedups,
install a compiler (Linux: `gcc`; macOS: Xcode CLT; Windows: Build Tools
for Visual Studio) and reinstall, or — easier — install from a wheel:
`pip install --upgrade --force-reinstall --no-cache-dir markupsafe`.

**Prevention:** Stick to wheels (`pip install markupsafe`) instead of
source builds in environments where you don't control the toolchain.

### `ImportError` from `markupsafe._speedups`

**Cause:** You're either on PyPy/Jython/GraalVM (where the speedups are
intentionally not built — `setup.py` checks
`platform.python_implementation()`), or you have a binary installed for a
different Python ABI than the one running.

**Solution:** Don't import `_speedups` directly — `markupsafe/__init__.py`
falls back to `_native` automatically. If you have a stale install,
`pip install --upgrade --force-reinstall markupsafe` rebuilds against the
current interpreter.

**Prevention:** Don't pin to a CPython-specific wheel in cross-runtime
environments. Pin the source distribution in PyPy-targeted lock files.

### Custom HTML tags appear as plain text in output

**Cause:** You wrote `{{ "<custom>" }}` in a Jinja template (or
`escape("<custom>")` in Python) and got `&lt;custom&gt;` in the output.
This is correct behaviour — the string was escaped because it was treated
as untrusted.

**Solution:** If the snippet is genuinely yours (not user input), wrap it
in `Markup`:

```python
{{ Markup("<custom>") }}    # in a context with autoescape
```

```jinja
{{ "<custom>"|safe }}       # equivalent in template
```

**Prevention:** Author HTML snippets in Python (with `Markup`) or in
templates (as literal markup), not as data passed in. Keep the safety
boundary between "code" and "data" sharp.

---

## 7. FAQ

**Q: Is MarkupSafe safe to use with non-HTML contexts (URLs, JSON, JS)?**

No. `escape()` only handles the five characters that matter in HTML/XML
text and attribute contexts. URLs need `urllib.parse.quote`; JavaScript
string literals need JSON encoding plus careful `</script>` handling;
shell arguments need `shlex.quote`. Mixing contexts is the most common
class of XSS bypass.

**Q: Does `escape()` handle Unicode?**

Yes, transparently. The function only touches `& < > ' "`. Every other
character — including emoji, RTL text, and the entire BMP and astral
planes — passes through unchanged. The C accelerator uses three
specialized paths for 1-byte, 2-byte, and 4-byte string kinds, all of
which are bytewise identical in semantics.

**Q: What's the performance difference between `_native` and `_speedups`?**

Run `python bench.py` from the repo root — it uses `pyperf` to time five
benchmarks (short escape, long escape, short plain, long plain, long
suffix) against both implementations. In typical CPython 3.13 environments
the C path is several times faster on long strings; on short strings the
Python overhead dominates and the difference is small. Either way,
`escape()` is rarely the bottleneck in a real application.

**Q: Why does `Markup("...")` not escape, but `Markup.escape("...")` does?**

Because they answer different questions. `Markup(s)` says "trust this
exactly as written" — useful for layout snippets, icons, pre-validated
content. `Markup.escape(s)` says "make this safe first" — equivalent to
the module-level `escape()` and what you should use for any data that
came from outside your code.

**Q: Can I subclass `Markup`?**

Yes. `Markup` is a regular `str` subclass with `__slots__ = ()` (no
instance dict). All operators preserve the calling subclass through
`self.__class__`, and the classmethod `Markup.escape` honours the calling
class. Define your subclass, optionally override `__html_format__`, and
the rest comes for free.

**Q: Does `Markup` work with f-strings?**

It works in the sense that interpolation produces plain `str`, *not*
`Markup`:

```python
m = Markup("<b>safe</b>")
result = f"prefix {m} suffix"   # type: str, NOT Markup
```

If you need to preserve the safety marker, use `%`, `Markup.format`, or
explicit concatenation instead of f-strings.

**Q: What happens if I `+` two `Markup` instances?**

The result is `Markup`. Both operands are already safe; concatenation
preserves that.

**Q: How do I tell if a value is `Markup` or plain `str`?**

```python
from markupsafe import Markup
isinstance(value, Markup)
```

In practice, you rarely need to ask. Use `soft_str` at boundary points
and let the API do the right thing.

**Q: Does MarkupSafe escape backticks or forward slashes?**

No. Only `& < > ' "`. Backticks (`` ` ``) and slashes (`/`) are not
HTML-special in standards-compliant contexts; legacy IE bugs that treated
backticks as quote characters are no longer relevant. If you need
extra-paranoid escaping for an old browser, use a dedicated sanitizer.

**Q: Is `striptags()` a safe HTML sanitizer?**

No. It is a one-way reduction to plain text. Do not feed its output back
into HTML expecting it to be safe in some intermediate way — once you've
stripped tags, the resulting string is just plain text and should be
re-escaped if it goes back into HTML.

**Q: Does MarkupSafe have any runtime configuration?**

None. The only build-time switch is the `CIBUILDWHEEL` environment
variable consumed by `setup.py`. There are no logger names, plugin
hooks, monkey-patch points, or dispatch registries. The library is
deliberately small.

**Q: Where can I read about how Jinja2 uses MarkupSafe?**

See the Jinja2 documentation on autoescape. The short version: every
`{{ ... }}` expression in an autoescape-enabled environment runs
`escape()` on the value, and the `| safe` filter wraps a value in
`Markup()`.

**Q: How do I test my own `__html__` implementation?**

Call `escape()` on an instance and check the return:

```python
from markupsafe import escape, Markup
result = escape(MyObject(data="<dangerous>"))
assert isinstance(result, Markup)
assert "<dangerous>" not in result      # if you escaped the data internally
assert "&lt;dangerous&gt;" in result
```

**Q: What versions of Python are supported?**

Modern CPython (3.10 and newer for current releases) plus PyPy / Jython /
GraalVM via the pure-Python fallback. Check the wheel index on PyPI for
the exact set published with the release you're installing.

**Q: Where do I report a bug?**

`https://github.com/pallets/markupsafe/issues`. Security-sensitive
findings (e.g. an escape bypass) should be reported privately following
the Pallets project's security policy rather than as a public issue.

---

_Generated April 2026 from pallets/markupsafe at b2e4d9c. AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it. Unofficial; not affiliated with or reviewed by the project._
