Skip to main content
Bookmark this · 20 errors covered

20 Python Errors Students Hit and How to Fix Each One

Twenty Python errors cover roughly 95% of crashes on university coursework, from NameError on a typo at line 4 to MemoryError on a 6 GB CSV. Each entry below shows the exact traceback, the broken code, the fixed code, and one prevention rule. Bookmark this page and keep it open the next time a Jupyter cell turns red.

Python 3.10 syntax Stdlib examples only Before and after side by side
Bucket 1 of 5

Syntax and indentation errors (3)

The interpreter refused to start the script. Fix these before any other category, because nothing else runs until the parser succeeds.

SyntaxError: invalid syntax

SyntaxError: invalid syntax

The parser stopped before the code ran. The interpreter cannot tokenize the line, often because a colon, parenthesis, quote, or keyword is misplaced. Python 3.10 and later print a caret pointing close to the failure column.

Broken
                    def total(items)
    s = 0
    for x in items:
        s += x
    return s

print(total([1, 2, 3]))
                  
Fixed
                    def total(items):  # colon was missing on the def line
    s = 0
    for x in items:
        s += x
    return s

print(total([1, 2, 3]))
                  

Prevention. Read the caret column. Check for a missing colon after def, for, while, if, elif, else, class, with, and try. Match every opening bracket.

IndentationError: expected an indented block

IndentationError: expected an indented block

A compound statement (function, loop, conditional) opened a new block, then nothing followed at a deeper indent. Empty bodies need a placeholder.

Broken
                    def placeholder():
# body left empty for later
print("done")
                  
Fixed
                    def placeholder():
    pass  # explicit no-op body

print("done")
                  

Prevention. Type pass into every stub the moment you write the def or class line. Empty bodies are not legal in Python.

TabError: inconsistent use of tabs and spaces

TabError: inconsistent use of tabs and spaces in indentation

Python 3 forbids mixing tabs and spaces inside one block. Pasting from a forum, a textbook PDF, or another editor frequently injects tabs into a 4-space file.

Broken
                    def grade(score):
    if score >= 50:
	return "pass"  # this line uses a literal tab
    return "fail"
                  
Fixed
                    def grade(score):
    if score >= 50:
        return "pass"  # 4 spaces, matches the rest of the block
    return "fail"
                  

Prevention. Configure the editor to expand tabs to 4 spaces. Run python -tt script.py to surface mixed indentation across the whole file.

Bucket 2 of 5

Name and attribute errors (4)

The parser passed, the code ran, then a lookup failed at runtime. Cause is almost always a typo, a scope mistake, or a method called on a value that is the wrong type.

NameError: name is not defined

NameError: name 'data' is not defined

The interpreter reached the identifier and found no binding in the local, enclosing, global, or built-in scope. Causes: typo, wrong case, variable assigned inside a different function, import missing.

Broken
                    def summary():
    print(Data)  # capital D, but the variable is lowercase

data = [10, 20, 30]
summary()
                  
Fixed
                    def summary(numbers):  # take the binding as an argument instead
    print(numbers)

data = [10, 20, 30]
summary(data)
                  

Prevention. Pass data into functions through parameters, not through implicit module-level lookups. Treat capitalization as load-bearing.

UnboundLocalError: local variable referenced before assignment

UnboundLocalError: local variable 'count' referenced before assignment

Python decided the name is local because the function assigns to it somewhere, then the first use comes before the assignment. The global value is shadowed but not yet bound.

Broken
                    count = 0

def bump():
    count = count + 1  # right-hand count is local and unbound
    return count

print(bump())
                  
Fixed
                    count = 0

def bump():
    global count  # declare the binding explicitly
    count = count + 1
    return count

print(bump())
                  

Prevention. Prefer explicit arguments and returns over global mutation. When global state is required, declare it with global or nonlocal at the top of the function.

AttributeError: object has no attribute

AttributeError: 'NoneType' object has no attribute 'split'

You called a method on an object that does not have that method. NoneType is the most frequent culprit: a function returned None and the next line called something on the result.

Broken
                    def first_word(s):
    if not s:
        return  # implicit None

text = first_word("")
print(text.split())  # NoneType crash
                  
Fixed
                    def first_word(s):
    if not s:
        return ""  # return an empty string, same type as input
    return s.split()[0]

text = first_word("")
print(text.split() if text else [])
                  

Prevention. Return a value of the same type the caller expects. Add an explicit None-check at every boundary where None could enter.

ImportError: cannot import name (circular import)

ImportError: cannot import name 'User' from partially initialized module 'models'

Two modules import each other at top level. When the first module starts importing the second, the second tries to read names from the first that are not bound yet.

Broken
                    # models.py
from services import audit_user

class User:
    pass

# services.py
from models import User

def audit_user(u: User):
    return u
                  
Fixed
                    # services.py with a deferred import inside the function
from __future__ import annotations

