-
Notifications
You must be signed in to change notification settings - Fork 46
Add a migration guide from httpx #1078
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 6 commits
0564d23
dc08d69
a8388d0
7e30fcc
8734942
5db6f6d
7248191
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -257,3 +257,7 @@ what gets sent over the wire.* | |
| - receive_json | ||
| - ping | ||
| - close | ||
|
|
||
| ## `alias_httpx` | ||
|
|
||
| ::: httpx2.alias_httpx | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,230 @@ | ||
| # Migrating from HTTPX | ||
|
|
||
| So, you have an application (or a library) using `httpx`, and you want to move to `httpx2`. | ||
|
|
||
| Good news: this is probably the easiest migration you will do this year. | ||
|
|
||
| `httpx2` is a fork of `httpx 0.28.1`. It has the same public API. The same `Client`, the same `AsyncClient`, the same `Response`, the same everything. The main thing that changed is the name. | ||
|
|
||
| Let's see how to do it, step by step. | ||
|
|
||
| ## In a Hurry? | ||
|
|
||
| If your application depends on `httpx` directly, the whole migration is: | ||
|
|
||
| * Replace `httpx` with `httpx2` in your dependencies. | ||
| * Replace `import httpx` with `import httpx2` in your code. | ||
|
|
||
| ```python | ||
| import httpx2 | ||
|
|
||
| r = httpx2.get("https://www.example.org/") | ||
| ``` | ||
|
|
||
| That's it. Everything else works the same. | ||
|
|
||
| If you want an even smaller diff, you can alias the import and keep the rest of your code untouched: | ||
|
|
||
| ```python | ||
| import httpx2 as httpx | ||
|
|
||
| r = httpx.get("https://www.example.org/") | ||
| ``` | ||
|
|
||
| Both styles are fine. The alias style is great for a first migration pass in a big codebase, and you can rename properly later (or never, we won't judge). | ||
|
|
||
| ## You Can Have Both Installed | ||
|
|
||
| This is the part that matters if you have a **huge codebase**, or dependencies you don't control. | ||
|
|
||
| `httpx` and `httpx2` are different packages, with different import names. They don't conflict. They can be installed in the same environment, at the same time, and each one works normally. | ||
|
|
||
| This means: | ||
|
|
||
| * Your transitive dependencies that still require `httpx` keep working, unchanged. | ||
| * You don't have to wait for every library in your dependency tree to migrate before you do. | ||
| * You can migrate your own code **incrementally**, one module at a time, one pull request at a time. There is no flag day. | ||
|
|
||
| !!! tip | ||
| Don't try to migrate a large application in one giant pull request. Add `httpx2` next to `httpx`, migrate your own modules gradually, and remove the `httpx` pin once nothing of yours (and none of your dependencies) imports it anymore. | ||
|
|
||
| To find what still uses `httpx` in your environment: | ||
|
|
||
| ```shell | ||
| grep -rn "import httpx\b" src/ | ||
| ``` | ||
|
|
||
| And to see which installed packages still depend on it: | ||
|
|
||
| ```shell | ||
| pip install pipdeptree | ||
| pipdeptree --reverse --packages httpx | ||
| ``` | ||
|
|
||
| ### But objects don't cross the boundary | ||
|
|
||
| There is one rule to keep in mind: the two packages can live side by side, but their objects can't stand in for each other. | ||
|
|
||
| `httpx2.Client` and `httpx.Client` are **distinct classes**. Same for `Response`, `Request`, `Timeout`, all of it. If a library that is still on `httpx` accepts a client and checks `isinstance(client, httpx.Client)` internally - or is simply written against `httpx` types - passing it an `httpx2.Client` will fail. The same goes for exceptions: an `httpx.HTTPError` raised inside a still-on-`httpx` dependency will not be caught by `except httpx2.HTTPError`. | ||
|
|
||
| So, the rule of thumb: **the package that receives the object decides which module you create it from.** | ||
|
|
||
| ```python | ||
| import httpx | ||
| import httpx2 | ||
| import some_library_still_on_httpx | ||
|
|
||
| # Your own code: httpx2. | ||
| client = httpx2.AsyncClient() | ||
|
|
||
| # A dependency that expects httpx: give it httpx. | ||
| some_library_still_on_httpx.configure(http_client=httpx.AsyncClient()) | ||
| ``` | ||
|
|
||
| This is exactly why incremental migration works: your code moves to `httpx2`, while each dependency keeps receiving the types it expects, until it migrates too. | ||
|
|
||
| And to be clear: this boundary is not a permanent feature of your life. It only exists while both packages are in your environment. Every dependency that moves to `httpx2` removes a piece of it - the [For Package Maintainers](#for-package-maintainers) section below gives maintainers two proven, low-effort paths to do it, so point them there (or send them the pull request yourself, it is usually a small one). The day your last dependency migrates, you uninstall `httpx`, and this whole section stops applying to you. | ||
|
|
||
| If you don't want to wait for that day - your application is on `httpx2`, but some dependencies still `import httpx`, and you want everyone to share the same classes now - there is an escape hatch, `alias_httpx()`: | ||
|
|
||
| ```python | ||
| import httpx2 | ||
|
|
||
| httpx2.alias_httpx() | ||
| ``` | ||
|
|
||
| After this call, `import httpx` resolves to `httpx2`, process-wide. Submodule imports included. Dependencies importing `httpx` get the `httpx2` classes, so `isinstance()` checks pass and `except` clauses catch across the boundary. You can pass your `httpx2.Client` anywhere and drop the `httpx` install entirely. | ||
|
|
||
| There are exactly two rules: | ||
|
|
||
| * **Call it first.** It must run at the very top of your entrypoint, before anything imports `httpx`. Modules that already imported `httpx` hold references the alias can't rewrite, so `alias_httpx()` raises a `RuntimeError` instead of half-working. | ||
| * **Applications only, never libraries.** It changes the meaning of `import httpx` for the whole process. That is an application's decision to make, not something a library should impose on its users. | ||
|
|
||
| !!! warning | ||
| Don't do `sys.modules["httpx"] = httpx2` by hand instead. It looks equivalent, but any `from httpx.something import ...` in a dependency loads the module a second time under the `httpx` name, quietly recreating the two-class-hierarchies problem. `alias_httpx()` handles submodules correctly. | ||
|
|
||
| And a few things no aliasing can fix, so you know where the edges are: | ||
|
|
||
| * `importlib.metadata.version("httpx")` reads installed package metadata, not imports - it will raise or report the old `httpx` version. | ||
| * A library configuring `logging.getLogger("httpx")` won't see `httpx2`'s logs (the loggers are `httpx2` and `httpcore2.*`). | ||
| * Subprocesses and spawn-based workers re-import from scratch: each new process needs to call `alias_httpx()` too, before importing `httpx`. | ||
|
|
||
| Think of it as a bridge, not a destination: it gives you the end state today - one set of classes, no `httpx` install - while your dependencies catch up. Once the last one migrates, you delete the call and the bridge was never there. | ||
|
|
||
| ## What Changed | ||
|
|
||
| The public API did not change. But a rename touches a few visible things, and there are a couple of behavior differences worth knowing about. | ||
|
|
||
| ### Renames | ||
|
|
||
| * **Package and import**: `httpx` is now `httpx2`. | ||
| * **The CLI**: the command line client is now `httpx2` (installed with `httpx2[cli]`). | ||
| * **The User-Agent header**: the default is now `python-httpx2/<version>`. If you have server-side logic or tests matching on `python-httpx/...`, update them. | ||
| * **The loggers**: `httpx` is now `httpx2`, and `httpcore.*` is now `httpcore2.*`. If you configure logging by logger name, update those names. | ||
| * **The transport package**: `httpcore` is now `httpcore2`. If you import `httpcore` directly (for example, to catch low-level exceptions in a transport), switch those imports to `httpcore2`. | ||
|
|
||
| ### Behavior differences | ||
|
|
||
| * **SSL verification uses the operating system's trust store**, via [`truststore`](https://truststore.readthedocs.io/), instead of `certifi`'s bundled certificates. For most users this just means fewer corporate-proxy headaches. The `SSL_CERT_FILE` and `SSL_CERT_DIR` environment variables are still honored first, and you can still pass `verify=` explicitly. See [SSL](advanced/ssl.md) for details. | ||
| * **Python 3.10 or newer is required**. Support for Python 3.9 was dropped. | ||
| * **Deprecation warnings are visible by default**. `httpx2` uses `HTTPXDeprecationWarning`, a `UserWarning` subclass, so you will actually see deprecations without configuring warning filters. | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This overstates the current runtime behavior: several active deprecations still emit the built-in Useful? React with 👍 / 👎. |
||
|
|
||
| ### New things you get | ||
|
|
||
| Not required for the migration, but nice to have: | ||
|
|
||
| * **Server-Sent Events** support is built in, via `client.sse()`. If you use `httpx-sse`, you can drop it. See [Server-Sent Events](sse.md). | ||
| * **WebSockets** support is built in, with the `httpx2[ws]` extra. If you use `httpx-ws`, you can drop it too. See [WebSockets](websockets.md). | ||
|
|
||
| ## For Package Maintainers | ||
|
|
||
| If you maintain a library that depends on `httpx`, you have two good options, depending on whether you can release a new major version or not. Both have been battle-tested by real projects. | ||
|
|
||
| !!! warning | ||
| Whatever you choose, don't mix the two packages in one code path. `httpx.Response` and `httpx2.Response` are distinct classes: an `isinstance()` check against one will not match objects from the other, and a `httpx2.Client` cannot be passed where your code truly requires a `httpx.Client`. Pick one module per code path, as the patterns below do. | ||
|
|
||
| ### If you can release a major version: replace it | ||
|
|
||
| The simple option. Swap the dependency, rename the imports, document it as a breaking change. | ||
|
|
||
| This is what the [MCP Python SDK](https://github.com/modelcontextprotocol/python-sdk) did in [#2972](https://github.com/modelcontextprotocol/python-sdk/pull/2972) for its v2 release: | ||
|
|
||
| * `httpx` (and `httpx-sse`) replaced with `httpx2` in `pyproject.toml`. | ||
| * Imports renamed across the codebase. | ||
| * The change documented in the migration guide, including the visible differences (trust store verification, logger names, User-Agent). | ||
|
|
||
| For your users, the impact is small: if your API lets them pass their own client (an `http_client=...` parameter, for example), they only need to change which module they build it from. Everything else is internal to your library. | ||
|
|
||
| ### If you can't release a major version: support both, prefer `httpx2` | ||
|
|
||
| Maybe your library is stable, widely deployed, and a major version is not on the table. You can still migrate, without breaking anyone. | ||
|
|
||
| The pattern: **try `httpx2` first, fall back to `httpx` with a deprecation warning, and point your install extra at `httpx2`** so that new installs get the new package. | ||
|
|
||
| This is what [Starlette](https://github.com/encode/starlette) did for its `TestClient` in [#3291](https://github.com/encode/starlette/pull/3291): | ||
|
|
||
| ```python | ||
| import warnings | ||
| from typing import TYPE_CHECKING | ||
|
|
||
| if TYPE_CHECKING: | ||
| import httpx2 as httpx | ||
| else: | ||
| try: | ||
| import httpx2 as httpx | ||
| except ModuleNotFoundError: | ||
| try: | ||
| import httpx | ||
| except ModuleNotFoundError: | ||
| raise RuntimeError( | ||
| "This module requires the httpx2 package to be installed.\n" | ||
| "You can install it with:\n" | ||
| " $ pip install httpx2\n" | ||
| ) from None | ||
| else: | ||
| warnings.warn( | ||
| "Using `httpx` with this package is deprecated; install `httpx2` instead.", | ||
| DeprecationWarning, | ||
|
Kludex marked this conversation as resolved.
|
||
| stacklevel=2, | ||
| ) | ||
| ``` | ||
|
|
||
| Then the rest of the module just uses `httpx.Client`, `httpx.Response`, etc., and it works with whichever package is installed. | ||
|
|
||
| How this behaves: | ||
|
|
||
| * Users with `httpx2` installed get `httpx2`. Silently, no changes needed. | ||
| * Users with only `httpx` installed keep working, with a deprecation warning telling them where things are going. | ||
| * Users with neither get a clear error pointing at `httpx2`. | ||
|
|
||
| And in your `pyproject.toml`, update the extra (or dependency) that installs the HTTP client so that new installs bring in `httpx2`: | ||
|
|
||
| ```toml | ||
| [project.optional-dependencies] | ||
| full = [ | ||
| "httpx2>=2.0.0", | ||
| ] | ||
| ``` | ||
|
|
||
| !!! tip | ||
| Keep the `TYPE_CHECKING` branch importing a single module (`httpx2`, aliased as `httpx`). That way your type checker always sees one consistent set of types, while the runtime fallback stays flexible. | ||
|
|
||
| !!! note | ||
| Use a warning class that is visible by default (a `UserWarning` subclass) if you have one. Starlette added `StarletteDeprecationWarning` for exactly this reason: a deprecation nobody sees is a deprecation that doesn't work. | ||
|
|
||
| Later, when you do get to release a major version, you delete the fallback branch and the pattern collapses into a plain `import httpx2`. | ||
|
|
||
| ## Checklist | ||
|
|
||
| A quick summary to review before you ship: | ||
|
|
||
| * Dependencies: `httpx` replaced by (or accompanied by) `httpx2`. | ||
| * Imports: `import httpx` replaced by `import httpx2` (or aliased). | ||
| * Direct `httpcore` imports switched to `httpcore2`. | ||
| * Logging configuration updated for the `httpx2` and `httpcore2.*` logger names. | ||
| * Anything matching on the `python-httpx/...` User-Agent updated. | ||
| * Custom CA setups verified against the trust store behavior (or `verify=` passed explicitly). | ||
| * `httpx-sse` and `httpx-ws` dependencies dropped if you migrate to the built-in [SSE](sse.md) and [WebSockets](websockets.md) support. | ||
| * Dependencies still importing `httpx`: either keep `httpx` installed for them, or call `alias_httpx()` at the top of your entrypoint. | ||
|
|
||
| If you get stuck on something not covered here, open a [discussion](https://github.com/pydantic/httpx2/discussions) and we will help you out - and probably update this page too. | ||
Uh oh!
There was an error while loading. Please reload this page.