By Ryan Calloway. Updated May 2026.
The five-year-old r/learnpython “help me understand” thread on this error still gets linked from new replies almost weekly because it captures the bug’s lifecycle perfectly: the OP has cd.value failing on a subscript, and the reply chain agrees in two messages — “cd.value isn’t None at first. Then it gets set to None somewhere before the last line you posted.” That is the entire shape of the error. A function returned a value upstream, you indexed it downstream, and somewhere in between the value flipped to None. Five sources cover almost every real instance, and Python 3.11+ tells you exactly which character caused the failure.
Quick answer
The variable immediately before the [ is None. Print it on the line above; trace why the producing function or lookup returned None; fix at the source. The five usual sources, in descending order of how often they show up: a function with a missing return, an in-place list method (.sort(), .reverse(), .append()) used as if it returned the modified list, a regex/next() that did not match, chained dict.get() calls without defaults, and a list comprehension that calls a function returning None on some inputs. The fix is rarely to wrap the call in try/except; it is to fix the producer.
How to diagnose in 30 seconds
- Read the traceback. On Python 3.11+ the caret (
~~~~~^^^) under the failing line points at the exact subscript that crashed — the variable to the left of the[is yourNone. - On the line above, paste
print("DEBUG", type(x), x)and re-run. You will see<class 'NoneType'> None. - Find where
xwas last assigned. One of the five causes below applies. - Fix at the source, not at the call site.
if x is None: x = []at the call site usually hides a real bug.
Python 3.13 colors the traceback by default (controlled via PYTHON_COLORS/NO_COLOR), per the 3.13 release notes; the caret line stands out from the rest. PEP 657 added the precise location indicators in 3.11. If you are still on 3.10, this single feature is worth the upgrade.
Cause #1: a function (or one of its branches) forgot to return
The most common source by a wide margin.
# Repro
def load_users(path):
with open(path) as f:
data = json.load(f)
# forgot to `return data`
users = load_users("users.json")
first = users[0]["name"]
Traceback (most recent call last):
File "app.py", line 7, in <module>
first = users[0]["name"]
~~~~~^^^
TypeError: 'NoneType' object is not subscriptable
The branchier version is more insidious. A function with one return path that handles “found” implicitly returns None when the loop finishes without finding anything:
# Repro
def find_user(user_id):
for u in users:
if u["id"] == user_id:
return u
# falling off the end implicitly returns None
u = find_user(99)
print(u["name"]) # TypeError when 99 not found
# Fix: be explicit about the not-found path
def find_user(user_id) -> dict | None:
for u in users:
if u["id"] == user_id:
return u
return None
u = find_user(99)
if u is None:
raise LookupError(f"user {user_id} not found")
print(u["name"])
The dict | None return annotation (Python 3.10+) is the version of Optional[dict] most teams use now. With mypy --strict or basedpyright, indexing u["name"] without the None guard fails the type check before runtime.
Cause #2: in-place list methods return None
The classic. list.sort(), list.reverse(), list.append(), list.extend(), set.add(), dict.update() all mutate in place and return None, not the modified container. Assigning the result almost never does what you wanted.
# Repro
scores = [3, 1, 2]
sorted_scores = scores.sort() # returns None
top = sorted_scores[0] # TypeError
Traceback (most recent call last):
File "app.py", line 3, in <module>
top = sorted_scores[0]
~~~~~~~~~~~~~^^^
TypeError: 'NoneType' object is not subscriptable
# Fix option 1: use the function form, which returns a new list
sorted_scores = sorted(scores)
top = sorted_scores[0]
# Fix option 2: mutate in place, then index the original
scores.sort()
top = scores[0]
# Same shape applies to reverse:
reversed_scores = list(reversed(scores)) # function form, returns iterator
scores.reverse() # in place, returns None
The same trap shows up with chained calls in expressions:
# Wrong: append returns None, so the result is None
last = data.append(new_item)[-1]
The mental model: any method named with the imperative tense (append, extend, sort, reverse, add, update) probably mutates and returns None; the function-form equivalent (sorted, reversed) returns a new value. The stdtypes docs list which methods return what.
Cause #3: regex (and next()) returns None on miss
re.match, re.search, re.fullmatch all return None when the pattern does not match. next(iter, None) with a default returns None on an empty iterator. dict.get(key) without a default returns None on a missing key.
# Repro
import re
m = re.match(r"(\d+)-(\d+)", "abc")
year = m.group(1) # TypeError when no match
# Fix: check before using
m = re.match(r"(\d+)-(\d+)", input_str)
if m is None:
raise ValueError(f"unexpected format: {input_str!r}")
year = m.group(1)
# Or with the walrus operator (3.8+)
if (m := re.match(r"(\d+)-(\d+)", input_str)) is not None:
year = m.group(1)
The same fix shape applies to every API whose docstring says “returns None on failure”: check the return value, then use it. The walrus operator pattern reads cleanly when the match is used inside the conditional body.
Cause #4: chained dict.get() without defaults
dict[key] raises KeyError. dict.get(key) returns None when the key is missing. People reach for .get() to silence the KeyError, then get bitten two lines later when they index the result.
# Repro
config = {"app": {"name": "demo"}}
db_host = config.get("database").get("host") # TypeError: "database" missing
# Fix option 1: give .get() a default at every level
db_host = config.get("database", {}).get("host", "localhost")
# Fix option 2: use a Pydantic model so missing keys fail at load time
from pydantic import BaseModel
class DBConfig(BaseModel):
host: str = "localhost"
port: int = 5432
class Config(BaseModel):
database: DBConfig = DBConfig()
cfg = Config.model_validate(raw)
db_host = cfg.database.host # always a string
Pydantic v2 validates the structure at load time and the rest of your code reads typed attributes instead of guessed dict lookups. The dataclasses vs Pydantic guide covers when each tool fits.
Cause #5: list comprehension calls a function that returns None
Harder to spot than the others because the traceback points at the comprehension, not at the function that returned None.
# Repro: first_admin returns None when no admin in the team
def first_admin(team):
for u in team:
if u["role"] == "admin":
return u
# implicit None
teams = [[{"role": "dev"}], [{"role": "admin", "name": "Ada"}]]
admin_names = [first_admin(t)["name"] for t in teams]
Traceback (most recent call last):
File "app.py", line 8, in <module>
admin_names = [first_admin(t)["name"] for t in teams]
~~~~~~~~~~~~~~^^^^^^^^
TypeError: 'NoneType' object is not subscriptable
# Fix option 1: filter first using the walrus to call once
admin_names = [
a["name"]
for t in teams
if (a := first_admin(t)) is not None
]
# Fix option 2: change first_admin to be honest about misses and use itertools
from itertools import filterfalse
candidates = (first_admin(t) for t in teams)
admin_names = [a["name"] for a in candidates if a is not None]
The walrus operator (:=, Python 3.8+) is the one comprehension upgrade worth learning for exactly this case — it lets you call first_admin once, bind the result, and reuse it inside the comprehension without a second call.
Decision tree: which cause is yours?
The variable before the [ came from… |
Most likely cause |
|---|---|
| A function call | #1 missing or implicit return |
An assignment of x = list.sort() / append / reverse / etc. |
#2 in-place method returns None |
re.match, re.search, next(), dict.get() without default |
#3 / #4 missing match or key |
A chained .get(...).get(...) on nested config |
#4 chained dict.get |
| Inside a list/dict comprehension | #5 comprehension over a function that misses |
Catch it before runtime
Two tools that eliminate this entire bug class:
- Type-check with mypy
--strictor basedpyright. Annotate functions with-> dict | Nonewhen a miss is possible. The type checker rejects subscripting on the returned value until you guard withif x is Noneorassert x is not None. - Lint with Ruff. Several rules catch the in-place-method trap (
RUF005,RUF015family) and chained.getpatterns.
The combination — explicit return types, a strict type checker, Ruff on save — pushes the error from the runtime traceback to a red squiggle in your editor. The bug class largely disappears.
FAQ
Why does Python say “NoneType” instead of just “None”?
NoneType is the class of the singleton None, the same way int is the class of integers and str is the class of strings. Python is reporting the type of the offending object, which happens to have only one possible value.
What does “not subscriptable” mean?
Subscriptable means the object supports the [ ] syntax. Lists, dicts, tuples, and strings are subscriptable. None, integers, booleans, and functions are not. The error says you tried [ ] on something that does not support it.
How do I find which variable is None?
Read the traceback. On Python 3.11+, the caret indicator points at the exact subscript that failed. The variable immediately to the left of the [ is your None. Print its type and value on the line above to confirm.
Can I catch TypeError with try/except to work around it?
Yes, technically. But the error almost always marks a real bug upstream. Catching it hides the bug; the next teammate trips on the same code path with a different symptom in a week. Fix the function that returned None when it should have returned a value (or a real default).
Does Python 3.13 change this error?
The error message text is the same in Python 3.8 through 3.13. What changed: 3.11 added the caret indicator (PEP 657) under the exact subscript; 3.13 colors the traceback by default. Both make finding the None faster.
Is this related to NoneType has no attribute X?
Same root cause, different surface. 'NoneType' object has no attribute 'name' means you wrote x.name; 'NoneType' object is not subscriptable means you wrote x["name"]. The five causes above produce both, depending on how you accessed the value.
Sources and further reading
- Python docs —
TypeError - Python docs — Mutable sequence types (which methods return
None) - What’s new in Python 3.13 — colored tracebacks
- What’s new in Python 3.11 — fine-grained error locations (PEP 657)
- PEP 657 — Include Fine Grained Error Locations in Tracebacks
- Pydantic v2 documentation
- Ruff — fast Python linter
Next steps
If this error keeps catching you on your own modules, the underlying cause is often a venv or path problem; ModuleNotFoundError is the next piece. For the type-system upgrade that kills this bug class at import time, see the dataclasses vs Pydantic guide. For the sister error elsewhere in the Python troubleshooting series, see SyntaxError: invalid syntax — the 7 most common causes.