aiofile moves file I/O off the event loop, and picks the backend for you
Real asynchronous file operations with asyncio support.
At a glance
- What is it?
- aiofile is a Python library for asynchronous file operations that delegates to caio, which picks between io_uring, libaio, a C threadpool and a pure Python fallback depending on the platform. It is a thin layer, and it comes with a real constraint: no internal file pointer, and no special files on the native Linux backends.
- Who is it for?
- Adopt aiofile when you have an asyncio service doing enough file work that blocking reads are showing up in your latency, and you are on Linux, macOS or Windows with Python 3.11 or newer. Skip it if your file volume is small, since the threadpool path aiofile falls back to is not faster than the blocking call it replaces, and skip it if you need to read from procfs or sysfs on the native backends.
- Can I use it commercially?
- Check first. The repository uses a licence we do not classify automatically, so read its LICENSE file before any commercial use.
- Is it still maintained?
- Yes. The repository last received commits 16 days ago.
- What is it written in?
- Mainly Python, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The library is a thin shim over caio, not an implementation
aiofile provides real asynchronous file operations with asyncio support, and since version 2.0.0 it uses caio, which provides multiple async I/O backends. That delegation is the central design fact. The Linux io_uring backend is described as the modern, high-performance Linux kernel AIO interface, and the Linux libaio backend is the classic kernel AIO mechanism. On POSIX systems such as macOS and Linux there is a thread-based C implementation backed by a threadpool, and for any platform there is a pure Python thread-based fallback. The best available backend is chosen automatically based on system compatibility. What this means for a user is that aiofile is mostly a Python API, and the performance comes from what sits underneath it. The package metadata makes that concrete: the only runtime dependency is `caio~=0.12.0`, the package requires Python 3.11 or newer, and it is built with hatchling. The repository itself is a small `aiofile/` package with `tests/` and a `benchmark/` directory alongside the packaging files.
No internal file pointer, so offset and chunk_size go on every call
The API decision that will affect your code most is stated plainly: `AIOFile` has no internal file pointer. You pass `offset` and `chunk_size` to each operation, or you use the `Reader` and `Writer` helpers, and if you want something that looks like a file object you use `async_open`. That is a departure from the synchronous mental model, where a file object remembers where you are. The reason is that an asynchronous object is expected to be usable concurrently, and a shared mutable cursor is exactly the thing that breaks when two coroutines touch the same descriptor. The price is that every read and write has to say where it belongs, and code that was written as a sequence of `read()` calls has to be restructured. The `async_open` helper is the escape hatch: it returns an object with equivalent asynchronous methods, so `async with async_open(path, 'r') as afp` gives you a familiar shape, and the async_open returns an object whose `.file` attribute reaches the underlying `AIOFile`.
The async_open helper and the methods it supports
The `async_open` helper mimics Python's file-like objects, returning an object with equivalent asynchronous methods. The supported list is short enough to read in one go. `read(length=-1)` reads a chunk, with -1 meaning to the end. `write(data)` writes a chunk. `seek(offset)` and `tell()` are plain synchronous methods, not coroutines, which makes sense for a cursor operation. `readline(size=-1, newline="\n")` reads until a newline or EOF. `__aiter__` returns a `LineReader` and `iter_chunked(chunk_size=32768)` returns a `Reader`. The README notes that since version 3.7.0 `__aiter__` returns a `LineReader`, and that `readline` is suboptimal for small lines because it does not reuse the read buffer, so `LineReader` is preferable when reading line by line. The basic example shows the whole lifecycle.
import asyncio
from pathlib import Path
from tempfile import gettempdir
from aiofile import async_open
tmp_filename = Path(gettempdir()) / "hello.txt"
async def main():
async with async_open(tmp_filename, 'w+') as afp:
await afp.write("Hello ")
await afp.write("world")
afp.seek(0)
print(await afp.read())
await afp.write("Hello from\nasync world")
print(await afp.readline())
print(await afp.readline())
asyncio.run(main())Note that the example writes twice into the same open file without truncating, which is what the `w+` mode does when reopened, and the seek before the read shows why the synchronous-looking cursor methods exist at all.
Writing a cat and a cp in the async style
The README's worked programs are worth reading because they show the shape of real async file code. The concatenate example takes one or more file names, opens each and writes its lines to standard output as they arrive.
import asyncio
import sys
from argparse import ArgumentParser
from pathlib import Path
from aiofile import async_open
parser = ArgumentParser(
description="Read files line by line using asynchronous io API"
)
parser.add_argument("file_name", nargs="+", type=Path)
async def main(arguments):
for src in arguments.file_name:
async with async_open(src, "r") as afp:
async for line in afp:
sys.stdout.write(line)
asyncio.run(main(parser.parse_args()))The copy example does the same thing in chunks rather than lines, with a chunk size of 65535 by default and a `--chunk-size` argument to change it.
async def main(arguments):
async with async_open(arguments.source, "rb") as src, \
async_open(arguments.dest, "wb") as dest:
async for chunk in src.iter_chunked(arguments.chunk_size):
await dest.write(chunk)There is also an example of opening an already-open file pointer, where `async_open` takes the `IO` object directly, and an example that skips the context manager, registering an `atexit` handler to unlink the temporary file. That second pattern is convenient in a script and is not something you would want in a long-running service.
The special file limitation is in the kernel, not in aiofile
The limitations section is short and specific. The Linux native AIO and io_uring backends cannot open special files, and asynchronous operations against special filesystems such as `/proc/` or `/sys/` are not supported by the kernel, which the README is careful to say is neither an aiofile nor a caio issue. The same note appears later: native AIO and io_uring do not support reading or writing special files such as procfs, sysfs and Unix pipes, so those operations require a compatible context object. The fix is documented, and it is a good illustration of the layering. You create a thread-based context from caio and pass it to `async_open`.
import asyncio
from aiofile import async_open
from caio import thread_aio_asyncio
from contextlib import AsyncExitStack
async def main():
async with AsyncExitStack() as stack:
# Custom context should be reused
ctx = await stack.enter_async_context(
thread_aio_asyncio.AsyncioContext()
)
# Open special file with custom context
src = await stack.enter_async_context(
async_open("/proc/cpuinfo", "r", context=ctx)
)The comment that the custom context should be reused matters, since each context carries its own threadpool. This is the case where aiofile stops being clever and asks you to make a decision, and it is a good one to know about before you write a monitoring agent that reads procfs.
clone exists because concurrency at the descriptor level is limited
The `clone` helper addresses a constraint that is easy to miss. An asynchronous context supports a limited number of concurrent operations at the low level, regardless of how many file descriptors are open. `clone` lets you create a second file-like object with its own independent offset from a single descriptor, without opening the file multiple times. That is the difference between a design where each consumer opens its own handle and one where several readers share a descriptor with separate cursors, which is what you want when you need to hash the same stream in several ways. The README's example for it is a hasher that computes multiple hash functions over one file, with the hashes running competitively and the results printed in the order of hashing completion. The example block in the documentation is truncated, so the full listing is not visible in the README, but the intent is clear from the docstring: a single descriptor, several independent readers, results as they finish rather than in a fixed order. If your workload is fan-out over one stream, this is the feature that makes aiofile usable rather than merely convenient.
Editorial conclusion
Adopt aiofile when you have an asyncio service doing enough file work that blocking reads are showing up in your latency, and you are on Linux, macOS or Windows with Python 3.11 or newer. Skip it if your file volume is small, since the threadpool path aiofile falls back to is not faster than the blocking call it replaces, and skip it if you need to read from procfs or sysfs on the native backends. Before you commit, check which backend caio selects on your machine, because the performance story differs sharply between io_uring and the pure Python fallback.
Frequently asked questions
What does aiofile need to install and what are its requirements?
The only runtime dependency is `caio~=0.12.0`, and the package requires Python 3.11 or newer. The classifiers list Python 3.11 through 3.14, and the operating systems are Linux and MacOS.
Which async I/O backend does aiofile use?
Since version 2.0.0 it uses caio, which offers Linux io_uring, Linux libaio, a C threadpool implementation for POSIX systems and a pure Python thread-based fallback. The best available backend is chosen automatically based on system compatibility.
Why does aiofile not have a file pointer?
`AIOFile` has no internal file pointer, so you pass `offset` and `chunk_size` to each operation or use the `Reader` and `Writer` helpers. The `async_open` helper provides a file-like interface, with `seek` and `tell` available as synchronous methods.
Can aiofile read from /proc or /sys?
Not with the Linux native AIO and io_uring backends, because the kernel does not support asynchronous operations on those special filesystems. You need to pass a compatible context, such as `thread_aio_asyncio.AsyncioContext()` from caio, to `async_open`.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/mosquito-aiofile)Was this useful?