Open-source project
pallets/markupsafe avatar
pallets/markupsafe

MarkupSafe: the string subclass that makes escaping the default

Safely add untrusted strings to HTML/XML markup.

699 stars194 forksPythonBSD-3-Clause

At a glance

What is it?
MarkupSafe is a Pallets library whose Markup type is a str subclass that escapes its arguments on the way in, so untrusted text can be inserted into HTML and XML without becoming markup. It suits anyone building templates that interpolate user data, and it does not decide what is safe for you, only how to mark it.
Who is it for?
MarkupSafe is worth depending on directly whenever you assemble markup in Python outside a template engine, because the escaping is attached to the type rather than to a call you might forget, and because Markup is a str subclass so the rest of your code keeps working with ordinary strings.
Can I use it commercially?
Yes. BSD-3-Clause is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 21 days ago.
What is it written in?
Mainly Python, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

A str subclass that carries its own escaping

MarkupSafe implements a text object that escapes characters so it is safe to use in HTML and XML. Characters that have special meanings are replaced so that they display as the actual characters, and the stated effect is that this mitigates injection attacks, meaning untrusted user input can safely be displayed on a page.

The design is a single class. Markup is a str subclass, and the class implements the string protocol, but with one difference: its methods and operators escape their arguments. That is the whole trick, and it is a good one, because the escaping follows the data rather than the call site.

The examples make the two directions explicit. Escaping a string produces a Markup object:

pycon
>>> from markupsafe import Markup, escape

>>> # escape replaces special characters and wraps in Markup
>>> escape("<script>alert(document.cookie);</script>")
Markup('&lt;script&gt;alert(document.cookie);&lt;/script&gt;')

And wrapping a string in Markup marks it as safe and prevents further escaping:

pycon
>>> # wrap in Markup to mark text "safe" and prevent escaping
>>> Markup("<strong>Hello</strong>")
Markup('<strong>hello</strong>')

>>> escape(Markup("<strong>Hello</strong>"))
Markup('<strong>hello</strong>')

Note the asymmetry, because it is the security boundary of the library. escape on a plain string produces escaped markup. escape on a Markup object produces the same thing, unchanged, because the object is already marked as safe. The decision of what to mark as safe is yours, and the library only enforces that marked things stay marked.

The repository is the Pallets project at pallets/markupsafe, licensed BSD-3-Clause, and it is the escaping layer that Jinja sits on, which the topic list reflects with the jinja and template-engine tags.

Format on a Markup object escapes the argument, which is the point

The example that matters most in practice is the format call, because that is the shape of the bug in a template:

pycon
>>> # Markup is a str subclass
>>> # methods and operators escape their arguments
>>> template = Markup("Hello <em>{name}</em>")
>>> template.format(name='"World"')
Markup('Hello <em>&#34;World&#34;</em>')

The template literal holds a placeholder, and the value substituted into it, a string containing a double quote, comes out as an HTML entity, &#34;. The user data is in the page as text and not as markup.

This is the property that makes the type worth using over a function call. If escaping were a function, the protection would depend on the author remembering to call it, and the failure would be silent. Because it is a method on the string type, the escaping happens because the value is being formatted into a Markup string, and a plain str being substituted into a Markup string is exactly the case where escaping is wanted.

The corollary is that the same mechanism cuts both ways. If you build a Markup string by concatenating a piece of user data into it, you have to be deliberate, because Markup will not escape a Markup argument, and there is no way for the type to know that the Markup you just created came from untrusted input. That is the discipline the library asks of you, and it is a reasonable one: it makes the safe path the default path without pretending the unsafe path is impossible.

A C extension that is allowed to fail

MarkupSafe ships a C extension, and the setup.py is unusual enough to be worth reading because it shows how the project treats the optional acceleration.

The extension is declared as a single C source, src/markupsafe/_speedups.c, and the build machinery is a custom build_ext subclass, ve_build_ext. Its run method catches PlatformError and turns it into a BuildFailed exception, and its build_extension method catches CCompilerError, ExecError and PlatformError the same way. There is also a special case for a ValueError whose message contains 'path', with a comment pointing at Python issue 7511, because that error can occur on 64 bit Windows.

The consequence is that a failed C extension build is not a build failure. The project prints a warning, the text says the C extension could not be compiled, speedups are not enabled, and it retries the build without the extension. The package installs and works, just without the accelerated path.

There is also a check on which interpreters get the extension at all. supports_speedups is false when the Python implementation is PyPy, Jython or GraalVM, so those platforms get the pure Python implementation by design rather than by accident. And in a CI wheel build, identified by the CIBUILDWHEEL environment variable being set to 1, the binary build is requested directly rather than attempted and retried.

So the extension is a genuine performance option, and the library is careful to degrade rather than fail. The package metadata lists the development status as Production/Stable, the operating system as OS Independent, and Typing :: Typed.

Packaging, typing and the development toolchain

