{"id":"ed5014756ac3662d","repo":"encode/httpx","slug":"queryparams-are-immutable-since-0-18-0-use-q-q","errorCode":null,"errorMessage":"QueryParams are immutable since 0.18.0. Use `q = q.merge(...)` to create an updated copy.","messagePattern":"QueryParams are immutable since 0\\.18\\.0\\. Use `q = q\\.merge\\(\\.\\.\\.\\)` to create an updated copy\\.","errorType":"exception","errorClass":"RuntimeError","httpStatus":null,"severity":"error","filePath":"httpx/_urls.py","lineNumber":632,"sourceCode":"\n    def __hash__(self) -> int:\n        return hash(str(self))\n\n    def __eq__(self, other: typing.Any) -> bool:\n        if not isinstance(other, self.__class__):\n            return False\n        return sorted(self.multi_items()) == sorted(other.multi_items())\n\n    def __str__(self) -> str:\n        return urlencode(self.multi_items())\n\n    def __repr__(self) -> str:\n        class_name = self.__class__.__name__\n        query_string = str(self)\n        return f\"{class_name}({query_string!r})\"\n\n    def update(self, params: QueryParamTypes | None = None) -> None:\n        raise RuntimeError(\n            \"QueryParams are immutable since 0.18.0. \"\n            \"Use `q = q.merge(...)` to create an updated copy.\"\n        )\n\n    def __setitem__(self, key: str, value: str) -> None:\n        raise RuntimeError(\n            \"QueryParams are immutable since 0.18.0. \"\n            \"Use `q = q.set(key, value)` to create an updated copy.\"\n        )\n","sourceCodeStart":614,"sourceCodeEnd":642,"githubUrl":"https://github.com/encode/httpx/blob/b5addb64f0161ff6bfe94c124ef76f6a1fba5254/httpx/_urls.py#L614-L642","documentation":"Since httpx 0.18.0, QueryParams is an immutable type. The legacy QueryParams.update(params) method is kept only to surface a clear migration error instead of silently no-op'ing or mutating in place. Calling it raises RuntimeError directing you to the functional replacement: q = q.merge(...), which returns a new QueryParams with the merged key/value pairs (last-wins on conflict).","triggerScenarios":"Calling QueryParams.update(...) on any instance, e.g. q = httpx.QueryParams('a=1'); q.update({'b': '2'}). Also reached indirectly by older client code or tutorials that predate 0.18.0 and treat QueryParams like a mutable dict. The raise is unconditional inside update() at httpx/_urls.py:632, so any call hits it.","commonSituations":"Upgrading httpx across the 0.18.0 boundary while relying on pre-upgrade tutorials, StackOverflow answers, or copy-pasted SDK wrappers that call params.update(...). Framework integrations (Starlette/TestClient older revisions) that mutated request query params in place. Anyone porting requests-style code where dict mutation was idiomatic.","solutions":["Replace params.update(new) with params = params.merge(new); merge takes a dict, list of pairs, query string, or another QueryParams and returns a new immutable instance.","If you were updating many keys, chain or build a single dict and merge once: params = params.merge({'a': '1', 'b': '2'}) to avoid repeated allocations.","If the call site actually wanted to set a single key, use params = params.set('key', value) instead of update.","Search the dependency tree (grep -r '.update(' on httpx.QueryParams / request kwargs) for SDKs pinning old httpx; bump or patch them, since their internal update() call is the real source."],"exampleFix":"// before\nq = httpx.QueryParams('a=1')\nq.update({'b': '2'})  # raises RuntimeError\n\n// after\nq = httpx.QueryParams('a=1')\nq = q.merge({'b': '2'})  # new immutable QueryParams('a=1&b=2')","handlingStrategy":"validation","validationCode":"import httpx\n\ndef safe_update(q, new):\n    \"\"\"Merge new params into q regardless of httpx version quirks.\n\n    Works for immutable (>=0.18.0) QueryParams by always returning a new\n    instance via merge; never calls the legacy update().\n    \"\"\"\n    if not isinstance(q, httpx.QueryParams):\n        q = httpx.QueryParams(q)\n    return q.merge(new)","typeGuard":"import httpx\n\ndef is_immutable_queryparams(q) -> bool:\n    \"\"\"True when q is a >=0.18.0 QueryParams (update raises by design).\"\"\"\n    return isinstance(q, httpx.QueryParams)","tryCatchPattern":"import httpx\n\ndef merge_or_fallback(q, new):\n    try:\n        return q.merge(new)\n    except (RuntimeError, AttributeError):\n        # Very old httpx without merge(), or unexpected immutable guard:\n        rebuilt = dict(q.multi_items())\n        rebuilt.update(dict(new))\n        return httpx.QueryParams(rebuilt)","preventionTips":["Treat httpx.QueryParams as a value type: always reassign the result (params = params.merge(...)), never call update().","Pin httpx >=0.18.0 in requirements and grep codebase for '.update(' on QueryParams to catch legacy calls before runtime.","Add a lint rule or pre-commit grep for 'QueryParams' followed within a few lines by '.update(' or '[.*] ='.","When wrapping httpx for a library, expose only merge/set/add/remove on your own params adapter so callers can't hit the legacy mutation API."],"tags":["httpx","query-params","immutability","migration","runtime-error"],"analyzedSha":"b5addb64f0161ff6bfe94c124ef76f6a1fba5254","analyzedAt":"2026-08-04T19:32:56.768Z","schemaVersion":2}