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...