2025, Oct 31 03:00

The Right Way to Send JSON with Python requests: Use Native Dicts, json.dumps, or the json= Param

Learn how to build valid JSON payloads in Python requests: construct dicts, let json.dumps or the json= parameter handle serialization, escaping, and headers.

When you build a JSON payload for Python’s requests and try to drop variables into a triple-quoted string, it looks convenient at first. In practice, it’s fragile, easy to break, and fails as soon as the structure changes or values contain characters that need escaping. A typical case: you want to make MAC and NAME dynamic while iterating over a CSV. The right way is to construct data as Python objects and serialize them—without hand-formatting JSON.

Problem example

Here’s a pattern that often causes trouble when turning variables into a JSON body:

payload = '''{
    "clients": [
        {
            "mac": HW_MAC,
            "name": HOST_ALIAS
        },
    ],}'''

It “looks” like JSON, but it isn’t valid JSON, and the placeholders won’t be interpreted as variables. On top of that, trailing commas and incorrect braces will break parsing.

What actually goes wrong

Manually formatting JSON is error-prone. Trailing commas after the last element are invalid and cause parse errors. If you ever try to use an f-string to inject variables, curly braces have to be doubled to avoid being treated as format markers. Most importantly, string interpolation won’t handle required escaping for you; values with quotes or special characters will produce invalid output. A serializer will do this correctly and consistently.

Solution: build a Python object and serialize with json.dumps

Construct a plain Python dict or list, then turn it into a JSON string using json.dumps. This approach keeps your data types intact as you iterate, while the serializer guarantees valid JSON and proper escaping.

import json
HW_MAC = 'xxxxxxxxxxxx'
HOST_ALIAS = 'example'
request_body = json.dumps({
  "clients": [
    {"mac": HW_MAC, "name": HOST_ALIAS}
  ]
}, indent=2)
print(request_body)

Output:

{
  "clients": [
    {
      "mac": "xxxxxxxxxxxx",
      "name": "example"
    }
  ]
}

As you iterate over your CSV rows, assign the current values to variables like HW_MAC and HOST_ALIAS, rebuild the dict, and serialize again. That’s all you need—no fragile string concatenation or manual quoting.

Do you need a JSON string at all?

Depending on the HTTP method and endpoint, you might not need to stringify anything yourself. For example, a GET can pass query parameters via params, and a PUT can send form-encoded data via data. In both cases, requests handles encoding.

import requests
query_args = {"key1": "value1", "key2": "value2"}
resp = requests.get("https://httpbin.org/get", params=query_args)
import requests
resp = requests.put("https://httpbin.org/put", data={"key": "value"})

You can also pass a Python dict using the json= argument, and requests will serialize it to JSON and set the Content-Type header automatically.

import requests
resp = requests.get("https://httpbin.org/get", json={"some": "data"})

Why this matters

Letting a serializer produce the JSON eliminates a class of subtle bugs. It ensures the structure is correct, trailing commas don’t slip in, and all necessary characters inside strings are properly escaped. You get cleaner code that’s easier to maintain when the payload evolves.

Takeaways

Stop hand-crafting JSON strings for requests. Use native Python structures and json.dumps when you genuinely need a JSON string, or pass data directly with params, data, or json as appropriate. This keeps your payloads valid, your intent clear, and your code resilient as you iterate over dynamic values like MAC and NAME.

The article is based on a question from StackOverflow by Joseph Jenkins and an answer by Mark Tolonen.