Async Python File IO Using Uring

meitham1 pts0 comments

GitHub - meitham/urio: Async file I/O for Python that is actually asynchronous · GitHub

/" data-turbo-transient="true" />

Skip to content

You signed in with another tab or window. Reload to refresh your session.<br>You signed out in another tab or window. Reload to refresh your session.<br>You switched accounts on another tab or window. Reload to refresh your session.

Dismiss alert

{{ message }}

meitham

urio

Public

Notifications<br>You must be signed in to change notification settings

Fork

Star

main

BranchesTags

Go to file

CodeOpen more actions menu

Folders and files<br>NameNameLast commit message<br>Last commit date<br>Latest commit

History<br>10 Commits<br>10 Commits

.github/workflows

.github/workflows

benchmarks

benchmarks

docs

docs

python/urio

python/urio

scripts

scripts

src

src

tests

tests

.gitignore

.gitignore

.readthedocs.yaml

.readthedocs.yaml

ARCHITECTURE.md

ARCHITECTURE.md

Cargo.lock

Cargo.lock

Cargo.toml

Cargo.toml

LICENSE

LICENSE

README.md

README.md

mkdocs.yml

mkdocs.yml

pyproject.toml

pyproject.toml

View all files

Repository files navigation

urio

Async file I/O for Python that is actually asynchronous.

On Linux, urio submits reads and writes through<br>io_uring and resolves completions on<br>the event loop, with no thread parked per operation. On Windows it can do the<br>same through overlapped I/O and an I/O completion port (opt-in, see below).<br>Everywhere else — macOS, older kernels, sandboxes that block the syscalls — it<br>falls back to a thread pool behind the same API, so one code path works on<br>every platform.

The API follows aiofiles, which<br>pioneered ergonomic async file access for asyncio by running the blocking<br>syscalls in a thread pool — the only portable option at the time, and still<br>the right fallback. urio keeps that familiar interface (which in turn mirrors<br>the builtin open()), so switching is mostly changing the import.

What you get

Real async reads and writes: io_uring on Linux, IOCP on Windows.

The familiar file API: read, readline, readlines, write,<br>writelines, seek, tell, truncate, flush, async iteration, text and<br>binary modes.

An async pathlib.Path: urio.Path with async stat, mkdir, unlink,<br>rename, symlink_to, read_text/write_text/read_bytes/write_bytes,<br>iterdir, glob, walk, and the full set of pure-path helpers.

A thread-pool fallback wherever native async I/O is unavailable, same API.

Runs GIL-free on free-threaded CPython (3.14t and later).

Requirements

Python 3.12+, any OS.

Linux 5.6+ for the io_uring backend.

Windows 10/11 for the IOCP backend (opt-in via URIO_WINDOWS_IOCP=1;<br>Windows otherwise uses the thread backend).

Anything else — macOS, older kernels, locked-down sandboxes — runs the<br>thread backend automatically.

Installation

pip install urio

Prebuilt abi3 wheels (one per platform covers every CPython ≥ 3.12):

Platform<br>Arch<br>Backend

Linux (manylinux + musllinux)<br>x86_64, aarch64, armv7<br>io_uring

Windows<br>x64, x86<br>thread pool by default, IOCP opt-in

macOS<br>x86_64, arm64<br>thread pool

Building from source needs a Rust toolchain and<br>maturin; the crate compiles on every OS:

pip install -e ".[dev]"<br>maturin develop

Usage

import asyncio<br>import urio

async def main():<br># Files — binary and text modes, just like the builtin open().<br>async with urio.open('greeting.txt', 'w') as f:<br>await f.write('hello\nworld\n')

async with urio.open('greeting.txt') as f:<br>async for line in f:<br>print(line.rstrip())

# Async pathlib.<br>p = urio.Path('greeting.txt')<br>print(await p.exists())<br>print((await p.stat()).st_size)<br>print(await p.read_text())

asyncio.run(main())

Async pathlib

urio.Path is an async pathlib.Path. Pure-path operations (/ joining,<br>name, suffix, parent, with_suffix, …) are ordinary synchronous<br>properties; everything that touches the filesystem is async and goes through<br>the active backend:

base = urio.Path('/tmp/project')<br>await base.mkdir(parents=True, exist_ok=True)

cfg = base / 'config.toml'<br>await cfg.write_text('name = "urio"\n')<br>print(await cfg.read_text())<br>print(await cfg.exists(), await cfg.is_file(), (await cfg.stat()).st_size)

async for child in base.iterdir():<br>print(child.name)

On Linux, stat/mkdir/unlink/rename/symlink_to and the read/write<br>helpers are genuine io_uring submissions; directory listing and metadata tweaks<br>use the thread pool.

Choosing a backend

urio picks the fastest available backend at runtime. You can force one:

urio.set_backend('auto') # default<br>urio.set_backend('thread') # force the thread pool<br>urio.set_backend('uring') # require io_uring (raises if unavailable)<br>urio.set_backend('iocp') # require IOCP (Windows, needs URIO_WINDOWS_IOCP=1)

or via the environment: URIO_BACKEND=thread python app.py. On Windows,<br>setting URIO_WINDOWS_IOCP=1 is enough on its own — auto-detection then<br>selects IOCP.

How it works

On Linux, a single ring is created per event loop. Its io_uring instance is<br>registered with an eventfd, and that fd is handed to loop.add_reader, so the<br>loop wakes whenever completions are ready. Each...

urio async thread await path backend

Related Articles