Home Python TypeError: NoneType Is Not Subscriptable or Iterable
Beginner 6 min · September 23, 2026

TypeError: NoneType Is Not Subscriptable or Iterable

TypeError: 'NoneType' object is not subscriptable means you indexed None.

N
Naren Founder & Principal Engineer

20+ years shipping production Python across data and backend systems. Notes here come from systems that actually shipped.

Follow
Production
production tested
September 23, 2026
last updated
1,905
articles · all by Naren
Before you start⏱ 14 min
  • Python basics: defining functions, lists, dicts, and for loops
  • Running small scripts with python3 and reading the last traceback line
  • Familiarity with return values and basic function calls
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • The fix is at the source: add the missing return, stop assigning list.sort() or .append() back (both return None), and reject or default a None argument with if x is None.
  • Read the traceback's last line first: 'not subscriptable' means brackets on None, 'not iterable' means a loop over None, and 'not callable' means you called None.
  • Prove it with print(repr(value)) on the line above the crash, then trace that variable back to the function or call that handed you None.
  • Prevent repeats with Optional type hints plus strict mypy, so a None flowing into brackets fails in CI instead of at 2 AM.
✦ Definition~90s read
What is Python TypeError NoneType Fix?

None is Python's null object — the singleton instance of NoneType, and the only value that means 'no value here'. Every function that finishes without return value produces it. In-place mutators like list.sort() produce it deliberately. Failed lookups and absent optionals smuggle it in.

Picture a coat-check counter: you hand over a ticket and expect a coat back.

None itself is perfectly legal to pass around, store, and compare; the TypeError fires only when code demands behavior None cannot provide.

The mechanism is slot lookup. Bracket access calls type(obj).__getitem__, iteration calls __iter__, and calling calls __call__. NoneType defines none of these slots, so the interpreter raises TypeError: 'NoneType' object is not subscriptable (or iterable, or callable), naming both the type and the missing capability.

The message is precise: it never claims your syntax is wrong, only that this particular object can't do that particular thing.

What this error is NOT matters as much as what it is. It is not a NameError — your variable exists and is bound; it just holds None. It is not an AttributeError — you didn't touch a missing attribute, you invoked a missing protocol. It is not an IndexError or KeyError — no container was searched and no key was missed at the crash line.

And it is never fixed at the crash line alone, because the crash line is where None was used, not where it was born. Treat the message as a pointer upstream: something promised an object and delivered nothing, and your real fix is one frame up the stack.

Plain-English First

Picture a coat-check counter: you hand over a ticket and expect a coat back. None is the attendant shrugging and handing you nothing — empty hands, no coat. The TypeError fires when you try to wear that nothing by reaching into its pockets. The fix sits one step earlier at the counter: the attendant forgot to fetch it (a missing return), kept the coat and handed back an empty hanger (list.sort() returns nothing), or you arrived with no ticket (a None argument).

You wrote clean indexing code. The list exists, the key looks right, and yet Python insists you're subscripting nothing: TypeError: 'NoneType' object is not subscriptable. Beginners read that as an accusation against their brackets. It isn't. It's a missing-person report — the value you expected never arrived, and None showed up in its place.

This error fires whenever an operation that needs a real object meets None instead. Brackets need something subscriptable, so None["key"] dies. Loops need something iterable, so for row in None dies. Calls need something callable, so callback() dies when callback is None. Three different messages, one identical root pattern: an upstream step produced None and nobody noticed until the value was used.

The usual producers are a short list you'll memorize fast. A function that forgets return on one branch hands back None. An in-place mutator like list.sort() or .append() returns None by design, and assigning it back poisons the variable. Or None slides in as an argument — a lookup missed, a default wasn't set, a caller passed its own unchecked result down the chain.

This article walks each producer with runnable code, teaches you to read the traceback's final line like a label, and shows the guard-clause and typing habits that keep None from ever reaching your brackets again.

Implicit None Returns: the Missing return That Poisons Every Caller

Python never leaves a function empty-handed. When execution falls off the end without hitting return value, the interpreter quietly substitutes None — no warning, no error, nothing in the log. That silence is the whole danger. The caller receives a value shaped exactly like a legitimate result slot, and the crash lands wherever that result gets used: a subscript two frames up, a loop in another module, a template render far from the cause.

