Smart, policy-driven query string merging & encoding for httpx powered by qs-codec.
httpx-qs provides:
- A transport wrapper
SmartQueryStringsthat merges existing URL query parameters with additional ones supplied viarequest.extensions. - A flexible
merge_queryutility with selectable conflict resolution policies. - Consistent, standards-aware encoding via
qs-codec(RFC3986 percent-encoding, structured arrays, nested objects, etc.).
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.
- CPython 3.8-3.14 or PyPy 3.8-3.11
httpx>=0.28.1,<1.0.0qs-codec>=1.3.1
pip install httpx-qsimport 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=yConflict 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: raiseValueErroron 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=overrideSmartQueryStrings 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())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=yqs-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.
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[]=betatry:
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)The project includes unit tests covering policy behaviors, error handling, and transport-level integration. Run them with:
pytest- HTTPX documentation: https://www.python-httpx.org
- qs-codec documentation: https://techouse.github.io/qs_codec/
BSD-3-Clause. See LICENSE for details.
Issues & PRs welcome. Please add tests for new behavior and keep doc examples in sync.