Async File I/O in Python with aiofiles
Learn how to perform non-blocking file operations in Python using aiofiles. This guide covers installation, real-world examples for concurrent file processing, logging, and copies, plus performance tips to keep your async code truly non-blocking.
Mastering Asynchronous File I/O in Python with aiofiles
When I first started working with asynchronous Python, I quickly realized that not everything plays nicely with async and await. Network requests? No problem. Database queries? Absolutely. But file operations? That's where things get tricky. The standard open() function blocks your entire event loop, which defeats the purpose of going async in the first place.
That's where aiofiles comes in. Let me show you how to handle file operations without blocking your event loop.
Why Regular File I/O Is a Problem in Async Code
Imagine you're building a web scraper that needs to save thousands of HTML files. With regular synchronous file operations, each write blocks your entire application. Even if you're using asyncio for network requests, the moment you try to save a file, everything pauses.
Here's what happens under the hood: - Standard file operations use the operating system's blocking I/O - The event loop can't do anything else while waiting for disk operations - Your carefully crafted async code becomes synchronous at the file level
Getting Started with aiofiles
First, install the library:
pip install aiofiles
Now you can use it just like open(), but with await:
import asyncio
import aiofiles
async def read_file_async():
async with aiofiles.open('data.txt', mode='r') as f:
contents = await f.read()
return contents
async def write_file_async():
async with aiofiles.open('output.txt', mode='w') as f:
await f.write('Hello from PythonSkillset!')
Real-World Examples
Example 1: Processing Multiple Files Concurrently
Let's say you need to read several configuration files at startup. Instead of reading them one by one, you can read them all at once:
import asyncio
import aiofiles
async def read_config(filename):
async with aiofiles.open(filename, 'r') as f:
content = await f.read()
return {filename: content}
async def load_all_configs():
files = ['config1.json', 'config2.json', 'config3.json']
tasks = [read_config(f) for f in files]
results = await asyncio.gather(*tasks)
return results
# Run it
configs = asyncio.run(load_all_configs())
Example 2: Logging Without Blocking
A common scenario at PythonSkillset is handling high-volume logging. Here's how you'd implement non-blocking logging:
import asyncio
import aiofiles
from datetime import datetime
class AsyncLogger:
def __init__(self, logfile='app.log'):
self.logfile = logfile
async def log(self, message):
timestamp = datetime.now().isoformat()
log_entry = f"[{timestamp}] {message}\n"
async with aiofiles.open(self.logfile, mode='a') as f:
await f.write(log_entry)
async def read_logs(self):
async with aiofiles.open(self.logfile, mode='r') as f:
return await f.readlines()
# Usage in an async context
logger = AsyncLogger()
await logger.log("User logged in successfully")
await logger.log("Data processing started")
Example 3: File Copy Made Efficient
Need to copy large files without freezing your application? Here's how:
import asyncio
import aiofiles
async def copy_file(source, destination, chunk_size=8192):
async with aiofiles.open(source, mode='rb') as src:
async with aiofiles.open(destination, mode='wb') as dst:
while True:
chunk = await src.read(chunk_size)
if not chunk:
break
await dst.write(chunk)
async def copy_multiple_files(file_pairs):
tasks = [copy_file(src, dst) for src, dst in file_pairs]
await asyncio.gather(*tasks)
Working with Different File Modes
aiofiles supports all the modes you're used to:
# Binary mode for images or zip files
async with aiofiles.open('image.png', mode='rb') as f:
data = await f.read()
# Appending to existing files
async with aiofiles.open('server.log', mode='a') as f:
await f.write('New log entry\n')
# Reading and writing in the same context
async with aiofiles.open('editor.txt', mode='r+') as f:
content = await f.read()
await f.seek(0)
await f.write(content.replace('old', 'new'))
Performance Tips from PythonSkillset
After working with aiofiles on several projects, here's what I've learned:
-
Don't make every file operation async - If you're reading small files (under a few KB), the overhead of async might not be worth it. Use sync for small, quick operations.
-
Batch your writes - Instead of writing to the same file 1000 times, buffer the content and write it all at once:
async def batch_write(lines, filename):
buffer = "\n".join(lines)
async with aiofiles.open(filename, mode='w') as f:
await f.write(buffer)
-
Use context managers always - Forgetting
async withmeans your file won't close properly, leading to resource leaks. -
Consider using aiofiles for temp files too - Temporary file operations benefit from async just as much as permanent ones.
When Not to Use aiofiles
Let's be honest: aiofiles isn't always the answer. Here's when you should stick with regular files:
- Reading or writing single configuration files at startup
- Processing files only once during script execution
- Working with very small files (under 100 bytes)
- Simple scripts where async isn't used anywhere else
The Bottom Line
aiofiles fills an important gap in Python's async ecosystem. It lets you handle file operations the same way you handle network requests - without blocking your event loop. When you're building high-performance async applications, especially ones that process many files simultaneously, this library is indispensable.
Remember, the goal isn't to make everything async. It's to make sure that when you do use async, nothing blocks your event loop. And aiofiles does exactly that for file operations.
Comments
Questions, corrections, and tips stay visible for everyone reading this page.
Join the discussion
No comments yet
Be the first to leave a note — it helps the next reader.