Async File I/O in Python With aiofiles

This article explores how Python manages asynchronous file operations using the aiofiles library. While Python's built-in asyncio library provides robust support for asynchronous network operations, standard local filesystem access remains inherently synchronous and blocking. This guide explains why the operating system creates this limitation, how aiofiles bypasses it using thread pools, and how to implement non-blocking read and write operations in your asynchronous applications.

The Problem With Standard File I/O in Asyncio

Python’s asyncio event loop is built around OS-level I/O multiplexing systems such as epoll on Linux, kqueue on macOS, and IOCP on Windows. These notification systems are designed for non-blocking network sockets, pipes, and IPC channels.

Regular disk files behave differently. Most operating systems do not provide universal, reliable, non-blocking notification interfaces for standard filesystem reads and writes. When you call standard functions like open(), read(), or write(), the operating system blocks the calling thread until the disk completes the physical I/O request. In an asynchronous Python application, running standard synchronous file functions directly freezes the single-threaded event loop, preventing all concurrent coroutines and network tasks from executing until the disk operation finishes.

How aiofiles Enables Asynchronous File I/O

The aiofiles library provides an asynchronous interface for file handling by delegating blocking file operations to a background thread pool managed by asyncio.

Instead of freezing the main thread where the event loop resides, aiofiles dispatches blocking system calls to separate worker threads using an executor (such as Python’s ThreadPoolExecutor). The coroutine awaits the completion of the thread's task, releasing the event loop to execute other concurrent tasks in the meantime. Once the worker thread finishes the read or write operation, the result is returned to the coroutine, and execution resumes seamlessly.

Basic Usage of aiofiles

To use aiofiles, install it via pip:

pip install aiofiles

The syntax matches Python’s standard file-handling conventions, utilizing asynchronous context managers (async with) and asynchronous iteration:

import asyncio
import aiofiles

async def write_data(filename: str, content: str) -> None:
    async with aiofiles.open(filename, mode='w') as f:
        await f.write(content)

async def read_data(filename: str) -> None:
    async with aiofiles.open(filename, mode='r') as f:
        # Read the entire file asynchronously
        data = await f.read()
        print(f"File content: {data}")

async def stream_lines(filename: str) -> None:
    async with aiofiles.open(filename, mode='r') as f:
        # Iterate over lines asynchronously
        async for line in f:
            print(line.strip())

async def main() -> None:
    await write_data("example.txt", "Hello, Async World!\nLine 2")
    await read_data("example.txt")
    await stream_lines("example.txt")

if __name__ == "__main__":
    asyncio.run(main())

Performance Considerations and Alternatives

Because aiofiles offloads work to a thread pool, it introduces minor thread synchronization overhead. It does not make physical disk read and write speeds faster. Instead, it prevents blocking the primary event loop.

import asyncio

def write_sync(filepath: str, text: str) -> None:
    with open(filepath, 'w') as f:
        f.write(text)

async def main() -> None:
    await asyncio.to_thread(write_sync, "file.txt", "content")

While asyncio.to_thread works well for simple one-off file tasks, aiofiles provides a more ergonomic, complete API with support for asynchronous file streaming, iteration, and seek operations.