Tools

Convert PDF to DOCX and DOCX to PDF Using Python

Tools · Published 2026-10-24

The edge of a tall stack of paper sheets, close up
The edge of a tall stack of printed pages.

For PDF → DOCX, use the pdf2docx library (Converter(pdf).convert(docx)); it works anywhere Python runs but only on PDFs that contain real text. For DOCX → PDF, use docx2pdf if Microsoft Word is installed (Windows or macOS), or call LibreOffice headless (soffice --headless --convert-to pdf) through subprocess, which is the reliable choice on Linux servers and in containers.

Which tool for which direction

DirectionToolPlatformsRequiresNotes
PDF → DOCXpdf2docxWindows, macOS, Linuxpip only (pulls in PyMuPDF)Reconstructs layout heuristically; no OCR
DOCX → PDFdocx2pdfWindows, macOSMicrosoft Word installedBest fidelity, since Word itself renders the file
DOCX → PDFLibreOffice headlessWindows, macOS, LinuxLibreOffice installedServer-friendly; minor layout differences possible

The two directions are fundamentally different problems. Producing a PDF from a Word file is rendering: a word processor lays out the document and prints it. Producing a Word file from a PDF is reverse engineering: a PDF is a set of positioned glyphs, lines and images with no notion of paragraphs or tables, so the converter has to guess the structure.

PDF to DOCX with pdf2docx

Install it with pip install pdf2docx. The Converter class opens the PDF, analyzes each page and writes a DOCX built with python-docx:

from pdf2docx import Converter

cv = Converter("report.pdf")
try:
    cv.convert("report.docx")               # all pages
    # cv.convert("part.docx", start=0, end=3)   # pages 0-2 (end is exclusive)
    # cv.convert("some.docx", pages=[0, 4, 9])  # specific pages
finally:
    cv.close()

Converter also accepts password= for encrypted files. For one-off calls there is a functional shortcut, from pdf2docx import parse; parse("report.pdf", "report.docx"). The package also installs a pdf2docx command-line tool (pdf2docx convert report.pdf report.docx). Page indexes are zero-based. The examples here were tested with pdf2docx 0.5.13; see the pdf2docx documentation for the full option list.

Conversion progress is printed through the logging module at INFO level (pdf2docx configures the root logger when imported). Call logging.getLogger().setLevel(logging.WARNING) after the import if you want quiet batch runs.

What pdf2docx can and cannot do

pdf2docx detects text blocks, fonts, images and tables (including tables drawn only with lines or background shading) and maps them to Word paragraphs and tables. It works well on PDFs exported from word processors. Expect problems with:

Licensing note: pdf2docx is MIT-licensed, but it depends on PyMuPDF, which is dual-licensed under the AGPL and a commercial license from Artifex. Check that this is acceptable before shipping it inside a closed-source product or SaaS.

Detecting scanned PDFs before converting

A cheap check saves users from receiving an "empty" Word file. If no page has a meaningful text layer, route the file to OCR instead:

import pdfplumber

def looks_scanned(path, min_chars=20):
    """True if no page has a usable text layer (likely an image-only scan)."""
    with pdfplumber.open(path) as pdf:
        return all(len((page.extract_text() or "").strip()) < min_chars
                   for page in pdf.pages)

print(looks_scanned("report.pdf"))
False

DOCX to PDF with docx2pdf (Word required)

docx2pdf drives Microsoft Word: through COM automation on Windows and through AppleScript/JXA on macOS. Because Word does the rendering, the PDF looks exactly as it would if you used "Save as PDF" manually.

from docx2pdf import convert

convert("report.docx")                      # writes report.pdf next to it
convert("report.docx", "out/report.pdf")    # explicit output file
convert("in_folder/", "out_folder/")        # every .docx in a folder (output folder must exist)

On Linux, convert() raises NotImplementedError because Word is not available there. It is also a poor fit for unattended servers even on Windows: Microsoft does not support Office automation from services, and a stuck Word dialog can hang the process. Use it for desktop scripts; use LibreOffice for anything that runs in the background.

DOCX to PDF with LibreOffice headless

LibreOffice can convert documents from the command line without opening a window. Install it with your package manager (for example apt install libreoffice-writer on Debian/Ubuntu), then:

soffice --headless --convert-to pdf --outdir out report.docx

From Python, wrap that command with subprocess.run, pass arguments as a list (never a shell string built from file names), set a timeout and verify that the output exists. LibreOffice can exit with status 0 even when it fails to convert a file, so checking for the PDF is essential:

import shutil
import subprocess
from pathlib import Path

def find_soffice():
    for name in ("soffice", "libreoffice"):
        path = shutil.which(name)
        if path:
            return path
    raise FileNotFoundError("LibreOffice not found; install it or add soffice to PATH")

