MarkdownKit

Python Markdown guide

Markdown for Python

Convert Markdown to HTML in Python with one function call. Start with a runnable example, then add file handling, extensions, sanitization, and production limits only where your application needs them.

Minimal runnable example

Convert Markdown with one function call

Python-Markdown exposes a convenience function for a single conversion. It returns an HTML string; it does not write a file or create a complete HTML document.

Preview the same Markdown →MarkdownKit uses a different parser, so extension behavior may differ from Python-Markdown.
python -m pip install markdown
import markdown

source = "# Release notes\n\n- Fixed search\n- Added export"
html = markdown.markdown(source)
print(html)

Read and write UTF-8 files

Explicit encodings make the behavior consistent across operating systems. This script reads a Markdown file and writes the converted HTML fragment next to it.

from pathlib import Path
import markdown

source_path = Path("article.md")
output_path = source_path.with_suffix(".html")

source = source_path.read_text(encoding="utf-8")
html = markdown.markdown(source)
output_path.write_text(html, encoding="utf-8")

Use pathlib.Path instead of concatenating path strings. When filenames come from users, constrain reads and writes to an intended directory and reject path traversal. File safety and HTML safety are separate boundaries.

Accepted dialect

Enable only the syntax you accept

Tables, fenced code, highlighted code, a table of contents, and footnotes are opt-in features in Python-Markdown. The extra bundle enables a documented group of common extensions.

Configure the extension list once at the application boundary and test a fixture containing every supported feature. Third-party extensions run Python code in the application process, so review them before installation.

html = markdown.markdown(
    source,
    extensions=["tables", "fenced_code", "codehilite", "toc", "footnotes"],
)

Sanitize Markdown controlled by users

Markdown can contain raw HTML, links, images, and extension-generated attributes. Removing raw HTML before parsing is insufficient because a sanitizer must evaluate the final tags, attributes, and URL schemes.

The Python-Markdown documentation currently points to JustHTML as a sanitization option. Install it separately, convert the Markdown, then sanitize the HTML fragment:

python -m pip install markdown justhtml
import markdown
from justhtml import JustHTML

unsafe_html = markdown.markdown(untrusted_markdown)
safe_html = JustHTML(unsafe_html, fragment=True).to_html()

Define which elements and URL schemes are allowed, whether images may load from third parties, and whether user-generated links need additional attributes. Test hostile inputs alongside normal fixtures and keep the sanitizer updated.

Production patterns

Install Python-Markdown in the active environment

Use the interpreter that will run the application to invoke pip. In a project, record and pin the dependency according to the project’s release policy. Do not name your own script markdown.py; that local file can shadow the installed package.

Reuse a Markdown instance for related conversions

The convenience function creates an instance for each call. When processing many documents with the same extension configuration, create Markdown once and call reset() before each independent conversion. Resetting clears parser state collected by extensions such as footnotes.

from markdown import Markdown

converter = Markdown(extensions=["tables", "fenced_code"])

for source in documents:
    converter.reset()
    html = converter.convert(source)
    save(html)

Do not share one mutable parser instance across unrelated concurrent work without designing the synchronization. A simple web handler can call the convenience function per request until profiling demonstrates that reuse is needed.

Highlight fenced code deliberately

fenced_code can preserve a language label in the generated HTML, but colored syntax usually requires an additional extension and a highlighter such as Pygments. Server-side highlighting increases generated markup. Client-side highlighting adds browser code and can change loading behavior. Choose one based on the publishing environment.

Always escape or highlight code through a library designed for that purpose. Do not insert captured code text into an HTML template without escaping. The code block is content even when it contains strings that look like HTML.

Keep HTML templates separate from Markdown conversion

The parser returns an HTML fragment. Let the web framework or template engine supply the document shell, navigation, metadata, and stylesheet. This separation avoids putting a complete page inside another page and makes the sanitization boundary easier to see.

Most template systems escape string values by default. That is correct for ordinary text, but it will display generated tags literally. Mark HTML as safe only after the application has established whether the Markdown is trusted or the resulting fragment has been sanitized. Keep that operation on the same line or in the same helper as the sanitizer so a later edit cannot bypass it accidentally.

Do not build a template by concatenating a page title, author name, or other untrusted values into the HTML string. Markdown conversion covers only the Markdown body. Escape every other text value through the normal template system.

Convert a directory with explicit input and output paths

A small script can publish several Markdown files without changing the source directory. Create the output directory once, skip files that should not be public, and derive each output name from the relative input path.

from pathlib import Path
import markdown

source_root = Path("content")
output_root = Path("public")

for source_path in source_root.rglob("*.md"):
    relative = source_path.relative_to(source_root)
    output_path = (output_root / relative).with_suffix(".html")
    output_path.parent.mkdir(parents=True, exist_ok=True)

    text = source_path.read_text(encoding="utf-8")
    html = markdown.markdown(text, extensions=["fenced_code", "tables"])
    output_path.write_text(html, encoding="utf-8")

This example writes fragments, not complete pages, and assumes the source files are trusted. A static-site build usually passes the fragment into a page template and copies referenced assets in a separate step. Preserve relative directories when links depend on them.

Extract metadata before rendering the page

Many content folders put YAML front matter above the Markdown body. Core Python-Markdown does not make that metadata part of the HTML page automatically. Its meta extension can parse supported metadata syntax and expose values on a reusable Markdown instance after conversion.

from markdown import Markdown

