pdfmux/blog
python

Extract PDF metadata in Python: document info, XMP, and what pipelines need

TL;DRRead PDF metadata in Python: the /Info dictionary, XMP, page count, and the extraction metadata (confidence, extractor) that actually matters for AI pipelines.

Direct answer: PDF metadata comes in two layers, and most tutorials only show you the first. The document layer is the /Info dictionary and XMP packet — title, author, creation date, producer — which you read with pypdf in about five lines. The extraction layer is what an AI pipeline actually needs: page count, whether a page has a real text layer or is a scan, and how much you can trust the text you pulled out. For that second layer, run the file through pdfmux with process("doc.pdf") and read result.page_count, result.confidence, and result.extractor_used. The document /Info tells you what the PDF claims about itself; the extraction metadata tells you what you can do with it. Production pipelines route on the second, not the first.


The two layers of PDF metadata

When people say “PDF metadata,” they usually mean one of two very different things:

  1. Document metadata — the descriptive fields embedded in the file: title, author, subject, keywords, the application that produced it, and creation/modification timestamps. This lives in the PDF’s /Info dictionary and, in newer files, an XMP packet. It is self-reported and frequently wrong, stale, or empty.
  2. Extraction metadata — facts you only learn by actually opening and processing the file: how many pages it really has, which pages carry a text layer versus a flattened scan, whether the document is encrypted, and how confident an extractor is in the text it returned.

A document might proudly declare Author: Microsoft® Word 2016 in its /Info dictionary while being a 40-page scanned contract with no text layer at all. The first layer lies; the second layer doesn’t. If you are building a RAG pipeline or any system that ingests PDFs at scale, you care far more about the second.

Layer 1: read the /Info dictionary with pypdf

The classic document metadata fields live in the /Info dictionary. pypdf exposes them as a simple object:

from pypdf import PdfReader

reader = PdfReader("report.pdf")
meta = reader.metadata

print("Title:    ", meta.title)
print("Author:   ", meta.author)
print("Subject:  ", meta.subject)
print("Creator:  ", meta.creator)      # the authoring app
print("Producer: ", meta.producer)     # the library that wrote the PDF
print("Created:  ", meta.creation_date)       # a datetime, or None
print("Modified: ", meta.modification_date)
print("Pages:    ", len(reader.pages))

A few things bite people here:

  • Every field is optional. meta.title returns None for a huge fraction of real-world PDFs. Never assume a field is present.
  • Dates are strings until parsed. Older pypdf versions hand you the raw D:20260618143000+04'00' string; recent versions parse creation_date into a datetime. Check your version and handle both.
  • creator vs producer are different. creator is the program a human used (Word, InDesign, Chrome’s print-to-PDF). producer is the library that serialized the bytes. A file creator-ed by Word is often producer-ed by a separate PDF engine.

Wrap it so missing fields never crash your loop:

def safe_doc_metadata(path: str) -> dict:
    reader = PdfReader(path)
    m = reader.metadata or {}
    return {
        "title": getattr(m, "title", None),
        "author": getattr(m, "author", None),
        "producer": getattr(m, "producer", None),
        "created": str(getattr(m, "creation_date", None)),
        "pages": len(reader.pages),
        "encrypted": reader.is_encrypted,
    }

Layer 1b: XMP, the metadata most code ignores

Since PDF 1.4, files can also carry an XMP packet — an XML block (Adobe’s Extensible Metadata Platform) that holds richer, namespaced metadata than the flat /Info dictionary. Design tools, document management systems, and many compliance workflows write XMP and leave /Info half-empty. If you only read /Info, you miss it.

from pypdf import PdfReader

reader = PdfReader("designed-brochure.pdf")
xmp = reader.xmp_metadata

if xmp:
    print("XMP creator tool:", xmp.xmp_creator_tool)
    print("Dublin Core title:", xmp.dc_title)       # {'x-default': '...'}
    print("Create date:", xmp.xmp_create_date)
else:
    print("No XMP packet — fall back to /Info")

The Dublin Core (dc_*) fields are the ones worth checking: dc_title, dc_creator, dc_description, dc_subject. They follow a published standard rather than a vendor’s whim, so when they’re present they’re more trustworthy than /Info. The catch is the same as before: the packet is often absent, so always branch on xmp is None.

Why document metadata is not enough

