Skip to main content

aioopenpyxl

CI PyPI Downloads Python versions License: MIT

Asynchronous wrapper around openpyxl with the complete openpyxl API surface, built on anyio so it works on asyncio and trio alike. openpyxl is synchronous, CPU-bound and not thread-safe: calling it on the event loop stalls every other request, and merely moving it to a thread lets a second coroutine corrupt the file silently. aioopenpyxl runs the operations that can block in worker threads, serialises all work on one workbook, and makes synchronous access to a workbook a thread currently owns raise instead of racing. It is an independent project, not affiliated with or endorsed by openpyxl; please report problems with this wrapper here.

Install

uv add aioopenpyxl   # or: pip install aioopenpyxl

Python 3.10 or newer (including the free-threaded 3.14t build); depends on anyio, openpyxl, types-openpyxl and typing_extensions.

Quick start

import aioopenpyxl

async with aioopenpyxl.load_workbook("data.xlsx", read_only=True) as wb:  # read, off the loop
    ws = wb.active
    async for row in ws.iter_rows(values_only=True):
        ...

wb = aioopenpyxl.Workbook()  # same signature as openpyxl.Workbook(write_only=..., iso_dates=...)
ws = wb.active
ws["A1"] = "header"  # in-memory: forwarded directly, no thread hop
await ws.append_rows(rows)  # whole batch in one thread hop
await wb.save("output.xlsx")  # off the event loop


def fill(raw_ws):  # bulk in-memory work: plain openpyxl code, one hop, workbook locked meanwhile
    for i, item in enumerate(items, start=2):
        raw_ws.cell(i, 1, item.name)


await ws.run(fill)

Wrap an openpyxl workbook you already have with aioopenpyxl.Workbook.wrap(existing_workbook). Workbook is a real class: isinstance, annotations and subclassing work as with openpyxl.Workbook, and a subclass can keep attributes of its own.

What runs where

Operation Where Why
await load_workbook(...), await load_workbook_bytes(data), await wb.save(...), await wb.to_bytes(), await wb.close() worker thread real I/O, XML parsing / writing
async for row in ws.iter_rows(..., chunk_size=1024, prefetch=0), ws.rows, ws.values, ws.columns, async for row in ws worker thread, one hop per chunk_size rows; with prefetch=N the thread parses N chunks ahead read-only sheets parse XML lazily
await ws.read_rows(...), await ws.fetch(key), await ws.calculate_dimension() worker thread, one hop coarse-grained reads
await ws.append_rows(rows) worker thread, one hop bulk insert
await ws.append(row) inline for regular sheets, thread for write-only sheets write-only sheets spool to a temp file
await wb.run(fn), await ws.run(fn) worker thread, one hop your bulk in-memory work
ws["A1"], ws.cell(), ws.title, ws.merge_cells(), ws.insert_rows(), wb.create_sheet(), styles, dimensions ... inline on the event loop in-memory operations
ws["A1"], ws.cell() on a read-only sheet raises BlockingCallError openpyxl would parse XML on the event loop; use fetch / read_rows / run

Keep chunk_size at 256 or more (each chunk is one thread round trip); for large exports use Workbook(write_only=True) with append_rows. The internals are explained in docs/design.md.

Rules of the road

  • A thread owns the workbook while an await is pending. Synchronous access meanwhile (ws.cell(...), wb.sheetnames, even repr(wb)) raises WorkbookBusyError; await first or move the work into run(...).
  • Inside a prefetch>0 loop, never await the same workbook. The loop holds the semaphore, so await ws.run(...) or a second async for from the body hangs rather than erroring. Use contextlib.aclosing(...) around the iterator when you may leave such a loop early.
  • Read-only sheets have no synchronous cell access. ws["A1"] / ws.cell() raise BlockingCallError; use await ws.fetch(key), await ws.read_rows(...) or run.
  • Cancellation waits for the thread, including native Task.cancel() / asyncio.timeout; it takes effect at the next checkpoint, and async with always closes the workbook.
  • import aioopenpyxl as openpyxl keeps the helpers synchronous. aioopenpyxl.styles, aioopenpyxl.workbook.Workbook ... are openpyxl's own modules; only the top-level Workbook, load_workbook and open are asynchronous.
  • The guard covers the wrappers, not the objects they hand out. A Cell from ws["A1"], a Font, a ColumnDimension or cell.parent is a plain openpyxl object; mutating it while an await on the same workbook is pending races with the thread. Finish or await the operation first, or do such edits inside run(...).
  • wb.wrapped / ws.wrapped are unguarded: the raw openpyxl objects, no busy check. Keep blocking calls on them inside run(...).

Differences from openpyxl

openpyxl aioopenpyxl
load_workbook(...) await load_workbook(...) or async with load_workbook(...) as wb
wb.save(f), wb.close() await wb.save(f), await wb.to_bytes(), await wb.close()
for row in ws.iter_rows() / ws.rows / ws.values / for row in ws async for versions (chunked); iter_rows / iter_cols gain chunk_size= and prefetch=
ws.append(row) await ws.append(row); plus await ws.append_rows(rows)
ws["A1"] on a read-only sheet BlockingCallError; use await ws.fetch("A1")
– await ws.read_rows(...), await x.run(fn), x.wrapped, limiter=

Every other public member is forwarded with its typed openpyxl signature. Workbook and AsyncWorksheet are generic in the kind of worksheet (Workbook() is a Workbook[Worksheet], load_workbook(..., read_only=True) yields a Workbook[ReadOnlyWorksheet]), so ws.run(fn) accepts a callback written for the concrete openpyxl class. The full table and the typing, sub-module and subclass rules are in docs/compatibility.md.

Development

uv sync && uv run pytest -q                                 # both backends, random order
uv run ruff check . && uv run ruff format --check .
uv run ty check src tests && uv run mypy
uv run python tools/gen_proxies.py && uv run ruff format src/aioopenpyxl/   # after an openpyxl / stubs bump

See CONTRIBUTING.md for the workflow and CHANGELOG.md for history.

Acknowledgements

All the real work is done by openpyxl (MIT), which this package depends on rather than vendors. The typed forwarders and .pyi shims are generated from the types-openpyxl stubs maintained in typeshed (Apache-2.0); concurrency primitives come from anyio (MIT).

License

MIT. openpyxl, typeshed and anyio remain under their own licenses.

Metadata

Release files for aioopenpyxl 0.1.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for aioopenpyxl 0.1.1
File Size Uploaded
aioopenpyxl-0.1.1.tar.gz 37.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for aioopenpyxl 0.1.1
File Interpreter ABI Platform
aioopenpyxl-0.1.1-py3-none-any.whl Python 3 none any Details

Total release size: 138.6 kB

Release files / aioopenpyxl-0.1.1.tar.gz

Download URL aioopenpyxl-0.1.1.tar.gz
Size 37.7 kB
Tags Source
SHA-256 checksum
How to use checksums
082b8f42caa7f026e2474d6cfce4bfb3dcefc204f472ced1e342f78a3cae17fb
BLAKE2b-256 checksum
How to use checksums
8d651e4f449bf09fcbdc12b5e1696048015833a908f29789c5a0d56d3f385811
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 7, 2026.

Transparency log

Release files / aioopenpyxl-0.1.1-py3-none-any.whl

Download URL aioopenpyxl-0.1.1-py3-none-any.whl
Size 101.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
520e737f0e040be220afbaf174a960fd76bb4735916c0306718b53216c6f6404
BLAKE2b-256 checksum
How to use checksums
0b5229c9e8e45dc1b777d40ee78226d2d53b70b4c53be65550da308ed95a40a6
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 7, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.1.1 This release

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page