Skip to content

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.
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:

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:

grep -rn "import httpx\b" src/

And to see which installed packages still depend on it:

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.

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 section below gives maintainers two proven, low-effort paths to do it, so point them there. 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():

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, 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 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.

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.
  • WebSockets support is built in, with the httpx2[ws] extra. If you use httpx-ws, you can drop it too. See WebSockets.

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 did in #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 did for its TestClient in #3291:

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,
                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:

[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 and WebSockets 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 and we will help you out - and probably update this page too.