DEV Community

Aviad Rozenhek
Aviad Rozenhek

Posted on

Stop writing platform checks around uvloop

If you write asyncio code that has to run on both Windows and Linux, you've probably written this:

import sys

if sys.platform == "win32":
    import winloop as fastloop
else:
    import uvloop as fastloop

fastloop.run(main())
Enter fullscreen mode Exit fullscreen mode

It's four lines. It works. Nobody thinks about it the first time.

But it doesn't stay four lines, and it doesn't stay in one place.

This post is about that branch: why it keeps showing up, what it usually leaves out, and winuvloop, the small package I wrote so it lives in one place.

TL;DR

  • uvloop isn't the Windows backend. winloop is. Code that runs on both ends up picking one at runtime, at every entry point.
  • The hand-written branch usually leaves out install markers, a readable error on PyPy, the 3.16 policy change, and a way to log which loop you got.
  • winuvloop is that branch plus those details, packaged once. It doesn't make anything faster. uvloop and winloop do.
  • If you only target one OS, you don't need it.

Why the branch exists

uvloop is the standard high-performance asyncio event loop on Linux and macOS. It isn't the Windows backend, so import uvloop fails on Windows.

winloop fills that gap. It provides a uvloop-compatible API for Windows.

So a project whose developers are on Windows laptops and whose production runs on Linux needs both, and it has to pick one at runtime. That's the setup I kept running into: developing on Windows, deploying to Linux.

The upstream projects publish their own numbers for why you'd bother. uvloop reports making asyncio 2-4x faster on its echo-server benchmarks. winloop reports this TCP connection benchmark:

Event loop policy (winloop's benchmark) Time
WinLoopPolicy 0.493s
WindowsProactorEventLoopPolicy 2.510s
WindowsSelectorEventLoopPolicy 2.723s

Those are upstream figures for specific workloads, not a promise for your app. Measure your own.

What the hand-written version leaves out

The if sys.platform branch handles the import. A real cross-platform setup needs a few more things.

1. Dependency markers. You don't want pip installing uvloop on Windows or winloop on Linux, so your pyproject.toml needs environment markers:

dependencies = [
  "uvloop; sys_platform != 'win32' and platform_python_implementation == 'CPython'",
  "winloop; sys_platform == 'win32' and platform_python_implementation == 'CPython'",
]
Enter fullscreen mode Exit fullscreen mode

The platform_python_implementation part is there because both backends are native CPython extensions. Without it, installing on PyPy tries to pull in a backend that doesn't support PyPy.

2. Every entry point. The branch gets copied into the CLI, the worker, the benchmark script, the README example, and the test helper. In my own projects I found near-identical copies of it in several places. Each copy is one more place for things to drift.

3. A useful error. When the backend isn't installed (on PyPy, say, or when someone installed with --no-deps), you get a bare ModuleNotFoundError: No module named 'uvloop'. That tells the reader what failed and nothing about why.

4. The policy API is going away. A lot of code calls uvloop.install() or asyncio.set_event_loop_policy(...) so a framework picks up the loop. asyncio deprecated its event-loop policy system in Python 3.14 and will remove it in 3.16. The replacement is asyncio.run(..., loop_factory=...). Hand-written branches that call install() will need to change.

5. Knowing which loop you actually got. When someone files a bug, "which event loop were you on, and which version?" should take one line to answer.

None of this is hard. It's just easy to get slightly wrong in five places.

What winuvloop does

winuvloop is that branch plus those details, packaged once.

The runtime module is small: platform detection, a clear error, re-exports, and typing stubs. It doesn't vendor or patch either loop.

import winuvloop


async def main() -> int:
    ...
    return 0


raise SystemExit(winuvloop.run(main()))
Enter fullscreen mode Exit fullscreen mode

On Windows that runs on winloop. On Linux, macOS, and other POSIX systems it runs on uvloop.

What's inside:

  • Dependency markers. winuvloop's own dependencies are the markers shown above: uvloop on non-Windows CPython, winloop on Windows CPython. pip install winuvloop or uv add winuvloop pulls in only the backend for the current platform.
  • Direct aliases, not wrappers. winuvloop.run, winuvloop.new_event_loop and winuvloop.Loop are the backend's own objects, so on Linux winuvloop.Loop is uvloop.Loop. There's no wrapper layer to debug. Any other attribute is delegated to the selected backend module.
  • A versioned policy API. install() and EventLoopPolicy still work for code that needs a global policy. For example, the README shows launching Uvicorn with loop="asyncio" after winuvloop.install(). Both come straight from the backend. Since 0.2.5, winuvloop lists them as public only on Python < 3.16 and avoids loading them at import time on 3.14.
  • A real error on unsupported interpreters. If the backend is missing, the import error names the selected backend, the platform, and the Python implementation, and says the backends target CPython. On PyPy you get told why instead of just what.
  • Diagnostics. backend_name() and backend_version(), shown below.
  • Typing. It ships py.typed and stubs for the public API.
  • CI on all three OSes. Linux, macOS and Windows, with unit tests that mock both backends and smoke tests that call winuvloop.run() on the real one.

The diagnostics take one line each:

import winuvloop

print(winuvloop.backend_name())     # "uvloop" or "winloop"
print(winuvloop.backend_version())
Enter fullscreen mode Exit fullscreen mode

They also make cross-platform tests simpler. Instead of branching on the OS all over your test suite, you assert the backend:

import sys

import winuvloop


def test_optimized_backend_is_available() -> None:
    expected = "winloop" if sys.platform == "win32" else "uvloop"
    assert winuvloop.backend_name() == expected
    assert winuvloop.backend_version() is not None
Enter fullscreen mode Exit fullscreen mode

For frameworks that create their own loop, install before the framework starts:

# serve.py
import uvicorn

import winuvloop

winuvloop.install()

uvicorn.run("myapp:app", host="127.0.0.1", port=8000, loop="asyncio")
Enter fullscreen mode Exit fullscreen mode

When not to use it

I'd skip winuvloop in these cases:

  • You only target one OS. Use uvloop (Linux/macOS) or winloop (Windows) directly. A selector buys you nothing.
  • You need backend-specific APIs and don't want a layer in between.
  • You're debugging an event-loop bug. Take out every wrapper, winuvloop included, and import the backend directly. If the bug still reproduces, it belongs upstream (uvloop issues, winloop issues), not in winuvloop.
  • You run on PyPy. Neither backend supports it. winuvloop only fails more clearly.
  • Your deployment platform already configures the loop. Use the platform's own setting.

One more honest caveat: most of winuvloop's maturity is really uvloop's and winloop's. It's marked Beta on PyPI. The selector itself is small, but runtime behavior is entirely the backend's.

Is anyone using it?

According to pypistats.org, winuvloop had 6,837 downloads in the last month (as of 2026-09-28).

Download counts include CI and mirrors, so I read that as "some projects depend on it", not as a user count. I haven't promoted it anywhere until now, so that usage is organic.

Bottom line

winuvloop doesn't make anything faster. The speed comes from uvloop and winloop.

What it removes is a small branch that gets copied into every entry point, plus the details that usually get left out of it: install markers, a readable error on PyPy, the 3.16 policy change, and a way to log which loop you're on.

If your asyncio code only runs on one OS, you don't need it. If it runs on both, give it a try:

pip install winuvloop
Enter fullscreen mode Exit fullscreen mode

Source, examples and issues: github.com/aviadr1/winuvloop. Happy to hear feedback in the issues. I'd especially like to hear from anyone running winloop in production on Windows.

Top comments (0)