Ch. 9 · Python

TypeError: 'NoneType' object is not subscriptable in Python

Fix TypeError: 'NoneType' object is not subscriptable: missing returns, list.sort(), failed re.match, dict.get and Optional type checks.

~7 min readbeginnerupdated Oct 4, 2026

The code indexes a value with [...] and Python stops with this (Python 3.14.7, path shortened; 3.12 and 3.13 print the same text):

Traceback (most recent call last):
  File "none1.py", line 8, in <module>
    print(user["email"])
          ~~~~^^^^^^^^^
TypeError: 'NoneType' object is not subscriptable
Text

user holds None, and None cannot be indexed. Its twin, AttributeError: 'NoneType' object has no attribute 'group', is the same bug with a dot instead of square brackets. The line in the traceback is where the None was used; the real fix is almost always where it was produced.

Quick fix checklist

  • Look at the ~~~^^^ markers under the failing line (Python 3.11+) to see exactly which expression was None.
  • Find where that variable was assigned and ask “can this return None?”
  • Make sure the function returns a value on every path; a function that reaches its end without return returns None.
  • Never assign the result of an in-place method: items = items.sort() sets items to None. Use items.sort() alone, or sorted(items).
  • Check re.match(...) / re.search(...) results before calling .group().
  • dict.get(key) returns None for a missing key: pass a default, or use d[key] if the key is required.
  • Annotate with -> X | None and run mypy or pyright to catch the remaining cases.

Before you start

You should be comfortable with functions, return, lists and dictionaries. The examples were run with Python 3.14.7, mypy 2.4.0 and pyright 1.1.414. On Python 3.10 and older the messages are identical but the ~~~^^^ position markers are missing, so you need to work out which subscription failed yourself.

Why it happens

obj[key] calls type(obj).__getitem__. Lists, dicts, strings and tuples define it; NoneType does not, so Python raises TypeError. Each operation you attempt on None produces its own wording, which is useful for recognising the family:

'NoneType' object is not subscriptable          # x[0]
'NoneType' object has no attribute 'append'     # x.append(1)
'NoneType' object does not support item assignment   # x[0] = 1
'NoneType' object is not iterable               # for i in x
object of type 'NoneType' has no len()          # len(x)
Text

None reaches your variable in four common ways:

  1. Implicit return. A function whose last statement is not return value, or which has a branch that skips the return, gives back None. find_user below only returns inside the loop, so a missing user yields None.
  2. In-place methods. list.sort(), list.append(), list.reverse(), dict.update() and random.shuffle() mutate the object and return None on purpose, so you cannot mistake them for functions that return a new object.
  3. “Maybe” APIs. re.match, re.search, dict.get, os.environ.get, shutil.which, next(it, None) and database fetchone() all return None when there is nothing to return.
  4. Defaults. A parameter or attribute initialised to None that nobody filled in.
def find_user(users, name):
    for u in users:
        if u["name"] == name:
            return u
    # falling off the end returns None

users = [{"name": "ana", "email": "ana@example.com"}]
user = find_user(users, "bob")
print(user["email"])   # TypeError: 'NoneType' object is not subscriptable
python

Step-by-step walkthrough

Step 1: Find which expression was None

With chained lookups, the markers matter. Here config has no "cache" key:

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

The ~~~ part is the object being indexed (config.get("cache")) and the ^^^ part is the subscript (["port"]). So config.get("cache") was None, not config.

Step 2: Trace it back to where it was assigned

Put breakpoint() on the line before the failure and inspect the value, or add a temporary print(repr(value)). Then read the code that produced it. For a function call, check every path through the function: loops that may not match, if branches without an else, early return statements with no value, and except blocks that log and carry on.

Step 3: Fix it at the source, by category

# 1. Missing return path: decide what "not found" means and make it explicit
def find_user(users, name):
    for u in users:
        if u["name"] == name:
            return u
    raise LookupError(f"no user named {name!r}")

# 2. In-place method: don't assign the result
scores = [3, 1, 2]
scores.sort()             # or: ranked = sorted(scores)
print(scores[0])          # 1

# 3. Regex: test the match before using it
import re
if (m := re.match(r"(\d+)-(\d+)", "10-20")) is not None:
    print(m.group(1))     # 10

# 4. dict.get: supply a default, or index directly if the key is required
port = config.get("cache", {}).get("port", 6379)
python

Which fix is right depends on meaning. If a missing user is a bug, raising an exception at the source gives a clearer error than a TypeError three calls later. If “no result” is normal, return None deliberately and make every caller handle it.

Step 4: Make the type checker find the rest

Annotate functions that can return nothing as X | None (or Optional[X]) and run a checker. Here users.py is the find_user example with -> dict[str, str] | None added, and tags.py calls .group() on an unchecked re.match result. Both checkers reject the unchecked use:

$ mypy users.py
users.py:9: error: Value of type "dict[str, str] | None" is not indexable  [index]

