aiofile review
aiofile 3.12.3 adds awaitable file reads and writes to asyncio programs. `async_open` gives ordinary sequential code a familiar cursor-based wrapper, while `AIOFile` accepts a byte offset for each operation so coroutines can work on separate regions of one file. The required `caio` package selects io_uring, Linux AIO, a C thread pool, or a Python thread fallback. Our Python 3.12 import completed in 0.37 seconds and the wheel included `py.typed`. The 3.12.3 release changed documentation and benchmark reporting only; the published comparison now measures fixed operation counts and reports latency for 256-byte and 65,536-byte blocks.
aiofile 3.12.3 installed in 0.3 seconds, occupied 1 MB across 2 packages, imported in 0.37 seconds, and had 0 audit findings in our sandbox. Install it for asyncio streaming or explicit-offset file work; use plain `open()`, aiofiles, or AnyIO when its backend rules solve no problem you have.
We installed it
| Install | ✓ · 0.3s | 2 packages on disk · 1 MB |
| Import | ✓ | import aiofile in 0.37s · pure Python · py.typed · requires Python >=3.11 |
| Known vulns | 0 | (pip-audit) |
Answers from our run
Does aiofile install cleanly?
Yes. In a fresh container with an empty cache, pip install aiofile finished in 0.3s, leaving 2 packages and 1 MB on disk. pip-audit reported no known vulnerabilities.
What does aiofile need to run?
Python >=3.11, and nothing compiled: it is pure Python. In our run import aiofile succeeded in 0.37s, and the package ships py.typed for type checkers.
aiofile or aiofiles: which should you use?
aiofiles: Use it for executor-backed file calls plus async wrappers around selected os and tempfile functions. aiofile 3.12.3 installed in 0.3 seconds, occupied 1 MB across 2 packages, imported in 0.37 seconds, and had 0 audit findings in our sandbox.
When should you not use aiofile?
Your code needs async directory walking, rename, stat, or temporary-file wrappers. aiofile covers file contents; aiofiles exposes wrappers for more os and tempfile calls.
Use it if
- Your asyncio service needs to stream large regular files without blocking the event loop on each read or write.
- Independent coroutines must write known byte ranges of the same file by explicit offset.
- A Linux deployment can use io_uring or libaio, with a documented thread fallback for incompatible hosts.
- You need buffered async line or chunk iteration but want access to lower-level fsync and positional operations too.
- Your code needs async directory walking, rename, stat, or temporary-file wrappers. aiofile covers file contents; aiofiles exposes wrappers for more `os` and `tempfile` calls.
- Python 3.10 or older is still in support. Every example and the current package require Python 3.11 or newer.
- The target is Windows and parallel cloned readers are the main attraction. The README warns that its clone hashing example will probably perform poorly there.
- You must read `/proc`, `/sys`, or Unix pipes through a native Linux AIO context. The kernel backends cannot open those special files, so those paths need a thread context.
- The program supports Trio as well as asyncio. aiofile is asyncio-specific; AnyIO supplies one file interface across both event-loop families.
- All file access is a short startup read. Synchronous `open()` keeps that path dependency-free and is easier to reason about.
Setup reality
We installed aiofile 3.12.3 in a clean Python 3.12 Bookworm container in 0.3 seconds. The environment ended with 2 packages and 1 MB on disk. pip-audit found 0 known vulnerabilities. The pure-Python package has 1 direct dependency, requires Python 3.11 or newer, carries Apache Software License metadata, and ships py.typed. import aiofile succeeded in 0.37 seconds.
No credentials or repository config are required. caio chooses the first usable backend at import time. Set CAIO_IMPL to uring, linux, thread, or python before importing when production hosts must behave alike. io_uring requires Linux kernel 5.1 or newer, and filesystem or container restrictions can still make a native backend unavailable. A distribution can also select a default through caio's default_implementation file.
async_open mixes awaited and immediate methods. Await read, write, readline, flush, and close; call seek and tell normally. Its async iterator uses a buffered LineReader, which the README recommends over repeated readline() calls for small lines. iter_chunked() defaults to 32,768-byte reads, though opening hundreds of paths still consumes hundreds of file descriptors unless the caller caps concurrency.
Low-level AIOFile has no shared cursor, and every read or write defaults to offset 0. A second write without an offset can replace the start of the file. Use Writer or Reader for sequential work, and give concurrent positional writes distinct ranges. Native io_uring and libaio contexts reject procfs, sysfs, and pipes; the documented solution is one reused thread_aio_asyncio.AsyncioContext for those special paths.
Patterns
Read a text file read-text-file
from aiofile import async_open
async def read_report(path):
async with async_open(path, "r", encoding="utf-8") as file:
return await file.read()`read()` is awaitable and reads to EOF when no length is supplied. Use chunk iteration for files that should not be held entirely in memory.
Write through the high-level wrapper write-and-reread
from aiofile import async_open
async def write_message(path):
async with async_open(path, "w+", encoding="utf-8") as file:
await file.write("first line\n")
await file.write("second line\n")
file.seek(0)
return await file.read()`seek()` is an immediate method on `async_open`; adding `await` to it is an API error. Reads and writes still require `await`.
Process lines with the buffered iterator iterate-lines
from aiofile import async_open
async def error_lines(path):
async with async_open(path, "r") as file:
async for line in file:
if "ERROR" in line:
yield line.rstrip("\n")Async iteration uses `LineReader`, which reuses its buffer. The project recommends it over repeated `readline()` calls for short lines.
Read bounded binary chunks stream-binary-chunks
from aiofile import async_open
async def consume(path):
async with async_open(path, "rb") as file:
async for chunk in file.iter_chunked(64 * 1024):
await send_chunk(chunk)`iter_chunked(64 * 1024)` requests 65,536-byte pieces. Backpressure still depends on awaiting the consumer inside the loop.
Copy without reading the whole source copy-file
from aiofile import async_open
async def copy_file(source, destination):
async with async_open(source, "rb") as src:
async with async_open(destination, "wb") as dst:
async for chunk in src.iter_chunked(128 * 1024):
await dst.write(chunk)Binary mode preserves bytes exactly. This copies file contents only; aiofile does not copy mode bits, ownership, or timestamps.
Write independent file regions concurrently write-at-offsets
import asyncio
from aiofile import AIOFile
async def write_blocks(path, blocks):
async with AIOFile(path, "wb+") as file:
await asyncio.gather(*(
file.write_bytes(block, offset=index * 4096)
for index, block in enumerate(blocks)
))
await file.fsync()`AIOFile` does not move a shared cursor. Each concurrent write needs a distinct byte offset or calls can overwrite the same region.
Add a cursor with Writer sequential-low-level-write
from aiofile import AIOFile, Writer
async def append_records(path, records):
async with AIOFile(path, "ab+") as file:
writer = Writer(file, offset=0)
for record in records:
await writer(record + "\n")
await file.fsync()`Writer` advances its own offset after every call. Separate writers do not coordinate positions, so one writer should own each sequential region.
Read procfs through a thread context read-special-file
from aiofile import async_open
from caio import thread_aio_asyncio
async def read_cpuinfo():
async with thread_aio_asyncio.AsyncioContext() as context:
async with async_open("/proc/cpuinfo", "r", context=context) as file:
return await file.read()io_uring and Linux AIO cannot open procfs, sysfs, or pipes. The README prescribes a thread context for those paths.
Pin the caio backend at process start select-backend
import os
os.environ["CAIO_IMPL"] = "thread"
from aiofile import async_open
async def read_data(path):
async with async_open(path, "rb") as file:
return await file.read()`CAIO_IMPL` is read during backend initialization, so set it before importing aiofile or caio. Valid documented values are `uring`, `linux`, `thread`, and `python`.
Clone a file wrapper for separate read positions parallel-independent-readers
import asyncio
import aiofile
async def read_prefix(file, size):
clone = await aiofile.clone(file)
return await clone.read(size)
async def prefixes(path):
async with aiofile.async_open(path, "rb") as source:
return await asyncio.gather(
read_prefix(source, 1024),
read_prefix(source, 4096),
)Each clone has an independent offset over the same descriptor. The README warns that this parallel-reader technique will probably perform poorly on Windows.
Bound concurrent file opens limit-open-files
import asyncio
from aiofile import async_open
async def read_one(path, limit):
async with limit:
async with async_open(path, "rb") as file:
return await file.read()
async def read_many(paths, max_open=32):
limit = asyncio.Semaphore(max_open)
return await asyncio.gather(*(read_one(path, limit) for path in paths))Async I/O does not raise the operating system's descriptor limit. The semaphore keeps this function to 32 simultaneous opens by default.
Force completed writes to storage sync-durable-data
from aiofile import AIOFile
async def store_checkpoint(path, payload):
async with AIOFile(path, "wb") as file:
await file.write_bytes(payload, offset=0)
await file.fsync()`fsync()` requests data and metadata synchronization after the write completes. Awaiting `write_bytes()` alone does not state that the storage device has committed the bytes.
Alternatives
More utils guides
lru-cache · ajv · type-fest · p-limit · find-up · js-yaml · the whole shelf →
How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.

