Skip to main content
PlatformArchitectures
macOSarm64, x86_64
Linuxx86_64, aarch64 (glibc)
Windowsx86_64
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

pip install iroh
Prebuilt wheels are published for:
  • Linux: x86_64 and aarch64 (manylinux 2_28)
  • macOS: arm64 and x86_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:
  1. 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, because asyncio.get_running_loop() only works while a loop is running.
  2. 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.

Next steps

Connect two endpoints

Conceptual walkthrough of endpoints, tickets, and ALPNs. The code samples are Rust, but every API maps 1:1 to Python.

hello-iroh-ffi example app

A headless Python peer for the cross-platform dot demo. It speaks the same wire format as the Swift and Kotlin apps and prints received positions to the console.

Python API reference

Full class, method, and enum reference generated from the uniffi-rs bindings.

iroh-ffi on GitHub

Source, examples, and issue tracker for the Python (and other-language) bindings.