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
class DocxLoader(BaseFileLoader):
SUPPORTED_EXTENSIONS = [".docx", ".doc"]
def __init__(self, file_path: str, config: Optional[DocxLoaderConfig] = None): ...
def load(self) -> List[LoadedDocument]: ...
Parameters
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..."ifpython-docxisn't installed (.docxpath)ImportError— with multi-option install instructions if.docand neither LibreOffice nortextractis available
Internal workflow
def load(self):
if self.file_path.suffix.lower() == ".doc":
return self._load_legacy_doc()
return self._load_docx()
.docx path — _load_docx()
- Open with
python-docx'sDocument(). - If
config.include_headers: extract text from each section's header. - 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. - If
config.include_tables: render each table as text (rows joined bytable_separator, cells within a row joined byrow_separator) and append, prefixed[Table]:. - If
config.include_footers: extract footer text per section, prefixed[Footer]:. - Join everything with
\nand strip. - 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
.docfiles using a PDF reader — which is invalid since a Word.docfile is not a PDF. The current implementation replaces that with a proper conversion strategy.
Strategy, tried in order:
- LibreOffice conversion — if
libreofficeorsofficeis found onPATH, runslibreoffice --headless --convert-to docx --outdir <tmpdir> <file>(30s timeout), then recursively constructs a newDocxLoaderon the converted file and calls_load_docx()on it. The resulting documents have theirsourceand anote: "Converted from .doc via LibreOffice"metadata field rewritten back to point at the original.docpath. textract— if LibreOffice conversion isn't available or fails, triestextract.process(path).- If both fail, raises
ImportErrorwith a three-option remediation message (convert manually, install LibreOffice, or installtextract).
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/sofficeprocess and writes a temporary.docxfile to atempfile.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:
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:
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:
docs = DocxLoader("old_memo.doc").load()
# Tries LibreOffice conversion, then textract, then raises ImportError
print(docs[0].metadata.get("note"))
Error handling — missing dependency:
try:
docs = DocxLoader("report.docx").load()
except ImportError as e:
print(f"Missing dependency: {e}")
Production usage — custom table formatting for RAG:
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
.docfiles in a production pipeline, ensure LibreOffice is installed on the host — it's both more reliable and richer (preserves headings/tables/metadata) than thetextractfallback. - Set
include_tables=Falseif your document's tables are decorative/layout-only and would add noise to embeddings.
Common mistakes
- Expecting
.docsupport out of the box with no extra installs —python-docxalone cannot read.doc; you need LibreOffice ortextract. - 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.
Related APIs
DocxLoaderConfigLoadedDocumentDirectoryLoader— dispatches.docx/.docto this loader automatically
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.