Guide
How to use paper-xlsx preserve mode, supported edits, computation, and errors.
This guide explains how to use the behavior paper-xlsx adds to openpyxl. The
generated API reference documents the complete public API,
including the surface inherited from openpyxl. The package's concise guide is
doc/paper.rst
in the repository. Standard openpyxl APIs remain available, while preserve
mode adds validation and can refuse unsafe edits or saves. Pass
preserve=False when code requires the upstream save path.
Safety contract
Loading an editable OOXML workbook retains the original package bytes as the
source of truth by default. A supported operation completes correctly or
raises an openpyxl.errors.PaperRefusal before delivery. Refusals expose
.kind, .anchor, and .options when applicable. Writes to locked cells can
emit ProtectedWriteWarning; set wb.strict_protection = True to refuse them.
Loading and saving
from openpyxl import load_workbook
wb = load_workbook(path) # preserve mode is the default
wb.validate() # run save validation without writing
receipt = wb.save(out_path, receipt=True) # save and return an EditReceiptPreserve mode is the default for editable OOXML workbooks, including files
pandas opens in append mode. Pass preserve=False for openpyxl's stock round
trip without Paper's ledger, scanners, warnings, or guards. Read-only and
unsupported-format loads keep stock behavior.
Preserve-mode file-like saves accept an open, exact io.BytesIO with no
exported buffer views, or the path-backed io.BufferedRandom pandas uses.
Use a filesystem path for other stream types.
Under preserve, docProps/core.xml is copied through and the modified
timestamp is not stamped unless you change wb.properties, so a no-op save
stays byte-identical.
Do not install openpyxl alongside
The PyPI distribution is paper-xlsx, but the import package remains
openpyxl. Do not install the separate openpyxl distribution in the same
environment. The two distributions own the same files, and package-manager
dependency metadata has no safe replacement mechanism.
Formula cache freshness
When formula text changes, or a value edit may feed a formula, preserve-mode saves remove the retained cached formula results from loaded worksheets and set Excel to recalculate the workbook on open. Style-only edits and unrelated value edits keep their caches.
Until Excel, LibreOffice, or another engine recalculates the saved file,
data_only=True may return None for invalidated formulas. Use the oracle
APIs when a task needs calculated outputs before delivery.
Perception
Start with the smallest inspection surface that answers the task:
wb.sheetnames, bounded worksheet ranges, wb.defined_names,
ws.calculate_dimension(), and the workbook's standard chart, validation,
protection, and relationship collections. Preserve checks run during load,
mutation, validation, and save; there is no package-wide preflight API.
Targeted helpers:
| API | What it does |
|---|---|
wb.search(text_or_regex, ...) | Finds text or regex matches across values and formulas. |
ws.allowed_values(cell) | Returns validation-derived allowed values for a cell. |
openpyxl.preserve.scan_errors(wb) | Scans for formula-error cells without LibreOffice. |
openpyxl.preserve.diff_workbooks(a, b, remaps=()) | Cell diffs classified as content-changed or shifted-by-structural-edit. Schema workbook_diff v1. |
Editing
Cell writes, styles, comment creation, tables, chart and image addition,
title and series-range edits on loaded charts, sheet rename/copy/delete/
reorder, row and column shifts, and move_range work under preserve. Row and
column insertions and deletions return an AddressRemap; sheet rename and
move_range() do not.
| API | What it does |
|---|---|
openpyxl.preserve.copy_format(ws, src, dst_range) | Copies formatting between ranges. |
chart.repoint(series_index, new_range) | Repoints one chart value series and removes its stale cache. |
ws.append_table_row(table_name, values) | Adds a row to a supported table and expands its range atomically. |
ws.replace_image(target, replacement, *, name=None) | Replaces one loaded image without rewriting the drawing. |
wb.set_pivot_refresh_on_load(pivots=[...]) | Permits a save after dependent edits and requests that Excel refresh the named pivots on open. Cached results remain stale until that refresh. Pass all=True for every pivot. |
Computation: the oracle
paper-xlsx does not calculate formulas, and ships no partial engine, because a partial engine returns wrong numbers on real workbooks. Headless, profile-isolated LibreOffice is the calculation oracle. It works on temporary copies; your file is never handed to it. All preservation guarantees hold with no LibreOffice installed.
| API | What it does |
|---|---|
oracle.recalc(source, *, output_path=None, timeout=120.0) | Recalculates a temporary copy, scans for errors, and can write a separate Paper-preserved candidate. It never overwrites the source. |
oracle.certify(source) | Reports whether LibreOffice reproduces the file's cached values: CERTIFIED, DIVERGED, or BASELINE_UNVERIFIABLE. result.classify_tolerance(...) can classify recorded numeric divergences under caller-supplied tolerances without changing strict status. |
oracle.evaluate(source, set={...}, read=[...]) | Applies inputs to a temporary copy and returns requested outputs. |
oracle.evaluate_many(source, cases, read) | Batch evaluation with a warm profile pool. |
Path saves use fsync-before-rename plus directory fsync, spool-to-disk archive builds, and central-vs-local ZIP header agreement on the copy-through path. Paper validates package integrity but does not impose fixed entry-count, size, or compression-ratio limits; callers control resource limits.
The error taxonomy
PaperRefusal is the base class. The pinned subclasses:
AmbiguousTargetErrorTargetNotFoundErrorUnsupportedStructureErrorBoundaryViolationErrorRelationshipPolicyErrorOracleUnavailableErrorOracleTimeoutError
A CI check verifies that every pinned class is produced and tested.
Compatibility opt-out
Code that depends on openpyxl's stock package regeneration can pass
preserve=False to load_workbook or through a caller's engine arguments.
The opt-out is explicit and local to the load. No process-wide environment
setting changes the safety contract.