Skip to content

Unify exception hierarchy under PythonDocxError #174

@loadfix

Description

@loadfix

Problem

python-docx ships four disjoint exception roots and has one type-name collision:

Root class Location Inherits
`PythonDocxError` `src/docx/exceptions.py` `Exception`
`XmlchemyError` `src/docx/oxml/exceptions.py` `Exception`
`OpcError` `src/docx/opc/exceptions.py` `Exception`
Image errors `src/docx/image/exceptions.py` `Exception` directly (no local root)

Plus: `InvalidXmlError` is defined twice — once in `docx.exceptions` (as child of `PythonDocxError`), once in `docx.oxml.exceptions` (as child of `XmlchemyError`). Users doing `from docx.exceptions import InvalidXmlError` may catch a different type from what's actually raised by the xmlchemy layer.

Impact

A user writing defensive code:

```python
try:
doc = Document(path)
...
except PythonDocxError:
...
```

will miss:

  • `OpcError` (raised at package-load boundary, e.g. `PackageNotFoundError`)
  • `XmlchemyError` (descriptor-layer violations)
  • `InvalidImageStreamError` / `UnrecognizedImageError` / `UnexpectedEndOfFileError` (raised when adding images)

This contradicts what most users expect from a library-root exception type.

Expected fix

  1. Make `OpcError`, `XmlchemyError`, and the three image exceptions inherit from `PythonDocxError` (directly or via re-rooting).
  2. Rename one of the two `InvalidXmlError` classes — suggest `docx.oxml.exceptions.InvalidXmlError` becomes `XmlchemyInvalidXmlError` or similar. Update internal callers.
  3. Add a regression test asserting that every raised custom exception is catchable via `except PythonDocxError:`.
  4. Document the hierarchy in `FEATURES.md` or `CLAUDE.md`.

Compat

Existing code doing `except OpcError:` or `except XmlchemyError:` continues to work — we're only widening the root. The rename is breaking for anyone deliberately catching the oxml-layer `InvalidXmlError` by dotted import; HISTORY entry + deprecation period needed.

Surfaced by: 2026-05-05 consistency audit (item 7).

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions