post · Python · Debugging

5 Python Mistakes Singapore Students Make Every Semester

I read a lot of Python from Singapore students. Across NUS CS1010, NTU CZ1003, polytechnic data modules, and SUSS evening classes, the same five mistakes show up every semester. They aren't the kind of bugs that crash your program, they're the kind that quietly cost marks and make your code harder to debug.

In the order I see them most:

1. Mutating a list while iterating over it

You wrote a function to remove all even numbers from a list. It works for [1,2,3] but not for [2,4,6]. The teacher takes off marks. You shrug and move on.

The bug:

nums = [2, 4, 6]
for n in nums:
    if n % 2 == 0:
        nums.remove(n)
# nums is now [4]

Removing from a list while iterating shifts every subsequent index. Python's iterator doesn't know you removed something, so it skips elements.

The fix, in order of preference:

nums = [n for n in nums if n % 2 != 0]   # best, build a new list
nums = list(filter(lambda n: n % 2, nums))  # also fine
nums[:] = [n for n in nums if n % 2 != 0]   # in-place if you really need it

Never modify the list you're iterating over. Apply this rule and a whole class of bugs vanishes.

2. Using mutable default arguments

This one quietly breaks production code at real companies. It's even more brutal in coursework because the bug only shows up on the second test case.

def add_student(name, students=[]):
    students.append(name)
    return students

print(add_student("Alice"))   # ['Alice']
print(add_student("Bob"))     # ['Alice', 'Bob']  ← should have been ['Bob']

Default arguments are evaluated once when the function is defined, not each time it is called. The same [] is reused. Use None as a sentinel:

def add_student(name, students=None):
    if students is None:
        students = []
    students.append(name)
    return students

Markers love asking about this in module quizzes. It also genuinely matters in real code.

3. Confusing == and is

== checks if two values are equal. is checks if two names refer to the same object in memory. They sometimes look identical, until they don't.

a = [1, 2, 3]
b = [1, 2, 3]
print(a == b)   # True, same contents
print(a is b)   # False, different objects in memory

Where this bites students:

x = 257
y = 257
print(x is y)   # False on most CPython, True for small ints

Python caches small integers (-5 to 256) and short strings, so is sometimes works on them by accident. Tests that pass on is for small values fail on bigger ones.

Rule: use == for equality, is for identity (mainly is None, is True, is False).

4. Not understanding that strings are immutable

This one trips up students who came from Java. You write a function to reverse a string in place. You get full marks in Java. You try the same in Python.

def reverse(s):
    for i in range(len(s) // 2):
        s[i], s[-i-1] = s[-i-1], s[i]   # TypeError

Strings in Python can't be modified in place. You build a new string:

def reverse(s):
    return s[::-1]

The same applies to tuples (immutable) versus lists (mutable). If you find yourself fighting Python to mutate something, check whether the type is even mutable. The error message is sometimes cryptic.

5. Overusing global variables

Most introductory courses don't teach scoping properly, and students end up writing 200-line scripts where every variable is implicitly global. Then they wonder why their function doesn't "see" the change.

count = 0

def increment():
    count = count + 1   # UnboundLocalError, Python sees the assignment and treats count as local

increment()

This fails because Python sees the assignment to count and assumes count is local in the function. To modify a global, you must declare it:

count = 0

def increment():
    global count
    count = count + 1

But the cleaner fix is almost always to stop using globals. Pass values in, return values out:

def increment(count):
    return count + 1

count = 0
count = increment(count)

Markers in CS2030S, IS200, and similar OOP-flavoured modules dock marks for over-reliance on globals. Even in CS1010 a clean function-based approach scores better than a working global-soup approach.

A debugging approach that prevents all five

If you spot any of these patterns in your own code, you're not stupid, every Python user wrote them at some point. The fastest way to stop writing them is to develop a habit of:

  1. Reading your code out loud, line by line. Sounds silly, works.
  2. Running it on a tiny example by hand, tracking the value of each variable on paper.
  3. Asking, "what type is this expression", at every line.

That last one solves most of the bugs above. Mutating a list, mutable defaults, identity vs equality, immutability, scope, are all type-and-context questions.

Want a second pair of eyes?

If you have an assignment where the bug just won't show itself, send me your code on Telegram. I'll find it, explain why it happened, and show you what pattern to use next time so it doesn't happen again.

Stuck on something specific?

Send your brief and I will reply with a fixed price, usually within the hour.