The pattern you'll see most is the search function with an unhandled miss. The happy path returns early inside the loop, and the author never considered the input where nothing matches. Every test passes because every test fixture contains a match. Production supplies the one input nobody imagined, the function falls through, and None flows downstream wearing the type the caller assumed.

The professional response has two halves. First, make every exit explicit: return a sentinel, an empty collection, or raise a lookup error — but never fall off the end by accident. Second, harden the caller with an identity check, if admin is None, before touching brackets. That check documents the contract for the next reader: this lookup can miss, and here's exactly what happens then. Linters can't catch intent, but an explicit return on each branch plus a test for the empty input closes the hole for good.

implicit_none_return.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def find_admin(users):
    for user in users:
        if user.get("role") == "admin":
            return user
    # Falls off the end: Python returns None.

users = [{"name": "amy", "role": "viewer"}, {"name": "bo", "role": "admin"}]
print(find_admin(users))
print(find_admin([{"name": "cy", "role": "viewer"}]))


def get_admin_name(users):
    admin = find_admin(users)
    if admin is None:
        return "no-admin"
    return admin["name"]

print(get_admin_name(users))
print(get_admin_name([{"name": "cy", "role": "viewer"}]))
📊 Production Insight
A search helper passed 40 tests because every fixture contained a match. The first production input with no match returned None and crashed the caller three frames up — the fix was one explicit return plus a test with an empty list.
🎯 Key Takeaway
Never let a function fall off the end by accident — return a sentinel or raise, and check is None before subscripting the result.

list.sort() and .append() Return None: Stop Assigning Mutators Back

In-place mutators are commands, not queries: they change the object and report nothing. list.sort(), list.append(), list.extend(), dict.update(), and set.add() all return None on purpose, signaling that there is no new object to hand you — the work happened inside the one you already hold. The moment you write result = items.sort(), you've bound None to a name that sounds like a list, and every later use of result is a TypeError waiting for its cue.

This bug survives review because the line reads so naturally. sorted_names = names.sort() looks like it should work; the method name promises sorted names, and the assignment promises to keep them. Python's convention disagrees: methods that mutate return None precisely so you can't mistake them for pure functions. Compare with sorted(names), the builtin that builds and returns a fresh list while leaving the original untouched — different spelling, opposite contract.

