mrkeyoor.com_
Thu 06 Aug 05:53 UTC
PyPIUtilsupdated 06 Aug 2026

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.

Verdict

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.

API stability5/5The 3.x line dates from 2018 and markdown.markdown() plus the Markdown class have not changed shape since; removals such as safe_mode and the html5/xhtml1 format aliases were deprecated for releases beforehand, and 2026 releases are bug fixes only.
Docs4/5python-markdown.github.io documents every bundled extension with its options, plus a generated API reference added in 3.5; the weak spot is the extension-authoring guide, which assumes you already understand the five-stage processor pipeline.
Maintenance4/5Pushed 30 July 2026 with 27 open issues (28 counting PRs) and steady patch releases through 2026, but the project is explicit that bundled extensions are in maintenance mode, so this is careful upkeep rather than active development.
Ecosystem5/5Around 28.4 million weekly downloads, with MkDocs and mkdocs-material built directly on its extension API and pymdown-extensions supplying dozens of extra syntaxes.

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
Skip it if

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, tables

Tables 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 it

md.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.md

Handy 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 }

![logo](logo.png){: 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

PackageRegistryPick it when
markdown-it-pyPyPIYou need CommonMark compliance, a plugin API modelled on markdown-it, or access to the token stream for non-HTML output.
mistunePyPIRendering speed is the priority and you can live with a smaller extension catalogue.
pymdown-extensionsPyPIYou are staying on Python-Markdown but need modern syntax: superfences, tabbed blocks, task lists, emoji, better admonitions.