“Response object is not subscriptable” means you used square brackets on a Response, not on its parsed body.
You’ll see this error when code treats a response wrapper like a dict or list. It shows up a lot with requests, Flask/Django responses, and newer SDKs that return typed objects. The fix is simple once you spot what type you actually have.
This guide walks you through the common triggers, the fastest checks, and clean patterns you can reuse so the error stays gone.
What “Response Object Is Not Subscriptable” Means In Plain English
Python only lets you use square brackets on things that implement indexing, like lists, tuples, and dicts. A Response object is a container that holds metadata (status code, headers, timing) plus a body. That container is not meant to be indexed.
So when you write code like res["id"], Python throws a TypeError. You meant “grab the parsed body and then index into that.”
Spot The Tell-Tale Line
The traceback usually points at a single bracket access. It may be direct, or it may be a chained expression like requests.get(... )["data"]. Your job is to split the steps so you can see what each step returns.
- Print The Type — Add
print(type(res), res)right before the failing line to confirm what you’re holding. - Check The Body Method — Look for
.json(),.text,.content, or SDK-specific fields that expose the payload. - Read The Error Carefully — If it says
'Response'it is the wrapper; if it says'NoneType'the wrapper body may be empty or a prior step returnedNone.
Fast Fixes For Requests, HTTPX, And Similar Clients
If you’re using requests or httpx, the fix is usually one line: parse the response before subscripting. The right parser depends on what the server returns.
When The Server Returns JSON
Most APIs return JSON. In that case, turn the payload into a Python object first, then use brackets.
- Call json() — Use
data = res.json()to get a dict or list. - Index The Parsed Data — Use
data["id"]ordata[0], depending on the shape. - Handle Bad JSON — Wrap parsing in
try/exceptso HTML error pages don’t crash your flow.
import requests
res = requests.get("https://api.example.com/items/42", timeout=15)
res.raise_for_status()
data = res.json()
item_id = data["id"]
name = data.get("name")
When The Server Returns Text Or HTML
If the endpoint returns text, res.text is your friend. If it returns bytes (files, images), use res.content or stream the response.
- Use text For Strings — Read
body = res.textfor UTF-8-ish responses. - Use content For Bytes — Read
blob = res.contentwhen you need raw bytes. - Parse After Reading — Convert the body into the structure you want, then index into that structure.
When The Endpoint Returns CSV Or A File Download
Some endpoints return CSV, PDF, or a zipped export. In that case you still start with the Response wrapper, but you read the body as bytes or as a stream, then you parse it with the right tool for the format.
- Read A Small Sample — Use
res.content[:200]to confirm you got data that matches the file type. - Save To Disk Safely — Write with
open(path, "wb")and use a fixed filename, not one taken from user input. - Parse With A Reader — Feed CSV into
csv.DictReader, or load JSON lines row by row if the export is large.
When You Expected JSON But Got Something Else
This is the sneaky case: your code calls res.json(), but the server sends an error page or an empty body. Then json() raises, or you parse a shape you didn’t expect. Fix it by checking status codes and content type before you index.
| What You Have | Quick Check | Next Move |
|---|---|---|
| Response wrapper | type(res) shows Response |
Parse with res.json(), res.text, or res.content |
| Parsed dict | isinstance(data, dict) |
Use data["key"] or data.get("key") |
| Parsed list | isinstance(data, list) |
Use data[index] and validate length first |
Fixing Response Object Is Not Subscriptable In Requests And APIs
The phrase “response object is not subscriptable” often appears after you copy a snippet from an old tutorial, then upgrade a library. Many libraries moved from “dict-like responses” to typed response models that use attributes.
If your code worked last year and broke after a package bump, check the return type. The fix is rarely a downgrade. Most of the time you just need to switch from brackets to dot-access, or convert the object into a dict in a single step.
Typed Responses Use Attributes, Not Brackets
SDKs that use data models often return objects like ChatCompletion or Message. Those objects may still contain JSON-ish data inside, but you access it with properties. Read the library docs for the exact field names, then follow the chain.
- Inspect The Object — Print
dir(obj)or use your IDE’s autocomplete to see available fields. - Follow The Data Path — Access nested values with dots, then only use brackets on real dicts or lists.
- Convert When Needed — Many model objects provide a method that dumps to a plain dict.
When The “Response” Is A Web Framework Response
In Flask and Django, you can end up with a Response object when you meant to return plain data. If you pass that Response into code that expects a dict, bracket access fails. Fix it by extracting the body, or by changing your view to return data in the right shape.
- Use get_json In Flask — Call
request.get_json()for inbound JSON, and return dicts viajsonify. - Use response.json In Django — If you’re calling an outbound API, parse the client response first, then build your Django response.
- Keep Layers Separate — Treat “HTTP response to the browser” and “data you work with” as two different things.
Debug Steps That Find The Real Cause In Minutes
Fixing the bracket line is easy. The harder part is making sure you’re indexing the right shape. The next steps help you catch mismatched payloads, auth failures, and silent redirects that return HTML.
Start With Status, Headers, And A Small Body Peek
Before you parse, print the status code and a slice of the body. You’ll spot 401/403 auth issues, 500 errors, and “login page returned” mistakes right away.
- Print status_code — A non-2xx code means your parsing path needs extra care.
- Check Content-Type — Look for
application/jsonbefore callingjson(). - Peek At The Body — Print
res.text[:300]so you see what came back without dumping a full page.
Validate The Shape Before You Index
Even when JSON parsing works, the payload might be a list one day and a dict the next, or the value you want might be nested. Add guards that fail with a clear message.
- Use isinstance — Check
dictvslistand branch cleanly. - Use get For Optional Keys —
data.get("id")won’t crash if the key is missing. - Raise On Missing Data — If a field must exist, raise a custom error that prints the payload.
Log The Request You Actually Sent
Lots of “wrong response shape” bugs come from a wrong URL, missing headers, or a swapped base path. Log the request inputs so you can compare them with the API docs.
- Print The URL — Include query params after they’re encoded, not just the base path.
- Print The Method — GET vs POST mismatches often change the response format.
- Print The Auth Mode — Token in header vs query string matters for many APIs.
When Timeouts And Retries Hide The Real Response
Retries can swap a clean JSON reply for a cached error page, and timeouts can leave you with a partial body. Set a timeout, log the attempt count, and save one failing payload to inspect. If the API uses rate limits, read the headers and back off between tries so you don’t hammer the same bad request.
Patterns That Prevent The Error From Coming Back
Once you fix it, bake the pattern into helper functions. That way every call gets consistent parsing, error messages, and safe defaults.
Wrap Parsing In One Function
A small helper keeps your code tidy and stops bracket mistakes. It can also catch non-JSON bodies and print a short preview when parsing fails.
import json
def read_json(res):
res.raise_for_status()
ctype = res.headers.get("Content-Type", "")
if "application/json" not in ctype:
snippet = (res.text or "")[:300]
raise ValueError(f"Expected JSON, got {ctype}. Body starts: {snippet!r}")
return res.json()
With that helper, you stop writing res["x"]. You write data = read_json(res), then index data.
Prefer Named Variables Over Chained Calls
Chaining hides types. Split work into two or three lines so you can print the intermediate values when things go sideways.
- Store The Response — Save
resto inspect headers, status, and raw body. - Store The Parsed Data — Save
dataas a dict or list, then work from there. - Store The Target Field — Pull out the field you need and validate it before using it downstream.
Use Safer Access For Nested Data
APIs often nest data. A chain like data["a"]["b"]["c"] can crash if any level is missing. A tiny getter avoids ugly try blocks.
def pick(d, *path):
cur = d
for key in path:
if not isinstance(cur, dict) or key not in cur:
return None
cur = cur[key]
return cur
Then you can do token = pick(data, "auth", "token") and check whether it’s None before you proceed.
Common Traps And Clean Fixes
This error shows up in a handful of repeatable scenarios. If you match the pattern, the fix is quick.
Trap: You Indexed The Response Instead Of The Payload
- Parse First — Use
data = res.json(). - Index Second — Use
data["field"]. - Add A Guard — Check status and content type before parsing.
Trap: You Got Redirected To HTML
- Follow Redirects Intentionally — Let the client follow redirects, or block them and handle 3xx codes yourself.
- Confirm The Endpoint — A missing trailing slash can send you to a different route.
- Check Auth — Many services return an HTML sign-in page when a token is missing.
Trap: The Value You Want Is Under A Different Key
Docs and real payloads drift. Print the parsed payload once, then update your access path to match what the API sends now.
- Print The Top Keys — Use
print(list(data)[:20])for dicts. - Scan Nested Data — Pretty-print with
json.dumps(data, indent=2)[:800]. - Use get During Migration — Swap to strict indexing after you confirm the shape in production logs.
If you’re still stuck, search your codebase for the exact string response object is not subscriptable in logs. It often repeats at the same call site, and that one file is the real culprit.
Once you switch to “parse, then index,” the error fades away. You get cleaner code, clearer exceptions, and fewer late-night surprises.