Here is the failure that motivates the whole second layer. You ingest 10,000 PDFs, you read each one’s /Info title and creation date, you index them, and you feel productive. Then your downstream search returns garbage for a few hundred documents — because those few hundred were scans with no text layer, and your text extractor returned empty strings (or worse, OCR-able mojibake) that your metadata pass never inspected.

Document metadata is silent about the thing that breaks pipelines: is there actually extractable text, and is it any good? A PDF’s /Info dictionary has no field for “this page is a photograph of a page.” You have to open the content stream to find out. That is extraction metadata, and it is where pdfmux focuses.

Layer 2: extraction metadata with pdfmux

Install the toolkit and process the file. Beyond the converted text, the result object carries the operational metadata a pipeline routes on:

from pdfmux import process

result = process("contract.pdf", quality="standard")

print("Pages:      ", result.page_count)
print("Confidence: ", f"{result.confidence:.0%}")   # 0.0–1.0, how trustworthy the text is
print("Extractor:  ", result.extractor_used)         # which backend handled it

The two fields that change how you build:

  • result.confidence is a 0.0–1.0 score derived from multiple signals — not a guess, but a measurable property of the extraction. A page that scores 0.45 is telling you the text is probably scrambled or incomplete. Most extractors give you no such signal; they return broken text with the same shrug as perfect text. pdfmux’s per-page confidence scoring is the field you gate on.
  • result.extractor_used records which engine actually ran. A born-digital page gets fast text extraction; a scanned page gets routed to OCR automatically by the self-healing pipeline. Reading this field tells you, after the fact, which of your 10,000 documents were scans — exactly the information /Info could never give you.

Triage without a full extract

When you have thousands of files and only want to classify them — digital vs scanned, how many pages, is it encrypted — you don’t need the full text. pdfmux exposes a lightweight analyze_pdf path (also available as an MCP tool for AI agents) that returns this triage metadata cheaply, so you can sort your corpus before committing compute to extraction.

A combined metadata function

In practice you want one record per document that merges both layers — the descriptive /Info/XMP fields for display and the extraction fields for routing:

from pypdf import PdfReader
from pdfmux import process

def full_metadata(path: str) -> dict:
    reader = PdfReader(path)
    info = reader.metadata or {}
    result = process(path, quality="standard")

    return {
        # Document layer — what the file claims
        "title": getattr(info, "title", None),
        "author": getattr(info, "author", None),
        "producer": getattr(info, "producer", None),
        "created": str(getattr(info, "creation_date", None)),
        "encrypted": reader.is_encrypted,
        # Extraction layer — what we can actually do
        "page_count": result.page_count,
        "confidence": round(result.confidence, 3),
        "extractor": result.extractor_used,
        "status": "ok" if result.confidence >= 0.80 else "review",
    }

That status field is the payoff. Anything below 0.80 confidence gets flagged for review instead of silently poisoning your index. A pure /Info metadata pass can’t produce this column, because nothing in the document metadata knows whether the text came out clean.

Which layer answers which question

QuestionLayerWhere it livesReliable?
Who is the stated author?Document/Info Author or XMP dc_creatorOften empty/stale
When was it created?Document/Info CreationDate, XMP xmp_create_dateUsually present
How many pages?Bothlen(reader.pages) or result.page_countReliable
Is it encrypted?Documentreader.is_encryptedReliable
Is this page a scan?Extractionresult.extractor_used per pageReliable
Can I trust the text?Extractionresult.confidenceReliable
What app produced it?Document/Info Producer/CreatorSelf-reported

Common pitfalls

  • Treating empty as an error. A missing /Info title is normal, not a failure. Default it and move on.
  • Trusting the page count from /Info. Some fields claim a /Pages count that disagrees with the real tree after a merge or split. Count len(reader.pages) (or result.page_count) instead of trusting a declared value.
  • Reading metadata on an encrypted file. If reader.is_encrypted is True, you may need to call reader.decrypt("") (empty owner password is common) before fields populate. See extracting text from encrypted PDFs for the full handling.
  • Skipping XMP. If a file’s /Info looks empty, check reader.xmp_metadata before concluding the document has no metadata — it may all be in the XMP packet.

The rule of thumb

Read the /Info dictionary and XMP when you need to describe a document — show its title in a UI, sort by author, display a created date. Read extraction metadata when you need to process one — decide whether a page is a scan, whether to trust its text, or whether to route it to review. The first layer is a label on the box. The second is what’s actually inside. Pipelines that only read the label are the ones that quietly break.

Once you have the metadata, the natural next step is the content itself — see the best PDF extraction library for Python for choosing the right extractor for your documents.