httpx params: query parameters, lists, client params (tested)
Use httpx.get(url, params={"q": "x", "tag": ["a", "b"]}). A list repeats the key and None is sent as an empty value. Watch out: params= replaces any ?query already in the URL, unlike requests.
Pass query parameters to httpx with params= as a dict, a list of tuples, or a string. Measured with httpx 0.28.1 on Python 3.13.5 against a local http.server (srv.py) that echoes back the path and query string it received.
1. httpx get params
import httpx
from srv import start
base = start()
r = httpx.get(base + "/search", params={"q": "café & bar", "page": 2})
print(r.json()["path"])
print(r.url)
print(r.url.params)
Output:
/search?q=caf%C3%A9+%26+bar&page=2
http://127.0.0.1:8963/search?q=caf%C3%A9+%26+bar&page=2
q=caf%C3%A9+%26+bar&page=2
Values are URL-encoded for you (space becomes +, & becomes %26), so never build the query string by hand with f-strings. r.url.params shows exactly what was sent.
2. httpx params list: repeated keys
import httpx
from srv import start
base = start()
# list value -> repeated key
r = httpx.get(base + "/items", params={"tag": ["a", "b"], "limit": 10})
print(r.json()["path"])
# list of tuples keeps order and allows repeats too
r = httpx.get(base + "/items", params=[("tag", "a"), ("tag", "b")])
print(r.json()["path"])
# no automatic tag[] suffix: write the key name the API expects
r = httpx.get(base + "/items", params={"tag[]": ["a", "b"]})
print(r.json()["path"])
Output:
/items?tag=a&tag=b&limit=10
/items?tag=a&tag=b
/items?tag%5B%5D=a&tag%5B%5D=b
A list value repeats the key (tag=a&tag=b). httpx does not add a [] suffix; if a PHP or Rails style API wants tag[]=, use that literal key.
3. Gotcha: params= replaces the query string already in the URL
import httpx
from srv import start
base = start()
# params= REPLACES a query string already in the URL
r = httpx.get(base + "/items?sort=asc", params={"page": 2})
print("URL + params: ", r.json()["path"])
# keep it by merging with copy_merge_params / copy_add_param
url = httpx.URL(base + "/items?sort=asc")
r = httpx.get(url.copy_merge_params({"page": 2}))
print("copy_merge_params:", r.json()["path"])
import requests
r = requests.get(base + "/items?sort=asc", params={"page": 2})
print("requests: ", r.json()["path"])
Output:
URL + params: /items?page=2
copy_merge_params: /items?sort=asc&page=2
requests: /items?sort=asc&page=2
This is a difference from requests, which merges the two. In httpx 0.28.1 sort=asc is silently dropped. Put every parameter in params=, or merge explicitly with httpx.URL(...).copy_merge_params().
4. httpx client params (default params on every request)
import httpx
from srv import start
base = start()
with httpx.Client(base_url=base, params={"api_key": "k1"}) as client:
r = client.get("/items", params={"page": 2})
print("merged: ", r.json()["path"])
r = client.get("/items", params={"api_key": "override"})
print("override:", r.json()["path"])
r = client.get("/items?page=3")
print("url qs: ", r.json()["path"])
Output:
merged: /items?api_key=k1&page=2
override: /items?api_key=override
url qs: /items?api_key=k1
Client-level params= are merged with per-request params=, and a per-request key overrides the client value. But a query string written into the URL ("/items?page=3") is dropped when the client has default params: pass it as params={"page": 3} instead.
5. httpx query params types: None, bool, float, dict
import httpx
from srv import start
base = start()
for value in [None, True, False, 1.5, ""]:
r = httpx.get(base + "/x", params={"v": value})
print(repr(value).ljust(6), "->", r.json()["path"])
# drop None values yourself if the API should not see the key at all
p = {"q": "x", "page": None}
r = httpx.get(base + "/x", params={k: v for k, v in p.items() if v is not None})
print("filtered ->", r.json()["path"])
try:
r = httpx.get(base + "/x", params={"d": {"a": 1}})
print("dict value ->", r.json()["path"])
except TypeError as e:
print("TypeError:", e)
Output:
None -> /x?v=
True -> /x?v=true
False -> /x?v=false
1.5 -> /x?v=1.5
'' -> /x?v=
filtered -> /x?q=x
dict value -> /x?d=%7B%27a%27%3A+1%7D
Unlike requests, httpx keeps a key whose value is None and sends it empty (v=); filter it out yourself if the API treats an empty value differently. Booleans become lowercase true/false. A nested dict is not rejected: it is sent as its Python str(), which no API expects, so JSON-encode it or flatten it first.
6. httpx asyncclient get params
import asyncio, httpx
from srv import start
base = start()
async def main():
async with httpx.AsyncClient(base_url=base, params={"api_key": "k1"}) as client:
r = await client.get("/items", params={"page": 2})
print(r.json()["path"])
asyncio.run(main())
Output:
/items?api_key=k1&page=2
AsyncClient takes the same params= argument on the constructor and on get(), with the same merge rules.
Related: httpx vs requests differences, httpx follow redirects, httpx timeout, httpx retries.
Frequently asked questions
How do I pass query parameters in httpx?
Use params= with a dict, a list of (key, value) tuples, or a string: httpx.get(url, params={'q': 'x'}). Values are URL-encoded for you.
Why does httpx drop the query string in my URL?
In httpx 0.28.1, params= replaces a query string already present in the URL (requests merges them). Put all parameters in params=, or use httpx.URL(url).copy_merge_params(params).
How do I send a list as query params in httpx?
Pass a list value: params={'tag': ['a', 'b']} sends tag=a&tag=b. There is no automatic tag[] suffix.
Does httpx skip params that are None?
No. httpx 0.28.1 sends a None value as an empty parameter (v=); filter None values out yourself.
Written with AI assistance. Measured on 2026-10-23 with httpx 0.28.1 (and requests 2.32 for comparison) on Python 3.13.5 against a local http.server that echoes the request path. Each block was run verbatim and the output pasted unedited. PlainHeaders is our extension; httpx is free.