Smart, policy-driven query string merging & encoding for httpx powered by qs-codec.
Overview
httpx-qs provides:
A transport wrapper SmartQueryStrings that merges existing URL query parameters with additional ones supplied via request.extensions.
A flexible merge_query utility with selectable conflict resolution policies.
Consistent, standards-aware encoding via qs-codec (RFC3986 percent-encoding, structured arrays, nested objects, etc.).
Why?
HTTPX already lets you pass params= when making requests, but sometimes you need to:
Inject additional query parameters from middleware/transport layers (e.g., auth tags, tracing IDs, feature flags) without losing the caller’s original intent.
Combine repeated keys or treat them deterministically (replace / keep / error) rather than always flattening.
Support nested data or list semantics consistent across clients and services.
qs-codec supplies the primitives (decoding & encoding with configurable ListFormat). httpx-qs stitches that into HTTPX’s transport pipeline so you can declaratively extend queries at request dispatch time.
Requirements
CPython 3.8-3.14, including free-threaded 3.14t, or PyPy 3.8-3.11
httpx>=0.28.1,<1.0.0
qs-codec>=1.5.0
Installation
pip install httpx-qs
Minimal Example
import httpx
from httpx_qs.transporters.smart_query_strings import SmartQueryStrings
client = httpx.Client(transport=SmartQueryStrings(httpx.HTTPTransport()))
response = client.get(
"https://www.google.com",
params={"a": "b", "c": "d"},
extensions={"extra_query_params": {"c": "D", "tags": ["x", "y"]}},
)
print(str(response.request.url))
# Example (order may vary): https://www.google.com/?a=b&c=d&c=D&tags=x&tags=y
Using Merge Policies
Conflict resolution when a key already exists is controlled by MergePolicy.
Available policies:
combine (default): concatenate values → existing first, new afterward (a=1&a=2)
replace: last-wins, existing value is overwritten (a=2)
keep: first-wins, ignore the new value (a=1)
error: raise ValueError on duplicate key
Specify per request:
from httpx_qs import MergePolicy
r = client.get(
"https://api.example.com/resources",
params={"dup": "original"},
extensions={
"extra_query_params": {"dup": "override"},
"extra_query_params_policy": MergePolicy.REPLACE,
},
)
# Query contains only dup=override
Async Usage
SmartQueryStrings works equally for AsyncClient:
import httpx
from httpx_qs.transporters.smart_query_strings import SmartQueryStrings
async def main() -> None:
async with httpx.AsyncClient(transport=SmartQueryStrings(httpx.AsyncHTTPTransport())) as client:
r = await client.get(
"https://example.com/items",
params={"filters": "active"},
extensions={"extra_query_params": {"page": 2}},
)
print(r.request.url)
# Run with: asyncio.run(main())
merge_query Utility
You can use the underlying function directly:
from httpx_qs import merge_query, MergePolicy
from qs_codec import EncodeOptions, ListFormat
new_url = merge_query(
"https://example.com?a=1",
{"a": 2, "tags": ["x", "y"]},
options=EncodeOptions(list_format=ListFormat.REPEAT),
policy=MergePolicy.COMBINE,
)
# → https://example.com/?a=1&a=2&tags=x&tags=y
Why ListFormat.REPEAT by Default?
qs-codec exposes several list formatting strategies (e.g. repeat, brackets, indices). httpx-qs defaults to ListFormat.REPEAT because:
It matches common server expectations (key=value&key=value) without requiring bracket parsing logic.
It preserves original ordering while remaining unambiguous and simple for log inspection.
Many API gateways / proxies / caches reliably forward repeated keys whereas bracket syntaxes can be normalized away.
If your API prefers another convention (e.g. tags[]=x&tags[]=y or tags[0]=x) just pass a custom EncodeOptions via extensions['extra_query_params_options'] or parameter options when calling merge_query directly.
Advanced Per-Request Customization
from qs_codec import EncodeOptions, ListFormat
r = client.get(
"https://service.local/search",
params={"q": "test"},
extensions={
"extra_query_params": {"debug": True, "tags": ["alpha", "beta"]},
"extra_query_params_policy": "combine", # also accepts string values
"extra_query_params_options": EncodeOptions(list_format=ListFormat.BRACKETS),
},
)
# Example: ?q=test&debug=true&tags[]=alpha&tags[]=beta
Error Policy Example
try:
client.get(
"https://example.com",
params={"token": "abc"},
extensions={
"extra_query_params": {"token": "xyz"},
"extra_query_params_policy": "error",
},
)
except ValueError as exc:
print("Duplicate detected:", exc)
Testing Strategy
The project includes unit tests covering policy behaviors, error handling, and transport-level integration. Run them with:
pytest
Further Reading
HTTPX documentation: https://www.python-httpx.org
qs-codec documentation: https://techouse.github.io/qs_codec/
License
BSD-3-Clause. See LICENSE for details.
Contributing
Issues & PRs welcome. Please add tests for new behavior and keep doc examples in sync.
Metadata
Release files for httpx-qs 0.2.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| httpx_qs-0.2.2.tar.gz | 12.3 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| httpx_qs-0.2.2-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 21.1 kB
Release files / httpx_qs-0.2.2.tar.gz
| Download URL | httpx_qs-0.2.2.tar.gz |
|---|---|
| Size | 12.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
2049c04ba17dcdd71ff0e193c5cd8142ff7988776d54ab0576623c46e5e035c7
|
|
BLAKE2b-256 checksum How to use checksums |
fbadc8c73dbe411b5daa56286ba4f47ce1545572e1fbf01ae27a433899b577d2
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.7
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Mar 31, 2026.
Transparency logRelease files / httpx_qs-0.2.2-py3-none-any.whl
| Download URL | httpx_qs-0.2.2-py3-none-any.whl |
|---|---|
| Size | 8.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
fbb1dce4f1c807daa69b656bd54cff81416ec3fefaa8c5fb3c160fa670c3a1ca
|
|
BLAKE2b-256 checksum How to use checksums |
9167262d1a73b59f63c2e529a343efd55c1c2d172259d8564b12d4ae6ec3adfc
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.7
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Mar 31, 2026.
Transparency logSecurity for httpx-qs 0.2.2
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for httpx_qs-0.2.2.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Mar 31, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.