$ pyright users.py tags.py
  tags.py:3:9 - error: "group" is not a known attribute of "None" (reportOptionalMemberAccess)
  users.py:9:7 - error: Object of type "None" is not subscriptable (reportOptionalSubscript)
Text

mypy also catches the in-place trap (scores = scores.sort()), because it knows list.sort() returns None:

scores.py:2: error: Incompatible types in assignment (expression has type "None", variable has type "list[int]")  [assignment]
Text

Worked scenario

A release script compares version tags. It works for 2.1.0 and crashes for tags with a v prefix:

import re

VERSION_RE = re.compile(r"(\d+)\.(\d+)\.(\d+)")


def parse_version(tag: str):
    match = VERSION_RE.match(tag)
    if match:
        return tuple(int(part) for part in match.groups())


def is_newer(tag: str, current: tuple[int, int, int]) -> bool:
    return parse_version(tag)[0] > current[0]

print(is_newer("2.1.0", (1, 9, 3)))    # True
print(is_newer("v2.1.0", (1, 9, 3)))
python
  File "release.py", line 13, in is_newer
    return parse_version(tag)[0] > current[0]
           ~~~~~~~~~~~~~~~~~~^^^
TypeError: 'NoneType' object is not subscriptable
Text

Diagnosis. The markers point at parse_version(tag). re.match anchors at the start of the string, so "v2.1.0" does not match, the if is skipped, and the function falls off the end. Running mypy on this file reports Success: no issues found, because parse_version has no return annotation, so mypy does not check its callers’ use of the result. A second bug hides here too: comparing only the major number ([0]) says 1.10.0 is not newer than 1.9.3.

Fix. Accept the prefix, return None explicitly with an honest annotation, and handle the None in the caller:

import re

VERSION_RE = re.compile(r"v?(\d+)\.(\d+)\.(\d+)")


def parse_version(tag: str) -> tuple[int, int, int] | None:
    if match := VERSION_RE.fullmatch(tag):
        major, minor, patch = (int(part) for part in match.groups())
        return major, minor, patch
    return None


def is_newer(tag: str, current: tuple[int, int, int]) -> bool:
    version = parse_version(tag)
    if version is None:
        raise ValueError(f"not a version tag: {tag!r}")
    return version > current
python

Comparing whole tuples handles minor and patch numbers too. With the annotation in place, deleting the if version is None check makes mypy report Value of type "tuple[int, int, int] | None" is not indexable, so the bug cannot quietly return.

Common mistake

The tempting fix is to silence the symptom:

try:
    port = config.get("cache")["port"]
except TypeError:
    port = 6379
python

This hides the real question (should cache exist?) and also swallows unrelated TypeErrors from the same line. Two other shortcuts cause new bugs:

  • if not result: instead of if result is None: treats 0, "", [] and {} as missing. An empty list of users is not the same as “the lookup failed”.
  • value = value or {} has the same problem and can replace a legitimately empty object the caller passed in. Use if value is None: value = {}, which is also how you avoid the shared-default trap in mutable default arguments.

Verify the behavior

Write a check for the input that used to fail, and for the “not found” path, so both branches are exercised:

from versions import parse_version, is_newer

assert parse_version("v2.1.0") == (2, 1, 0)
assert parse_version("latest") is None
assert is_newer("1.10.0", (1, 9, 3))
try:
    is_newer("latest", (1, 9, 3))
except ValueError:
    pass
else:
    raise AssertionError("expected ValueError")
print("all checks passed")
python
$ python3 test_versions.py
all checks passed
$ mypy --strict versions.py
Success: no issues found in 1 source file
Text

Adding mypy or pyright to CI keeps it that way.

Interview exercise

“What does result = [3, 1, 2].sort() leave in result, why was the API designed that way, and how would you design a function that might not find what it is looking for?”

Answer and reasoning

result is None. list.sort() sorts the list in place and returns None deliberately: the Python docs explain that in-place methods return None so you cannot confuse them with a call that produces a new object. sorted() is the version that returns a new list. Mutate-or-return is a form of command-query separation.

For a lookup that can fail there are three reasonable designs. Return X | None when “not found” is normal and callers are expected to branch on it; annotate it so type checkers force the check. Raise an exception (KeyError, LookupError or a domain error) when absence means something is wrong, so the failure surfaces at the source with a useful message rather than as a 'NoneType' object is not subscriptable somewhere downstream. Or accept a default argument, as dict.get(key, default) does, when there is a sensible fallback. The wrong design is the accidental one: a function that returns None only because one branch forgot its return.

Continue learning

Drill these patterns with the Python interview questions and the Python MCQs. Related notes: mutable default arguments, exception chaining for raising clear errors at the source, and KeyError: ‘x’ in Python for the case where the dictionary exists but the key does not. Official references: the Python docs on list.sort(), re.match, the TypeError exception and mypy’s optional types and the None type.

More in Python

read ✓Python · hard

Python asyncio.gather and Timeouts

Run coroutines concurrently with asyncio.gather, enforce timeouts, and handle partial failures and cancellation correctly.

~2 min readread →
esc