mrkeyoor.com_
Sun 20 Sept 07:01 UTC
PyPIUtilsupdated 20 Sept 2026

aiofiles review

aiofiles 25.1.0 gives asyncio code an awaitable version of Python's familiar file interface. Calls such as read, write, seek, flush, and close still use normal blocking file IO; aiofiles moves each call to an executor so it does not occupy the event-loop thread. It also covers temporary files, standard streams, and a named subset of os and os.path functions. The current release adds Python 3.14 support, adopts uv for the project, and removes Python 3.8 support. Our installed wheel was pure Python and had no py.typed marker.

Verdict

aiofiles 25.1.0 installed in 0.2 seconds and occupied 1 MB in our sandbox, with no dependencies or audit findings, so it is a cheap fix for blocking local-file calls inside an asyncio service. Leave it out of one-time startup reads, Trio code, and projects that require a py.typed marker.

We installed it

Lab card: what happened when we installed aiofilesScreenshot of aiofiles documentation
Install✓ · 0.2s1 package on disk · 1 MB
Importimport aiofiles in 0.28s · pure Python · requires Python >=3.9
Known vulns0(pip-audit)

Answers from our run

Does aiofiles install cleanly?

Yes. In a fresh container with an empty cache, pip install aiofiles finished in 0.2s, leaving 1 package and 1 MB on disk. pip-audit reported no known vulnerabilities.

What does aiofiles need to run?

Python >=3.9, and nothing compiled: it is pure Python. In our run import aiofiles succeeded in 0.28s.

aiofiles or anyio: which should you use?

Pick anyio when file helpers must follow either asyncio or Trio through one portability layer. aiofiles 25.1.0 installed in 0.2 seconds and occupied 1 MB in our sandbox, with no dependencies or audit findings, so it is a cheap fix for blocking local-file calls inside an asyncio service.

When should you not use aiofiles?

You need true kernel asynchronous file IO or expect one file operation to become faster. The README says aiofiles delegates blocking calls to threads.

API stability5/5Version 25.1.0 still centers on `aiofiles.open()`, async context management, awaitable file methods, and async line iteration, all shaped after Python's built-in file objects. The last three published releases added Python-version coverage and small os or tempfile parity fixes without replacing that model. The concrete compatibility break is runtime support: 25.1.0 requires Python 3.9 or later, and its release notes direct Python 3.8 users to 24.1.0.
Docs3/5The README states that file operations run in an executor, identifies every delegated file method, enumerates the available os and os.path wrappers, and shows temporary-file and test-mocking examples. That is enough to use the package correctly. It gives no worked guidance for pool sizing, cancellation during a delegated call, backpressure, or typing without py.typed, so production tuning still depends on the asyncio and concurrent.futures documentation.
Maintenance4/5GitHub showed an unarchived repository pushed on August 26, 2026, with 56 open issues and pull requests. Release 25.1.0 shipped Python 3.14 support, switched project tooling to uv, and explicitly retired Python 3.8. The previous releases addressed Android imports, Windows exports, and conditional Unix functions. That work tracks current interpreter and platform behavior, although published releases arrive less often than changes in the repository.
Ecosystem4/5The package records 37,856,940 weekly downloads, and its GitHub repository has 3,255 stars. Its API fits existing asyncio code because modes, encodings, seeks, reads, writes, and context management resemble built-in files. The package also covers temporary files and selected filesystem calls. It does not cover Trio, all of pathlib, or declared inline typing, which leaves real reasons to choose AnyIO or an async path library instead.

Use it if

  • An asyncio server occasionally reads or writes local files and those blocking calls are stalling unrelated requests.
  • Your team wants async with and await while keeping method names close to built-in file objects.
  • Temporary files or a documented subset of os operations must run through the same asyncio executor pattern.
  • You can cap file concurrency or assign a separate executor when disk work becomes sustained.
Skip it if

Setup reality

We installed aiofiles 25.1.0 in a clean Python 3.12 Bookworm container. Installation took 0.2 seconds and left one package using 1 MB. It has zero direct dependencies, is pure Python, and requires Python 3.9 or later. pip-audit found zero known vulnerabilities. import aiofiles succeeded in 0.28 seconds, while our package inspection found no py.typed marker.

No service account, environment variable, or config file is involved. aiofiles.open() accepts the usual path, mode, buffering, and encoding arguments, plus optional loop and executor values. With no executor supplied, version 25.1.0 uses the event loop's default executor. A busy file workload can therefore compete with other blocking functions that the application sends to that same pool.

Every delegated operation needs await, including read, write, flush, seek, and close. An async with block is the safest ownership boundary because it closes the file when control leaves the block. Async line iteration limits memory use, though each read still crosses to a worker thread. Reading useful chunks usually creates less executor traffic than issuing many tiny reads.

aiofiles.os and aiofiles.tempfile are separate imports. The README lists the supported os calls, including stat, sendfile, replace, remove, mkdir, and selected path checks. Those wrappers keep the underlying platform rules: a cross-filesystem replace can fail, and some calls are unavailable on some systems. Release 25.1.0 supports Python 3.14 and drops 3.8; Python 3.8 applications must stay on 24.1.0.

