PDFtoMD
DevelopersPythonPDFMarkdown

How to Convert PDF to Markdown in Python: Libraries and Code Examples

A practical guide to converting PDFs to Markdown in Python, from quick one-off scripts to batch pipelines, with code samples and the trade-offs of each library.

9 min readBy Rafael Abellan

If you work with documents in Python, sooner or later you need to pull text out of PDFs. And once you have the text, the next question is always the same: how do you keep the structure? Headings, lists, tables, and code blocks all carry meaning, and a flat wall of text throws that meaning away. Markdown is the format that preserves it while staying plain text, version-control friendly, and easy to feed into other tools.

This guide walks through converting PDF to Markdown in Python, from a five-line throwaway script to a batch pipeline you can run over a folder of hundreds of files. We will look at the most common libraries, show real code for each, and be honest about where each one falls short. By the end you will know which tool fits your job and roughly how much cleanup to expect.

Why Markdown Instead of Plain Text?

Plenty of Python tutorials stop at extract_text() and call it done. That is fine if all you need is a search index. But if a human or a language model is going to read the result, structure matters. Markdown gives you a lightweight way to keep that structure without the overhead of HTML or the binary mess of the original PDF.

  • It is plain text: diff it, grep it, commit it to Git, and review changes line by line.
  • It is portable: the same .md file renders in GitHub, Obsidian, Notion, static site generators, and almost every documentation tool.
  • It is token-efficient: when you pass documents to an LLM, Markdown carries the structure with far less noise than raw PDF text or HTML.
  • It is editable: a reviewer can fix a mangled table or a wrong heading in seconds, no special software required.

If you are building retrieval systems, that last point compounds. Clean structure means cleaner chunks, which is why teams converting documents for RAG pipelines almost always normalize to Markdown first.

The Landscape of Python PDF Libraries

There is no single library that does everything well, because PDFs are not really a text format. A PDF describes where to paint glyphs on a page, not what the document means. Reconstructing headings and tables from glyph positions is genuinely hard, and different libraries make different trade-offs. Here is the short version before we get into code.

LibraryBest forMarkdown outputEffort
pypdfQuick text extractionManual (you build it)Low
pdfplumberTables and layout detailManual (you build it)Medium
PyMuPDFSpeed plus built-in MarkdownGood, via helperLow to medium
marker / doclingComplex layouts, accuracyStrongHigh (heavy deps)
Hosted APINo setup, consistent resultsStrongLowest

Option 1: pypdf for a Quick One-Off

When you just need the words out of a simple, text-based PDF, pypdf is the lightest option. Install it with pip install pypdf. It does not produce Markdown on its own, but for a clean document you can get surprisingly far by treating each page as a paragraph block.

from pypdf import PdfReader

def pdf_to_text(path):
    reader = PdfReader(path)
    pages = []
    for page in reader.pages:
        text = page.extract_text() or ""
        pages.append(text.strip())
    return "\n\n".join(pages)

markdown = pdf_to_text("report.pdf")
with open("report.md", "w", encoding="utf-8") as f:
    f.write(markdown)

This works, but notice what it does not do. There are no headings, no list bullets, and no tables. Everything comes out as flat paragraphs. For a memo or a letter that is acceptable. For anything with structure, you will be adding the Markdown syntax yourself, and that is where the next libraries earn their place.

Adding Light Structure with Heuristics

You can improve pypdf output with simple rules. Short lines in all caps or title case often signal headings. Lines starting with a bullet character or a number are probably list items. These heuristics are fragile, but for a known document template they can save real time.

def guess_markdown(text):
    out = []
    for line in text.splitlines():
        stripped = line.strip()
        if not stripped:
            continue
        if len(stripped) < 60 and stripped == stripped.title():
            out.append("## " + stripped)
        elif stripped[0] in "-*\u2022":
            out.append("- " + stripped.lstrip("-*\u2022 "))
        else:
            out.append(stripped)
    return "\n\n".join(out)

The lesson here is that heuristics break the moment a document does not match your assumptions. They are a stopgap, not a strategy.

Option 2: pdfplumber for Tables