def audit_user(u: "User"):
    from models import User  # imported on call, not at module load
    assert isinstance(u, User)
    return u
                  

Prevention. Push imports that only types need into TYPE_CHECKING blocks or function bodies. Restructure modules so dependency arrows point one way.

Bucket 3 of 5

Type and value errors (5)

The expression evaluated, then Python refused the operation. Most of these trip students who pass input() straight into arithmetic, or who index into a value of the wrong shape.

TypeError: object is not subscriptable

TypeError: 'int' object is not subscriptable

You used square brackets on a value that does not support indexing. Often an integer was returned where a list or dict was expected, then code calls result[0].

Broken
                    def get_scores(student_id):
    return 87  # supposed to return a list

first = get_scores(1)[0]
print(first)
                  
Fixed
                    def get_scores(student_id):
    return [87, 91, 76]  # actual list of scores

first = get_scores(1)[0]
print(first)
                  

Prevention. Annotate return types with -> list[int] or -> dict[str, int]. Type-check the function before reaching for [0] on its result.

TypeError: unsupported operand type(s)

TypeError: unsupported operand type(s) for +: 'int' and 'str'

Python refused to coerce one side of an operator. + between int and str is a frequent input() trap because input() always returns a string.

Broken
                    age = input("age? ")
next_year = age + 1
print(next_year)
                  
Fixed
                    age_text = input("age? ")
age = int(age_text)  # explicit conversion
next_year = age + 1
print(next_year)
                  

Prevention. Convert input() results to int, float, or whatever the math needs at the boundary. Wrap the cast in try/except ValueError when the input may be malformed.

ValueError: invalid literal for int

ValueError: invalid literal for int() with base 10: '17.5'

The conversion target is the wrong shape. int("17.5") fails because the parser expects whole digits, not a decimal point. ValueError covers many sibling cases including bad date formats.

Broken
                    raw = "17.5"
years = int(raw)
print(years)
                  
Fixed
                    raw = "17.5"
years = int(float(raw))  # parse as float first, then truncate
print(years)
                  

Prevention. Validate the input shape before converting. Use float() when the source might contain a decimal, then cast to int when whole numbers are required.

IndexError: list index out of range

IndexError: list index out of range

The index points past the last element. Common on off-by-one bugs at the end of a loop, on empty lists from filtering, and on hard-coded indices into command-line arguments.

Broken
                    import sys
filename = sys.argv[1]  # crashes if the user runs the script with no args
print(filename)
                  
Fixed
                    import sys
if len(sys.argv) < 2:
    sys.exit("usage: script.py <filename>")
filename = sys.argv[1]
print(filename)
                  

Prevention. Guard with len() checks at every boundary. Prefer iteration with for x in items over numeric indexing where possible.

KeyError: key not in dict

KeyError: 'email'

You looked up a key that the dictionary does not contain. JSON payloads, environment variables, and DataFrame column names are the usual sources.

Broken
                    record = {"name": "Asha", "age": 22}
contact = record["email"]  # key absent
print(contact)
                  
Fixed
                    record = {"name": "Asha", "age": 22}
contact = record.get("email", "not_provided")  # default when missing
print(contact)
                  

Prevention. Use dict.get(key, default) when the key may be absent. Use in for explicit presence checks. Validate JSON payloads against a schema before reading fields.

Bucket 4 of 5

Runtime and control-flow errors (4)

Logic problems that survive type checks and slip past unit tests with thin coverage. Each one trips up real student submissions every semester.

ZeroDivisionError: division by zero

ZeroDivisionError: division by zero

A divisor evaluated to 0. Frequent when averaging an empty collection, when computing rates from per-period counts, or when normalizing data with zero variance.

Broken
                    def average(nums):
    return sum(nums) / len(nums)

print(average([]))  # empty list, division by 0
                  
Fixed
                    def average(nums):
    if not nums:
        return 0.0  # documented behaviour on empty input
    return sum(nums) / len(nums)

print(average([]))
                  

Prevention. Inspect every divisor before dividing. Decide what an empty or zero input means for the function, then encode that decision as an explicit branch.

RecursionError: maximum recursion depth exceeded

RecursionError: maximum recursion depth exceeded

The recursion never reached its base case before Python hit the default depth limit of 1000. Either the base case is wrong, the argument shrinks the wrong way, or the problem needs iteration.

Broken
                    def countdown(n):
    print(n)
    countdown(n - 1)  # base case missing

countdown(5)
                  
Fixed
                    def countdown(n):
    if n < 0:  # explicit base case
        return
    print(n)
    countdown(n - 1)

countdown(5)
                  

Prevention. Write the base case first, the recursive call second. Sketch the call stack for input sizes 0, 1, and 2 on paper before running.

RuntimeError: dictionary changed size during iteration

RuntimeError: dictionary changed size during iteration

You added or deleted entries while a for loop walked the same dict. Python detects the structural change between iterations and refuses.

