Markdown to PDF with Python

Five ways to turn Markdown into PDF from a Python script, what each one needs installed and where each one breaks. Or skip the toolchain: paste your Markdown below and download the PDF now, free.

Free · no sign-up

Nightly report generator

This script reads yesterday's metrics and writes a Markdown report that gets converted to PDF.

from __future__ import annotations

        import csv
        from datetime import date, timedelta
        from pathlib import Path


        def load_rows(path: Path) -> list[dict[str, str]]:
            """Read the metrics CSV exported by the warehouse job."""
            with path.open(newline="") as fh:
                return list(csv.DictReader(fh))


        def build_report(rows: list[dict[str, str]], day: date) -> str:
            total = sum(int(r["orders"]) for r in rows)
            lines = [f"# Orders for {day:%d %B %Y}", "", "| Region | Orders |", "| --- | ---: |"]
            lines += [f"| {r['region']} | {int(r['orders']):,} |" for r in rows]
            lines += ["", f"**Total:** {total:,}"]
            return "\n".join(lines)


        if __name__ == "__main__":
            yesterday = date.today() - timedelta(days=1)
            md = build_report(load_rows(Path("metrics.csv")), yesterday)
            Path("report.md").write_text(md, encoding="utf-8")

The report.md file is then sent to the converter; see the options below.

— 1 —

Converted in your browser. Your file never leaves your device.
1

Pick the approach that fits

A one-off file, a CI job, or a service that needs to convert thousands a day each point to a different library. The guide below compares them.

2

Know the pitfalls before pip install

Most of the pain is system dependencies: Cairo, Pango, a TeX distribution or a 300 MB headless browser. Each section lists what you actually need.

3

Or call an API

If you just want a PDF back from a POST request, the MDtoDOCX API does the rendering with the same engine as this page, with no local dependencies.

Converting Markdown to PDF in Python: the five real options

There is no single pip install markdown-to-pdf that does the whole job well, because the job has two halves: parsing Markdown into HTML, which Python does fine, and laying HTML out onto pages, which Python does not do natively. Every library below solves the second half by wrapping something else: a CSS layout engine, a TeX distribution or a browser. The choice therefore comes down to what you are willing to install and how much control over the output you need. All snippets assume Python 3.10 or newer and a file called report.md, like the one produced by the script in the converter above.

1. markdown + WeasyPrint: the pure-Python route

Python-Markdown turns the text into HTML and WeasyPrint lays the HTML out with a CSS engine written in Python. This is the most common combination and the one to reach for first.

import markdown
    from weasyprint import HTML

    html = markdown.markdown(
    open("report.md", encoding="utf-8").read(),
    extensions=["tables", "fenced_code", "codehilite", "toc"],
    )
    css = "@page { size: A4; margin: 2cm } body { font: 11pt Georgia }"
    HTML(string=f"<style>{css}</style>{html}").write_pdf("report.pdf")

Pitfalls. WeasyPrint needs Pango, Cairo and GDK-PixBuf as system libraries; on macOS that is brew install pango, on Debian apt install libpango-1.0-0 libpangoft2-1.0-0, and on Windows the GTK runtime, which is the step that fails most often. The codehilite extension only colours code if Pygments is installed and you include a Pygments CSS file; otherwise you get plain black text. Python-Markdown's tables extension is not GitHub-flavoured: task lists, strikethrough and autolinks need extra extensions or a switch to markdown-it-py. Remote images are fetched synchronously and time out slowly.

2. md2pdf and markdown-pdf: thin wrappers

Packages such as md2pdf bundle the two steps above into one call: md2pdf("report.pdf", md_file_path="report.md", css_file_path="style.css"). They save ten lines of code but inherit every WeasyPrint dependency and usually pin an older Python-Markdown, so the same table and code-highlighting gaps appear. Use them for a quick script; for anything you maintain, call the two libraries directly so you can upgrade them independently.

3. pypandoc: Pandoc plus a LaTeX engine

Pandoc is the most complete Markdown parser available, and pypandoc calls it from Python. For PDF output Pandoc hands the document to a LaTeX engine, which produces the best-looking typography of any option here, especially for math.

import pypandoc

    pypandoc.convert_file(
    "report.md", "pdf", outputfile="report.pdf",
    extra_args=["--pdf-engine=xelatex", "-V", "geometry:margin=2cm",
                "-V", "mainfont=Georgia"],
    )

Pitfalls. You need Pandoc and a TeX distribution installed; TeX Live is 2 to 4 GB and MiKTeX installs packages on first use, which breaks in an offline CI container. The default pdflatex engine cannot handle most Unicode, so an em dash or a Chinese character stops the build with a cryptic error; always pass --pdf-engine=xelatex or lualatex. Wide tables overflow the page unless you convert them to LaTeX longtable with a filter. Raw HTML inside the Markdown, common in files exported from Notion or ChatGPT, is silently dropped. Build times of five to fifteen seconds per document are normal.

4. pdfkit: wkhtmltopdf, which you should not start new projects on

pdfkit wraps the wkhtmltopdf binary, a 2012-era WebKit build. It renders HTML fast and used to be the default answer, but the project was archived in 2023 and its WebKit has no CSS grid, no flexbox gap and patchy break-inside support. It still works for simple documents, and many tutorials still recommend it, but every year it renders less of the CSS you write. If you already have it in production, keep it; if you are choosing today, pick option 1 or 5.