def docx_to_pdf(src, outdir=None, timeout=120):
    src = Path(src).resolve()
    outdir = Path(outdir or src.parent).resolve()
    outdir.mkdir(parents=True, exist_ok=True)
    cmd = [find_soffice(), "--headless", "--convert-to", "pdf",
           "--outdir", str(outdir), str(src)]
    result = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
    pdf = outdir / (src.stem + ".pdf")
    if result.returncode != 0 or not pdf.exists():
        raise RuntimeError(f"conversion failed: {result.stderr or result.stdout}")
    return pdf

print(docx_to_pdf("report.docx", "out"))

Tested with LibreOffice 24.2 on Linux, converting a one-page DOCX took about two seconds including startup. On Windows the executable is usually C:\Program Files\LibreOffice\program\soffice.exe, which is not on PATH by default; pass the full path in that case. On macOS it lives at /Applications/LibreOffice.app/Contents/MacOS/soffice. The same command converts .doc, .odt, .rtf, .pptx and .xlsx files too. Options are described in the LibreOffice command-line parameters guide.

Batch conversion

Starting LibreOffice is the expensive part, so convert many files in one invocation rather than one process per file. A throwaway user profile (-env:UserInstallation) prevents the conversion from silently doing nothing when another LibreOffice instance is already running under the same user, and lets parallel workers run without stepping on each other:

import shutil
import subprocess
import tempfile
from pathlib import Path

def batch_docx_to_pdf(indir, outdir, timeout=600):
    files = sorted(str(p.resolve()) for p in Path(indir).glob("*.docx"))
    if not files:
        return []
    outdir = Path(outdir).resolve()
    outdir.mkdir(parents=True, exist_ok=True)
    soffice = shutil.which("soffice") or shutil.which("libreoffice")
    with tempfile.TemporaryDirectory() as profile:
        cmd = [soffice, f"-env:UserInstallation=file://{profile}",
               "--headless", "--convert-to", "pdf", "--outdir", str(outdir), *files]
        subprocess.run(cmd, check=True, capture_output=True, timeout=timeout)
    missing = [f for f in files if not (outdir / (Path(f).stem + ".pdf")).exists()]
    if missing:
        raise RuntimeError(f"not converted: {missing}")
    return sorted(outdir.glob("*.pdf"))

for pdf in batch_docx_to_pdf("in", "batch_out"):
    print(pdf.name, pdf.stat().st_size > 0)
doc1.pdf True
doc2.pdf True
doc3.pdf True

On Windows, build the profile URL with Path(profile).as_uri() instead of the f-string, since Windows paths need a different file-URL form. For the opposite direction, loop over PDFs and collect failures instead of letting one malformed file stop the batch:

from pathlib import Path
from pdf2docx import Converter

def batch_pdf_to_docx(indir, outdir):
    outdir = Path(outdir)
    outdir.mkdir(parents=True, exist_ok=True)
    failed = []
    for pdf in sorted(Path(indir).glob("*.pdf")):
        cv = Converter(str(pdf))
        try:
            cv.convert(str(outdir / (pdf.stem + ".docx")))
        except Exception as exc:          # malformed or unusual PDFs can fail
            failed.append((pdf.name, exc))
        finally:
            cv.close()
    return failed

Verifying the output

An existing file is not proof of a correct conversion. Two quick automated checks catch most problems: confirm the page count and that the expected text is present.

import pdfplumber
from docx import Document

with pdfplumber.open("out/report.pdf") as pdf:
    print("pages:", len(pdf.pages))
    print(pdf.pages[0].extract_text())

doc = Document("report.docx")
print([p.text for p in doc.paragraphs if p.text.strip()])
print(len(doc.tables), "top-level tables")
pages: 1
Quarterly report
This document was generated with python-docx for a conversion test.
Region Revenue
North 1,200
South 980
 Item one
 Item two
['Quarterly report', 'This document was generated with python-docx for a conversion test.', 'Item one', 'Item two']
1 top-level tables

Note that python-docx's doc.paragraphs excludes text inside tables, and doc.tables lists only top-level tables, so a converted DOCX whose content was wrapped in layout tables can look nearly empty to a naive check. Search doc.tables recursively (each cell has its own .paragraphs and .tables) when validating pdf2docx output. For visual regressions, render the first page of each PDF to an image and compare it with a reference.

Troubleshooting

Summary

Use pdf2docx to turn text-based PDFs into editable Word files, accept that layout reconstruction is approximate, and run OCR first for scans. For DOCX → PDF, docx2pdf gives Word-perfect output on desktops with Word installed, while LibreOffice headless via subprocess works on every platform and is the right choice for servers, containers and batch jobs. Whichever tool you use, verify page counts and text afterwards instead of trusting the exit status.

Read next

More in Tools