If your PDFs contain tables, pdfplumber is the open-source library to reach for. Install it with pip install pdfplumber. It exposes detailed layout information, including detected tables as lists of rows, which you can convert directly into Markdown table syntax.

import pdfplumber

def table_to_markdown(rows):
    if not rows:
        return ""
    header = rows[0]
    body = rows[1:]
    lines = ["| " + " | ".join(c or "" for c in header) + " |"]
    lines.append("| " + " | ".join("---" for _ in header) + " |")
    for row in body:
        lines.append("| " + " | ".join(c or "" for c in row) + " |")
    return "\n".join(lines)

with pdfplumber.open("invoice.pdf") as pdf:
    parts = []
    for page in pdf.pages:
        parts.append(page.extract_text() or "")
        for table in page.extract_tables():
            parts.append(table_to_markdown(table))
    markdown = "\n\n".join(parts)

This is meaningfully better for data-heavy documents. The catch is that table detection is not perfect. Merged cells, borderless tables, and multi-line cells can confuse the extractor, so you should always eyeball the output. Still, for invoices, financial statements, and reports with clean grids, pdfplumber is hard to beat among the free tools.

Skip the boilerplate. Drop a PDF into PDFtoMD and get clean Markdown back in seconds.

Option 3: PyMuPDF with Built-In Markdown

PyMuPDF (imported as fitz) is fast, and its companion package pymupdf4llm includes a helper that emits Markdown directly, including headings and tables, with very little code. Install both with pip install pymupdf pymupdf4llm. For many developers this is the sweet spot between effort and quality.

import pymupdf4llm

markdown = pymupdf4llm.to_markdown("whitepaper.pdf")

with open("whitepaper.md", "w", encoding="utf-8") as f:
    f.write(markdown)

That is the whole script. The helper inspects font sizes to infer heading levels, preserves lists, and renders detected tables in Markdown. It is genuinely good on well-structured, text-based PDFs and fast enough to run over large batches. Where it struggles is the same place every layout-based tool struggles: scanned pages, heavy multi-column layouts, and documents where visual styling does not map cleanly to semantic structure.

Option 4: Marker and Docling for Hard Documents

When accuracy matters more than setup time, machine-learning-based converters like marker and docling are the strongest open-source options. They use layout models to understand the page rather than guessing from font sizes, so they handle multi-column academic papers, complex tables, and mixed content far better.

# pip install marker-pdf
from marker.converters.pdf import PdfConverter
from marker.models import create_model_dict
from marker.output import text_from_rendered

converter = PdfConverter(artifact_dict=create_model_dict())
rendered = converter("paper.pdf")
markdown, _, _ = text_from_rendered(rendered)

with open("paper.md", "w", encoding="utf-8") as f:
    f.write(markdown)

The trade-off is weight. These libraries pull in large model dependencies, want a decent amount of memory, and run much faster on a GPU. For a one-off conversion that is overkill, but if you are processing thousands of research papers, the accuracy gain is worth the infrastructure. Researchers handling large reading lists often land here, the same way many do when they prepare documents for local LLMs and need reliable structure offline.

Building a Batch Pipeline

A single file is easy. The real value of doing this in Python shows up when you have a folder full of PDFs to process unattended. Here is a small, dependency-light pipeline using pymupdf4llm that walks a directory, converts every PDF, mirrors the folder structure into an output directory, and keeps going if one file fails.

import pathlib
import pymupdf4llm

SRC = pathlib.Path("pdfs")
OUT = pathlib.Path("markdown")

def convert_all():
    failures = []
    for pdf_path in SRC.rglob("*.pdf"):
        rel = pdf_path.relative_to(SRC).with_suffix(".md")
        out_path = OUT / rel
        out_path.parent.mkdir(parents=True, exist_ok=True)
        try:
            md = pymupdf4llm.to_markdown(str(pdf_path))
            out_path.write_text(md, encoding="utf-8")
            print("ok:", pdf_path)
        except Exception as err:
            failures.append((pdf_path, repr(err)))
            print("fail:", pdf_path, err)
    if failures:
        print(f"{len(failures)} file(s) failed")

if __name__ == "__main__":
    convert_all()

