Compatibility with openpyxl

fastpyxl is a drop-in replacement for openpyxl >= 3.0.0 on Python >= 3.11.

Users on openpyxl 3.x should be able to change imports only:

# before
from openpyxl import Workbook, load_workbook

# after
from fastpyxl import Workbook, load_workbook

No other code changes are required for supported openpyxl 3.x usage.

Supported baseline

Constraint Value
openpyxl API surface >= 3.0.0
Python >= 3.11
CI enforcement pin openpyxl 3.1.5

The public claim is compatibility with the openpyxl 3.x API family starting at 3.0.0. Continuous integration pins the current openpyxl 3.1.x release used for benchmarks and runs the dedicated compatibility suite against that pin.

Intentional non-goals

fastpyxl does not aim to preserve:

  • APIs removed before openpyxl 3.0
  • Behaviour only present in openpyxl 2.x
  • Legacy load options such as use_iterators (replaced by read_only)
  • Historical dual worksheet implementations (IterableWorksheet vs Worksheet); read-only mode uses ReadOnlyWorksheet

Running the compatibility suite

The openpyxl drop-in contract is enforced by tests under fastpyxl/tests/compat, marked openpyxl_compat. CI runs them in a dedicated job; locally:

uv sync --all-groups
uv run pytest fastpyxl/tests/compat -m openpyxl_compat

openpyxl==3.1.5 is installed via the dev dependency group.

Documenting intentional divergences

When fastpyxl deliberately differs from openpyxl, document that difference as a reviewed xfail (or an allowlist entry in the API-surface tests) rather than allowing silent drift. Prefer:

  1. A short comment next to the xfail / allowlist entry explaining why
  2. A note in this page if the divergence affects end users
  3. A changelog entry when the divergence is introduced or removed

vba_archive filtering (keep_vba)

With keep_vba=True, openpyxl 3.1.5 copies the entire package into Workbook.vba_archive. fastpyxl instead stores a filtered VBA cache: only macro-relevant parts (xl/vba*, VML drawings, ActiveX, ctrlProps, customUI, EMF media, plus [Content_Types].xml and _rels/.rels).

That filtered cache is enough for load→edit→save of macros; the writer already applies the same ARC_VBA allowlist on save. Filtering avoids cloning unused worksheet/XML parts on large .xlsm files.

Callers that inspect non-VBA members of wb.vba_archive can opt into openpyxl-identical behaviour with the fastpyxl-specific escape hatch:

wb = load_workbook("macros.xlsm", keep_vba=True)    # filtered cache (default)
wb = load_workbook("macros.xlsm", keep_vba="full")  # full package mirror
wb = load_workbook("macros.xlsm", keep_vba=False)   # no vba_archive

True / False remain drop-in compatible with openpyxl’s bool API. keep_vba="full" is fastpyxl-only.

Dual-load formulas and caches (keep_formula_cache)

Excel sheet XML often stores both a formula (<f>) and a last-calculated cache (<v>) on the same cell. openpyxl (and default fastpyxl) keep only one or the other via data_only.

fastpyxl adds an opt-in dual-load mode that keeps both in a single parse:

wb = load_workbook("book.xlsx", keep_formula_cache=True)
assert wb.data_only is False
c = wb.active["D2"]
c.value          # formula text, e.g. "='Sheet2'!D5"
c.cached_value   # cached result from the file, or None

Compatibility constraints:

  • load_workbook(path) and load_workbook(path, data_only=True) remain unchanged (and openpyxl-compatible) for .value, .data_type, iteration, and save output. In those modes cell.cached_value is always None.
  • data_only stays a bool; do not pass "both".
  • data_only=True and keep_formula_cache=True together raise ValueError.
  • Assigning .value clears .cached_value (the cache is stale).
  • On standard worksheets, caches are stored in a sheet-level side map (ws._formula_caches) rather than a per-cell slot, so default-mode loads do not pay a pointer per cell. Only the live sheet cell at a coordinate may read or write that map entry; detached helpers such as WriteOnlyCell are ignored. read_only cells keep instance storage because they are ephemeral and must not accumulate a sheet-wide map.
  • Save still writes empty <v> for formula cells in v1; emitting real caches is deferred.

keep_formula_cache is fastpyxl-only.

Indexed / on-demand reads (access)

Default load_workbook and read_only=True keep their openpyxl semantics. fastpyxl adds an opt-in indexed read strategy for sparse / random access:

wb = load_workbook("book.xlsx", access="indexed")
assert wb.read_only is True
print(wb.active["Z100000"].value)
wb.close()

Compatibility constraints:

  • access=None (default) is unchanged and openpyxl-compatible.
  • access="indexed" implies read-only worksheets; read_only=True may be passed together and is accepted.
  • Unknown access values raise ValueError.
  • Misses return EMPTY_CELL (no densifying writes).
  • Styles stay eagerly loaded; only sheet rows and shared strings are indexed.
  • Like read_only, indexed worksheets do not bind merges, comments, tables, drawings, or other worksheet relationship metadata.

access is fastpyxl-only.

Environment variables

Prefer the FASTPYXL_* names:

Setting Preferred Legacy alias (deprecated)
Use lxml when available FASTPYXL_LXML OPENPYXL_LXML
Use defusedxml when available FASTPYXL_DEFUSEDXML OPENPYXL_DEFUSEDXML
Keep VBA on load (tests) FASTPYXL_KEEP_VBA OPENPYXL_KEEP_VBA

Using a legacy OPENPYXL_* name still works for one release cycle but emits a DeprecationWarning. Migrate to the FASTPYXL_* equivalent.

Removal plan for inherited compatibility cruft

The openpyxl lineage left several deprecated call sites and shims. With the 3.0+ contract published, removals proceed in phases:

Phase 1 — warn (current)

  • Emit DeprecationWarning for legacy OPENPYXL_* environment aliases
  • Keep deprecated openpyxl 3.x workbook helpers (get_sheet_by_name, get_sheet_names, remove_sheet, get_index, create_named_range) and Worksheet.merged_cell_ranges while they remain part of the openpyxl 3.x surface that callers still use
  • Clean obsolete comments/docstrings that refer to pre-3.0 dual implementations (IterableWorksheet, use_iterators)

Phase 2 — remove aliases

  • Drop OPENPYXL_* environment aliases after at least one released version has carried the Phase 1 warnings
  • Refactor reader/excel.py so production code does not import test helpers such as KEEP_VBA from fastpyxl.tests

Phase 3 — major-version API trim

  • Remove deprecated openpyxl 3.x methods/properties only in a major release (v2.0), with release notes listing replacements (wb[sheetname], wb.sheetnames, wb.remove(), wb.index(), etc.)
  • Revisit type-checker carve-outs in pyproject.toml that exist only for legacy Python 2 / transitional code once that code is gone

Deprecated APIs that openpyxl itself still exposes in 3.x remain available in fastpyxl v1.x so import-only migrations keep working. They are not a promise of indefinite support beyond the next major version.