The rule is absolute and easy to enforce. Never assign the result of a mutating call. Put scores.sort() on its own line and keep using scores, or reach for sorted() when you need a new list. If you spot = ...sort() or = ...append( in review, flag it every time — no exceptions, because the crash it produces always lands far from the line that caused it.

mutator_returns_none.pyPYTHON
1
2
3
4
5
6
7
8
9
10
scores = [3, 1, 2]
result = scores.sort()
print(result)
print(scores)
names = ["b", "a"]
print(sorted(names))
print(names)
items = []
print(items.append("x"))
print(items)
⚠ Never Assign a Mutator's Result
If you see something = x.sort(), = x.append(, or = x.update( anywhere in review, flag it on sight. The assignment always binds None, and the crash always lands far from this line.
📊 Production Insight
A data pipeline assigned cleaned = rows.sort() and shipped None into a CSV writer that crashed 40 minutes into an hourly job. Splitting the sort onto its own line fixed it permanently.
🎯 Key Takeaway
Mutators return None by design — call them on their own line and keep using the original object.

None Slipped In as an Argument: Defaults, Chains, and Guard Clauses

None rarely starts at the crash site — it arrives as luggage. A lookup misses, a config key is absent, an API returns an empty body, and some intermediate layer passes that None along instead of dealing with it. Each frame assumes the frame above validated, so the value drifts deeper until an operation finally demands something real. By then the traceback's bottom frame is innocent code doing exactly what it should.

The fix is positional: validate where data crosses a boundary, not where it explodes. A function receiving prices should decide immediately what None means there. If prices are required, raise a loud ValueError naming the parameter — fail fast, fail attributably. If absence is legitimate, substitute a deliberate default like an empty list or a fallback config dict, and document that choice in the signature. What you must never do is accept None silently and hand it to the next layer down.

Defaults deserve the same scrutiny. prices or [] is convenient but treats 0 and "" as missing too, which surprises callers passing legitimate falsy values. Prefer the explicit if prices is None branch so your intent survives the next reader. The discipline compounds: when every function guards its own door, None can never travel more than one frame, and tracebacks start pointing at causes instead of victims.

none_argument_guard.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def total(prices):
    if prices is None:
        raise ValueError("prices must be a list, got None")
    return sum(prices)

print(total([10, 20, 30]))


def load_config(raw):
    if raw is None:
        return {"retries": 3}
    return raw

print(load_config(None))
print(load_config({"retries": 5}))
📊 Production Insight
Three service layers each assumed the one above had checked the payload, so None traveled from an API client to a template before crashing. One is None guard at the entry function would have named the culprit immediately.
🎯 Key Takeaway
Validate at the boundary where None enters — raise for required inputs, substitute explicit defaults for optional ones.

Traceback Reading: Name the Operation That Touched None

The traceback's final line is a label, not a riddle. Python tells you the exact operation that met None: 'not subscriptable' means brackets were applied to None, 'not iterable' means a loop, comprehension, or unpacking met None, and 'not callable' means None was invoked like a function. Each message names a different producer to hunt, so reading that one word before touching code saves entire debugging sessions.

Work the traceback backwards from there. The bottom frame shows the failed operation and the file and line where it happened. The frame above shows the call that delivered the value. Your job is to walk up until you find the frame where a real object should have been built — the search that missed, the mutator that was assigned back, the argument that arrived empty. Drop print(repr(variable)) at each step; repr() separates None from "None", [], and 0, which all look alike in ordinary prints but crash in entirely different ways.

Beginners often patch the bottom frame — wrapping the crash line in try/except or sprinkling a guard where the symptom appeared. That hides the wound without treating it. The professional habit is the opposite: the crash line is evidence, and the fix belongs wherever None was born. Read the last line, walk one frame up, confirm with repr(), and repair the producer.

traceback_none_operation.pyPYTHON
1
2
3
4
5
6
7
8
9
10
cases = {
    "subscript": lambda: None["key"],
    "iterate": lambda: [x for x in None],
    "call": lambda: None(),
}
for name, fn in cases.items():
    try:
        fn()
    except TypeError as exc:
        print(name, "->", type(exc).__name__ + ":", exc)
📊 Production Insight
An on-call engineer spent an hour guarding a template line before reading the last traceback word, which said 'not callable' — the bug was a callback, not the template. Reading the final line first would have aimed the fix correctly in seconds.
🎯 Key Takeaway
The final traceback line names the failed operation — walk one frame up from the crash to find where None was born.

Optional Hints and Strict mypy: Catch None Before It Ships

Type hints turn this runtime surprise into a build-time refusal. Marking a value Optional[str] declares that None is a legal resident, not an intruder — and under strict mypy, every unguarded use of that value becomes a static error. Subscripting, iterating, or calling an Optional without narrowing it first fails CI with Item "None" has no attribute, pointing at the exact line long before production data arrives.

The mechanism is narrowing. After if name is None: return ..., the checker knows the surviving path holds a real str and permits .strip() and .lower(). Without that guard, the same calls are rejected. This is the machine enforcing the discipline the article preaches: check before you touch. Bare annotations like -> dict make the opposite promise — 'this never returns None' — so a function that can fall off the end violates its own contract, and strict mode flags the lie.

Adoption is incremental, which is why teams actually stick with it. Annotate the money paths and the shared helpers first, run mypy --strict on those modules in CI, and widen the net over time. You don't need a fully typed codebase to kill this error class — you need strict checking on the functions whose results get subscripted, looped over, or called.

optional_hints_strict.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from typing import Optional


def greet(name: Optional[str]) -> str:
    if name is None:
        return "hello, guest"
    return "hello, " + name.strip().lower()

print(greet("  ADA "))
print(greet(None))


def first(items: list[str]) -> Optional[str]:
    if not items:
        return None
    return items[0]

print(first([]))
print(first(["x", "y"]))
💡Strict Mode on Five Files Beats Loose Mode on Five Hundred
You don't need a fully typed codebase. Run mypy --strict on just the modules whose results get subscripted, looped over, or called, and widen the net over time.
📊 Production Insight
A team added strict mypy to just their billing package and caught four functions whose -> dict promises could fall off the end. Each would have been a production TypeError; instead they became afternoon pull-request fixes.
🎯 Key Takeaway
Annotate Optional honestly and run strict mypy in CI — unguarded None becomes a build failure, not a 2 AM page.

Boundary Guards That Scale: Raise Early, Default Deliberately

Guards scale when they live at boundaries and speak plainly. The entry to a service, the top of a request handler, the start of a batch job — these are the checkpoints where external or optional data becomes internal fact. A guard there converts every downstream assumption into something verified: after if customer is None: raise ValueError("customer is required"), the rest of the function can use customer["id"] with total confidence, and every reader sees the contract up front.

Choose the guard's voice deliberately. Raise for required inputs, because a missing customer is a caller bug that must be loud and attributable. Substitute defaults for optional inputs, because a missing amount is a business decision (amount = 0) that should be visible, not hidden inside an or that also swallows zeroes. The two shapes look similar but communicate opposite things, and mixing them up — defaulting a required value, raising on an optional one — creates bugs subtler than the crash you started with.

Resist scattering is None checks at every use site. A function body littered with guards is a sign the boundary was left open; push the check outward until each function can trust its own parameters. One guard at the door beats ten guards in the hallway, and the traceback — should one ever fire again — will point at the caller that broke the contract.

boundary_guards.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
def charge(customer, amount):
    if customer is None:
        raise ValueError("customer is required")
    if amount is None:
        amount = 0
    return {"customer": customer["id"], "charged": amount}

print(charge({"id": "c_1"}, 50))
print(charge({"id": "c_2"}, None))
try:
    charge(None, 50)
except ValueError as exc:
    print("guard fired:", exc)
📊 Production Insight
A checkout service replaced a dozen scattered None checks with three boundary guards and saw its NoneType alerts drop to zero for six straight months — the remaining failures arrived as clear ValueErrors naming the offending caller.
🎯 Key Takeaway
One explicit guard at the boundary beats scattered checks — raise for required inputs, default deliberately for optional ones.
● Production incidentPOST-MORTEMseverity: high

The Missing Return That Skipped 3,020 Invoices at 2:14 AM

Symptom
The 2:14 AM billing run died mid-batch: 1,180 invoices posted, then silence. The job log's final line was TypeError: 'NoneType' object is not subscriptable pointing at the invoice template line discount["pct"] — code that hadn't changed in eight months. Finance found the gap at 8:40 AM when the day's revenue report showed roughly 72% of expected subscription income missing.
Assumption
The team assumed get_discount() always returned a dict because it did for every plan they'd tested — monthly, annual, and trial. The enterprise-plan branch was added three weeks earlier by a contractor, reviewed quickly, and covered by a test that only checked the discount amount, never the code path that computed it. Nobody realized the new branch computed a value into a local variable and never returned it.
Root cause
The enterprise-plan branch of get_discount() in payments/discounts.py calculated pct and code into locals but ended without a return, so Python handed back None. The invoice builder then ran discount["pct"] on that None and raised TypeError: 'NoneType' object is not subscriptable on invoice 1,181 of 4,200. The job had no per-invoice try/except and no boundary check, so the single None aborted the entire run — 1,180 invoices were already written to the ledger while 3,020 never generated, and the job's exit code was the only alert.
Fix
Two changes shipped in the same hotfix. First, the missing return {"pct": pct, "code": code} was added to the enterprise branch of get_discount(), and the function's annotation was tightened from -> dict to -> Discount (a TypedDict) so strict mypy would flag any future bare path — the very next CI run validated all 14 branches return. Second, the invoice builder gained a boundary guard: if discount is None: raise ValueError(f"no discount for {plan}"), converting any future silent None into a loud, attributable failure before a single invoice renders. The 1,180 already-written invoices were verified untouched; the remaining 3,020 were regenerated and all 4,200 reconciled to the cent against Stripe.
Key lesson
  • A function that returns the right type on 13 of 14 branches is still broken — branch coverage on return statements matters more than line coverage on happy paths.
  • Silent None is the most expensive default in Python: a loud ValueError at the boundary would have paged once instead of letting 3,020 invoices silently skip.
  • Strict mypy on money paths pays for itself the first time it flags a -> dict function that can fall off the end — make it a merge gate, not a suggestion.
Production debug guideSix moves that take you from a bare TypeError to the exact line that produced None — with the commands you'd actually run.6 entries
Symptom · 01
Logs show only TypeError: 'NoneType' object is not ... with no context about which variable held None
Fix
Rerun to capture the full traceback — never debug from the one-line summary alone: python3 billing.py 2>&1 | tail -20. Read the LAST line first: 'not subscriptable' means brackets on None, 'not iterable' means a loop over None, 'not callable' means you called None. That single word tells you which operation to audit and which variable held None.
Symptom · 02
You know the crashing line but not which of its variables is None
Fix
Insert print("DEBUG", repr(suspect)) on the line directly above the crash and rerun: python3 billing.py. repr() distinguishes None from "None", [], and 0 — all of which print similarly but crash differently. Once you confirm the value is None, delete the print and move one frame up the traceback to find who produced it.
Symptom · 03
The None comes from a helper that works for most inputs but fails on edge cases
Fix
Check every return path of the producing function: grep -n "return" payments/discounts.py. If some branches return a value and others fall off the end, you've found it. Confirm with python3 -c "from payments.discounts import get_discount; print(repr(get_discount('edge-case')))" — a None result from a function typed to return a dict is the smoking gun.
Symptom · 04
A variable that should be a list is None right after a sort, append, or update
Fix
Search for assigned-back mutators: grep -rn "= .\.sort()\|= .\.append(\|= .\.update(" --include=".py" .. Any match binds None by design. Fix by splitting the call onto its own line (items.sort() then use items) or switching to sorted(items) when you need a new list.
Symptom · 05
None flows through several layers and you need the full path, not just the crash site
Fix
Run the type checker over the suspect module: python3 -m mypy --strict payments/discounts.py. Strict mode flags every unguarded use of Optional[...] with Item "None" has no attribute at the exact line. If mypy isn't installed, pip install mypy and add the strict run to CI so the next None fails the build instead of the night shift.
Symptom · 06
The crash is intermittent and only reproduces on certain customer records
Fix
Add a boundary assertion in a staging replica, not production: assert customer is not None, "customer missing" before the crash line, then python3 billing.py --limit 50. The assertion fires at the entry point instead of deep in the logic, showing which caller passed None. Convert the assertion into a permanent if x is None: raise ValueError(...) guard once confirmed.
NoneType TypeError Root Causes at a Glance
Root CauseHow to ConfirmFixPrevention
Missing return on some pathprint(repr(fn())) shows None for inputs that take the bare branch; grep -n return shows uneven coverageAdd an explicit return on every path, or return a sentinelEarly-return style plus a test per branch
Assigned-back mutator (sort/append)print(repr(result)) right after assignment shows None while the original list changedUse the original list after sort(); use sorted() for a new listNever assign a mutating call; lint for = .*\.sort()
None passed as an argumentTraceback's bottom frame is innocent; repr() one frame up shows None entering the callGuard at the boundary: raise for required, default for optionalValidate inputs where functions meet, not three layers down
Wrong message, wrong fixLast traceback line says subscriptable, iterable, or callable — each names a different operationFix the operation the message names, on the variable it namesRead the final line before touching any code
Untyped Optional flowing downstreammypy --strict flags Item "None" has no attribute at the exact lineAnnotate Optional[...] and narrow with is None checksStrict mypy in CI on every pull request
⚙ Quick Reference
6 commands from this guide
FileCommand / CodePurpose
implicit_none_return.pydef find_admin(users):Implicit None Returns
mutator_returns_none.pyscores = [3, 1, 2]list.sort() and .append() Return None
none_argument_guard.pydef total(prices):None Slipped In as an Argument
traceback_none_operation.pycases = {Traceback Reading
optional_hints_strict.pyfrom typing import OptionalOptional Hints and Strict mypy
boundary_guards.pydef charge(customer, amount):Boundary Guards That Scale

Key takeaways

1
None is a value, not an absence of one
the error names the operation that met it, not broken syntax.
2
A missing return on any branch hands the caller None; add an explicit return to every path.
3
In-place mutators return None by design
never assign sort(), append(), or update() back.
4
The traceback's last line is the diagnosis
subscriptable, iterable, or callable names the exact operation.
5
Guard with if x is None at boundaries
raise for required inputs, default deliberately for optional ones.
6
Optional hints plus strict mypy turn this 2 AM crash into a red CI build on the pull request.

Common mistakes to avoid

6 patterns
×

Forgetting return on one branch of a search or lookup function

Symptom
TypeError: 'NoneType' object is not subscriptable on the caller's line, far from the function that silently handed back None. The traceback blames perfectly good indexing code.
Fix
Add an explicit return on every path, including the fall-through at the end. If 'no result' is legitimate, return a sentinel like "no-admin", an empty list, or document Optional in the signature so callers know to check.
×

Assigning `result = items.sort()` or `x = items.append(v)`

Symptom
The very next use of result explodes — subscripting, iterating, or calling it. print() shows None where a list was expected, and the sort itself worked fine.
Fix
Call the mutator on its own line and keep using the original list: scores.sort() then scores. When you need a new list, use sorted(scores) or a slice copy instead.
×

Passing None down a call chain instead of failing fast

Symptom
The crash lands two or three frames below the real mistake. Each layer assumed the layer above checked, so nobody did, and the traceback's bottom frame is innocent.
Fix
Validate at the boundary: if prices is None: raise ValueError(...) for required inputs, or substitute a default (prices or []) when empty input is genuinely fine. Never let None drift three calls deep.
×

Treating all three messages as the same bug

Symptom
You 'fix' the indexing line with a guard while the real None comes from the loop two lines down. The patch looks sensible, tests pass on happy paths, and production still crashes.
Fix
Match the message to the producer: subscriptable means a bad index target, iterable means a bad loop target, callable means a bad call target. Check that exact variable with repr() instead of fixing all three.
×

Catching TypeError instead of checking for None

Symptom
The except clause swallows real type bugs along with the None — a misspelled attribute or wrong argument type vanishes silently, and debugging gets harder instead of easier.
Fix
Wrap the suspect value in a guard at the point of use: if admin is None: return "no-admin". Reserve try/except TypeError for genuinely unpredictable external data, not for your own functions.
×

Leaving type hints off functions that sometimes return None

Symptom
Reviewers can't tell whether None is a legal result or a bug. Callers subscript confidently, and nothing catches the mismatch until the code runs against real data.
Fix
Annotate producers with Optional[...] when None is legal and bare types when it isn't, then run mypy --strict. A bare -> dict return that sometimes yields None becomes a CI failure instead of a 2 AM page.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
Why does a function with no return statement produce None, and how does ...
Q02JUNIOR
Why do list.sort() and list.append() return None instead of the list?
Q03SENIOR
How do you tell apart 'NoneType is not subscriptable, iterable, or calla...
Q04SENIOR
Where should None be checked — at the boundary, at each use, or with try...
Q05SENIOR
How do Optional annotations plus strict mypy eliminate this error class ...
Q01 of 05JUNIOR

Why does a function with no return statement produce None, and how does that become a TypeError?

ANSWER
Every Python function returns something, and a function that finishes without hitting return value returns None. If the caller treats that result as a list, dict, or callable — brackets, a loop, or a call — the operation's slot lookup fails and raises TypeError naming NoneType. The fix is an explicit return on every path.
FAQ · 6 QUESTIONS

Frequently Asked Questions

01
Should I fix the line the traceback points at?
02
How does a missing return cause this error?
03
Do list.sort() and append() really return None?
04
What do the subscriptable, iterable, and callable variants mean?
05
How do I stop None from reaching production code?
06
Should the guard use `is None`, `== None`, or truthiness?
N
Naren Founder & Principal Engineer

20+ years shipping production Python across data and backend systems. Notes here come from systems that actually shipped.

Follow
Verified
production tested
September 23, 2026
last updated
1,905
articles · all by Naren
🔥

That's Errors. Mark it forged?

6 min read · try the examples if you haven't

Previous
Python ModuleNotFoundError Fix
4 / 11 · Errors
Next
Python IndentationError Fix