Photo by Hitesh Choudhary on Unsplash

Most of us get comfortable because our code works, not because we fully understand why. And that illusion breaks the moment you hit edge cases that don’t behave the way you expect.

1. Default Mutable Arguments (but the real gotcha)

You already know this is bad:

def add_item(x, lst=[]):
    lst.append(x)
    return lst

But here’s what people miss: It’s not just a bug — it’s intentional state retention:

def counter(x, cache={}):
    cache[x] = cache.get(x, 0) + 1
    return cache

This acts like a hidden static variable.

💡 Used carefully → performance trick
💀 Used accidentally → nightmare debugging

2. is vs == (Worse Than You Think)

Everyone says: use ==, not is

But here’s the twist:

a = 256
b = 256
print(a is b)  # True

a = 257
b = 257
print(a is b)  # False

Python interns small integers (-5 to 256).

Even worse:

a = "hello"
b = "hello"
print(a is b)  # True (sometimes)

💡 String interning is inconsistent across contexts.

3. Late Binding in Closures (Classic Trap)

funcs = []
for i in range(3):
    funcs.append(lambda: i)

print([f() for f in funcs])  # [2, 2, 2]

👉 All lambdas capture same variable, not value.

Fix:

funcs.append(lambda i=i: i)

4. Dict Order Is Guaranteed (But That Changes Design)

Since Python 3.7:

d = {"a": 1, "b": 2}

👉 Order is preserved.

Hidden impact:

People now rely on dict order → implicit logic coupling
Old code assumptions break when ported.

💡 Dicts are now often used like lightweight ordered structures.

5. set Removes Duplicates… But Also Reorders

list(set([3, 1, 2, 1]))
# [1, 2, 3]  (but not guaranteed order)

👉 Many devs accidentally introduce non-determinism.

6. Everything Is a Reference (But Not Always Obvious)

a = [1, 2]
b = a
b.append(3)

print(a)  # [1, 2, 3]

But:

a = [1, 2]
b = a[:]
b.append(3)

print(a)  # [1, 2]

👉 Copy vs reference bugs show up in:

7. Tuple Isn’t Always Immutable

t = ([1, 2], 3)
t[0].append(99)

print(t)  # ([1, 2, 99], 3)

👉 Tuple is immutable, but its contents might not be.

8. += Can Mutate… or Not

a = [1, 2]
b = a
a += [3]

print(b)  # [1, 2, 3]

But:

a = (1, 2)
b = a
a += (3,)

print(b)  # (1, 2)

👉 List mutates in-place
👉 Tuple creates new object.

Same operator. Different behavior.

9. Exception Handling Can Hide Bugs

try:
    return something()
finally:
    return "oops"

👉 finally overrides return.

10. for-else Exists (and Almost Nobody Uses It Right)

for x in data:
    if x == target:
        break
else:
    print("Not found")

👉 else runs only if loop did NOT break.

11. Floating Point Lies

0.1 + 0.2 == 0.3  # False

👉 You know this… but it still bites in:

12. List Multiplication Shares References

grid = [[0]*3]*3
grid[0][0] = 1

print(grid)
# [[1,0,0],[1,0,0],[1,0,0]]

👉 All rows point to same list.

13. bool Is a Subclass of int

True + True == 2  # True
isinstance(True, int)  # True

👉 This leaks into:

14. __del__ Is Not Reliable

class A:
    def __del__(self):
        print("deleted")

👉 Garbage collection timing is unpredictable.

💀 Don’t rely on it for cleanup.

15. Iterators Get Exhausted Silently

it = iter([1,2,3])
list(it)  # [1,2,3]
list(it)  # []

👉 This causes subtle bugs in:

16. Pattern Matching (3.10+) Has Sharp Edges

match x:
    case 1:
        ...
    case y:
        ...

But:

👉 This captures variable, not compares.

💀 Many devs think it’s equality.

17. Shadowing Built-ins Breaks Everything

list = [1,2,3]
list("abc")  # 💀

👉 Happens more in notebooks than you think.

18. globals() and locals() Are Writable (Sometimes)

You can do wild stuff like:

globals()['x'] = 10

👉 Useful for metaprogramming
💀 Dangerous in large systems.

Final Take

Most Python bugs aren’t syntax issues.

They’re mental model mismatches.

Python looks simple — but it’s full of “gotchas by design.”