florimondmanca: @tomchristie @sethmlarson Ready for re-review! :-)
diff --git a/httpx/models.py b/httpx/models.py
index f70fdf4..136aa41 100644
--- a/httpx/models.py
+++ b/httpx/models.py
@@ -32,6 +32,7 @@ from .exceptions import (
 from .multipart import multipart_encode
 from .status_codes import StatusCode
 from .utils import (
+    flatten_queryparams,
     guess_json_utf,
     is_known_encoding,
     normalize_header_key,
@@ -51,7 +52,7 @@ URLTypes = typing.Union["URL", str]
 
 QueryParamTypes = typing.Union[
     "QueryParams",
-    typing.Mapping[str, PrimitiveData],
+    typing.Mapping[str, typing.Union[PrimitiveData, typing.Sequence[PrimitiveData]]],
     typing.List[typing.Tuple[str, PrimitiveData]],
     str,
 ]
@@ -311,14 +312,15 @@ class QueryParams(typing.Mapping[str, str]):
 
         value = args[0] if args else kwargs
 
+        items: typing.Sequence[typing.Tuple[str, PrimitiveData]]
         if isinstance(value, str):
             items = parse_qsl(value)
         elif isinstance(value, QueryParams):
             items = value.multi_items()
         elif isinstance(value, list):
-            items = value  # type: ignore
+            items = value
         else:
-            items = value.items()  # type: ignore
+            items = flatten_queryparams(value)
 
         self._list = [(str(k), str_query_param(v)) for k, v in items]
         self._dict = {str(k): str_query_param(v) for k, v in items}
diff --git a/httpx/utils.py b/httpx/utils.py
index c8fcb1e..8aea5e7 100644
--- a/httpx/utils.py
+++ b/httpx/utils.py
@@ -1,4 +1,5 @@
 import codecs
+import collections
 import logging
 import netrc
 import os
@@ -11,6 +12,9 @@ from time import perf_counter
 from types import TracebackType
 from urllib.request import getproxies
 
+if typing.TYPE_CHECKING:  # pragma: no cover
+    from .models import PrimitiveData
+
 
 def normalize_header_key(value: typing.AnyStr, encoding: str = None) -> bytes:
     """
@@ -30,7 +34,7 @@ def normalize_header_value(value: typing.AnyStr, encoding: str = None) -> bytes:
     return value.encode(encoding or "ascii")
 
 
-def str_query_param(value: typing.Optional[typing.Union[str, int, float, bool]]) -> str:
+def str_query_param(value: "PrimitiveData") -> str:
     """
     Coerce a primitive data type into a string value for query params.
 
@@ -256,6 +260,31 @@ def unquote(value: str) -> str:
     return value[1:-1] if value[0] == value[-1] == '"' else value
 
 
+def flatten_queryparams(
+    queryparams: typing.Mapping[
+        str, typing.Union["PrimitiveData", typing.Sequence["PrimitiveData"]]
+    ]
+) -> typing.List[typing.Tuple[str, "PrimitiveData"]]:
+    """
+    Convert a mapping of query params into a flat list of two-tuples
+    representing each item.
+
+    Example:
+    >>> flatten_queryparams_values({"q": "httpx", "tag": ["python", "dev"]})
+    [("q", "httpx), ("tag", "python"), ("tag", "dev")]
+    """
+    items = []
+
+    for k, v in queryparams.items():
+        if isinstance(v, collections.abc.Sequence) and not isinstance(v, (str, bytes)):
+            for u in v:
+                items.append((k, u))
+        else:
+            items.append((k, typing.cast("PrimitiveData", v)))
+
+    return items
+
+
 class ElapsedTimer:
     def __init__(self) -> None:
         self.start: float = perf_counter()

Query params should support list-of-strings values.
Hi,

I recently tried to use `httpx` instead of `requests` in my simple app that calls few API endpoints and  suddenly it did not work.

The problem here is that `httpx` and `requests` process query parameters differently.

Here's a simple script to see how both libraries process `params`:
```python
from httpx.models import QueryParams
from requests.models import RequestEncodingMixin

dict_params = {
    "documentIds": ["a55b2f894b90dd1213201471_25", "b54b2f894b90dd1213201471_9"]
}

httpx_params = str(QueryParams(dict_params))
print("httpx", httpx_params)

requests_params = RequestEncodingMixin._encode_params(dict_params)
print("requests", requests_params)
```

Results:
```
httpx documentIds=%5B%27a55b2f894b90dd1213201471_25%27%2C+%27b54b2f894b90dd1213201471_9%27%5D
requests documentIds=a55b2f894b90dd1213201471_25&documentIds=b54b2f894b90dd1213201471_9
```

If we try to parse the query string `httpx` produced, here's what we get:
```python
from urllib.parse import parse_qs

httpx_params_parsed = parse_qs('documentIds=%5B%27a55b2f894b90dd1213201471_25%27%2C+%27b54b2f894b90dd1213201471_9%27%5D')
print(httpx_params_parsed )
```
Result:
```
{'documentIds': ["['a55b2f894b90dd1213201471_25', 'b54b2f894b90dd1213201471_9']"]}
```
Which is not what you would expect, I guess. At least, it works differently than in `requests`.

If it is an intended behavior, can you please recommend me the proper way of passing a list into the query params?

Thanks!
diff --git a/tests/models/test_queryparams.py b/tests/models/test_queryparams.py
index 8c4df49..1303170 100644
--- a/tests/models/test_queryparams.py
+++ b/tests/models/test_queryparams.py
@@ -1,8 +1,18 @@
+import pytest
+
 from httpx import QueryParams
 
 
-def test_queryparams():
-    q = QueryParams("a=123&a=456&b=789")
+@pytest.mark.parametrize(
+    "source",
+    [
+        "a=123&a=456&b=789",
+        {"a": ["123", "456"], "b": 789},
+        {"a": ("123", "456"), "b": 789},
+    ],
+)
+def test_queryparams(source):
+    q = QueryParams(source)
     assert "a" in q
     assert "A" not in q
     assert "c" not in q

pip install -e .[dev]
parse_log_pytest
requirements.txt
pytest pytest-asyncio pytest-trio pytest-cov trio trustme uvicorn
3.7
test-requirements.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
attrs==24.2.0
brotlipy==0.7.0
certifi @ file:///croot/certifi_1671487769961/work/certifi
cffi==1.15.1
chardet==3.0.4
click==8.1.8
coverage==7.2.7
cryptography==44.0.2
exceptiongroup==1.2.2
flake8==5.0.4
flake8-bugbear==23.3.12
flake8-comprehensions==3.13.0
flake8-pie==0.16.0
h11==0.8.1
h2==3.2.0
hpack==3.0.0
hstspreload==2025.1.1
-e git+https://github.com/encode/httpx.git@31730e709597baaa7b2364fee041dfa985169789#egg=httpx
hyperframe==5.2.0
idna==2.10
importlib-metadata==4.2.0
iniconfig==2.0.0
isort==5.11.5
mccabe==0.7.0
mypy==1.4.1
mypy-extensions==1.0.0
outcome==1.3.0.post0
packaging==24.0
pluggy==1.2.0
pycodestyle==2.9.1
pycparser==2.21
pyflakes==2.5.0
pytest==7.4.4
pytest-asyncio==0.21.2
pytest-cov==4.1.0
pytest-trio==0.8.0
rfc3986==1.5.0
sniffio==1.3.1
sortedcontainers==2.4.0
tomli==2.0.1
trio==0.22.2
trustme==1.0.0
typed-ast==1.5.5
typing_extensions==4.7.1
uvicorn==0.22.0
uvloop==0.12.2
zipp==3.15.0

name: httpx
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2022.12.7=py37h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - pip=22.3.1=py37h06a4308_0
  - python=3.7.16=h7a1cb2a_0
  - readline=8.2=h5eee18b_0
  - setuptools=65.6.3=py37h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - wheel=0.38.4=py37h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - attrs==24.2.0
      - brotlipy==0.7.0
      - cffi==1.15.1
      - chardet==3.0.4
      - click==8.1.8
      - coverage==7.2.7
      - cryptography==44.0.2
      - exceptiongroup==1.2.2
      - flake8==5.0.4
      - flake8-bugbear==23.3.12
      - flake8-comprehensions==3.13.0
      - flake8-pie==0.16.0
      - h11==0.8.1
      - h2==3.2.0
      - hpack==3.0.0
      - hstspreload==2025.1.1
      - hyperframe==5.2.0
      - idna==2.10
      - importlib-metadata==4.2.0
      - iniconfig==2.0.0
      - isort==5.11.5
      - mccabe==0.7.0
      - mypy==1.4.1
      - mypy-extensions==1.0.0
      - outcome==1.3.0.post0
      - packaging==24.0
      - pluggy==1.2.0
      - pycodestyle==2.9.1
      - pycparser==2.21
      - pyflakes==2.5.0
      - pytest==7.4.4
      - pytest-asyncio==0.21.2
      - pytest-cov==4.1.0
      - pytest-trio==0.8.0
      - rfc3986==1.5.0
      - sniffio==1.3.1
      - sortedcontainers==2.4.0
      - tomli==2.0.1
      - trio==0.22.2
      - trustme==1.0.0
      - typed-ast==1.5.5
      - typing-extensions==4.7.1
      - uvicorn==0.22.0
      - uvloop==0.12.2
      - zipp==3.15.0
prefix: /opt/conda/envs/httpx

tests/models/test_queryparams.py::test_queryparams[source1] tests/models/test_queryparams.py::test_queryparams[source2]
tests/models/test_queryparams.py::test_queryparams[a=123&a=456&b=789] tests/models/test_queryparams.py::test_queryparam_types tests/models/test_queryparams.py::test_queryparam_setters
httpx/utils.py httpx/models.py
httpx/utils.py httpx/models.py