Paper Office
paper-xlsx

vs openpyxl

High-level changes relative to openpyxl 3.1.5, and why.

paper-xlsx is a fork of openpyxl 3.1.5. It keeps the openpyxl import name and object model, while preserve mode changes how loaded editable workbooks are validated and saved. Paper changes that path to preserve content openpyxl does not model.

openpyxl regenerates the entire file on save, so content it does not model is degraded or dropped. As upstream's tutorial notes:

openpyxl does currently not read all possible items in an Excel file so shapes will be lost from existing files if they are opened and saved with the same name.

Each of these was verified against openpyxl 3.1.5 before designing the fork:

  • data_only=True then save destroys formulas — a sheet with 3 formulas round-trips to 0.
  • insert_rows and delete_rows move cells without updating formulas, defined names, or chart ranges, so downstream SUM ranges point at the wrong cells.
  • Written formulas carry stale or empty cached values.
  • VBA is stripped from .xlsm files unless keep_vba=True.
  • Shapes, textboxes, mc:AlternateContent, chart extLst, sparklines, and x14 validations are lost on a plain load and save.

High-level changes

  • Preserve mode. load_workbook(path) retains the original archive bytes. A dirty ledger records edits, and save splices dirty ranges into the original parts instead of regenerating. Untouched parts copy through byte-identical; a no-op save is byte-identical.
  • Typed error taxonomy. openpyxl.errors.PaperRefusal and subclasses (AmbiguousTargetError, TargetNotFoundError, UnsupportedStructureError, BoundaryViolationError, RelationshipPolicyError, OracleUnavailableError, OracleTimeoutError). Guarded helpers validate before mutating; a save refusal writes no destination file.
  • Guarded delivery. Path saves validate ZIP consistency and use fsync-before-rename. preserve=False keeps the stock openpyxl save path.
  • Inspection and editing. wb.search(...), ws.allowed_values(cell), openpyxl.preserve.scan_errors(), and diff_workbooks() return targeted structured data. Structural edits (insert_rows, delete_rows, sheet renames, range moves) rewrite dependent formulas, defined names, print areas, table ranges, and chart references when supported, or refuse. Row and column insertions and deletions return an AddressRemap.
  • Targeted editing helpers. copy_format(), chart.repoint(), append_table_row(), replace_image(), and explicit pivot-refresh consent.
  • Formula state. Editing a formula or one of its inputs strips affected stale cached results and requests recalculation on open. Style-only edits keep their caches.
  • The oracle. openpyxl.oracle uses a headless, profile-isolated LibreOffice for recalc(), certify(), evaluate(), and evaluate_many(). It works on temporary copies and never overwrites the source.
  • Compatibility and installation. Preserve mode is the default for editable OOXML workbooks; read_only=True and preserve=False keep stock behavior. paper-xlsx-doctor verifies the installed distribution owns the openpyxl import tree.

At a glance

A real workbook, edited and savedstock openpyxl 3.1.5paper-xlsx preserve mode
Shapes, textboxes, mc:AlternateContentdropped without warningcopied through byte-identical
Chartsregenerated from the model; chart extLst and auxiliary parts lostuntouched charts copied through byte-identical; title and series-range edits spliced
Sparklines, x14 validations/formattingdropped (load-time warning)copied through byte-identical
VBA project in .xlsmstripped unless keep_vba=Truesurvives
data_only=True, then saveevery formula replaced by its cached valuerefused unless explicitly allowed
insert_rows / delete_rowscells move, references do not updatereferences rewritten (or edit refused); AddressRemap returned
Formula caches after an editcached results can be cleared; formulas are not calculatedaffected caches invalidated; recalculation requested on open
Unsafe or ambiguous operationbest guesstyped PaperRefusal, file unchanged

On this page