MarkupSafe
raw JSON → 3.0.3 verified Tue May 12 auth: no python install: verified quickstart: verified
MarkupSafe implements a text object (Markup, a str subclass) that escapes characters so it is safe to use in HTML and XML. Characters with special meanings are replaced so they display as literal characters, mitigating injection attacks. It is the escaping backbone for Jinja2 and Flask. Current version is 3.0.3 (released Sep 2025); the project follows a feature-release + fix-branch cadence under the Pallets organization, with the 3.0.x branch as the active supported line.
pip install markupsafe Common errors
error ImportError: cannot import name 'soft_unicode' from 'markupsafe' ↓
cause The `soft_unicode` function was removed in MarkupSafe version 2.1.0, leading to this error when older versions of dependent libraries (like Jinja2 or Flask) try to import it from newer MarkupSafe installations.
fix
Downgrade MarkupSafe to a compatible version, typically
2.0.1, or upgrade the dependent library to a version compatible with MarkupSafe 2.1.0+. pip install markupsafe==2.0.1 error ModuleNotFoundError: No module named 'markupsafe' ↓
cause The `markupsafe` package is not installed in the Python environment, or the environment where it's installed is not active.
fix
Install the
markupsafe package using pip: pip install markupsafe or python -m pip install markupsafe error ImportError: cannot import name 'Markup' from 'jinja2' ↓
cause In newer versions of Jinja2 (3.0 and above), `Markup` is no longer directly imported from `jinja2` but from `markupsafe`, which is its underlying dependency. Older codebases attempting to import `Markup` directly from `jinja2` will fail.
fix
Change the import statement from
from jinja2 import Markup to from markupsafe import Markup. error TypeError: expected str, bytes or os.PathLike object, not Markup ↓
cause This error occurs when a function expects a standard string (`str`), bytes, or a path-like object, but instead receives a `markupsafe.Markup` object, which is a subclass of `str` but has special handling to prevent cross-site scripting (XSS). This often happens when `Markup` objects are passed to file I/O operations or other functions not designed to handle them.
fix
Explicitly convert the
Markup object to a standard string using str(my_markup_object) before passing it to the function that expects str, bytes, or a path-like object. error AttributeError: 'Markup' object has no attribute 'decode' ↓
cause This error typically arises when attempting to call the `decode()` method on a `Markup` object in Python 3. In Python 3, `str` (and thus `Markup` which is a subclass of `str`) already represents Unicode, so `decode()` is not a valid operation; it's meant for `bytes` objects. This might be a remnant from Python 2 code or incorrect handling of encoding.
fix
If you intend to work with bytes, ensure the object is indeed a
bytes object before calling decode(). If the Markup object contains the desired string, no decoding is needed. If you need to convert it to bytes, use my_markup_object.encode('utf-8') instead. Warnings
breaking soft_unicode was permanently removed in 2.1.0. Caused mass ImportError in any project depending on Jinja2 < 3 or Flask < 2, which tried 'from markupsafe import soft_unicode'. ↓
fix Replace all 'from markupsafe import soft_unicode' with 'from markupsafe import soft_str'. Upgrade Jinja2 to >= 3.0 and Flask to >= 2.0.
breaking Python 3.7 and 3.8 support was dropped in 3.0.0. The minimum required Python version is now 3.9. ↓
fix Upgrade your Python runtime to 3.9+ before upgrading MarkupSafe to 3.x.
gotcha Markup(user_input) does NOT escape — the constructor marks a string as already safe without touching its content. Passing dynamic/user-controlled data directly to Markup() is an XSS vulnerability. ↓
fix Use escape(user_input) or Markup.escape(user_input) to escape untrusted strings. Reserve Markup(literal) for hard-coded HTML strings only.
gotcha Using an f-string or %-formatting inside Markup() bypasses escaping: Markup(f'<b>{user_input}</b>') is unsafe. Only Markup.format(), Markup.__mod__, and Markup-level operators auto-escape their arguments. ↓
fix Use Markup('<b>{}</b>').format(user_input) or Markup('<b>%s</b>') % user_input instead of f-strings inside Markup().
gotcha str.join() on a list containing Markup objects does not escape plain-str items in the list. Only Markup.join() escapes non-Markup items in the sequence. ↓
fix Use Markup('<separator>').join(items) instead of '<separator>'.join(items) when mixing Markup and plain strings.
deprecated markupsafe.__version__ is deprecated since 3.0.0 and now raises DeprecationWarning (upgraded from UserWarning in 3.0.3). Do not read the version via the module attribute. ↓
fix Use importlib.metadata.version('markupsafe') for version detection.
breaking In 3.0.0 several Markup str-methods (strip, lstrip, rstrip, removeprefix, removesuffix, partition, rpartition) no longer escape their argument; replace() only escapes its replacement argument. Code relying on those methods escaping search-pattern arguments will silently change behavior. ↓
fix Manually escape any argument to these methods that contains untrusted content before passing it in, or use escape() on the result.
Install compatibility verified last tested: 2026-05-12
python os / libc status wheel install import disk
3.10 alpine (musl) - - 0.01s 17.9M
3.10 slim (glibc) - - 0.01s 18M
3.11 alpine (musl) - - 0.02s 19.7M
3.11 slim (glibc) - - 0.02s 20M
3.12 alpine (musl) - - 0.02s 11.6M
3.12 slim (glibc) - - 0.01s 12M
3.13 alpine (musl) - - 0.01s 11.2M
3.13 slim (glibc) - - 0.01s 12M
3.9 alpine (musl) - - 0.01s 17.4M
3.9 slim (glibc) - - 0.01s 18M
Imports
- Markup wrong
from jinja2 import Markupcorrectfrom markupsafe import Markup - escape wrong
from jinja2 import escapecorrectfrom markupsafe import escape - soft_str wrong
from markupsafe import soft_unicodecorrectfrom markupsafe import soft_str
Quickstart verified last tested: 2026-04-23
from markupsafe import Markup, escape
# Escape untrusted input — returns a Markup (str subclass)
user_input = "<script>alert('xss')</script>"
safe = escape(user_input)
print(safe) # <script>alert('xss')</script>
# escape() is idempotent: escaping a Markup object is a no-op
assert escape(safe) == safe
# Build HTML safely: use Markup.format() so arguments are auto-escaped
template = Markup("<p>Hello, <em>{name}</em>!</p>")
html = template.format(name='<World>')
print(html) # <p>Hello, <em><World></em>!</p>
# Join a mixed list safely — use Markup.join(), NOT str.join()
lines = [Markup("<b>Title</b>"), "user & data"]
result = Markup("<br>").join(lines)
print(result) # <b>Title</b><br>user & data
# Check the version correctly (markupsafe.__version__ is deprecated)
import importlib.metadata
version = importlib.metadata.version("markupsafe")
print(version)