Why Requests Get Params Fail: URL vs. params Argument in Python
Passing a dictionary as the third positional argument to requests.get() causes query parameters to be silently omitted because the function interprets it as the timeout value, not the params mapping.
When working with the psf/requests library, developers often expect requests.get(url, {"key": "value"}) to automatically append query parameters to the URL. While this works when passed as the second positional argument, subtle mistakes in argument positioning or naming cause the params dictionary to be ignored entirely. This behavior stems from the method signature in src/requests/api.py and the URL preparation logic in src/requests/models.py.
How Requests Processes the params Argument
The requests library follows a strict two-step pipeline for assembling GET requests. Understanding this flow clarifies why parameters sometimes fail to appear in the final URL.
The API Entry Point in api.py
The high-level get() function in src/requests/api.py (lines 62-74) defines params as the second positional argument:
def get(url, params=None, **kwargs):
return request("get", url, params=params, **kwargs)
When you call requests.get("https://api.example.com", {"search": "python"}), the dictionary correctly maps to the params parameter. However, if you add a third positional argument—such as requests.get(url, {"search": "python"}, 5)—Python raises a TypeError because get() accepts only two positional arguments (url and params), with all other options requiring keyword syntax.
URL Preparation in models.py
Once the params dictionary reaches the PreparedRequest class in src/requests/models.py, the prepare_url() method (lines 72-84) handles the actual encoding:
def prepare_url(self, url, params):
# ...
enc_params = self._encode_params(params)
if enc_params:
if query:
query = f"{query}&{enc_params}"
else:
query = enc_params
# ...
self.url = requote_uri(urlunparse([scheme, netloc, path, None, query, fragment]))
The _encode_params method converts dictionaries, lists of tuples, or raw strings into URL-encoded query strings. If the original URL already contains a query component (e.g., https://api.com?existing=1), the method appends the new parameters with an & separator. This ensures that params are always sent when provided correctly, regardless of whether the URL contains existing query strings.
Common Mistakes That Cause Params to Disappear
Despite the robust internal handling, several caller-side errors result in empty query strings:
-
Passing
paramsas a third positional argument: Becauserequests.get()only acceptsurlandparamspositionally, adding a timeout value positionally (e.g.,requests.get(url, params, 5)) triggers aTypeErroror causes the dictionary to be misinterpreted depending on the library version. -
Misspelling the keyword: Using
param(singular) instead ofparamsbypasses the encoding logic entirely, as the function receives an unexpected keyword argument that gets swallowed by**kwargsbut never processed as query parameters. -
Passing
Noneor an empty dictionary: While technically valid,params=Noneorparams={}results in no query string being appended, which can appear as a failure if the developer expects default behavior. -
Custom
PreparedRequestsubclasses: Overridingprepare_url()without callingsuper().prepare_url()or failing to handle theparamsargument in the subclass drops the encoded query string.
Correct Implementation Examples
To ensure query parameters reach the server, use explicit keyword arguments or proper positional placement:
import requests
# Method 1: Keyword argument (recommended)
resp = requests.get(
"https://httpbin.org/get",
params={"search": "python", "page": 2},
timeout=5,
)
print(resp.url)
# → https://httpbin.org/get?search=python&page=2
# Method 2: Positional argument (valid but less readable)
resp = requests.get(
"https://httpbin.org/get",
{"search": "python", "page": 2},
)
print(resp.url)
# → https://httpbin.org/get?search=python&page=2
# Method 3: Combining with existing URL query strings
resp = requests.get(
"https://httpbin.org/get?existing=1",
params={"new": "2"},
)
print(resp.url)
# → https://httpbin.org/get?existing=1&new=2
Avoid ambiguous positional calls that include timeout values without keywords:
# ❌ Incorrect: TypeError or misinterpretation
resp = requests.get(
"https://httpbin.org/get",
{"search": "python"}, # This is params
5 # This causes TypeError (unexpected positional arg)
)
Summary
- The
requests.get()function acceptsparamsas its second positional argument or as an explicit keyword argument. - In
src/requests/models.py, theprepare_url()method encodes theparamsdictionary and appends it to the base URL, merging with any existing query strings. - Parameters fail to appear when passed as a third positional argument (causing a
TypeError), when the keyword is misspelled (e.g.,param), or when using a customPreparedRequestthat bypasses the encoding logic. - Always use explicit keyword syntax (
params={...}) alongside other options liketimeoutto avoid ambiguity.
Frequently Asked Questions
Why does this behave differently from Axios?
While both libraries use a params configuration object, Axios requires params to be nested inside a config object as the second argument (e.g., axios.get(url, {params: {...}})), whereas Python requests accepts params as a top-level argument (e.g., requests.get(url, params={...})). Passing a plain object as the second argument to Axios sends it as the request body or config, not as URL parameters, which explains the confusion between the two libraries.
Can I pass params as the second positional argument safely?
Yes, requests.get("https://api.com", {"key": "val"}) works correctly because the function signature get(url, params=None, **kwargs) assigns the dictionary to the params parameter. However, this approach is less readable and breaks if you add a third positional argument (such as a timeout value), which triggers a TypeError. For maintainability, always use the explicit keyword syntax params={...}.
What happens if I mix URL query strings and the params argument?
The prepare_url() method in src/requests/models.py merges the two sources. If your URL already contains ?existing=1 and you pass params={"new": "2"}, the final URL becomes ?existing=1&new=2. The library handles the & concatenation automatically, ensuring no query parameters are lost during the merge.
Why are my params not URL-encoded correctly?
The _encode_params() method handles encoding via urllib.parse.urlencode (accessed through internal utilities in src/requests/_internal_utils.py). If you pass a nested dictionary or a custom object that doesn't implement the mapping protocol, the encoding might fail or produce unexpected results. Ensure you pass flat dictionaries {"key": "value"} or lists of tuples [("key", "value")] for proper URL encoding.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →