Docling in Python: A Practical 2026 Guide to Parsing PDFs, DOCX, and HTML for RAG

IBM's open-source Docling library turns PDFs, DOCX, PPTX, XLSX, and HTML into clean Markdown or JSON for RAG. This 2026 guide covers install, table extraction, HybridChunker, LangChain and LlamaIndex integrations, plus how it stacks up to Unstructured and LlamaParse.

Docling Python Guide: PDFs to RAG (2026)

Updated: July 16, 2026

Docling is an open-source Python library from IBM Research that converts PDFs, DOCX, PPTX, XLSX, HTML, images, and audio into a structured DoclingDocument you can export to Markdown or JSON, with real table structure, layout, formulas, and code blocks preserved. If you've spent a weekend gluing pdfplumber, python-docx, and Tesseract together for a RAG pipeline, Docling collapses that pile into one converter. I've been shipping it behind FastAPI for about a year now, and honestly, this is the guide I wish I'd had when I started.

  • Docling 2.113.0 (July 2026) parses PDF, DOCX, PPTX, XLSX, HTML, EPUB, images, and audio into one DoclingDocument Pydantic model with Markdown, JSON, HTML, and DocTags exporters.
  • It's MIT-licensed, runs entirely locally, and needs Python 3.10+. No API keys, no per-page cost, and no data leaves your machine.
  • The bundled TableFormer model scores 98.5% TEDS on simple tables and 95% on complex ones, closer to LlamaParse than to pdfplumber.
  • HybridChunker is the RAG-ready chunker. It respects headings, then splits and merges chunks against your embedding model's token limit.
  • First-party integrations exist for LangChain, LlamaIndex, Haystack, and the Bee/watsonx agent stack, plus a FastAPI wrapper called docling-serve.
  • Docling is 5–30x slower than PyMuPDF4LLM per document but handles complex tables and scanned pages that PyMuPDF cannot. Pick based on your PDFs, not benchmarks in isolation.

What is Docling used for?

Docling is a document-understanding toolkit whose job is to turn messy human documents into something an LLM or an ETL job can actually consume. The library was contributed by IBM Research to the LF AI & Data Foundation in April 2025 and now sits at roughly 61,000 GitHub stars with a release almost every business day. The core value isn't "read a PDF" (half a dozen libraries do that). It's that Docling emits a lossless, typed data model called DoclingDocument, with reading order, table cells, formulas, captions, headings, code blocks, and picture regions all attached.

That single output feeds every downstream use case I care about: retrieval-augmented generation over a company knowledge base, invoice and contract extraction, a data-cleaning step ahead of a lakehouse load, or a batch job that summarises a folder of scientific PDFs. Because DoclingDocument is a Pydantic model, my FastAPI services can validate it at the boundary and my pipelines can serialise it to JSON without any custom code. If you've read the Docling technical report, this is the punchline: the models exist to feed the schema, not the other way around.

The other reason I ended up using it in production is boring: license and privacy. Docling is MIT-licensed, ships model weights under Apache 2.0, and runs on your own CPU or GPU. For any team that can't ship customer PDFs to a hosted parser, that's the whole ballgame.

How to install Docling in Python

Docling requires Python 3.10 or newer (3.9 support was dropped in v2.70.0) and works on macOS, Linux, and Windows on both x86_64 and arm64. Install the main package:

pip install docling

That pulls in docling-core (types, transforms, serializers), docling-parse (the C++/Python PDF backend), and docling-ibm-models (layout and table weights). On first run the library downloads about 500 MB of model weights into your Hugging Face cache, so expect a cold start of 20–60 seconds. Subsequent runs load from disk and take a second or two.

For RAG chunking you'll want the extras:

# Hugging Face tokenizers (most common)
pip install "docling-core[chunking]"

# Or OpenAI/tiktoken-compatible tokenizers
pip install "docling-core[chunking-openai]"

Optional OCR engines come as extras: pip install "docling[tesserocr]", pip install "docling[rapidocr]", and on macOS pip install "docling[ocrmac]" to use the Apple Vision framework. EasyOCR is bundled by default, so you can skip these unless you specifically need a different engine. On Apple Silicon the models automatically use MPS. On NVIDIA GPUs CUDA is picked up when a compatible torch is present. If you want to keep the install lean, use the newer docling-slim package introduced in 2026 to skip audio dependencies you probably won't use.

