Skip to main content

Asynchronous File I/O: `aiofiles`

We've seen how asyncio is perfect for I/O-bound tasks and how aiohttp solves the problem of blocking network requests. But what about the other major type of I/O: reading and writing files?

By default, Python's built-in open() function is blocking. If you use it to read a large file in an async function, it will stop the entire event loop until the file operation is complete, freezing all other concurrent tasks.

To solve this, the community created aiofiles, a library that provides an asynchronous version of the file handling API you're already familiar with, allowing file I/O to be integrated seamlessly into an asyncio application.


πŸ“š Prerequisites​

You should be comfortable with asyncio fundamentals, including async/await and the async with statement.


🎯 Article Outline: What You'll Master​

In this article, you will learn:

  • βœ… The Problem with Blocking I/O: Understand why standard open() is not suitable for asyncio programs.
  • βœ… Installing aiofiles: How to add the library to your environment.
  • βœ… Async File Operations: How to use aiofiles.open() with async with to read and write files without blocking the event loop.
  • βœ… A Concurrent Example: See how aiofiles allows other tasks to run while waiting for file operations to complete.

🧠 Section 1: Installation​

aiofiles is a third-party library. You must install it from PyPI.

pip install aiofiles

πŸ’» Section 2: The aiofiles API​

The beauty of aiofiles is that its API is designed to be nearly identical to Python's built-in open(). The main difference is that its operations are coroutines, so they must be used with async and await.

The core of the library is aiofiles.open(), which is an asynchronous context manager. This means you use it with async with.

Async Writing​

Let's write some text to a file asynchronously.

import asyncio
import aiofiles # Import the library

async def write_to_file():
"""Writes a simple message to a file asynchronously."""
# Use 'async with' to open the file
async with aiofiles.open('my_async_file.txt', mode='w') as f:
print("File opened, about to write...")
# The .write() method is now a coroutine, so it must be awaited
await f.write("Hello from aiofiles!\n")
await f.write("This is non-blocking I/O.")
print("Finished writing.")

asyncio.run(write_to_file())

The syntax is almost identical to the standard with open(...), but with the addition of async and await.

Async Reading​

Reading from a file follows the same pattern.

import asyncio
import aiofiles

async def read_from_file():
"""Reads content from a file asynchronously."""
async with aiofiles.open('my_async_file.txt', mode='r') as f:
print("File opened, about to read...")
# .read() is also a coroutine
contents = await f.read()
print("Finished reading.")

print("\n--- File Contents ---")
print(contents)

asyncio.run(read_from_file())

πŸ› οΈ Section 3: Proving It's Non-Blocking​

How can we be sure this is actually non-blocking? Let's create an example that runs a file operation concurrently with another simple task.

import asyncio
import aiofiles
import time

async def write_large_file():
"""Simulates writing a large file by writing in a loop with sleeps."""
print("Task 1: Starting to write a large file...")
async with aiofiles.open('large_file.txt', 'w') as f:
for i in range(5):
await f.write(f"This is line {i+1}\n")
# Simulate the I/O wait time for the disk to write
await asyncio.sleep(0.5)
print("Task 1: Finished writing.")

async def counter_task():
"""A simple counter that runs in the background."""
print("Task 2: Counter started.")
count = 0
while count < 3:
await asyncio.sleep(1)
count += 1
print(f"Task 2: Counter is at {count}...")
print("Task 2: Counter finished.")

async def main():
start_time = time.time()
await asyncio.gather(
write_large_file(),
counter_task()
)
end_time = time.time()
print(f"\nBoth tasks completed in {end_time - start_time:.2f} seconds.")

asyncio.run(main())

Output:

Task 1: Starting to write a large file...
Task 2: Counter started.
Task 2: Counter is at 1...
Task 2: Counter is at 2...
Task 1: Finished writing.
Task 2: Counter is at 3...
Task 2: Counter finished.

Both tasks completed in 3.01 seconds.

This output clearly demonstrates the non-blocking nature. While write_large_file was "waiting" during its await asyncio.sleep() calls, the event loop was free to run the counter_task. The two tasks ran concurrently, interleaving their execution. If we had used standard time.sleep(), it would have blocked everything, and the total time would have been much longer.


✨ Conclusion & Key Takeaways​

While asyncio is primarily known for network operations, aiofiles provides the missing piece for creating fully asynchronous applications that also need to perform file I/O without blocking the event loop.

Let's summarize the key takeaways:

  • Standard file I/O is blocking and should be avoided in asyncio applications.
  • aiofiles provides an async alternative with an API that mirrors the built-in open() function.
  • Use async with aiofiles.open(...) as the standard pattern for safe, asynchronous file handling.
  • await the file operations: Methods like .write(), .read(), and .readlines() are coroutines and must be awaited.

➑️ Next Steps​

You now have a comprehensive understanding of how to handle the two most common forms of I/Oβ€”networking and file accessβ€”in an asynchronous, non-blocking way. In our final article of this series, we'll discuss "Deadlocks and Best Practices in Asynchronous Python" to help you avoid common pitfalls and write clean, effective async code.

Happy coding!