Skip to content

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_asyncio mixins.
  • 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.