DocxLoader

On this page

Module: docx_loader · Extends BaseFileLoader

Overview

Loads Microsoft Word documents. Modern .docx files are parsed directly via python-docx; legacy .doc files (the pre-2007 binary format) are handled through a conversion strategy, since .doc is a fundamentally different binary format that python-docx cannot read.

Signature

Python · 6 lines
class DocxLoader(BaseFileLoader):
    SUPPORTED_EXTENSIONS = [".docx", ".doc"]

    def __init__(self, file_path: str, config: Optional[DocxLoaderConfig] = None): ...

    def load(self) -> List[LoadedDocument]: ...

Parameters

NameTypeDefaultDescription
file_pathstrrequiredPath to the .docx or .doc file
configOptional[DocxLoaderConfig]NoneSee DocxLoaderConfig

Returns

A single-element list — the entire document (headers, paragraphs, tables, footers, in that order, per the config flags) concatenated into one LoadedDocument.

Raises

  • FileNotFoundError / ValueError — at construction (missing file / wrong extension)
  • ImportError — "python-docx is required..." if python-docx isn't installed (.docx path)
  • ImportError — with multi-option install instructions if .doc and neither LibreOffice nor textract is available

Internal workflow

Python · 4 lines
def load(self):
    if self.file_path.suffix.lower() == ".doc":
        return self._load_legacy_doc()
    return self._load_docx()

.docx path — _load_docx()

  1. Open with python-docx's Document().
  2. If config.include_headers: extract text from each section's header.
  3. Iterate doc.paragraphs; for paragraphs whose style name contains "Heading", prefix with the appropriate number of # characters (derived from the digit in the style name, e.g. "Heading 2" → ##); otherwise append as plain text.
  4. If config.include_tables: render each table as text (rows joined by table_separator, cells within a row joined by row_separator) and append, prefixed [Table]:.
  5. If config.include_footers: extract footer text per section, prefixed [Footer]:.
  6. Join everything with \n and strip.
  7. Pull core_properties (title, author, subject, created, modified) plus paragraph/table counts into metadata.

.doc path — _load_legacy_doc()

[!TIP] Implementation note The source docstring documents that an earlier version of this method incorrectly tried to read .doc files using a PDF reader — which is invalid since a Word .doc file is not a PDF. The current implementation replaces that with a proper conversion strategy.

Strategy, tried in order:

  1. LibreOffice conversion — if libreoffice or soffice is found on PATH, runs libreoffice --headless --convert-to docx --outdir <tmpdir> <file> (30s timeout), then recursively constructs a new DocxLoader on the converted file and calls _load_docx() on it. The resulting documents have their source and a note: "Converted from .doc via LibreOffice" metadata field rewritten back to point at the original .doc path.
  2. textract — if LibreOffice conversion isn't available or fails, tries textract.process(path).
  3. If both fail, raises ImportError with a three-option remediation message (convert manually, install LibreOffice, or install textract).

Metadata produced

.docx: title, author, subject, created, modified (all from core_properties, stringified), paragraph_count, table_count, plus base file metadata.

.doc via LibreOffice: same as .docx (since it's converted then loaded as one), plus note. Via textract: just note: "Loaded via textract" plus base file metadata — no rich Word metadata is available through this path.

Side effects

  • Subprocess execution: the LibreOffice conversion path spawns an external libreoffice/soffice process and writes a temporary .docx file to a tempfile.TemporaryDirectory() (auto-cleaned on exit).
  • No network requests, no caching, no persistent file writes outside the temp directory.

Thread safety

The .docx path has no shared mutable state and is safe to run concurrently across different instances/files. The .doc LibreOffice path spawns a subprocess per call — running many of these concurrently is bounded by system resources (LibreOffice startup is relatively heavy), so high concurrency on .doc conversion specifically may need throttling beyond what DirectoryLoader's default max_concurrency=4 provides, though nothing in the code enforces a lower limit.

Examples

Basic:

Python · 4 lines
from zerda.document_loaders import DocxLoader

docs = DocxLoader("report.docx").load()
print(docs[0].metadata["author"], docs[0].metadata["table_count"])

Skip tables and footers, keep only body text:

Python · 4 lines
from zerda.document_loaders import DocxLoader, DocxLoaderConfig

config = DocxLoaderConfig(include_tables=False, include_footers=False)
docs = DocxLoader("contract.docx", config=config).load()

Legacy .doc file:

Python · 3 lines
docs = DocxLoader("old_memo.doc").load()
# Tries LibreOffice conversion, then textract, then raises ImportError
print(docs[0].metadata.get("note"))

Error handling — missing dependency:

Python · 4 lines
try:
    docs = DocxLoader("report.docx").load()
except ImportError as e:
    print(f"Missing dependency: {e}")

Production usage — custom table formatting for RAG:

Python · 5 lines
from zerda.document_loaders import DocxLoader, DocxLoaderConfig

config = DocxLoaderConfig(table_separator="\n", row_separator=" — ")
docs = DocxLoader("financial_report.docx", config=config).load()
# Table cells joined with " — " instead of the default " | "

Best practices

  • For .doc files in a production pipeline, ensure LibreOffice is installed on the host — it's both more reliable and richer (preserves headings/tables/metadata) than the textract fallback.
  • Set include_tables=False if your document's tables are decorative/layout-only and would add noise to embeddings.

Common mistakes

  • Expecting .doc support out of the box with no extra installs — python-docx alone cannot read .doc; you need LibreOffice or textract.
  • Assuming heading level detection (_heading_level) is always accurate — it's a regex search for the first digit in the style name (e.g. "Heading 2" → 2), so non-standard or localized style names without a trailing digit fall back to level 1.

Version History

Cannot be determined from the repository, beyond the in-code note about the legacy .doc handling having previously been broken (incorrectly using a PDF reader) and since fixed.

Related pages