Lesson 5 of 5

Common Gotchas

The first four lessons covered the big structural differences. This one is a field guide to the smaller, sneakier traps — the ones that don't stop your code from running, they just make it quietly do the wrong thing, which is worse.

💡 The Bottom Line

Most C#-to-Python bugs aren't syntax errors — Python's syntax errors are loud and easy to fix. The dangerous ones are places where Python code looks like it should behave like C#, runs without complaint, and produces a subtly wrong result.

Gotcha 1: mutable default arguments

What it looks like
def add_item(item, cart=[]):
    cart.append(item)
    return cart

add_item("apple")   # ["apple"]
add_item("banana")  # ["apple", "banana"] !!
What's actually happening
# the default list is created ONCE,
# when the function is defined —
# and reused across every call that
# doesn't pass its own cart

def add_item(item, cart=None):
    if cart is None:
        cart = []
    cart.append(item)
    return cart

In C#, a default parameter value is re-evaluated at every call site. In Python, a default argument is evaluated once, when the function is defined — and if that default is a mutable object like a list or dict, every call that relies on the default shares the exact same object. This has no C# equivalent to reason from; it's Python-specific and catches almost everyone at least once.

⚠️ Gotcha 2: everything is passed "by object reference," and it's not quite either of C#'s models

C# has a clean split: value types (int, struct) are copied on assignment/passing, reference types (class) share the same object unless you copy it. Python doesn't have that split — every variable is a reference to an object, full stop. Reassigning a parameter inside a function never affects the caller's variable, but mutating a mutable object (like calling .append() on a list parameter) does — because the caller and the function are pointing at the same list.

Gotcha 3: equality — == already does what C# needs an override for

C#
var a = new List<int> { 1, 2 };
var b = new List<int> { 1, 2 };
Console.WriteLine(a == b); // False —
  // reference equality unless Equals is overridden
Python
a = [1, 2]
b = [1, 2]
print(a == b)   # True — value equality, built in
print(a is b)   # False — this checks reference identity

Python's == checks value equality by default for built-in types — no override needed, unlike C#'s reference types. The C#-style reference check is Python's is operator, not ==. Using is where you meant == (very tempting for a C# developer checking "are these the same") is a classic source of confusing bugs, especially with strings and small integers where Python's internal caching can make is appear to work by coincidence.

📋 Key differences to keep straight

  • Use == for "are these equal in value" (almost always what you want) — same intent as C#'s overridden Equals.
  • Use is only for "are these literally the same object in memory" (rare — mainly for is None checks).
  • is None is the idiomatic null check, preferred over == None even though both usually work.

Gotcha 4: no block scope

In C#, a variable declared inside an if or for block doesn't exist outside it. In Python, if/for/while blocks don't create their own scope at all — a variable assigned inside a loop is still visible after the loop ends:

for i in range(5):
    last = i

print(last)  # 4 — "last" leaked out of the loop; totally legal in Python

Only functions (and a few other constructs like classes) create a new scope in Python. This is convenient sometimes and a source of subtle bugs other times — particularly when a loop that's expected to run at least once doesn't run at all, and code after it references a variable that was never assigned.

🙋 There Are No Dumb Questions

Q: Is there anything like C#'s using statement for disposing resources?
A: Yes — Python's with statement is the direct equivalent: with open("file.txt") as f: ... guarantees the file is closed when the block exits, exactly like C#'s using (var f = ...) { ... } guarantees Dispose() is called. The underlying protocol (Python calls it a "context manager") is different in name but solves precisely the same problem.

🔧 Try It Yourself

Write a Python function def make_list(item, target=[]): that's supposed to return a brand-new list containing just item each time it's called. Call it three times with different items and print the result each time — watch the mutable-default bug happen live, then fix it using the cart=None pattern shown above.

✅ Quick Check

Why does a Python function with def f(items=[]): behave unexpectedly across multiple calls?


That's the whole guide. Time for the recap — and a look at where these five lessons leave off, and what's worth learning next.

← Back to
Next →