Markdown
Python-Markdown (the PyPI project named Markdown) turns Markdown text into HTML. markdown.markdown(text) is the whole entry point for most people. Under it sits a multi-stage pipeline: preprocessors clean the source lines, block processors build an ElementTree, inline processors handle emphasis and links, tree processors rewrite the element tree, and postprocessors patch the serialized HTML. Every stage is a named, priority-ordered registry you can insert into, which is why so much documentation tooling is built on it, MkDocs most visibly. It follows the original Markdown reference implementation rather than CommonMark, and ships extensions for tables, footnotes, definition lists, attribute lists, fenced code, abbreviations, table of contents and Markdown inside HTML blocks.
The default choice for rendering trusted Markdown in Python, and the one your documentation stack has probably already picked for you. Reach for markdown-it-py when you need CommonMark exactness or a token stream, and always sanitize the output if the input came from a user.
Use it if
- You are in the MkDocs or mkdocs-material world already, where this parser and its extension API are what the whole toolchain expects
- You need the classic extension set out of the box: tables, footnotes, fenced code, attr_list, def_list, md_in_html and abbr, plus a table of contents with slugified anchor ids and permalinks
- You want to change the output structurally, for example adding CSS classes to every table or rewriting image URLs, and you would rather hook a tree processor than run regexes over the finished HTML
- You want zero dependencies and pure Python: it installs anywhere, including locked-down build images with no compiler
- The Markdown comes from users. There is no sanitizer here; safe_mode was removed in 3.0 and raw HTML in the input passes straight through to the output. You must run the result through a sanitizer such as nh3 or bleach, and forgetting that is a stored XSS bug
- You need to match CommonMark or GitHub Flavored Markdown exactly. This parser follows the original reference implementation with documented differences, so nested lists, emphasis edge cases and HTML blocks will not always render the way GitHub renders them
- Throughput matters. The multi-pass regex and ElementTree design is not built for speed; if you render many documents per request, benchmark mistune or markdown-it-py against your own content before committing
- You want tokens rather than HTML, for example to render to PDF, a terminal, or a React tree. Python-Markdown gives you an ElementTree mid-pipeline and an HTML string at the end, while markdown-it-py hands you a documented token stream
- You are hoping for new syntax in the bundled extensions. As of 3.10.3 the project officially documents all included extensions as being in maintenance mode, so new syntax comes from third-party packages like pymdown-extensions
Setup reality
pip install markdown, pure Python, no dependencies, Python 3.10 or newer. The surprises are behavioural. The default output format is xhtml, so you get self-closing tags like <br /> until you pass output_format="html". The convenience function markdown.markdown() builds a fresh parser on every call, which is wasteful in a loop; build one Markdown instance and reuse it, but then you must call reset() between documents or footnotes, abbreviations and TOC state leak from one document into the next. Extensions are named by string ("toc", "extra") or passed as instances, and their settings go in a separate extension_configs dict keyed by the same string, which is easy to get subtly wrong because an unknown option raises while a misspelled extension name raises a different error entirely. The codehilite extension needs Pygments installed separately. Values like md.toc and md.Meta only exist after convert() has run.
Patterns
Render Markdown to HTMLconvert-a-string
import markdown
html = markdown.markdown("# Title\n\nSome **bold** text.")
# <h1>Title</h1>\n<p>Some <strong>bold</strong> text.</p>This convenience function constructs a new parser, converts, and throws it away. Fine for one document, wasteful inside a loop.
Reuse one parser across many documentsreuse-parser-instance
from markdown import Markdown
md = Markdown(extensions=["extra", "toc"])
for doc in documents:
html = md.convert(doc.body)
doc.toc = md.toc
md.reset()reset() is not optional here. Without it, footnote numbering, abbreviation definitions and the TOC accumulate across documents, and the second page ends up with the first page footnotes.
Turn on the common extensionsenable-extensions
import markdown
html = markdown.markdown(
text,
extensions=["extra", "toc", "sane_lists", "smarty"],
)
# "extra" is a bundle: abbr, attr_list, def_list,
# fenced_code, footnotes, md_in_html, tablesTables and fenced code blocks are not on by default; plain Markdown has neither. Third-party extensions are given by their dotted path or an instance, for example "pymdownx.superfences".
Pass options to an extensionconfigure-extensions
import markdown
html = markdown.markdown(
text,
extensions=["toc", "codehilite"],
extension_configs={
"toc": {"permalink": True, "toc_depth": "2-4", "baselevel": 2},
"codehilite": {"linenums": False, "guess_lang": False},
},
)Config keys must match the extension name string exactly, and an unrecognised option raises KeyError rather than being ignored. codehilite needs Pygments installed or it silently degrades to plain code blocks.
Build a table of contentstable-of-contents
from markdown import Markdown
md = Markdown(extensions=["toc"])
body = md.convert(source)
print(md.toc) # nested <ul> HTML
print(md.toc_tokens) # [{"level": 1, "id": "title", "name": "Title", "children": [...]}]
# or put the literal marker [TOC] in the source to inline itmd.toc and md.toc_tokens only exist after convert() and are cleared by reset(). toc_tokens carries both the plain name and the rich html for each heading, which is what you want if headings contain inline code or links.
Emit HTML instead of XHTMLhtml5-output
import markdown
markdown.markdown(text) # <br />, <img ... />
markdown.markdown(text, output_format="html") # <br>, <img ...>The default really is xhtml. The old aliases html5 and xhtml1 still work but were deprecated in 3.4 in favour of html and xhtml.
Render user-submitted Markdown safelysanitize-untrusted-input
# pip install nh3
import markdown
import nh3
raw_html = markdown.markdown(user_text, extensions=["extra"])
safe_html = nh3.clean(raw_html)Python-Markdown deliberately passes raw HTML and javascript: URLs through; the removed safe_mode option was never a real defence. Sanitize after conversion, not before, or you will mangle the Markdown syntax.
Read key-value metadata from the top of a filefront-matter-metadata
from markdown import Markdown
md = Markdown(extensions=["meta"])
html = md.convert("Title: My post\nTags: python, markdown\n\nBody text.")
print(md.Meta) # {"title": ["My post"], "tags": ["python, markdown"]}The meta extension reads MultiMarkdown-style headers, not YAML front matter: keys are lowercased and every value is a list of strings. For real YAML front matter use a separate parser before handing the body over.
Convert a file on diskconvert-files
import markdown
markdown.markdownFromFile(
input="README.md",
output="README.html",
encoding="utf-8",
extensions=["extra"],
)input and output accept paths or open file objects; file objects must be opened in binary mode. Omitting output writes to stdout.
Use it from the shellcommand-line
python -m markdown -x extra -x toc -o html README.md > README.html
# with extension config from a YAML or JSON file
python -m markdown -x toc -c toc-config.yml README.mdHandy for one-off conversions and CI checks. The -c flag reads a config file mapping extension names to their options, which is the CLI equivalent of extension_configs.
Write an extension that rewrites the treecustom-extension
from markdown import Markdown
from markdown.extensions import Extension
from markdown.treeprocessors import Treeprocessor
class TableClasses(Treeprocessor):
def run(self, root):
for table in root.iter("table"):
table.set("class", "table table-striped")
class TableClassExtension(Extension):
def extendMarkdown(self, md):
md.treeprocessors.register(TableClasses(md), "table_classes", 5)
md = Markdown(extensions=["tables", TableClassExtension()])Every stage is a priority registry: higher numbers run first, and the built-in processors document their own priorities so you can slot in around them. Call md.registerExtension(self) in extendMarkdown only if your extension keeps state that reset() should clear.
Attach ids, classes and attributes in the sourcelinkify-and-attributes
import markdown
source = """
## Install {: #install .anchor }
{: width="120" loading="lazy" }
"""
markdown.markdown(source, extensions=["attr_list"])attr_list (part of the extra bundle) is the escape hatch for adding classes without writing an extension. It only applies to the element it trails, and the braces must be separated from the content by a space.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| markdown-it-py | PyPI | You need CommonMark compliance, a plugin API modelled on markdown-it, or access to the token stream for non-HTML output. |
| mistune | PyPI | Rendering speed is the priority and you can live with a smaller extension catalogue. |
| pymdown-extensions | PyPI | You are staying on Python-Markdown but need modern syntax: superfences, tabbed blocks, task lists, emoji, better admonitions. |