Patterns

Read one text file read-text

import aiofiles

async def load_note(path: str) -> str:
    async with aiofiles.open(path, 'r', encoding='utf-8') as file:
        return await file.read()

Both the context manager and read are asynchronous in 25.1.0; omitting await returns a coroutine instead of file contents.

Write and flush text write-text

import aiofiles

async def save_note(path: str, text: str) -> None:
    async with aiofiles.open(path, 'w', encoding='utf-8') as file:
        await file.write(text)
        await file.flush()

`write()` and `flush()` each enter the executor. Exiting the async context closes the handle.

Filter a file line by line stream-lines

import aiofiles

async def failed_rows(path: str):
    async with aiofiles.open(path, encoding='utf-8') as file:
        async for line in file:
            if line.startswith('FAIL,'):
                yield line.rstrip('\n')

Async iteration avoids loading the complete file, but aiofiles still performs the reads on an executor thread.

Yield bounded binary chunks read-chunks

import aiofiles

async def read_blocks(path: str, block_size: int = 1024 * 1024):
    async with aiofiles.open(path, 'rb') as file:
        while block := await file.read(block_size):
            yield block

The 1 MiB value is an application choice, not a package default. Very small blocks create more executor handoffs.

Copy with bounded memory copy-binary-file

import aiofiles

async def copy_file(source: str, target: str) -> None:
    async with aiofiles.open(source, 'rb') as src:
        async with aiofiles.open(target, 'wb') as dst:
            while chunk := await src.read(1024 * 1024):
                await dst.write(chunk)

This controls memory use, although reads and writes remain blocking OS operations delegated to threads.

Read bytes from an offset read-range

import aiofiles

async def read_range(path: str, offset: int, count: int) -> bytes:
    async with aiofiles.open(path, 'rb') as file:
        await file.seek(offset)
        return await file.read(count)

Seek and read are two separately delegated calls in the documented 25.1.0 interface.

Round-trip through a temporary file temporary-file

import aiofiles.tempfile

async def stage(data: bytes) -> bytes:
    async with aiofiles.tempfile.TemporaryFile('wb+') as file:
        await file.write(data)
        await file.seek(0)
        return await file.read()

Temporary-file methods use await just like handles returned by aiofiles.open().

Create a file under a temporary directory temporary-directory

from pathlib import Path
import aiofiles
import aiofiles.tempfile

async def render_preview() -> bytes:
    async with aiofiles.tempfile.TemporaryDirectory() as folder:
        path = Path(folder) / 'preview.bin'
        async with aiofiles.open(path, 'wb') as file:
            await file.write(b'preview')
        async with aiofiles.open(path, 'rb') as file:
            return await file.read()

The directory value is an ordinary path. Calling synchronous pathlib IO on it can still block the event-loop thread.

Inspect a path without blocking the loop stat-path

import aiofiles.os

async def file_size(path: str) -> int | None:
    if not await aiofiles.os.path.isfile(path):
        return None
    result = await aiofiles.os.stat(path)
    return result.st_size

Import `aiofiles.os` explicitly. Only the os and os.path functions named in the README have wrappers.

Replace a completed file atomic-replace

import aiofiles
import aiofiles.os

async def replace_text(temp_path: str, final_path: str, text: str) -> None:
    async with aiofiles.open(temp_path, 'w', encoding='utf-8') as file:
        await file.write(text)
        await file.flush()
    await aiofiles.os.replace(temp_path, final_path)

`os.replace` keeps native filesystem semantics. It can fail when source and destination are on different filesystems.

Limit simultaneous file reads bound-file-concurrency

import asyncio
import aiofiles

async def read_one(path: str, slots: asyncio.Semaphore) -> str:
    async with slots:
        async with aiofiles.open(path, encoding='utf-8') as file:
            return await file.read()

async def read_all(paths: list[str]) -> list[str]:
    slots = asyncio.Semaphore(8)
    return await asyncio.gather(*(read_one(path, slots) for path in paths))

The limit of 8 is an example. Measure the storage and executor instead of opening an unbounded list of files.

Give file work its own thread pool use-private-executor

from concurrent.futures import ThreadPoolExecutor
import aiofiles

file_workers = ThreadPoolExecutor(max_workers=4, thread_name_prefix='file-io')

async def load_report(path: str) -> str:
    async with aiofiles.open(path, encoding='utf-8', executor=file_workers) as file:
        return await file.read()

Shut down the executor during application teardown. A private pool keeps these calls out of the loop's default executor.

Alternatives

PackageRegistryPick it when
anyioPyPIPick it when file helpers must follow either asyncio or Trio through one portability layer.
aiofilePyPIConsider it for its lower-level file API when you have checked its platform-specific implementation and fallback.
aiopathPyPIUse it when an asynchronous pathlib-style interface is more useful than a close copy of open().

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.