diff --git a/README.md b/README.md
index 2ccecd578c..96042e4c41 100644
--- a/README.md
+++ b/README.md
@@ -136,7 +136,7 @@ As well as these optional installs:
* `rich` - Rich terminal support. *(Optional, with `httpx[cli]`)*
* `click` - Command line client support. *(Optional, with `httpx[cli]`)*
* `brotli` or `brotlicffi` - Decoding for "brotli" compressed responses. *(Optional, with `httpx[brotli]`)*
-* `zstandard` - Decoding for "zstd" compressed responses. *(Optional, with `httpx[zstd]`)*
+* `backports.zstd` - Decoding for "zstd" compressed responses on Python before 3.14. *(Optional, with `httpx[zstd]`)*
A huge amount of credit is due to `requests` for the API layout that
much of this work follows, as well as to `urllib3` for plenty of design
diff --git a/docs/quickstart.md b/docs/quickstart.md
index e140b53cd7..49e2eb4de3 100644
--- a/docs/quickstart.md
+++ b/docs/quickstart.md
@@ -100,8 +100,8 @@ b'\n\n
\nExample Domain...'
Any `gzip` and `deflate` HTTP response encodings will automatically
be decoded for you. If `brotlipy` is installed, then the `brotli` response
-encoding will be supported. If `zstandard` is installed, then `zstd`
-response encodings will also be supported.
+encoding will be supported. If the Python version used is 3.14 or higher or
+if `backports.zstd` is installed, then `zstd` response encodings will also be supported.
For example, to create an image from binary data returned by a request, you can use the following code:
diff --git a/httpx/_decoders.py b/httpx/_decoders.py
index 899dfada87..f35de81fb9 100644
--- a/httpx/_decoders.py
+++ b/httpx/_decoders.py
@@ -8,6 +8,7 @@
import codecs
import io
+import sys
import typing
import zlib
@@ -28,9 +29,12 @@
# Zstandard support is optional
try:
- import zstandard
+ if sys.version_info >= (3, 14):
+ from compression import zstd # pragma: no cover
+ else:
+ from backports import zstd # pragma: no cover
except ImportError: # pragma: no cover
- zstandard = None # type: ignore
+ zstd = None # type: ignore
class ContentDecoder:
@@ -162,42 +166,41 @@ class ZStandardDecoder(ContentDecoder):
"""
Handle 'zstd' RFC 8878 decoding.
- Requires `pip install zstandard`.
+ Requires `pip install backports.zstd` for Python before 3.14.
Can be installed as a dependency of httpx using `pip install httpx[zstd]`.
"""
# inspired by the ZstdDecoder implementation in urllib3
def __init__(self) -> None:
- if zstandard is None: # pragma: no cover
+ if zstd is None: # pragma: no cover
raise ImportError(
"Using 'ZStandardDecoder', ..."
"Make sure to install httpx using `pip install httpx[zstd]`."
) from None
- self.decompressor = zstandard.ZstdDecompressor().decompressobj()
+ self.decompressor = zstd.ZstdDecompressor()
self.seen_data = False
def decode(self, data: bytes) -> bytes:
- assert zstandard is not None
+ assert zstd is not None
self.seen_data = True
output = io.BytesIO()
try:
output.write(self.decompressor.decompress(data))
while self.decompressor.eof and self.decompressor.unused_data:
unused_data = self.decompressor.unused_data
- self.decompressor = zstandard.ZstdDecompressor().decompressobj()
+ self.decompressor = zstd.ZstdDecompressor()
output.write(self.decompressor.decompress(unused_data))
- except zstandard.ZstdError as exc:
+ except zstd.ZstdError as exc:
raise DecodingError(str(exc)) from exc
return output.getvalue()
def flush(self) -> bytes:
if not self.seen_data:
return b""
- ret = self.decompressor.flush() # note: this is a no-op
if not self.decompressor.eof:
raise DecodingError("Zstandard data is incomplete") # pragma: no cover
- return bytes(ret)
+ return b""
class MultiDecoder(ContentDecoder):
@@ -389,5 +392,5 @@ def flush(self) -> list[str]:
if brotli is None:
SUPPORTED_DECODERS.pop("br") # pragma: no cover
-if zstandard is None:
+if zstd is None:
SUPPORTED_DECODERS.pop("zstd") # pragma: no cover
diff --git a/pyproject.toml b/pyproject.toml
index fc3e95ea74..48bd19629e 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -52,7 +52,7 @@ socks = [
"socksio==1.*",
]
zstd = [
- "zstandard>=0.18.0",
+ "backports.zstd>=1.0.0 ; python_version < '3.14'",
]
[project.scripts]
diff --git a/tests/test_decoders.py b/tests/test_decoders.py
index 9ffaba189d..e1192ac004 100644
--- a/tests/test_decoders.py
+++ b/tests/test_decoders.py
@@ -1,15 +1,20 @@
from __future__ import annotations
import io
+import sys
import typing
import zlib
import chardet
import pytest
-import zstandard as zstd
import httpx
+if sys.version_info >= (3, 14):
+ from compression import zstd # pragma: no cover
+else:
+ from backports import zstd # pragma: no cover
+
def test_deflate():
"""