Broken
                    scores = {"a": 30, "b": 80, "c": 45}
for key in scores:
    if scores[key] < 50:
        del scores[key]  # mutating while iterating
print(scores)
                  
Fixed
                    scores = {"a": 30, "b": 80, "c": 45}
for key in list(scores):  # snapshot the keys first
    if scores[key] < 50:
        del scores[key]
print(scores)
                  

Prevention. Iterate over a list(dict) snapshot when mutating. Build a new dict with a comprehension when filtering, instead of editing in place.

StopIteration leaked from a generator

RuntimeError: generator raised StopIteration

Inside a generator, a call to next() ran past the end and the bare StopIteration leaked. PEP 479 made this a RuntimeError in Python 3.7 and later.

Broken
                    def first_pairs(seq):
    it = iter(seq)
    while True:
        yield next(it), next(it)  # leaks StopIteration when the source ends

print(list(first_pairs([1, 2, 3])))
                  
Fixed
                    def first_pairs(seq):
    it = iter(seq)
    for a in it:
        try:
            b = next(it)
        except StopIteration:
            return  # clean termination
        yield a, b

print(list(first_pairs([1, 2, 3])))
                  

Prevention. Catch StopIteration inside generators or replace bare next() calls with for loops, which terminate on their own.

Bucket 5 of 5

Resource and I/O errors (4)

The code is correct, the environment is not. File paths, permissions, character encodings, and memory limits cause the last 20% of student crashes.

FileNotFoundError: No such file or directory

FileNotFoundError: [Errno 2] No such file or directory: 'data.csv'

Python looked for the path and the operating system reported nothing there. The path is relative to the current working directory, not the location of the script.

Broken
                    with open("data.csv") as f:
    print(f.read())  # crashes when run from a different directory
                  
Fixed
                    from pathlib import Path

HERE = Path(__file__).resolve().parent  # directory of this script
with (HERE / "data.csv").open() as f:
    print(f.read())
                  

Prevention. Anchor file paths on __file__ instead of the working directory. Print the resolved path on first error to confirm the lookup target.

PermissionError: access denied

PermissionError: [Errno 13] Permission denied: 'output.csv'

The file exists, but the operating system refuses the requested mode. Most often the file is open in Excel, the directory is read-only, or the script is writing to a system path on Windows.

Broken
                    with open("/etc/notes.txt", "w") as f:
    f.write("hello")  # writes to a system path
                  
Fixed
                    from pathlib import Path

out = Path.home() / "notes.txt"  # user-owned location
with out.open("w") as f:
    f.write("hello")
print(f"wrote {out}")
                  

Prevention. Write to a path you own (Path.home(), tempfile.gettempdir(), the project directory). Close any other program that holds the file open before retrying.

UnicodeDecodeError on read

UnicodeDecodeError: 'utf-8' codec can't decode byte 0x96 in position 312

Python opened the file as UTF-8 and hit a byte that is not legal UTF-8. Common on CSVs exported from Excel on Windows, which default to cp1252 or latin-1.

Broken
                    with open("export.csv") as f:
    rows = f.read()  # default utf-8 decode fails on cp1252 bytes
print(rows[:200])
                  
Fixed
                    with open("export.csv", encoding="cp1252") as f:  # match the source encoding
    rows = f.read()
print(rows[:200])
                  

Prevention. Ask the data provider for the encoding, or open the file in a hex viewer to detect a BOM. Standardise on UTF-8 for files you generate yourself.

MemoryError on large input

MemoryError

The interpreter could not allocate enough memory. Loading a 6 GB CSV into a list or a DataFrame on a laptop with 8 GB RAM trips this almost every time.

Broken
                    with open("huge.csv") as f:
    rows = f.readlines()  # entire file into a list
print(len(rows))
                  
Fixed
                    count = 0
with open("huge.csv") as f:
    for _ in f:  # iterate, one line at a time
        count += 1
print(count)
                  

Prevention. Stream large files line by line. For pandas, read with chunksize=100_000 and aggregate per chunk. Profile peak RSS with tracemalloc before scaling input size.

The 5-line traceback drill

Every Python traceback ends with the error type and one line of detail. Five steps turn that into a fix.

  1. Read the error type first, the message second. The type tells you the bucket above; the message names the value.
  2. Open the file at the line number printed in the deepest frame. Treat earlier frames as breadcrumbs, not the crime scene.
  3. Print the offending value with repr(). Strings, bytes, and None look different that way.
  4. Reproduce the crash on the smallest possible input. A 4-line example beats a 400-line notebook every time.
  5. Write a test that fails on the broken code and passes on the fix. The test stops the same bug coming back next week.

Stuck on an error that is not on this page?

Paste the traceback and the surrounding 20 lines into the contact form. A named Python developer reads it inside 15 minutes during peak hours, quotes the fix, and ships tested code with a 6-hour urgent option. Pay 50% to start, 50% after the code runs on your data.