converter = Markdown(extensions=["meta"])
html = converter.convert(source)
title = converter.Meta.get("title", ["Untitled"])[0]

Treat metadata as data, not trusted HTML. Validate required fields, normalize dates, and escape titles or descriptions when the template renders them. Reset the converter before the next independent document so metadata from one file cannot be mistaken for metadata from another.

Set limits before accepting large or hostile input

Sanitization addresses dangerous HTML, but a public endpoint also needs operational limits. Reject bodies larger than the application is prepared to process. Bound request time at the web-server layer, and avoid extensions that fetch arbitrary remote resources while parsing. If uploaded files are supported, validate their type and keep their paths outside executable code directories.

Very deep nesting, enormous tables, and unusually long lines can consume more time and memory than ordinary articles. Add fixtures near the size limit and observe the whole request, including sanitization and syntax highlighting. A parser benchmark on a short README does not predict that workload.

Return a clear client error for invalid or oversized input without including the entire source in logs. Production exceptions can contain excerpts or filenames, so review logging and error-reporting settings as part of the privacy boundary.

Cache output using all conversion inputs

Static Markdown does not need to be parsed on every request. Cache or prebuild the HTML when the source changes. A correct cache key includes more than the Markdown text: parser version, enabled extensions, extension configuration, sanitizer policy, and relevant template version can all change the result.

Invalidate the cache when any of those inputs changes. Do not cache sanitized and unsanitized fragments under the same key. If pages are personalized, keep user-specific data out of a shared rendered fragment and let the template add it later.

Customize output at the narrowest useful layer

Before writing an extension, check whether a documented built-in extension or configuration option already produces the required structure. If the application only needs to wrap the finished fragment, do that in the template. If one element needs different HTML, use a documented renderer or tree-processing hook rather than applying broad regular-expression replacements to generated markup.

Regular expressions can be useful for inspecting plain source conventions, but they are a poor HTML transformation layer. Once Markdown has become HTML, use an HTML parser for structural changes. This also prevents replacements from accidentally modifying code samples or attribute values.

Diagnose common failures

  • ModuleNotFoundError: run python -m pip show markdown with the same interpreter that starts the application, and check for a local markdown.py file.
  • Fences appear as text: enable fenced_code, verify blank lines, and check whether the fence is nested where the extension accepts it.
  • Tables stay as pipes: enable tables and confirm the header separator row is valid.
  • Footnotes leak between documents: call reset() before converting the next independent source.
  • Tags appear literally: find where the template escapes the HTML string; do not disable escaping before resolving the trust and sanitization decision.
  • Unexpected tags survive: inspect the sanitizer policy and final HTML rather than trying to remove raw HTML only from the Markdown source.

Choose a library by compatibility requirements

OptionGood fitCompatibility decision
Python-MarkdownPython applications using its documented core and extension ecosystemEnable the exact extensions your content requires
markdown-it-pyProjects that want the markdown-it parsing model and plugin ecosystemSelect a preset and plugins deliberately
MistuneProjects that need configurable renderers and pluginsTest output against the destination’s expected dialect
PandocFile conversion among Markdown, DOCX, EPUB, LaTeX, and other document formatsRun an external executable and specify input/output formats

Performance depends on input, extensions, renderer work, and deployment shape. Measure the representative documents in your application instead of choosing a parser from a generic benchmark. Compatibility failures usually cost more than small parsing differences.

Test the output contract

Tests should assert the structure your application depends on, not every byte of library output. A useful fixture includes headings, emphasis, a link, a list, a fenced code block, a table, raw HTML, and non-ASCII text.

def test_markdown_contract():
    source = "# Café\n\n- one\n- two"
    html = markdown.markdown(source)

    assert "<h1>Café</h1>" in html
    assert html.count("<li>") == 2

Keep a separate security suite for the sanitization boundary. If HTML is placed into a framework template, verify whether the framework escapes strings by default and keep the deliberate “safe HTML” operation as close as possible to the sanitizer.

Production checklist

  • Use the same Python interpreter for installation and execution.
  • Keep user files from shadowing the markdown package.
  • Define one supported Markdown dialect and test its extensions.
  • Read and write files with an explicit UTF-8 encoding.
  • Sanitize final HTML when the source is not fully trusted.
  • Keep parser reuse local, reset state, and avoid unsafe concurrent sharing.
  • Measure representative content before optimizing or changing libraries.
  • Preview generated HTML with the same CSS and security headers used in production.

Common questions

Frequently asked questions

What is the simplest way to convert Markdown to HTML in Python?

Install Python-Markdown, import markdown, and pass a string to markdown.markdown(). Add extensions only when the input requires them.

Does Python-Markdown sanitize untrusted input?

No. Treat its HTML output as untrusted when users control the Markdown. Sanitize the resulting HTML with a maintained allow-list sanitizer before placing it in a web page.

Which Python Markdown library should I choose?

Python-Markdown is a direct choice for conventional Markdown plus extensions. markdown-it-py follows the markdown-it ecosystem and supports plugins. Mistune offers a parser and renderer architecture. Pandoc is better treated as an external document converter.

Why is my table or fenced code block not rendering?

Those features are extensions in Python-Markdown. Enable tables and fenced_code explicitly, and make sure blank lines and fence indentation are valid.

Evidence

Sources and verification

Compatibility and workflow details were last checked on 2026-07-21. Anything we could not confirm is marked “Not verified.”