The pyproject.toml is a modern Pallets layout and it is worth a glance for what it says about the project's expectations.

The package name is MarkupSafe, and the version in the source tree is 3.1.0.dev, a development version. The Python floor is 3.11. The build backend is setuptools with a requirement of setuptools>=77, and the setup.py exists alongside it to handle the C extension.

The dependency groups are split by purpose rather than lumped together. The dev group holds ruff, tox and tox-uv. The docs group holds pallets-sphinx-themes, sphinx and sphinxcontrib-log-cabinet, with a separate docs-auto group for sphinx-autobuild. There is a gha-update group for the GitHub Actions update action, gated on python_full_version being at least 3.12, and a pre-commit group with pre-commit and pre-commit-uv. The tests group holds pytest, plus pytest-run-parallel on Python 3.13 and above, and the typing group holds mypy, pyright and pytest.

The pytest configuration is strict in a way that tells you about the project. filterwarnings is set to error, so any warning raised during a test fails the test rather than printing a line. There is a custom marker, thread_unsafe, for tests that are not safe to run in multiple threads, which exists because pytest-run-parallel is not always installed.

The repository top level shows the rest of the shape: a src/ directory for the package source, tests/, docs/, a .devcontainer/ for development environments, a .pre-commit-config.yaml, a .readthedocs.yaml for the documentation build, a uv.lock, a CHANGES.rst for the changelog, and a bench.py at the root, which is consistent with a library that has a C fast path and wants to measure it.

Where MarkupSafe is the wrong tool

The first limit is that it is a mechanism and not a policy. MarkupSafe escapes characters that have special meaning in HTML and XML. It does not know whether your output is going into an HTML element body, an attribute value, a script block, a URL, or a CSS context, and those contexts have different dangerous characters and different correct answers. If you are building something where the output context varies, you need a context-aware escaper, and MarkupSafe on its own is the wrong level of abstraction.

The second is the trust boundary. Markup marks text as safe, and once text is marked, escape will not touch it again. If untrusted data reaches a Markup constructor, the library will not save you. The type makes the safe path easy and the unsafe path possible, and a code review has to be the thing that catches the second case.

The third is the scope of the escaping. It is for HTML and XML. JSON, YAML, CSV, a shell command line, a SQL statement and a log format all have their own escaping rules, and using Markup for any of them produces output that is wrong rather than merely imperfect. It is also worth being explicit that escaping is not sanitisation: it makes text display as text, it does not remove a script tag from a URL, it does not block a javascript: link, and it does not make a download safe.

The fourth is the pure Python fallback. The project is careful to allow the C extension build to fail, and that is a good decision for portability, but it means the performance characteristics of your install are not guaranteed to match the ones the project measures. If you are depending on the accelerated path for throughput, an install that silently fell back to Python is worth knowing about, and the warning it prints at build time is the only signal you get.

The fifth is that it is a small library with a big dependency footprint in some stacks. Jinja depends on it, and many web frameworks pull Jinja in, so MarkupSafe is often already installed whether you wanted it or not. If you are adding it as a direct dependency, check first, because the version already on the machine may be the one you get.

Editorial conclusion

MarkupSafe is worth depending on directly whenever you assemble markup in Python outside a template engine, because the escaping is attached to the type rather than to a call you might forget, and because Markup is a str subclass so the rest of your code keeps working with ordinary strings. It is the wrong tool if you want a policy engine, since the library provides the mechanism and not the rules, and it is the wrong tool for content that is not HTML or XML, where escaping is meaningless. Before you adopt it, check the three things the project tells you: that Markup marks text as safe and prevents further escaping, which is the property you must not hand to untrusted input; that the escape function replaces special characters and wraps the result in Markup, so the return type tells you what happened; and that Markup methods and operators escape their arguments, which is what makes the format call in the example safe without any extra work on your part.

Frequently asked questions

What does MarkupSafe actually do?

It implements a text object, the Markup class, that escapes characters so they are safe to use in HTML and XML, replacing characters that have special meanings so they display as the actual characters. Because Markup is a str subclass whose methods and operators escape their arguments, untrusted user input can be safely displayed on a page.

What is the difference between escape and Markup in MarkupSafe?

escape replaces special characters and wraps the result in Markup, so plain text becomes safe markup. Markup marks text as safe and prevents further escaping, so escaping a Markup object returns it unchanged. That is the trust boundary: never wrap untrusted input in Markup directly.

Does MarkupSafe require a C compiler to install?

No. The setup.py catches compiler and platform errors, prints a warning that the C extension could not be compiled and that speedups are not enabled, and retries the build without the extension. The extension is also skipped entirely on PyPy, Jython and GraalVM, so the package installs and works there using the pure Python implementation.

Official sources

  1. License: BSD-3-Clause
  2. pallets/markupsafe on GitHub
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/pallets-markupsafe.svg)](https://hysenlabs.com/projects/pallets-markupsafe)

Was this useful?