Skip to content

🐍 Python Boot Camp — Part 1: The Foundation

Mutability, Data Structures & Memory References

Goal: Zero tolerance for basic errors. Instant recall of the primitives that trip people up.


1. Mutability vs. Immutability

The #1 trap in interviews. Know this cold.

Type Mutable? Example
list ✅ Yes [1, 2, 3]
dict ✅ Yes {"a": 1}
set ✅ Yes {1, 2, 3}
tuple ❌ No (1, 2, 3)
str ❌ No "hello"
int/float ❌ No 42

Why it matters in functions

# MUTABLE: list — the original IS modified
def append_item(lst, item):
    lst.append(item)

my_list = [1, 2, 3]
append_item(my_list, 99)
print(my_list)  # [1, 2, 3, 99] — CHANGED outside the function!


# IMMUTABLE: tuple — cannot be modified in place
def try_modify_tuple(t):
    # t[0] = 99  ← this raises TypeError: 'tuple' object does not support item assignment
    t = t + (99,)  # This creates a NEW tuple — original is untouched
    return t

my_tuple = (1, 2, 3)
new_tuple = try_modify_tuple(my_tuple)
print(my_tuple)   # (1, 2, 3)    — UNCHANGED
print(new_tuple)  # (1, 2, 3, 99) — new object

The "accidental shared reference" trap

# WRONG: Both rows point to the same list!
grid = [[0] * 3] * 3
grid[0][0] = 1
print(grid)  # [[1, 0, 0], [1, 0, 0], [1, 0, 0]] — all rows changed!

# CORRECT: Each row is a separate object
grid = [[0] * 3 for _ in range(3)]
grid[0][0] = 1
print(grid)  # [[1, 0, 0], [0, 0, 0], [0, 0, 0]] — only first row changed

Interview line: "I'm careful with mutable defaults and shared references — Python passes objects by reference, so modifying a list inside a function mutates the original."


2. Mutable Default Argument — The Classic Gotcha

# WRONG — the default list is shared across ALL calls
def add_to_cart(item, cart=[]):
    cart.append(item)
    return cart

print(add_to_cart("apple"))   # ['apple']
print(add_to_cart("banana"))  # ['apple', 'banana']  ← bug!


# CORRECT — use None as default, create fresh list each call
def add_to_cart(item, cart=None):
    if cart is None:
        cart = []
    cart.append(item)
    return cart

print(add_to_cart("apple"))   # ['apple']
print(add_to_cart("banana"))  # ['banana']  ✓

3. Data Structures — When to Use What

List vs. Set vs. Dict: The Decision Matrix

items = ["apple", "banana", "apple", "cherry", "banana"]

# LIST — ordered, allows duplicates, use when sequence matters
shopping_list = list(items)
print(shopping_list)  # ['apple', 'banana', 'apple', 'cherry', 'banana']

# SET — unordered, no duplicates, O(1) lookup — use for existence checks
unique_items = set(items)
print(unique_items)   # {'apple', 'banana', 'cherry'}
print("apple" in unique_items)  # True — O(1) ✓

# DICT — key-value, O(1) lookup by key — use when you need association
item_count = {}
for item in items:
    item_count[item] = item_count.get(item, 0) + 1
print(item_count)  # {'apple': 2, 'banana': 2, 'cherry': 1}

Time Complexity Cheat Sheet

my_list = [1, 2, 3, 4, 5]
my_set = {1, 2, 3, 4, 5}
my_dict = {"a": 1, "b": 2}

# LIST
my_list.append(6)       # O(1) average — fast
my_list.insert(0, 0)    # O(N) — shifts everything
_ = 3 in my_list        # O(N) — linear scan ← slow for large data
_ = my_list[2]          # O(1) — index access is instant

# SET
_ = 3 in my_set         # O(1) — hash lookup ← always use set for "in" checks
my_set.add(6)            # O(1)

# DICT
_ = my_dict["a"]        # O(1)
_ = "a" in my_dict      # O(1)

Interview line: "If I need to check membership repeatedly, I'll convert to a set first — O(N) to build, but then O(1) per lookup instead of O(N)."


4. Variables as Labels (References)

Python variables don't contain values — they point to objects in memory.

a = [1, 2, 3]
b = a           # b points to the SAME list

b.append(4)
print(a)        # [1, 2, 3, 4] — a changed too!

# To get an independent copy:
c = a.copy()        # shallow copy — one level deep
# or
import copy
d = copy.deepcopy(a)  # deep copy — safe for nested structures
# Shallow copy limitation
original = [[1, 2], [3, 4]]
shallow = original.copy()

shallow[0].append(99)
print(original)  # [[1, 2, 99], [3, 4]] — inner list still shared!

import copy
deep = copy.deepcopy(original)
deep[0].append(100)
print(original)  # [[1, 2, 99], [3, 4]] — untouched ✓

✅ Phase 1 Checklist

  • [ ] Can explain mutable vs. immutable without hesitation
  • [ ] Know the mutable default argument bug and fix
  • [ ] Know when to use list vs. set vs. dict (and why)
  • [ ] Can recite: list append O(1), list search O(N), set/dict lookup O(1)
  • [ ] Understand shallow vs. deep copy

Next up → Part 2: The Scalpel — Pythonic idioms, comprehensions, generators, and context managers.