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 params as a third positional argument: Because requests.get() only accepts url and params positionally, adding a timeout value positionally (e.g., requests.get(url, params, 5)) triggers a TypeError or causes the dictionary to be misinterpreted depending on the library version.

  • Misspelling the keyword: Using param (singular) instead of params bypasses the encoding logic entirely, as the function receives an unexpected keyword argument that gets swallowed by **kwargs but never processed as query parameters.

  • Passing None or an empty dictionary: While technically valid, params=None or params={} results in no query string being appended, which can appear as a failure if the developer expects default behavior.

  • Custom PreparedRequest subclasses: Overriding prepare_url() without calling super().prepare_url() or failing to handle the params argument 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 accepts params as its second positional argument or as an explicit keyword argument.
  • In src/requests/models.py, the prepare_url() method encodes the params dictionary 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 custom PreparedRequest that bypasses the encoding logic.
  • Always use explicit keyword syntax (params={...}) alongside other options like timeout to 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →