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.