Your first document conversion

The API is deliberately tiny. A single DocumentConverter handles every supported format:

from docling.document_converter import DocumentConverter

converter = DocumentConverter()
result = converter.convert("https://arxiv.org/pdf/2408.09869")

# Markdown output ready to feed a model or a Jekyll site
print(result.document.export_to_markdown())

# Or the lossless JSON with full structure
result.document.save_as_json("paper.json")

Notice what didn't happen: no format sniffing, no engine selection, no page-range plumbing. convert() takes a local path, a URL, an InputDocument, or a byte stream, figures out the type from the MIME/extension, and dispatches to the right backend. Behind the scenes it runs page rendering, the RT-DETR layout model trained on DocLayNet, TableFormer for tables, and OCR only where the page has no embedded text. On my M2 Pro laptop the 25-page technical report converts in about 6 seconds after warm-up.

For batch work, convert_all() takes an iterable and yields results one at a time so you never hold the whole batch in memory. The CLI mirrors the Python API:

docling https://arxiv.org/pdf/2408.09869 --to md --output ./out
docling ./contracts/*.pdf --to json --num-threads 4

Both paths return the same DoclingDocument, so any post-processing you write once works everywhere.

Parsing PDFs, DOCX, PPTX, XLSX, and HTML

Docling's supported formats in 2026 go well past "PDF plus a couple of extras." The full list currently includes PDF, DOCX, PPTX, XLSX, HTML, EPUB, Markdown, AsciiDoc, CSV, XML, LaTeX, ODT/ODS/ODP, XBRL, EML and MSG email, WebVTT and DocLang, PNG/TIFF/JPEG images, and (new this year) WAV and MP3 audio through a Whisper backend. Every one of them ends up in the same DoclingDocument, which means you can point a single pipeline at a folder of mixed formats:

from pathlib import Path
from docling.document_converter import DocumentConverter

converter = DocumentConverter()
inbox = Path("./inbox")

for result in converter.convert_all(inbox.glob("*")):
    out = Path("./out") / (result.input.file.stem + ".md")
    out.write_text(result.document.export_to_markdown())
    print(f"{result.input.file.name}: {result.status}")

A few backend notes worth knowing. The DOCX backend understands headings, lists, tables, images, and footnotes and keeps their structural relationships. PPTX gained native chart parsing this year, so pie, bar, and line charts land in the document as classified pictures with a machine-readable representation. XLSX conversion preserves sheets, header rows, and merged cells rather than flattening to CSV. The HTML backend runs a readability pass to strip nav, ads, and boilerplate, which is useful if you're ingesting scraped pages instead of hand-curated files. The 2.109+ releases also added GCS, Azure Blob, and Google Drive as first-class source and target URIs, so you can point the CLI at a bucket without shuffling files locally.

How do I extract tables from a PDF with Docling?

This is the single most common thing people come to Docling for, so it gets its own section. Table structure recognition is on by default. You only need to enable it explicitly if you've previously disabled it in PdfPipelineOptions. To get the tables as pandas DataFrames:

from docling.document_converter import DocumentConverter

result = DocumentConverter().convert("./10k-filing.pdf")

for i, table in enumerate(result.document.tables):
    df = table.export_to_dataframe()
    df.to_parquet(f"table_{i:03d}.parquet")
    print(f"Table {i}: {df.shape}, page {table.prov[0].page_no}")

Each Table object carries the reconstructed grid of cells, headers, row and column spans, and its provenance (page number and bounding box). Under the hood, TableFormer predicts the table's logical structure independent of visual cues, which is why it handles borderless, misaligned, or partially empty tables that Camelot and Tabula both trip over. The Docling technical report reports 98.5% TEDS on simple tables and 95% on complex ones. Independent benchmarks put its F1 at 88–97% depending on domain, which is competitive with paid parsers.

Two knobs matter for tables. First, table_structure_options.mode is either fast or accurate. The default in 2026 is accurate, and unless you're throughput-bound, leave it alone. Second, table_structure_options.do_cell_matching defaults to True and re-projects cell text from the PDF layer to guarantee character-perfect content. Turn it off only for scanned pages where you've already forced OCR.

OCR, formulas, and code: PdfPipelineOptions

Every non-trivial deployment eventually ends up in PdfPipelineOptions. This is the object that decides which enrichment models run, which OCR engine is used, and which pipeline the converter dispatches to. The pattern is always the same:

from docling.datamodel.base_models import InputFormat
from docling.datamodel.pipeline_options import (
    PdfPipelineOptions,
    RapidOcrOptions,
)
from docling.document_converter import DocumentConverter, PdfFormatOption

opts = PdfPipelineOptions(
    do_ocr=True,
    do_table_structure=True,
    do_formula_enrichment=True,
    do_code_enrichment=True,
    ocr_options=RapidOcrOptions(force_full_page_ocr=False),
    generate_page_images=False,
)

converter = DocumentConverter(
    format_options={InputFormat.PDF: PdfFormatOption(pipeline_options=opts)}
)

A few things worth knowing about the enrichment models. do_formula_enrichment runs a formula-detection head and returns LaTeX in the output document, which is genuinely useful for scientific papers where equations would otherwise turn to jibberish. do_code_enrichment detects code blocks and language, so a Markdown export emits fenced blocks with the right lexer. Both add roughly 30–100 ms per page.

OCR is where people trip. Docling auto-skips OCR when the PDF already has an embedded text layer, so you rarely need force_full_page_ocr=True. When you do need OCR, pick your engine deliberately. EasyOCR is the safe default and bundled. RapidOCR is faster and lighter but slightly less accurate. Tesseract has the best Latin-script accuracy but requires the system package. And on macOS, OcrMacOptions uses Apple's Vision framework, which is astonishingly fast on Apple Silicon. On my M3 machine RapidOCR runs at roughly 3x the speed of EasyOCR with about the same recall on clean scans.

Chunking for RAG with HybridChunker

Extracting a document is only half of a RAG pipeline. The other half is turning it into chunks that fit an embedding model's context window without shredding the meaning. Docling ships two chunkers in docling-core: HierarchicalChunker, which walks the document structure and emits one chunk per section, and HybridChunker, which then re-splits oversized chunks and merges undersized ones against your tokenizer's budget. Use HybridChunker unless you have a very specific reason not to.

from docling.document_converter import DocumentConverter
from docling.chunking import HybridChunker
from docling_core.transforms.chunker.tokenizer.huggingface import HuggingFaceTokenizer
from transformers import AutoTokenizer

doc = DocumentConverter().convert("./whitepaper.pdf").document

tokenizer = HuggingFaceTokenizer(
    tokenizer=AutoTokenizer.from_pretrained("sentence-transformers/all-MiniLM-L6-v2"),
    max_tokens=384,
)

chunker = HybridChunker(tokenizer=tokenizer, merge_peers=True)

for chunk in chunker.chunk(dl_doc=doc):
    text = chunker.serialize(chunk)  # heading-prefixed, embedder-ready
    metadata = {
        "headings": [h.text for h in chunk.meta.headings],
        "page": chunk.meta.doc_items[0].prov[0].page_no if chunk.meta.doc_items else None,
    }
    yield text, metadata

The important call is chunker.serialize(chunk). It prepends the chunk's heading path (for example "Section 3 > 3.2 Results > ..."), so an embedding model sees the context, not just the raw text. This alone lifts retrieval quality noticeably compared to naive fixed-window chunking, because it kills the ambiguity of chunks that just say "The results were significant." If you're already using structured LLM outputs with Pydantic to validate downstream extractions, the chunk metadata slots straight into your models.

Integrating with LangChain, LlamaIndex, and Haystack

Docling has first-party integrations for every serious agent framework. Pick the one your project already uses.

LangChain. A document loader that returns either chunks or plain Markdown:

pip install -qU langchain-docling
from langchain_docling import DoclingLoader
from langchain_docling.loader import ExportType

loader = DoclingLoader(
    file_path="./report.pdf",
    export_type=ExportType.DOC_CHUNKS,  # or ExportType.MARKDOWN
)
docs = loader.load()

LlamaIndex. A reader and a node parser that keep Docling's structure through the ingestion pipeline:

pip install llama-index-readers-docling llama-index-node-parser-docling
from llama_index.readers.docling import DoclingReader
from llama_index.node_parser.docling import DoclingNodeParser

reader = DoclingReader(export_type=DoclingReader.ExportType.JSON)
docs = reader.load_data(file_path="./report.pdf")

parser = DoclingNodeParser()
nodes = parser.get_nodes_from_documents(docs)

Haystack. A component you drop into a pipeline:

pip install docling-haystack
from docling_haystack.converter import DoclingConverter

converter = DoclingConverter()
documents = converter.run(sources=["./report.pdf"])["documents"]

For agent frameworks that speak Model Context Protocol, docling-mcp exposes Docling as an MCP tool, so Claude Desktop or any MCP-compatible agent can call it directly. And for teams already running Bee or IBM watsonx, Docling is the default document backend, so you inherit the same pipeline whether you self-host or use the managed service.

Docling vs Unstructured, LlamaParse, PyMuPDF, and Marker

The Python document-parsing space in 2026 is crowded but reasonably well-differentiated. Here's the honest comparison based on benchmarks I trust plus my own production experience:

FeatureDoclingUnstructured (OSS)LlamaParsePyMuPDF4LLMMarker
LicenseMITApache 2.0Commercial APIAGPL-3.0 / commercialGPL + OpenRAIL-M weights
Runs locallyYesYesNo (VPC on Enterprise)YesYes
Table quality (TEDS/F1)95–98% / 88–97%~84–93% (hi-res)~92% F1Basic, no ML model~92% F1
Speed (GPU, sec/page)~0.49 (L4)Slower, CPU-bound~5–20 sec/doc (API)~0.10 (CPU-only)~0.86 (L4)
Formats supported25+ inc. audio25+PDF-firstPDF, some XPS/EPUBPDF, EPUB, images
Chunking for RAGHybrid + Hierarchicalby_title, by_similarityvia LlamaIndexExternalExternal
Price at 1M pages/moFree (self-host)Free OSS / $30k API~$3.75k–$12.5kFree / ~$10–50k commercialFree (below revenue threshold)

My rule of thumb goes like this. Reach for PyMuPDF4LLM if you have clean, digitally-born PDFs and you care about latency more than layout. Reach for LlamaParse if you can afford a per-page API and you need the best possible quality on messy, mixed real-world documents. Use Unstructured if your pipeline is already built around its typed elements or you need the widest raw format coverage. Pick Docling when you want local, MIT-licensed, RAG-native output with strong tables, which for most teams shipping enterprise RAG is the default. If your workload is heavy on CJK text or physics/math papers, MinerU is worth a look; it beats everyone on formulas and CJK OCR.

Granite-Docling: the vision-language mode

In 2026 Docling added a full vision-language pipeline. Instead of the standard "layout model + OCR + table model" stack, the VLM pipeline sends each page image to a small end-to-end multimodal model that emits DocTags (Docling's compact structural markup), which is then decoded into the same DoclingDocument. Two models are supported today: SmolDocling-256M-preview (research release, ICCV 2025) and Granite-Docling-258M, the production successor released by IBM in early 2026 under Apache 2.0. Granite-Docling uses a Granite 3 backbone with a SigLIP2 encoder and was trained on IBM's Blue Vela H100 cluster via the nanoVLM framework.

docling --pipeline vlm --vlm-model granite_docling ./scan.pdf

Why bother with a VLM when the standard pipeline is already good? Two situations. First, tightly integrated visual content, like a scientific figure with an embedded caption and a data table, where the classical pipeline treats those as separate objects but the VLM understands them together. Second, extremely low-quality scans where OCR would misclassify blocks; the VLM has been trained to handle that well. The trade-off is speed: the VLM pipeline is 2–5x slower and needs a GPU to be tolerable. I keep it behind a flag in production and route to it only for known-hard documents flagged by a first-pass heuristic.

Running Docling in production with FastAPI

Docling in a notebook is easy. Docling behind a request handler is where the interesting decisions start. Two things bite you if you're not careful. First, DocumentConverter is expensive to construct (it loads models), so you must instantiate it once at startup, not per request. Second, convert() is CPU-bound and blocks the event loop. Don't call it directly from an async def handler. Use a thread pool or, better, a dedicated worker.

from contextlib import asynccontextmanager
from anyio import to_thread
from fastapi import FastAPI, UploadFile
from docling.document_converter import DocumentConverter

converter: DocumentConverter | None = None

@asynccontextmanager
async def lifespan(app: FastAPI):
    global converter
    converter = DocumentConverter()  # warms models on startup
    yield

app = FastAPI(lifespan=lifespan)

@app.post("/convert")
async def convert(file: UploadFile):
    data = await file.read()
    # off-load CPU-bound work so the event loop stays responsive
    result = await to_thread.run_sync(
        lambda: converter.convert(source=data, filename=file.filename)
    )
    return {"markdown": result.document.export_to_markdown()}

For anything above single-digit RPS, put a queue in front of it. The Docling team ships docling-serve, a FastAPI wrapper with async and streaming endpoints already wired up, plus docling-jobkit for batch orchestration. Combine those with a real job runner and you have something production-shaped. If you're already using async ETL with httpx and asyncio to fetch documents from S3 or an internal API, the pattern above slots straight into it.

On the pipeline side, once you have Markdown or JSON coming out of Docling, the natural next step is loading it into a warehouse or vector store. I use dlt as the load tool to push extracted rows into DuckDB or Postgres and reserve the vector database for the chunked-and-embedded RAG payload. If you're also serving an LLM alongside this parsing service, the same "one heavy dependency, warm at startup, offload to a worker" pattern applies. See the guide on LLM inference servers for that side of the stack.

Frequently Asked Questions

Is Docling free?

Yes. The Docling Python library is MIT-licensed and the shipped model weights (layout, TableFormer, Granite-Docling) are Apache 2.0. You can use it commercially, self-host it, and modify it without any per-page fee. IBM's watsonx offers a managed service on top of the same open stack for teams that would rather not run it themselves.

What Python version does Docling require?

Python 3.10 or newer. Python 3.9 support was dropped in v2.70.0. Docling works on macOS, Linux, and Windows across x86_64 and arm64, with automatic MPS acceleration on Apple Silicon and CUDA when a compatible PyTorch is installed.

Which is better, Docling or Unstructured?

Docling generally wins on table quality and gives you a richer, RAG-native structural output (DoclingDocument plus HybridChunker). Unstructured wins on breadth of exotic formats and its typed-element output is well suited to pipelines that filter by element type. For a self-hosted RAG stack on PDFs, DOCX, and PPTX, Docling is my default. Unstructured is the safer pick if your inputs include email archives, EPUBs, or many uncommon formats at once.

Can Docling handle scanned PDFs?

Yes, through its OCR pipeline. EasyOCR is bundled by default; Tesseract, RapidOCR, and macOS Vision (via ocrmac) are opt-in extras. Docling auto-detects PDFs with no embedded text layer and applies OCR only where needed, so you rarely have to force it. For very poor-quality scans, the VLM pipeline with Granite-Docling is often more resilient than classic OCR.

How fast is Docling compared to PyMuPDF?

Roughly 5–30x slower per document than PyMuPDF4LLM, because Docling runs ML models for layout and table recognition where PyMuPDF only walks the PDF's own structure. On a laptop CPU expect 0.5–3 seconds per page. On an L4 GPU it's around 0.5 seconds per page including all enrichments. If pure speed on digital-born PDFs is your priority and you don't need real table structure, PyMuPDF is the right tool. Otherwise the extra latency buys you accuracy that's worth it.

Does Docling work with LangChain and LlamaIndex?

Yes, with first-party packages: langchain-docling, llama-index-readers-docling, llama-index-node-parser-docling, and docling-haystack. Each preserves the Docling structural output through the ingestion pipeline so downstream retrieval and reranking can use heading paths and page provenance as metadata.

Tomás Oliveira
About the Author Tomás Oliveira

Python backend developer who came to data work via FastAPI. Bridges the messy world between APIs and pipelines.