Disclaimer: this is not a well-thought-out proposal. This is me thinking out loud after staring at a dependency problem that affects me personally as both a library maintainer and a consumer of similar libraries and feeling unhappy with every solution I’ve come up with so far.
Let me start with the use case:
- I have an API wrapper.
- It doesn’t rely on anything beyond the usual, universally common HTTP client features.
- It offers both
FooClientandAsyncFooClient. - They share everything except the actual HTTP client implementation.
Example:
@dataclass
class Movie:
title: str
director: str
year: int
genre: str
rating: float
@dataclass
class TVShow:
title: str
creator: str
seasons: int
genre: str
rating: float
class FooClient:
def __init__(self, client: SomeHTTPClient | None = None) -> None:
self.client = client or SomeHTTPClient()
def get_movie(self, title: str) -> Movie:
r = self.client.get(f"https://api.foo.com/movies/{title}")
# Imagine there's code here
return Movie(...)
def get_tv_show(self, title: str) -> TVShow:
r = self.client.get(f"https://api.foo.com/tvshows/{title}")
# And here
return TVShow(...)
class AsyncFooClient:
def __init__(self, client: SomeAsyncHTTPClient | None = None) -> None:
self.client = client or SomeAsyncHTTPClient()
async def get_movie(self, title: str) -> Movie:
r = await self.client.get(f"https://api.foo.com/movies/{title}")
# Imagine there's code here, but this time it's async
return Movie(...)
async def get_tv_show(self, title: str) -> TVShow:
r = await self.client.get(f"https://api.foo.com/tvshows/{title}")
# And here
return TVShow(...)
Now the problem:
Realistically, a user will only ever need one of these, either sync or async. That leaves me with these options:
-
Unconditionally depend on both a sync and an async HTTP library.
This means I’m forcing two arbitrary HTTP clients on users. If they already use something else in their app, congrats, they now have three HTTP libraries in their dependency tree for absolutely no good reason. -
Pick a favorite (sync or async), depend on it by default, and throw the other one in extras.
If you use the “favored” one, you save a dependency. If you don’t, you’re right back to option 1. -
Move both to extras.[1]
Technically valid, but this basically ships a library that’s broken by default and relies on docs and error messages to tell users how to make it work. -
Split into three packages: core + sync + async.
This still forces at least one HTTP library of my choice on downstream users, just with more packaging and more maintenance overhead.
I honestly dislike every single one of these.
So far, I’ve been using httpx purely because it gives me both sync and async in one dependency, which is the least terrible version of option 1. But even then, if you’re using requests in your app and want to depend on my library, you now have two HTTP libraries in your environment for no real reason.
Now, what actually prompted this post is that httpx’s long-term future feels a bit uncertain to me[2][3][4], so I’m back to rethinking which flavor of bad decision I’m going to lock into if I have to switch HTTP clients again.
Sometimes, I really wish the stdlib just defined something like HTTPClientProtocol and AsyncHTTPClientProtocol so third-party HTTP libraries could interoperate properly. My wrapper could just accept “anything that satisfies HTTPClientProtocol” and be done with it. We have standardized similar boundaries before (e.g., WSGI in PEP 333 and the DB-API in PEP 249), so the idea itself isn’t totally unprecedented, but I honestly don’t know what combination of technical and social factors made those succeed, or whether that would translate cleanly to HTTP clients today.
After all, HTTP itself is standardized, and most Python HTTP clients already expose similar APIs. requests, httpx, and niquests are almost swappable in practice, with aiohttp being the biggest outlier.
Even in that world, though, I’d still want to ship one default HTTP library so the package works out of the box, ideally something the user can opt out of entirely if they’re passing their own client.
So what are people doing in such a scenario? Or is this just not a common enough problem?
This can become a viable alternative if PEP 771 - Default Extras gets accepted. Personally, I’m very excited for that PEP, and I really hope it gets accepted. ↩︎
https://github.com/encode/httpx/discussions/3344#discussioncomment-14170874 ↩︎
I get that
httpxis a FOSS project maintained by humans with limited time. This is not a complaint about the maintainers. ↩︎