Paper Office
paper-xlsx

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 EditReceipt

Preserve 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:

APIWhat 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.

APIWhat 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.

APIWhat 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:

  • AmbiguousTargetError
  • TargetNotFoundError
  • UnsupportedStructureError
  • BoundaryViolationError
  • RelationshipPolicyError
  • OracleUnavailableError
  • OracleTimeoutError

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.

On this page