A few production touches turn this from a script into something you can trust. Wrap each conversion in its own try block so one bad PDF does not stop the batch. Log failures to a file with the path and error so you can retry them later. If you have thousands of files, parallelize with concurrent.futures.ProcessPoolExecutor, since conversion is CPU bound and processes sidestep the global interpreter lock.

Post-Processing the Output

Whatever library you use, a cleanup pass pays off. Common fixes include collapsing runs of blank lines, stripping repeated page headers and footers, and normalizing heading levels so the document starts at a single top heading. A few regular expressions handle most of it.

import re

def clean_markdown(md):
    md = re.sub(r"\n{3,}", "\n\n", md)        # collapse blank lines
    md = re.sub(r"[ \t]+\n", "\n", md)         # trailing whitespace
    return md.strip() + "\n"

When to Skip the Code Entirely

Writing and maintaining a converter is worth it when you control the pipeline, need it offline, or process documents at a volume that rules out anything manual. But that is not every situation. If you are converting a handful of files, or if the PDFs are messy enough that tuning a library would eat a day, a hosted converter is often the pragmatic choice.

That is the gap PDFtoMD fills. You upload a PDF and get back clean Markdown with headings, lists, and tables preserved, no dependencies to install and no model weights to download. The free tier gives you 3 conversions per month with no credit card required, which is enough to check the output quality against your own documents before you commit to building anything. Plenty of developers use it for the occasional file and save their Python time for the batches that genuinely need it. You can see more workflows on the use cases page, and if your documents are headed into a repo or docs site, the guide on PDF to Markdown for developers covers that side in detail.

Frequently Asked Questions

What is the best Python library to convert PDF to Markdown?

For most developers, pymupdf4llm hits the best balance: a single function call gives you Markdown with headings and tables, and it is fast enough for large batches. If you need maximum accuracy on complex or academic layouts, use marker or docling. If you only need raw text, pypdf is the lightest. There is no single winner, only the right tool for your document type.

How do I convert a scanned PDF to Markdown in Python?

Scanned PDFs are images, so plain text extraction returns nothing. You need OCR first, using a tool like pytesseract with Tesseract installed, or a converter such as marker that includes OCR. Run OCR to recover the text, then apply the same Markdown structuring steps. Expect more cleanup than with a native digital PDF.

Can these libraries preserve tables?

Yes, to varying degrees. pdfplumber exposes detected tables you can render yourself, and pymupdf4llm, marker, and docling emit Markdown tables directly. None are perfect with merged cells or borderless layouts, so review table-heavy output before trusting it.

How do I handle hundreds of PDFs at once?

Use the batch pattern shown above: walk the source folder with pathlib.rglob, convert each file inside its own try block, mirror the output structure, and log failures for retry. For large volumes, run conversions in parallel with ProcessPoolExecutor because the work is CPU bound.

Is a hosted converter better than a Python library?

Neither is strictly better. A library gives you full control, offline operation, and zero per-file cost, which matters at scale. A hosted converter gives you consistent results with no setup, which matters for occasional files or messy documents. Many developers use both, reaching for the library on big batches and the hosted tool for one-offs.

The Takeaway

Converting PDF to Markdown in Python is a spectrum, not a single recipe. Start with pypdf when you just need text, move to pdfplumber for tables, reach for pymupdf4llm when you want clean Markdown with almost no code, and bring in marker or docling when accuracy on hard layouts is worth the heavier setup. Wrap your chosen tool in a small batch loop with error handling and a cleanup pass, and you have a pipeline that scales.

And when the document is messy or the job is small, do not over-engineer it. Convert it once, check the result, and get on with your work. The goal was never to write a PDF parser. It was to get clean, structured Markdown you can actually use.

Rafael Abellan

About the author

Rafael Abellan

Founder, PDFtoMD

Rafael Abellan is the founder of Agência Triva and ships independent side projects in parallel. PDFtoMD came out of a personal frustration: he kept burning through Claude AI's token limit by uploading long PDFs, then losing hours waiting for the cap to reset. He built the tool to fix his own workflow, and now uses it every day.

Ready to convert your PDFs to Markdown?

Free account · 3 conversions/month · No credit card required