What you'll learn
Quick Answer
All three mean a lookup failed. KeyError means the key is not in the dictionary. IndexError means the position is past the end of the list or string. AttributeError means the object does not have that name on it, and it usually means the object is None or is not the type you assumed. Read the traceback from the bottom: the last line names the error, the frame above it names the line, and everything higher is only how you got there.
Read the traceback from the bottom up
Python prints Traceback (most recent call last) at the top of every crash. That sentence is instruction, not decoration. The bottom of a traceback is where your program actually died. Everything above it is the chain of calls that led there.
Traceback (most recent call last):
File "fees.py", line 22, in <module>
print(total_fees(students))
File "fees.py", line 17, in total_fees
return sum(s["fee"] for s in rows)
File "fees.py", line 17, in <genexpr>
return sum(s["fee"] for s in rows)
KeyError: 'fee'Read it in this order. Last line first, because it names the failure. Then the frame directly above it, because that is the line that failed. Only then walk upwards, and only if you need to know who passed the bad data in. Most beginners start at line 22 and waste ten minutes staring at print, which is innocent.
The three errors in this post are all lookup errors. Python was asked to fetch something by name or by position and could not. The message tells you what was missing but never why it was missing, and the why is always somewhere earlier in your own code.
One more habit worth building. When the failing line has several lookups on it, split it across two lines before you start guessing. A line like data["user"]["address"]["city"] can raise KeyError at three different points, and the message only ever names the one key that failed. Splitting it tells you immediately which layer is empty.
KeyError: the dictionary does not have that key
KeyError is raised when you index a dictionary with a key it does not contain. The message is just the key itself, in quotes, with no explanation attached.
students = [
{"name": "Asha", "city": "Pune", "fee": 12000},
{"name": "Rahul", "city": "Kochi"},
]
for s in students:
print(s["fee"])
# 12000
# KeyError: 'fee'Notice how the loop printed one value before failing. Partial output before a KeyError is a strong hint that your data is inconsistent rather than your code being wrong everywhere. The second record simply never had a fee.
Three things trigger this far more often than a genuine typo. JSON parsed from an API where a field is optional and only appears sometimes. A dictionary you built with dict[key] = value inside a branch that did not run. And case or whitespace mismatches, where the key really is "Fee " with a trailing space from a CSV.
Before you reach for a fix, print the keys you actually have:
print(list(s.keys()))
# ['name', 'city']That one line settles the argument. If the key is genuinely absent from the source data, you need a default. If it is present but spelled differently, you need to fix the producer, not the consumer. Patching a spelling problem with a default is how a report quietly starts showing zero rupees for half your students.
IndexError: the position is past the end
IndexError means you asked for element n of a sequence that does not have an element n. Valid indices run from 0 to len(x) - 1, and the classic cause is a loop that goes one step too far.
marks = [88, 91, 76]
for i in range(len(marks) + 1):
print(marks[i])
# 88
# 91
# 76
# IndexError: list index out of rangeHere is the part that catches people: indexing raises, slicing does not. marks[5] is an error, but marks[5:10] quietly returns an empty list, and marks[:100] returns the whole list. That asymmetry is genuinely useful when you want the first three of a possibly shorter list, and genuinely dangerous when a slice silently produces nothing and your code carries on with empty data.
Negative indices count from the end, so marks[-1] is the last item. They can still go out of range: with three items, marks[-4] raises. The message varies slightly by type, so "Pune"[9] says string index out of range and a tuple says tuple index out of range.
In practice the commonest real-world source is splitting text that does not have the shape you expected:
line = "Asha" # a row missing its fee column
name, fee = line.split(",") # ValueError, not IndexError
parts = line.split(",")
print(parts[1]) # IndexError: list index out of rangeGuard with if len(parts) < 2: continue rather than wrapping the whole loop in a bare except, so that malformed rows are skipped deliberately instead of the entire file being abandoned at row 400.
AttributeError: the object is not what you think it is
AttributeError fires when you use a dot on an object that has no such name. The message follows a fixed shape and the first half of it is the important half.
AttributeError: 'NoneType' object has no attribute 'strip'Read 'NoneType' object before you read 'strip'. Python is telling you the type of the thing on the left of the dot. If that type is NoneType, the bug is not on this line at all. Something earlier returned None and you did not notice.
import re
text = "Roll: 21BCE1043"
roll = re.search(r"\d{2}[A-Z]{3}\d+", text).group() # fine
text = "Roll: not provided"
roll = re.search(r"\d{2}[A-Z]{3}\d+", text).group()
# AttributeError: 'NoneType' object has no attribute 'group'The same shape appears whenever a function returns None on failure: dict.get() with a missing key, re.match with no match, a function where one branch forgets to return, and list.sort() which sorts in place and hands you nothing back.
When the type in the message is not NoneType, you have a different problem: you are calling a method that belongs to another type. my_dict.append(x) gives 'dict' object has no attribute 'append' because you thought you had a list. text.lowercase() fails because the method is lower(). Newer Python versions helpfully add a Did you mean hint for near misses, but older ones do not, so print(type(x)) and print(dir(x)) remain the fastest way to see what you are really holding.
When to use .get(), and when it hides the bug
dict.get() returns None instead of raising, and takes an optional default. It is the right tool when a missing key is a normal, expected state.
fee = s.get("fee", 0) # missing fee counts as zero
city = s.get("city", "unknown")The trap is using .get() without a default on data you actually depend on. The KeyError disappears and reappears three functions later as TypeError: unsupported operand type(s) for +: 'int' and 'NoneType', at a line that has nothing to do with the real mistake. You have not fixed the bug, you have moved it somewhere harder to find.
Use try/except when the missing key means something went wrong and you want to react to it, not paper over it:
try:
fee = s["fee"]
except KeyError:
print(f"skipping {s['name']}: no fee recorded")
continueTwo rules keep this honest. Catch the specific exception, never bare except:, which also swallows KeyboardInterrupt and every typo you have not found yet. And keep the try block down to the one line that can fail, because a wide try block catches errors from code you never meant to protect.
For counting and grouping, avoid the whole problem instead of handling it. collections.defaultdict(int) creates missing entries on access, and collections.Counter is better still for tallies. Both remove an entire class of KeyError from your code rather than catching it repeatedly.
from collections import Counter
cities = ["Pune", "Kochi", "Pune"]
print(Counter(cities)) # Counter({'Pune': 2, 'Kochi': 1})Choosing between the three comes down to one question: is the missing thing normal? A survey field that respondents may leave blank is normal, so use .get() with a sensible default. A student record with no roll number is not normal, so let it raise and fix the data. A running tally that starts empty is not really a missing key at all, so use a structure that never has one.
The general principle is worth carrying beyond dictionaries. Silencing an error is only a fix when you have decided what the correct behaviour is in that case. Otherwise you are converting a loud, precise failure into a quiet, wrong answer, and quiet wrong answers are what get shipped.