5. Playwright: a real browser, when fidelity matters most

Headless Chromium renders exactly what Chrome shows, which makes it the right choice when the Markdown contains Mermaid diagrams, KaTeX or anything that needs JavaScript to draw.

import markdown
    from playwright.sync_api import sync_playwright

    html = markdown.markdown(open("report.md", encoding="utf-8").read(),
                         extensions=["tables", "fenced_code"])
    with sync_playwright() as p:
    browser = p.chromium.launch()
    page = browser.new_page()
    page.set_content(html, wait_until="networkidle")
    page.pdf(path="report.pdf", format="A4",
             margin={"top": "2cm", "bottom": "2cm", "left": "2cm", "right": "2cm"},
             print_background=True)
    browser.close()

Pitfalls. playwright install chromium downloads about 300 MB and needs a dozen shared libraries on a bare Linux image, so container builds get slow and fragile. Each conversion starts a browser process, roughly one second of overhead, and memory use climbs quickly under concurrency. Running headers and footers use Chromium's own template syntax rather than @page margin boxes. And you now operate a browser in production, with all the sandboxing and update work that implies.

What about ReportLab, FPDF and PyMuPDF?

These libraries write PDF pages directly and are excellent for invoices and forms, but none of them understands Markdown or HTML. Using them means writing your own layout code for headings, lists, tables and code blocks, which is weeks of work to reach what a CSS engine gives you for free. They are the wrong tool for this job unless you need pixel-exact placement of a few fields.

The sixth option: call the MDtoDOCX API from Python

If what you want is "POST Markdown, get a PDF", none of the above is worth maintaining. The MDtoDOCX API renders with the same engine as this page, so tables, code highlighting, images, equations and Mermaid diagrams behave exactly as they do in the preview, and there is nothing to install beyond requests.

import requests

    resp = requests.post(
    "https://api.mdtodocx.work/v1/convert",
    headers={"Authorization": "Bearer YOUR_API_KEY"},
    json={
        "from": "markdown",
        "to": "pdf",
        "content": open("report.md", encoding="utf-8").read(),
        "options": {"paper": "A4", "font": "Georgia", "page_numbers": True},
    },
    timeout=60,
    )
    resp.raise_for_status()
    open("report.pdf", "wb").write(resp.content)

Uploaded content is processed in memory and deleted within 24 hours; see the privacy policy for the details. API plans start at $19 for 5,000 conversions and are listed on Pricing; the API is currently in private beta, so email [email protected] for a key. For one file today, the converter at the top of this page is free and runs entirely in your browser.

Python Markdown-to-PDF libraries compared

ApproachInstallsTables & code highlightingMath & MermaidSpeed per docBest for
markdown + WeasyPrintpip + Pango/CairoYes, with extensions and Pygments CSSNo JS: math needs pre-rendering0.3–1 sReports, CI jobs
md2pdfSame as aboveSame, older pinsNo0.3–1 sQuick scripts
pypandoc + xelatexPandoc + 2–4 GB TeXYes; wide tables need a filterMath excellent, Mermaid via filter5–15 sAcademic documents
pdfkit + wkhtmltopdfArchived binaryPartial, old WebKitLimited JS0.5 sLegacy systems
Playwright / Chromium300 MB browserYesYes, anything JS renders1–2 sDiagrams, exact browser fidelity
MDtoDOCX APIrequestsYesYes~1 s round tripServices that just need a PDF back

Frequently asked questions

What is the simplest way to convert Markdown to PDF in Python?
Install markdown and weasyprint, convert the Markdown to HTML with the tables and fenced_code extensions, and call HTML(string=html).write_pdf("out.pdf"). The only hard part is installing WeasyPrint's Pango and Cairo system libraries.
Why does WeasyPrint fail to install on Windows?
It needs the GTK runtime for Pango and Cairo. Install the GTK for Windows runtime, restart the shell so the DLLs are on PATH, then pip install weasyprint. Alternatively use WSL or the Playwright route, which has no such dependency.
Can I convert Markdown to PDF with Python without installing LaTeX?
Yes. WeasyPrint, Playwright and the MDtoDOCX API all produce PDFs without TeX. Only the Pandoc route needs a LaTeX engine, and only if you want LaTeX-quality math typesetting.
How do I get syntax-highlighted code blocks in the PDF?
With Python-Markdown, enable the codehilite extension, install Pygments, and include a Pygments stylesheet (pygmentize -S default -f html) in your CSS. Playwright and the MDtoDOCX API highlight code without extra steps.
Is pdfkit still a good choice?
Not for new projects. wkhtmltopdf, the binary pdfkit wraps, was archived in 2023 and lacks modern CSS such as grid and flexbox gap. Use WeasyPrint for pure Python or Playwright for full browser fidelity.
How do I render Mermaid diagrams or KaTeX from Python?
They need JavaScript, so use Playwright with a page that loads Mermaid and KaTeX and waits for networkidle, or send the Markdown to the MDtoDOCX API, which renders both server-side.
Does the MDtoDOCX API keep my documents?
Content is processed in memory and any temporary copies are deleted within 24 hours. The converter on this page does not upload anything at all; it runs in your browser.
Is the converter on this page free?
Yes. Markdown with Python code blocks converts to PDF free with no sign-up. Line numbers and page numbers are Pro options; the code itself is highlighted for free.