Python - iroh
| Platform | Architectures |
|---|---|
| macOS | arm64, x86_64 |
| Linux | x86_64, aarch64 (glibc) |
| Windows | x86_64 |
- API reference: Python API docs
- Example app: hello-iroh-ffi, a console reader for the cross-platform dot demo
- All languages: platform support matrix
The Python bindings to iroh are published as prebuilt wheels on PyPI and shipped from iroh-ffi. The bindings are generated by uniffi-rs, so every class, method, and enum carries a docstring you can introspect from a REPL.
Install
Prebuilt wheels are published for:
- Linux:
x86_64andaarch64(manylinux 2_28) - macOS:
arm64andx86_64 - Windows:
amd64
Register your event loop
Before calling any other iroh API, hand your asyncio event loop to the bindings:
async def main():
iroh.iroh_ffi.uniffi_set_event_loop(asyncio.get_running_loop())
# ... the rest of your program
Here is why. iroh’s Rust runtime runs on its own background threads. When an operation completes (a connection arrives, a stream read finishes), one of those threads schedules the wakeup back onto your Python event loop. A Rust thread has no running asyncio loop of its own, so the bindings cannot discover yours: uniffi_set_event_loop tells them which loop to use. Without it, the bindings fall back to asyncio.get_running_loop(), which raises RuntimeError: no running event loop when called from a Rust thread. In practice this surfaces as that exact error, or as an await that never completes.
The awkward name comes from uniffi, the tool that generates the bindings, which is also why it lives under iroh.iroh_ffi rather than on the iroh module itself.
Two rules of thumb:
- Call it from inside a running coroutine, typically as the first line of the function you pass to
asyncio.run(). It cannot go at module level, becauseasyncio.get_running_loop()only works while a loop is running. - Register once per event loop. If your program creates more than one loop (repeated
asyncio.run()calls, or pytest-asyncio’s per-test loops), register each new loop before using iroh on it. In a test suite, an autouse fixture handles this:
# conftest.py
import asyncio
import pytest
import iroh
@pytest.fixture(autouse=True)
async def _uniffi_event_loop():
iroh.iroh_ffi.uniffi_set_event_loop(asyncio.get_running_loop())
yield
Hello, iroh
import asyncio
import iroh
ALPN = b"hello-iroh/0"
async def main():
iroh.iroh_ffi.uniffi_set_event_loop(asyncio.get_running_loop())
ep = await iroh.Endpoint.bind(
iroh.EndpointOptions(preset=iroh.preset_n0(), alpns=[ALPN])
)
print("endpoint id:", ep.id())
asyncio.run(main())
This binds an endpoint with the n0 preset, advertises a single ALPN, and prints the endpoint id once binding completes.
A two-peer echo
The main.py example in the iroh-ffi repo runs a sender/receiver pair over QUIC:
# Terminal 1
python main.py serve
# Terminal 2: paste the ticket printed from Terminal 1
python main.py connect <ticket>
The client opens a bi-directional stream, sends hello, and prints what the other peer echoes back.