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
- Make `OpcError`, `XmlchemyError`, and the three image exceptions inherit from `PythonDocxError` (directly or via re-rooting).
- Rename one of the two `InvalidXmlError` classes — suggest `docx.oxml.exceptions.InvalidXmlError` becomes `XmlchemyInvalidXmlError` or similar. Update internal callers.
- Add a regression test asserting that every raised custom exception is catchable via `except PythonDocxError:`.
- 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).
Problem
python-docx ships four disjoint exception roots and has one type-name collision:
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:
This contradicts what most users expect from a library-root exception type.
Expected fix
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).