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
def add_item(item, cart=[]):
cart.append(item)
return cart
add_item("apple") # ["apple"]
add_item("banana") # ["apple", "banana"] !!
# 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
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
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 overriddenEquals. - Use
isonly for "are these literally the same object in memory" (rare — mainly foris Nonechecks). is Noneis the idiomatic null check, preferred over== Noneeven 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.