Compatibility¶
Compatibility is checked at four levels:
- Every change runs zuvloop's full suite on Linux and macOS and its portable
suite on Windows. Linux also runs musl runtime tests and a subprocess-isolated
selection of CPython's own
test_asynciomixins. - Every pull request runs pinned upstream coverage from aiohttp, uvicorn, AnyIO, websockets, aioquic, Tornado and HTTPX2 with zuvloop installed as the asyncio loop. The upstream suites are used broadly where they test public behavior; a small public client/server smoke supplements websockets tests that otherwise inspect CPython's private server attributes.
- Every pull request builds with native safety checks and the C sanitizer, then repeats the loop lifecycle and resource-ownership scenario 500 times.
- The compatibility job builds and tests the declared floor (CPython 3.14.0), the newest CPython 3.14 patch, and standard and free-threaded CPython 3.15. It also adds service-backed smoke tests for gRPC AsyncIO, asyncpg, Psycopg and redis-py.
The pinned commits and exact commands live in
.github/workflows/compatibility.yml. Pinning makes a new upstream release an
explicit review instead of allowing an unrelated moving target to turn the signal
red. Stable aggregate checks sit above each matrix and workflow, so branch
protection does not need to know when a new operating system or upstream project
is added; it requires the aggregate and the aggregate requires every child job.
zuvloop's smaller in-repository aiohttp, anyio and uvicorn integration tests still
run on every change too.
Tests that assert the identity of the platform's stock loop rather than an asyncio
behavior, and the intentional timer difference below, remain in the run as strict
expected failures. Their exact node ids and reasons are registered in
scripts/upstream/zuvloop_upstream_expectations.py; an unexpected pass is a
failure too, so this list cannot quietly go stale.
aiohttp's optional blockbuster plugin is disabled for this run because it
exempts blocking calls by stdlib asyncio source filename and therefore reports
the equivalent os.stat and os.sendfile calls from every third-party loop.
One concurrent WebSocket-close test is skipped because it assumes selector-loop
ready/I/O ordering and fails intermittently on uvloop too; every other aiohttp
test remains enforced.
For reference, uvloop cannot complete that suite: it fails fifteen tests in
test_client_functional.py and then hangs.
Where each loop diverges from asyncio¶
These were measured, not asserted — each is a case where one loop disagrees with the standard library.
| Behaviour | asyncio | uvloop | zuvloop |
|---|---|---|---|
isinstance(t, asyncio.Transport) |
True | False | True |
isinstance(t, asyncio.DatagramTransport) |
True | False | True |
isinstance(t, asyncio.ReadTransport) for a read pipe |
True | False | True |
get_extra_info("socket") |
TransportSocket |
PseudoSocket |
TransportSocket |
loop.time() equals time.monotonic() |
yes | yes | yes |
getaddrinfo("fe80::1%lo0") keeps the zone |
yes | no | yes |
connect_read_pipe on a regular file |
ValueError |
accepted | ValueError |
The isinstance rows are not pedantry. aiohttp's test suite asserts
isinstance(transport, asyncio.Transport) inside connection_made; a loop that
fails it leaves the protocol half-initialised and the connection open with
nobody to answer on it.
The getaddrinfo row is the sharpest: uvloop's literal shortcut drops the IPv6
zone index, so fe80::1%lo0 resolves to scope 0 — the wrong interface. Across
2430 combinations of host, port, family, type and flags, zuvloop disagrees with
socket.getaddrinfo on 81 and uvloop on 303.
Known differences¶
Patching loop.time() does not move the scheduler. asyncio runs its timers
off self.time(), so replacing that method fast-forwards the loop — a trick test
suites use to expire timeouts without waiting. zuvloop keeps the timer heap in Zig
and reads the clock directly, so a patched time() changes what loop.time()
returns and nothing else.
Making the scheduler consult Python on every timer operation would cost more than the compatibility is worth. Code that needs a controllable clock should schedule against one explicitly.
host="" is not treated as NULL. socket.getaddrinfo resolves the empty
string as an unspecified host; zuvloop raises OSError. This is the whole of its
81-case disagreement above.
Interpreter and platform policy¶
Standard and free-threaded CPython 3.14 and 3.15 are supported on Linux, macOS and Windows. The minimum 3.14.0 release and both variants of 3.15 are continuously exercised rather than inferred from the newest local interpreter.
Linux glibc, Linux musl and macOS execute the full in-repository suite. Windows
builds run the portable suite natively; tests that require
Unix-domain sockets, POSIX signal semantics, POSIX pipe descriptors or POSIX
subprocess expectations are explicitly skipped.
Windows does not provide Unix-domain socket methods or SO_REUSEPORT through
zuvloop. Published wheels cover Linux x86-64/AArch64, macOS x86-64/arm64 and
Windows AMD64/ARM64.