Python

TypeError: 'NoneType' object is not subscriptable in Python

Python · Published 2026-10-24

An open, empty metal mailbox compartment above a locked one numbered 11
An apartment mailbox left open, with nothing inside.

TypeError: 'NoneType' object is not subscriptable means you used square brackets (x[0], x["key"]) on a value that is None. Something earlier returned None instead of the list, dict or match object you expected; the fix is to find that something and either make it return a real value or check for None before indexing.

What the error means

"Subscriptable" means the object supports obj[key], i.e. it implements __getitem__. Lists, tuples, strings, dicts and re.Match objects do; None does not. The error is never about the index itself; it is about the object in front of the brackets. Related messages come from the same root cause:

Operation on NoneError
x["a"]TypeError: 'NoneType' object is not subscriptable
x["a"] = 1TypeError: 'NoneType' object does not support item assignment
for i in xTypeError: 'NoneType' object is not iterable
len(x)TypeError: object of type 'NoneType' has no len()
x.append(1)AttributeError: 'NoneType' object has no attribute 'append'

The usual causes

1. Methods that modify in place return None

The classic case. list.sort(), list.append(), list.reverse(), dict.update(), random.shuffle() and pandas methods called with inplace=True all mutate the object and return None. Assigning their result throws away your data:

scores = [42, 7, 19]
top = scores.sort(reverse=True)
print(top)
print(top[0])
None
Traceback (most recent call last):
  File "t1.py", line 4, in <module>
    print(top[0])
          ~~~^^^
TypeError: 'NoneType' object is not subscriptable

Either use the function that returns a new object, or call the method and keep using the original name:

words = ["pear", "fig", "apple"]
print(sorted(words)[0])          # sorted() returns a new list
words.sort()                     # sort() sorts in place, returns None
print(words[0])
apple
apple

The same applies to pandas: df.sort_values("score", inplace=True) returns None. Prefer the non-inplace form, df = df.sort_values("score").

2. A function that forgets to return

A Python function without a return statement, or one where some branch falls off the end, returns None implicitly:

def load_user(user_id):
    users = {1: {"name": "Ada"}, 2: {"name": "Linus"}}
    if user_id in users:
        return users[user_id]
    # falls off the end -> returns None

print(load_user(3)["name"])
    print(load_user(3)["name"])
          ~~~~~~~~~~~~^^^^^^^^
TypeError: 'NoneType' object is not subscriptable

Decide what the "not found" case should do: return a default, raise a specific exception (KeyError, LookupError, a custom error), or return None deliberately and document it with a type hint (see below).

3. dict.get() on a missing key

dict.get(key) returns None when the key is absent, so chaining an index after it fails:

config = {"db": {"host": "localhost"}}
port = config.get("cache")["port"]
    port = config.get("cache")["port"]
           ~~~~~~~~~~~~~~~~~~~^^^^^^^^
TypeError: 'NoneType' object is not subscriptable

Pass a default of the right type so the chain keeps working:

port = config.get("cache", {}).get("port", 6379)
print(port)
6379

4. re.match() and re.search() found nothing

The regex functions return None when there is no match. re.match only matches at the start of the string, which surprises people:

import re
m = re.match(r"(\d+)-(\d+)", "version 3-14")
print(m[1])
    print(m[1])
          ~^^^
TypeError: 'NoneType' object is not subscriptable

Check the match before using it. The walrus operator (Python 3.8+) keeps this compact:

if (m := re.match(r"(\d+)-(\d+)", "3-14 release")) is not None:
    print(m[1], m[2])
3 14

5. JSON null and API responses

json.loads turns JSON null into Python None. The key exists, so .get() with a default does not help:

import json
payload = json.loads('{"user": {"name": "Ada", "address": null}}')
city = payload["user"]["address"]["city"]
    city = payload["user"]["address"]["city"]
           ~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^
TypeError: 'NoneType' object is not subscriptable

Use or to replace both a missing key and an explicit null:

address = payload["user"].get("address") or {}
print(address.get("city", "unknown"))
unknown

Other common sources: cursor.fetchone() when the query returns no rows, os.environ.get() for an unset variable, BeautifulSoup's find() when the element does not exist, and ORM .first() methods on an empty result.

Reading the traceback

The last line names the error; the line above it shows the exact source line. Since Python 3.11 the interpreter also underlines the failing sub-expression (PEP 657): the ~~~~ part is the object that was None, the ^^^^ part is the subscript you applied to it. In payload["user"]["address"]["city"] above, the tildes cover payload["user"]["address"], so that is the value that was None, not payload or payload["user"].

On Python 3.10 and older there is no underline, so a long chain gives you only the line. Break it into steps (or inspect it in a debugger) to find which link is None:

user = payload["user"]
address = user["address"]
print(repr(address))        # None -> this is the culprit
city = address["city"]

Then ask the real question: why is it None? Go to where the variable was assigned and look for one of the causes above. Running the script with python -m pdb script.py or adding breakpoint() just before the failing line lets you inspect every variable at that point.

Defensive patterns

Let mypy find it before runtime

Annotate functions that may return nothing as Optional[T] (or T | None on Python 3.10+). A type checker such as mypy then flags every place where you index the result without checking it first. Standard-library stubs already declare that dict.get and re.match can return None.

import re
from typing import Optional


def find_user(users: dict[int, dict[str, str]], user_id: int) -> Optional[dict[str, str]]:
    return users.get(user_id)


def greeting(users: dict[int, dict[str, str]], user_id: int) -> str:
    user = find_user(users, user_id)
    return "Hello, " + user["name"]


def major_version(text: str) -> int:
    m = re.match(r"(\d+)\.", text)
    return int(m[1])
$ mypy t8.py
t8.py:11: error: Value of type "dict[str, str] | None" is not indexable  [index]
t8.py:16: error: Value of type "Match[str] | None" is not indexable  [index]
Found 2 errors in 1 file (checked 1 source file)

Adding the checks narrows the type, and mypy is satisfied:

def greeting(users: dict[int, dict[str, str]], user_id: int) -> str:
    user = find_user(users, user_id)
    if user is None:
        return "Hello, stranger"
    return "Hello, " + user["name"]      # mypy knows user is a dict here


def major_version(text: str) -> int:
    m = re.match(r"(\d+)\.", text)
    if m is None:
        raise ValueError(f"not a version string: {text!r}")
    return int(m[1])
Success: no issues found in 1 source file

mypy only checks annotated functions by default; unannotated ones are skipped unless you pass --check-untyped-defs. Pyright and the type checkers built into most editors report the same problem.

Summary

The error means the value before the brackets is None. Read the traceback (the underlined part on Python 3.11+) to find which value, then trace back to where it was assigned. The usual suspects are in-place methods such as list.sort(), functions missing a return, dict.get() on missing keys, failed re.match() calls and JSON null. Fix the source, check with is None where absence is legitimate, and annotate with Optional so mypy catches the next one for you.