Variables and Data Types — Find the Bug¶
Find and fix the bug in each code snippet. Each exercise contains exactly one bug related to variables and data types.
Bug 1: The Disappearing Default (Junior)¶
def add_student(name, students=[]):
"""Add a student to the list and return the list."""
students.append(name)
return students
# Expected: each call returns a list with only one student
result1 = add_student("Alice")
result2 = add_student("Bob")
result3 = add_student("Charlie")
print(result1) # Expected: ["Alice"]
print(result2) # Expected: ["Bob"]
print(result3) # Expected: ["Charlie"]
Bug
The default argument `students=[]` is created once at function definition time and shared across all calls.Fix
def add_student(name, students=None):
"""Add a student to the list and return the list."""
if students is None:
students = []
students.append(name)
return students
result1 = add_student("Alice")
result2 = add_student("Bob")
result3 = add_student("Charlie")
print(result1) # ["Alice"]
print(result2) # ["Bob"]
print(result3) # ["Charlie"]
Bug 2: Wrong Comparison (Junior)¶
def check_none(value):
"""Check if a value is None."""
if value == None:
return "Value is None"
elif value == False:
return "Value is False"
elif value == 0:
return "Value is zero"
else:
return f"Value is {value}"
# This function gives wrong results for some inputs
print(check_none(None)) # Expected: "Value is None" -> OK
print(check_none(False)) # Expected: "Value is False" -> BUG: "Value is None"? No, gets "Value is False"
print(check_none(0)) # Expected: "Value is zero" -> BUG: returns "Value is False"
print(check_none("")) # Expected: "Value is " -> OK
Bug
`0 == False` is `True` in Python because `bool` is a subclass of `int`. The function uses `==` instead of `is` for singleton/identity checks. When `value=0`, it matches `value == False` before reaching `value == 0`.Fix
def check_none(value):
"""Check if a value is None, False, or zero — using identity for singletons."""
if value is None:
return "Value is None"
elif value is False:
return "Value is False"
elif value == 0:
return "Value is zero"
else:
return f"Value is {value}"
print(check_none(None)) # "Value is None"
print(check_none(False)) # "Value is False"
print(check_none(0)) # "Value is zero"
print(check_none("")) # "Value is "
Bug 3: The Shared List (Junior)¶
def create_matrix(rows, cols, default=0):
"""Create a matrix (list of lists) filled with default value."""
row = [default] * cols
matrix = [row] * rows
return matrix
grid = create_matrix(3, 3, 0)
grid[0][0] = 99
print(grid)
# Expected: [[99, 0, 0], [0, 0, 0], [0, 0, 0]]
# Actual: [[99, 0, 0], [99, 0, 0], [99, 0, 0]]
Bug
`[row] * rows` creates a list with `rows` references to the SAME list object. Modifying one row modifies all of them.Fix
Bug 4: Float Precision (Junior)¶
def calculate_total(prices):
"""Calculate total price with tax."""
subtotal = sum(prices)
tax = subtotal * 0.1
total = subtotal + tax
# Verify the math
expected = 33.0
if total == expected:
print(f"Total: ${total:.2f} - CORRECT")
else:
print(f"Total: ${total:.2f} - ERROR! Expected ${expected:.2f}")
return total
prices = [10.0, 10.0, 10.0] # subtotal = 30.0, tax = 3.0, total = 33.0
calculate_total(prices)
# Try with these prices:
prices2 = [0.1, 0.2, 0.3] # subtotal = 0.6, tax = 0.06, total = 0.66
# But 0.1 + 0.2 + 0.3 != 0.6 in floating point!
total = sum(prices2)
print(f"0.1 + 0.2 + 0.3 = {total}") # Not exactly 0.6
print(f"Equal to 0.6? {total == 0.6}") # False!
Bug
Floating-point arithmetic introduces precision errors. `0.1 + 0.2 + 0.3` does not equal `0.6` exactly due to IEEE 754 representation.Fix
import math
from decimal import Decimal
def calculate_total(prices):
"""Calculate total price with tax — using proper float comparison."""
subtotal = sum(prices)
tax = subtotal * 0.1
total = subtotal + tax
expected = 33.0
if math.isclose(total, expected, rel_tol=1e-9):
print(f"Total: ${total:.2f} - CORRECT")
else:
print(f"Total: ${total:.2f} - ERROR! Expected ${expected:.2f}")
return total
# For financial calculations, use Decimal:
def calculate_total_precise(prices: list[str]) -> Decimal:
"""Calculate total using Decimal for exact arithmetic."""
decimal_prices = [Decimal(p) for p in prices]
subtotal = sum(decimal_prices)
tax = subtotal * Decimal("0.1")
return subtotal + tax
print(calculate_total_precise(["0.1", "0.2", "0.3"])) # 0.66 exactly
Bug 5: Scope Confusion (Middle)¶
Bug
`UnboundLocalError: local variable 'x' referenced before assignment`. Python sees `x = x + 1` and marks `x` as a local variable for the entire function. So when `print(f"x before: {x}")` runs, the local `x` has not been assigned yet.Fix
# Option 1: Use global keyword
x = 10
def modify():
global x
print(f"x before: {x}")
x = x + 1
print(f"x after: {x}")
modify()
# Option 2 (better): Pass as argument and return
def modify_pure(value: int) -> int:
print(f"x before: {value}")
value = value + 1
print(f"x after: {value}")
return value
x = 10
x = modify_pure(x)
Bug 6: Late Binding Closure (Middle)¶
def create_multipliers():
"""Create a list of multiplier functions."""
multipliers = []
for i in range(5):
multipliers.append(lambda x: x * i)
return multipliers
mults = create_multipliers()
print(mults[0](10)) # Expected: 0, Actual: 40
print(mults[1](10)) # Expected: 10, Actual: 40
print(mults[2](10)) # Expected: 20, Actual: 40
print(mults[3](10)) # Expected: 30, Actual: 40
print(mults[4](10)) # Expected: 40, Actual: 40
Bug
All lambdas share the same enclosing variable `i`. By the time they are called, the loop has finished and `i = 4`. This is the classic late-binding closure bug.Fix
# Fix 1: Capture i as a default argument
def create_multipliers():
multipliers = []
for i in range(5):
multipliers.append(lambda x, i=i: x * i) # i=i captures current value
return multipliers
# Fix 2: Use functools.partial
from functools import partial
def create_multipliers_v2():
def multiply(i, x):
return x * i
return [partial(multiply, i) for i in range(5)]
# Fix 3: Use a factory function
def create_multipliers_v3():
def make_mult(i):
return lambda x: x * i
return [make_mult(i) for i in range(5)]
mults = create_multipliers()
for idx in range(5):
print(f"mults[{idx}](10) = {mults[idx](10)}") # 0, 10, 20, 30, 40
Bug 7: String Identity Trap (Middle)¶
def cache_lookup(key, cache):
"""Look up a key in cache using identity check for performance."""
for cached_key, value in cache.items():
if key is cached_key: # Using 'is' for "faster" comparison
return value
return None
cache = {}
cache["user_123"] = {"name": "Alice"}
cache["user_456"] = {"name": "Bob"}
# This works (string literals are interned):
print(cache_lookup("user_123", cache)) # {"name": "Alice"}
# This might fail (dynamically created string):
user_id = "user_" + str(123)
print(cache_lookup(user_id, cache)) # None! Even though user_id == "user_123"
Bug
Using `is` for string comparison instead of `==`. Dynamically constructed strings may not be interned, so identity comparison fails even when values are equal.Fix
def cache_lookup(key, cache):
"""Look up a key in cache using value comparison."""
for cached_key, value in cache.items():
if key == cached_key: # Use == for value comparison
return value
return None
# Or simply use dict's built-in lookup (which uses == internally):
def cache_lookup_simple(key, cache):
return cache.get(key)
Bug 8: Type Coercion Surprise (Middle)¶
def parse_config(config_str: str) -> dict:
"""Parse a simple key=value config string."""
config = {}
for line in config_str.strip().split("\n"):
key, value = line.split("=", 1)
key = key.strip()
value = value.strip()
# "Smart" type detection
if value.lower() == "true":
config[key] = True
elif value.lower() == "false":
config[key] = False
elif value.isdigit():
config[key] = int(value)
else:
config[key] = value
return config
config_text = """
port = 8080
debug = true
name = MyApp
timeout = -30
ratio = 3.14
count = 0
"""
config = parse_config(config_text)
print(config)
# Bug: timeout = "-30" stays as string (isdigit() is False for negative numbers)
# Bug: ratio = "3.14" stays as string (isdigit() is False for floats)
# Bug: count = 0 is parsed as int, but what about "00" or "007"?
Bug
`str.isdigit()` only returns True for strings of digits (no negative sign, no decimal point). So `-30` and `3.14` are not parsed as numbers. Also, `isdigit()` returns True for Unicode digit characters like `\u00b2` (superscript 2).Fix
def parse_config(config_str: str) -> dict:
"""Parse a simple key=value config string with proper type detection."""
config = {}
for line in config_str.strip().split("\n"):
if not line.strip() or "=" not in line:
continue
key, value = line.split("=", 1)
key = key.strip()
value = value.strip()
if value.lower() == "true":
config[key] = True
elif value.lower() == "false":
config[key] = False
else:
# Try int first, then float, then keep as string
try:
config[key] = int(value)
except ValueError:
try:
config[key] = float(value)
except ValueError:
config[key] = value
return config
config = parse_config(config_text)
print(config)
# {'port': 8080, 'debug': True, 'name': 'MyApp', 'timeout': -30, 'ratio': 3.14, 'count': 0}
Bug 9: Shallow Copy Trap (Senior)¶
import copy
def process_users(users_data: list[dict]) -> list[dict]:
"""Process user data: add computed fields without modifying the original."""
processed = users_data.copy() # "Copy" the list
for user in processed:
user["full_name"] = f"{user['first_name']} {user['last_name']}"
user["email_lower"] = user["email"].lower()
return processed
original = [
{"first_name": "Alice", "last_name": "Smith", "email": "Alice@Example.com"},
{"first_name": "Bob", "last_name": "Jones", "email": "Bob@Example.com"},
]
processed = process_users(original)
# Check: did the original get modified?
print("original[0] keys:", list(original[0].keys()))
# Bug: original[0] now has 'full_name' and 'email_lower' too!
Bug
`users_data.copy()` is a shallow copy. It creates a new list, but the dicts inside are the same objects. Modifying `user` in the loop modifies the original dicts.Fix
import copy
def process_users(users_data: list[dict]) -> list[dict]:
"""Process user data: add computed fields without modifying the original."""
# Option 1: Deep copy
processed = copy.deepcopy(users_data)
# Option 2: Create new dicts
# processed = [{**user} for user in users_data]
for user in processed:
user["full_name"] = f"{user['first_name']} {user['last_name']}"
user["email_lower"] = user["email"].lower()
return processed
original = [
{"first_name": "Alice", "last_name": "Smith", "email": "Alice@Example.com"},
]
processed = process_users(original)
print("original keys:", list(original[0].keys())) # Only original keys
print("processed keys:", list(processed[0].keys())) # Includes computed fields
Bug 10: Boolean Arithmetic Trap (Senior)¶
def count_active_users(users: list[dict]) -> dict:
"""Count active and inactive users."""
active = 0
inactive = 0
for user in users:
active += user["is_active"] # True = 1, False = 0
inactive += not user["is_active"] # not True = False = 0, not False = True = 1
return {"active": active, "inactive": inactive, "total": active + inactive}
users = [
{"name": "Alice", "is_active": True},
{"name": "Bob", "is_active": False},
{"name": "Charlie", "is_active": 1}, # 1 instead of True
{"name": "Dave", "is_active": 0}, # 0 instead of False
{"name": "Eve", "is_active": "yes"}, # String! Truthy but not True
{"name": "Frank", "is_active": ""}, # Empty string! Falsy but not False
]
result = count_active_users(users)
print(result)
# Bug: "yes" adds 1 to active (because bool("yes") = True, and "yes" is truthy)
# But "yes" + 0 = "yes0"... actually no, + on str and int raises TypeError!
# Actually: active += "yes" raises TypeError: unsupported operand type(s)
Bug
The code assumes `is_active` is always `bool` or `int`. When `is_active` is a string like `"yes"`, `active += "yes"` raises `TypeError`. Even if it did not error, the arithmetic semantics would be wrong.Fix
def count_active_users(users: list[dict]) -> dict:
"""Count active and inactive users with proper type handling."""
active = 0
inactive = 0
for user in users:
# Convert to bool explicitly — handles any truthy/falsy value
is_active = bool(user["is_active"])
if is_active:
active += 1
else:
inactive += 1
return {"active": active, "inactive": inactive, "total": active + inactive}
users = [
{"name": "Alice", "is_active": True},
{"name": "Bob", "is_active": False},
{"name": "Charlie", "is_active": 1},
{"name": "Dave", "is_active": 0},
{"name": "Eve", "is_active": "yes"},
{"name": "Frank", "is_active": ""},
]
result = count_active_users(users)
print(result) # {'active': 3, 'inactive': 3, 'total': 6}
Bug 11: Global Variable Race (Senior)¶
import threading
counter = 0
def increment(n: int) -> None:
"""Increment global counter n times."""
global counter
for _ in range(n):
counter += 1 # Read, increment, write — NOT atomic!
threads = [threading.Thread(target=increment, args=(100_000,)) for _ in range(10)]
for t in threads:
t.start()
for t in threads:
t.join()
print(f"Expected: 1,000,000")
print(f"Actual: {counter:,}")
# Bug: counter is usually less than 1,000,000 due to race condition
Bug
`counter += 1` is not atomic. It compiles to multiple bytecodes: `LOAD_GLOBAL`, `LOAD_CONST`, `BINARY_ADD`, `STORE_GLOBAL`. The GIL can release between any of these, causing lost updates.Fix
import threading
counter = 0
lock = threading.Lock()
def increment(n: int) -> None:
"""Increment global counter n times — thread-safe."""
global counter
for _ in range(n):
with lock:
counter += 1
# Or better: avoid global state entirely
from collections import defaultdict
def increment_local(n: int) -> int:
"""Return local count — no shared state."""
return n
threads = []
results = []
def worker(n: int) -> None:
results.append(increment_local(n))
lock2 = threading.Lock()
def worker_safe(n: int) -> None:
count = increment_local(n)
with lock2:
results.append(count)
threads = [threading.Thread(target=worker_safe, args=(100_000,)) for _ in range(10)]
for t in threads:
t.start()
for t in threads:
t.join()
print(f"Total: {sum(results):,}") # Always 1,000,000
Bug 12: Dictionary Key Type Collision (Professional)¶
def build_lookup(items: list) -> dict:
"""Build a lookup table from items."""
lookup = {}
for item in items:
lookup[item["key"]] = item["value"]
return lookup
items = [
{"key": True, "value": "boolean_true"},
{"key": 1, "value": "integer_one"},
{"key": 1.0, "value": "float_one"},
{"key": False, "value": "boolean_false"},
{"key": 0, "value": "integer_zero"},
{"key": 0.0, "value": "float_zero"},
]
lookup = build_lookup(items)
print(f"Number of entries: {len(lookup)}")
print(lookup)
# Expected: 6 entries
# Actual: 2 entries! Because True==1==1.0 and False==0==0.0
Bug
`True == 1 == 1.0` and `hash(True) == hash(1) == hash(1.0)`. Same for `False/0/0.0`. Python treats them as the same dict key, so later values overwrite earlier ones.Fix
def build_lookup(items: list) -> dict:
"""Build a lookup table with type-aware keys."""
lookup = {}
for item in items:
key = item["key"]
# Use (type_name, value) as composite key to distinguish types
typed_key = (type(key).__name__, key)
lookup[typed_key] = item["value"]
return lookup
items = [
{"key": True, "value": "boolean_true"},
{"key": 1, "value": "integer_one"},
{"key": 1.0, "value": "float_one"},
{"key": False, "value": "boolean_false"},
{"key": 0, "value": "integer_zero"},
{"key": 0.0, "value": "float_zero"},
]
lookup = build_lookup(items)
print(f"Number of entries: {len(lookup)}") # 6
for k, v in lookup.items():
print(f" {k} -> {v}")