LiteLLM proxy on Python 3.14: an uvloop ABI break post-mortem
Why `litellm[proxy]` crashes on import with Python 3.14, the asyncio API removal that caused it, and what dependency-pinning patterns would have prevented it.
Python 3.14 shipped in October 2026, and a few weeks later issue #20933 appeared in the LiteLLM repo:
ImportError: cannot import name 'BaseDefaultEventLoopPolicy' from 'asyncio.events'
The LiteLLM proxy crashes immediately on startup. The core LiteLLM library (from litellm import completion) works fine. It’s only the proxy that fails, because the proxy depends on uvicorn[standard], which depends on uvloop, which has not yet shipped a Python 3.14-compatible release.
This is a textbook ABI/API break post-mortem. Nothing in LiteLLM is wrong. Nothing in uvicorn is wrong. Nothing in uvloop is wrong, exactly. The break is in the seam between Python’s stdlib evolution and a native extension that imports a private-ish stdlib symbol. This post walks through what broke, why none of the obvious workarounds work, and what dependency-management strategies would have prevented it.
What actually broke
uvloop is a high-performance asyncio event loop replacement implemented in Cython, on top of libuv. To install itself as the default loop policy, it subclasses asyncio.events.BaseDefaultEventLoopPolicy:
# uvloop/__init__.py (paraphrased)
from asyncio.events import BaseDefaultEventLoopPolicy as __BasePolicy
class EventLoopPolicy(__BasePolicy):
...
Python 3.14 removed BaseDefaultEventLoopPolicy from asyncio.events. The class still exists internally (as asyncio.events._BaseDefaultEventLoopPolicy or similar), but the un-underscored public-ish name is gone. uvloop’s __init__.py runs at import time, hits the missing name, and raises ImportError. The interpreter doesn’t get past loading the module.
Why did Python remove it? BaseDefaultEventLoopPolicy was undocumented. It existed in asyncio.events because the asyncio stdlib needed an internal base class for WindowsSelectorEventLoopPolicy and friends, and it leaked into the module’s public namespace. The 3.14 cleanup removed it as part of a broader event-loop policy deprecation effort (asyncio.set_event_loop_policy itself is deprecated in 3.14). uvloop reached into the stdlib’s private API and got bitten when the API changed. The Python core team is technically correct that nothing public broke; uvloop’s authors are technically correct that the symbol had been there for years and nobody warned them.
Both can be right. The user experience is still that pip install 'litellm[proxy]' on a fresh Python 3.14 install crashes immediately.
Why every workaround fails
The reporter on #20933 tried the obvious fixes. None of them work, and the reasons are interesting.
Downgrade uvloop to 0.20.x. Older uvloop versions don’t import BaseDefaultEventLoopPolicy, so they should work. But uvloop 0.20 is a Cython extension that has to compile from source on Python 3.14, and the C extension uses CPython internals (_PyInterpreterState_GetConfig, _PyDict_SetItem_KnownHash, _PyLong_AsByteArray) whose signatures changed between 3.13 and 3.14. The build fails:
error: call to undeclared function '_PyInterpreterState_GetConfig'
error: call to undeclared function '_PyDict_SetItem_KnownHash'
error: too few arguments to function call '_PyLong_AsByteArray'
So you can’t downgrade. The old version doesn’t compile and the new version doesn’t import.
Set UVICORN_LOOP_TYPE=asyncio. This should tell uvicorn to use the default asyncio loop instead of uvloop. The catch is that uvicorn imports uvloop first (to detect availability) before honoring the env var. The import fails before the env var is consulted. So the env var is silently useless.
Uninstall uvloop. Same problem in reverse — uvicorn[standard] depends on uvloop, and litellm[proxy] depends on uvicorn[standard]. Uninstalling uvloop breaks the dependency tree and pip refuses.
Use a different loop entirely. You can install plain uvicorn (no [standard]) which doesn’t pull uvloop. But LiteLLM’s proxy setup.py pins uvicorn[standard], so even if you uninstall the standard variant, reinstalling LiteLLM’s proxy puts it back. You’d have to fork LiteLLM’s dependency list.
The reporter’s documented workarounds are all dead ends. The only thing that actually works is don’t use Python 3.14. Run the proxy on 3.13 until uvloop ships a 3.14-compatible release.
What the real fix is
The fix has to come from one of three places:
1. uvloop ships a 3.14-compatible release. This is the proper fix and the one that will eventually happen. uvloop’s maintainers need to update their import to use the new (or a backward-compatible) event-loop policy API and update their CPython internal usage to match 3.14’s signatures. There’s an open issue tracking this on the uvloop side. Until it merges, every framework that depends on uvloop is stuck.
2. uvicorn makes uvloop genuinely optional and lazy. Currently, uvicorn[standard] imports uvloop at startup to determine the loop type. If the import fails, the user’s UVICORN_LOOP_TYPE=asyncio should win. The fix is to wrap the uvloop import in a try/except and fall back to asyncio gracefully. This is a one-PR change and would unblock everyone immediately, regardless of uvloop’s release schedule.
3. LiteLLM unpins uvicorn[standard] in favor of plain uvicorn. The [standard] extras pull a few performance dependencies (uvloop, websockets, httptools, watchdog) that aren’t strictly required for the proxy to function. Plain uvicorn works on Python 3.14 today. The cost is a small performance regression on the event loop — asyncio’s default loop is slower than uvloop. For most LiteLLM proxy workloads (which are I/O-bound waiting on upstream model providers), this is invisible. For latency-sensitive setups it’s a meaningful regression.
The right answer is probably “all three.” The first two are upstream fixes; the third is a LiteLLM-side mitigation that doesn’t require waiting for upstream.
What teams do today
Workarounds in the wild:
- Pin Python to 3.13. The most common answer. Run your container base image on
python:3.13-slim. This works fine and is what most production deployments are doing regardless, because Python 3.14 is too new for many other ecosystem dependencies. - Run the LiteLLM SDK on 3.14, but the proxy on 3.13. The core SDK has no uvloop dependency and works fine on 3.14. If you’re running the SDK directly (not via the proxy), this is a non-issue. The bug only bites if you specifically need the proxy server.
- Build your own uvicorn from a fork. Some teams have published
uvicornbuilds with the uvloop fallback fix backported. This works but adds a maintenance burden you didn’t want. - Wait. uvloop will eventually release. The waiting period is days to months, depending on how active uvloop’s maintenance is. As of writing, the relevant uvloop PR is in review.
The broader lesson
This kind of break is structural. Any Python project with native extensions that touch CPython internals will eventually hit a Python release that breaks the build. The Python core team’s API stability guarantees are about documented APIs, and most native extensions reach into undocumented territory for performance reasons.
For projects like LiteLLM that ship a server, the defenses are:
- Build matrix testing in CI. Run the test suite against every Python version you support, including the upcoming release as soon as alphas are available. LiteLLM does this for the SDK (Python 3.8–3.13) but the proxy variant doesn’t have a 3.14 entry yet, which is why this issue was found by a user instead of by CI.
- Soft dependencies for performance extensions. Anything that’s “the fast version of X” should be importable optionally, with a fallback. uvloop should not be a hard dependency of uvicorn — it should be a runtime optimization that’s used when present. The fact that it’s listed in
[standard]extras doesn’t help when those extras are themselves hard requirements of downstream packages. - Document the supported Python range. Users should know that LiteLLM proxy supports Python 3.x through 3.y, and 3.z is in beta. Right now, the supported versions are listed for the SDK but not separately for the proxy, which is why a user reasonably tried 3.14 and got the crash.
For users, the defense is the boring one: don’t deploy on a Python version newer than your dependencies have officially tested against. Python release-day excitement is fun; Python release-day production deployment is a way to find bugs in everyone else’s code.
References
- Upstream issue: #20933
- Python 3.14 What’s New (asyncio changes): docs.python.org
- uvloop repository: github.com/MagicStack/uvloop
- uvicorn loop selection: www.uvicorn.org/settings/#implementation