aioopenpyxl
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
awaitis pending. Synchronous access meanwhile (ws.cell(...),wb.sheetnames, evenrepr(wb)) raisesWorkbookBusyError; await first or move the work intorun(...). - Inside a
prefetch>0loop, never await the same workbook. The loop holds the semaphore, soawait ws.run(...)or a secondasync forfrom the body hangs rather than erroring. Usecontextlib.aclosing(...)around the iterator when you may leave such a loop early. - Read-only sheets have no synchronous cell access.
ws["A1"]/ws.cell()raiseBlockingCallError; useawait ws.fetch(key),await ws.read_rows(...)orrun. - Cancellation waits for the thread, including native
Task.cancel()/asyncio.timeout; it takes effect at the next checkpoint, andasync withalways closes the workbook. import aioopenpyxl as openpyxlkeeps the helpers synchronous.aioopenpyxl.styles,aioopenpyxl.workbook.Workbook... are openpyxl's own modules; only the top-levelWorkbook,load_workbookandopenare asynchronous.- The guard covers the wrappers, not the objects they hand out. A
Cellfromws["A1"], aFont, aColumnDimensionorcell.parentis a plain openpyxl object; mutating it while anawaiton the same workbook is pending races with the thread. Finish or await the operation first, or do such edits insiderun(...). wb.wrapped/ws.wrappedare unguarded: the raw openpyxl objects, no busy check. Keep blocking calls on them insiderun(...).
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)
| File | Size | Uploaded | |
|---|---|---|---|
| aioopenpyxl-0.1.1.tar.gz | 37.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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 logRelease 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