-
-
Notifications
You must be signed in to change notification settings - Fork 980
Use standard libary Zstandard for Python 3.14+ #3613
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
Conversation
|
Ooh interesting, thanks. Any idea on how widely zstd is currently supported? (Wrt both browsers and servers.) Related to this... compression is one of the currently unimplemented features in the httpx 1.0 prerelease... https://www.encode.io/httpnext/ |
| def _new_decompressor(self) -> None: | ||
| decompressor = zstandard.ZstdDecompressor() | ||
| if hasattr(decompressor, "decompressobj"): | ||
| self.decompressor = decompressor.decompressobj() # prgama: no cover | ||
| else: | ||
| self.decompressor = decompressor # pragma: no cover |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you explain this part?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is admittedly a little awkward. Python upstreamed the pyzstd package into compress.zstd because it's API was closer to existing standard library compression APIs. The zstandard package provides a ZstdDecompressObj facade that implements the standard library style API.
An alternative would be to import the libraries under separate names so the method would look more like this:
def _new_compressor(self) -> None:
if compression_zstd is not None:
self.decompressor = compression_zstd.ZstdDecompressor()
else:
self.decompressor = zstandard.ZstdDecompressor().decompressobj()
What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if the requirement in pyproject.toml is changed to the 3.9+ backport at backports.zstd published by the pyzstd maintainer (which got moved into cpython 3.14 standard library):
backports-zstd==1.0.0 ; python_version < "3.14"
this diff would get much simpler right?
maybe with a simpler diff, this PR has a better chance of landing?
reading from chunked streams gets streamlined like that: aio-libs/aiohttp@df8ad83
At least crates.io, packagist and sourceforge support it. (My tests blow up because I cache http responses and there are zstd responses from other Python versions; the "zstandard" module doesn't support 3.14 yet.) |
mollymorphous
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the review! Firefox and Chrome currently support zstd (caniuse). Safari does not yet, but plans to: WebKit/standards-positions#168
I wasn't able to find a hard number on server-side deployments, the answer seems to be not a lot, but CloudFlare recently added support to the CDN.
| def _new_decompressor(self) -> None: | ||
| decompressor = zstandard.ZstdDecompressor() | ||
| if hasattr(decompressor, "decompressobj"): | ||
| self.decompressor = decompressor.decompressobj() # prgama: no cover | ||
| else: | ||
| self.decompressor = decompressor # pragma: no cover |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is admittedly a little awkward. Python upstreamed the pyzstd package into compress.zstd because it's API was closer to existing standard library compression APIs. The zstandard package provides a ZstdDecompressObj facade that implements the standard library style API.
An alternative would be to import the libraries under separate names so the method would look more like this:
def _new_compressor(self) -> None:
if compression_zstd is not None:
self.decompressor = compression_zstd.ZstdDecompressor()
else:
self.decompressor = zstandard.ZstdDecompressor().decompressobj()
What do you think?
|
From a bit of time reviewing this I've not been able to track down good examples of URLs to use for comparison purposes here. Eg...
Here's one example of a URL that does support zstd, though in this particular case it appears less efficient than gzip... The CloudFlare pitch for zstd https://blog.cloudflare.com/new-standards/ isn't neccessarily convincing... gzip is essentially just as fast, and looks to have slightly less efficient though notably more stable compression ratios. I'm reviewing this for the purposes of Does anyone have some useful real world examples that'd help verify if that is / isn't a good policy? |
|
I suggest adding Python 3.14 to the test suite as in: |
|
I would suggest dropping |
gzip is definitely not just as fast (without hardware offloading), it's just that they measure the whole response time, where the compression speed difference does not seem to matter much on average.
I had used this to stream huge JSONL chunks over HTTP, and while it does not make traffic much less or a great overall speed improvement, the CPU load situation has improved a lot (relative to gzip). |
|
@tuffnatty Would you be willing to create an alternative pull request that uses the backport and adds automated tests on Py3.14 like |
|
Okay, so my review of this was that supporting gzip only would be a sensible policy. |
Would there be a mechanism to add support for other compression methods via third-party code then? I'm a bit worried about interoperability with not-so-good servers and proxies. |
|
That's a good question... ...I can't answer that fully at the moment. There might not be any API explicitly for that purpose. Here's how interop. with the streams class would be... # A custom stream on top of the streams API...
class ZstdStream(httpx.Stream):
def __init__(self, stream: httpx.Stream):
self._wrapped = stream
# Implement `.read()` and `.close()`
# Usage...
stream = ZStdStream(response.stream)
body = stream.read()We probably don't want specific dials to "support for other compression methods via third-party code", since the less config the better. However we do want the tooling itself to be flexible enough to support customisation. There's a current example in the docs demo'ing a custom client which is vastly more simple than with httpx 0.28. Perhaps building out that example to also demo custom response classes would be a good way to point users towards adaptability without increasing API surface area. |
@cclauss Thanks but the issue has been resolved in another way. |
fwiw, the major alternatives to httpx support zstd (and for sure brotli): supporting only gzip/deflate might be insufficient in 2025. especially brotli accounts for 33% of compressed http responses already in 2021 (Figure 22.4) and 44% of compressed javascript versus 41% gzip in 2024 (Figure 1.21) |
How was the issue resolved? |
|
|
disregard my last comment, I see that they're all supported 👍 |
Summary
PEP 784 add a Zstandard implementation to the Python standard library under
compression.zstd, and is scheduled for release in Python 3.14. This PR adaptsZStandardDecoderto work with either the standard library implementation or the implementation from thezstandardpackage.This has the implication that Zstandard content decoding is available by default on Python 3.14 and later, without the need to install the
zstdextra.Checklist