encode/httpx · error · RuntimeError

QueryParams are immutable since 0.18.0. Use `q = q.set(key,

Error message

QueryParams are immutable since 0.18.0. Use `q = q.set(key, value)` to create an updated copy.

What it means

Since httpx 0.18.0, QueryParams is immutable, so item assignment via __setitem__ (q[key] = value) is disabled and raises RuntimeError. The functional replacement is q = q.set(key, value), which copies the internal dict, overwrites the single key, and returns a new QueryParams instance. Existing keys are replaced; keys not present are added.

Source

Thrown at httpx/_urls.py:638

            return False
        return sorted(self.multi_items()) == sorted(other.multi_items())

    def __str__(self) -> str:
        return urlencode(self.multi_items())

    def __repr__(self) -> str:
        class_name = self.__class__.__name__
        query_string = str(self)
        return f"{class_name}({query_string!r})"

    def update(self, params: QueryParamTypes | None = None) -> None:
        raise RuntimeError(
            "QueryParams are immutable since 0.18.0. "
            "Use `q = q.merge(...)` to create an updated copy."
        )

    def __setitem__(self, key: str, value: str) -> None:
        raise RuntimeError(
            "QueryParams are immutable since 0.18.0. "
            "Use `q = q.set(key, value)` to create an updated copy."
        )

View on GitHub (pinned to b5addb64f0)

Solutions

  1. Replace params[key] = value with params = params.set(key, value); set returns a new instance with that key overwritten (or added if absent).
  2. For bulk replacement, collect into a dict and use params = params.merge({...}) once, rather than many chained .set() calls.
  3. If you need append semantics (multi-value key), use params = params.add(key, value) instead of set.
  4. Audit generic helpers that accept Mapping and mutate via [] =; branch on isinstance(x, httpx.QueryParams) or require callers to pass plain dicts that you convert at the boundary.

Example fix

// before
q = httpx.QueryParams('a=1')
q['b'] = '2'  # raises RuntimeError

// after
q = httpx.QueryParams('a=1')
q = q.set('b', '2')  # new immutable QueryParams('a=1&b=2')
Defensive patterns

Strategy: validation

Validate before calling

import httpx

def safe_set(q, key, value):
    """Set a single key on q, returning a new QueryParams.

    Never uses __setitem__, so the immutability guard cannot fire.
    """
    if not isinstance(q, httpx.QueryParams):
        q = httpx.QueryParams(q)
    return q.set(key, value)

Type guard

import httpx

def is_immutable_queryparams(q) -> bool:
    """True when q is a >=0.18.0 QueryParams (item assignment raises)."""
    return isinstance(q, httpx.QueryParams)

Try / catch

import httpx

def set_or_fallback(q, key, value):
    try:
        return q.set(key, value)
    except (RuntimeError, AttributeError):
        items = dict(q.multi_items())
        items[key] = value
        return httpx.QueryParams(items)

Prevention

When it happens

Trigger: Any item-assignment on a QueryParams instance, e.g. q = httpx.QueryParams('a=1'); q['b'] = '2'. The raise at httpx/_urls.py:638 is unconditional inside __setitem__, so every assignment hits it. Also triggered by generic dict-style helpers that do params[k] = v when handed a QueryParams.

Common situations: Code migrated from requests or older httpx where query params were treated as mutable dicts. Tutorial/blog code predating 0.18.0. Generic web helpers that uniformly mutate whatever Mapping they receive. Type-checkers won't catch it because __setitem__ is typed to accept the assignment.

Related errors


AI-assisted analysis of encode/httpx@b5addb64f0 (2026-08-04). Data as JSON: /data/errors/71b09d39c56c9477.json. Report an issue: GitHub.