TypeError: NoneType Is Not Subscriptable or Iterable
TypeError: 'NoneType' object is not subscriptable means you indexed None.
20+ years shipping production Python across data and backend systems. Notes here come from systems that actually shipped.
- ✓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
- The fix is at the source: add the missing
return, stop assigninglist.sort()or.append()back (both return None), and reject or default a None argument withif 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
Optionaltype hints plus strict mypy, so a None flowing into brackets fails in CI instead of at 2 AM.
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 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.callback()
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.
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(), and dict.update() 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 set.add()result = , you've bound None to a name that sounds like a list, and every later use of items.sort()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 on its own line and keep using scores.sort()scores, or reach for when you need a new list. If you spot sorted()= ...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.
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.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.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.
is None guard at the entry function would have named the culprit immediately.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; separates None from repr()"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 , and repair the producer.repr()
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.
mypy --strict on just the modules whose results get subscripted, looped over, or called, and widen the net over time.-> dict promises could fall off the end. Each would have been a production TypeError; instead they became afternoon pull-request fixes.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.
The Missing Return That Skipped 3,020 Invoices at 2:14 AM
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.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.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.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.- 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
ValueErrorat 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
-> dictfunction that can fall off the end — make it a merge gate, not a suggestion.
TypeError: 'NoneType' object is not ... with no context about which variable held Nonepython3 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.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.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.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.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.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.| File | Command / Code | Purpose |
|---|---|---|
| implicit_none_return.py | def find_admin(users): | Implicit None Returns |
| mutator_returns_none.py | scores = [3, 1, 2] | list.sort() and .append() Return None |
| none_argument_guard.py | def total(prices): | None Slipped In as an Argument |
| traceback_none_operation.py | cases = { | Traceback Reading |
| optional_hints_strict.py | from typing import Optional | Optional Hints and Strict mypy |
| boundary_guards.py | def charge(customer, amount): | Boundary Guards That Scale |
Key takeaways
sort(), append(), or update() back.if x is None at boundariesCommon mistakes to avoid
6 patternsForgetting return on one branch of a search or lookup function
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.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)`
result explodes — subscripting, iterating, or calling it. print() shows None where a list was expected, and the sort itself worked fine.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
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
repr() instead of fixing all three.Catching TypeError instead of checking for None
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
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 Questions on This Topic
Why does a function with no return statement produce None, and how does that become a TypeError?
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.Frequently Asked Questions
20+ years shipping production Python across data and backend systems. Notes here come from systems that actually shipped.
That's Errors. Mark it forged?
6 min read · try the examples if you haven't