Building Blocks Quiz
Quiz
TypeError. Values, on the other hand, can be any Python object, including lists and nested dicts.TypeError. Values, on the other hand, can be any Python object, including lists and nested dicts.s = "Python"
print(s[::2])[::2] starts at index 0, goes to the end, with step 2 (every 2nd character). This gives: P(0), t(2), o(4) = “Pto”.[::2] starts at index 0, goes to the end, with step 2 (every 2nd character). This gives: P(0), t(2), o(4) = “Pto”.x in list) is O(n) because it requires linear search. list.pop(0) is also O(n) — removing from the front shifts every remaining element.x in list) is O(n) because it requires linear search. list.pop(0) is also O(n) — removing from the front shifts every remaining element..title() method capitalizes the first letter of each word. .capitalize() only capitalizes the first letter of the entire string..title() method capitalizes the first letter of each word. .capitalize() only capitalizes the first letter of the entire string.words = ['apple', 'banana', 'cherry']
result = ', '._____(words)join() method is called on the separator string and takes an iterable as argument: separator.join(iterable). This produces ‘apple, banana, cherry’.join() method is called on the separator string and takes an iterable as argument: separator.join(iterable). This produces ‘apple, banana, cherry’..replace() or .upper() return new strings..replace() or .upper() return new strings."Ha" * 3?* operator with strings performs repetition, concatenating the string with itself n times. “Ha” * 3 = “HaHaHa”. No spaces are inserted between copies.* operator with strings performs repetition, concatenating the string with itself n times. “Ha” * 3 = “HaHaHa”. No spaces are inserted between copies.numbers = [1, 2, 3]
numbers.append(4)
numbers.extend([5, 6])
print(len(numbers))append(4) adds one element → [1, 2, 3, 4] (length 4). extend([5, 6]) adds two elements → [1, 2, 3, 4, 5, 6] (length 6).append(4) adds one element → [1, 2, 3, 4] (length 4). extend([5, 6]) adds two elements → [1, 2, 3, 4, 5, 6] (length 6)..remove() and .pop() for lists?.remove() and .pop() for lists?.remove(value) removes the first occurrence of a value and raises ValueError if not found.
.pop(index) removes and returns the element at an index (default: last item) and raises IndexError if index is out of range.
Key differences:
remove()searches by value,pop()removes by positionpop()returns the removed item,remove()returns None- Different error types when operation fails
Did you get it right?
(x for x in ...) instead.(x for x in ...) instead.matrix = [[j for j in range(2)] for i in range(2)]
print(matrix[1][0])matrix[1] accesses the second sublist [0, 1], then [0] gets the first element = 0.matrix[1] accesses the second sublist [0, 1], then [0] gets the first element = 0..sort() method returns a new sorted list without modifying the original..sort() sorts the list in-place and returns None. To get a new sorted list without modifying the original, use the sorted() function instead..sort() sorts the list in-place and returns None. To get a new sorted list without modifying the original, use the sorted() function instead.(1,) instead of (1) to create a single-element tuple?(1) is just integer 1 with parentheses (grouping). (1,) uses the comma to signal a tuple. This is necessary because parentheses are used for grouping in Python.(1) is just integer 1 with parentheses (grouping). (1,) uses the comma to signal a tuple. This is necessary because parentheses are used for grouping in Python.t = (1, 2, 3, 2, 4)?.index(value) method returns the index of the first occurrence of a value. For t = (1, 2, 3, 2, 4), t.index(3) returns 2..index(value) method returns the index of the first occurrence of a value. For t = (1, 2, 3, 2, 4), t.index(3) returns 2.Dictionary keys must be hashable (immutable).
Tuples are immutable → can be hashed → valid as keys
Lists are mutable → cannot be hashed → TypeError if used as keys
Example:
# Valid
locations = {(0, 0): "Origin", (1, 2): "Point A"}
# Invalid - TypeError
# bad_dict = {[0, 0]: "Origin"}Hashability ensures the key’s hash value never changes, which is critical for dictionary lookup performance.
Did you get it right?
numbers = (1, 2, 3, 4)
first, _____ = numbers
# first = 1, rest = [2, 3, 4]* operator in unpacking captures remaining elements into a list. first, *rest = (1, 2, 3, 4) assigns 1 to first and [2, 3, 4] to rest.* operator in unpacking captures remaining elements into a list. first, *rest = (1, 2, 3, 4) assigns 1 to first and [2, 3, 4] to rest.user.get('email', 'N/A') return if the key ’email’ doesn’t exist?.get(key, default) method returns the default value (‘N/A’) if the key doesn’t exist — and does not modify the dictionary. Without a default, it returns None. It never raises KeyError. To also insert the default, use .setdefault() instead..get(key, default) method returns the default value (‘N/A’) if the key doesn’t exist — and does not modify the dictionary. Without a default, it returns None. It never raises KeyError. To also insert the default, use .setdefault() instead.counts = {}
counts.setdefault('apple', 0)
counts['apple'] += 1
print(counts['apple'])setdefault('apple', 0) sets counts[‘apple’] = 0 (key doesn’t exist yet). Then counts['apple'] += 1 increments it to 1.setdefault('apple', 0) sets counts[‘apple’] = 0 (key doesn’t exist yet). Then counts['apple'] += 1 increments it to 1.dict[key] = value, deleting with del, and merging with .update(). .get() and .keys() only read data without modification.dict[key] = value, deleting with del, and merging with .update(). .get() and .keys() only read data without modification..get(key, default) never raises errors (returns default). .get(key) returns None if missing (no error). dict[key] raises KeyError if missing..get(key, default) never raises errors (returns default). .get(key) returns None if missing (no error). dict[key] raises KeyError if missing.original = {'a': 1, 'b': 2}
reversed_dict = {_____ for k, v in original.items()}{key_expr: value_expr for ...}. To reverse keys and values, use {v: k for k, v in original.items()}.{key_expr: value_expr for ...}. To reverse keys and values, use {v: k for k, v in original.items()}.dict.keys(), dict.values(), and dict.items()?dict.keys(), dict.values(), and dict.items()?.keys() → Returns view of all keys
- Example:
dict_keys(['name', 'age'])
.values() → Returns view of all values
- Example:
dict_values(['Alice', 30])
.items() → Returns view of (key, value) pairs as tuples
- Example:
dict_items([('name', 'Alice'), ('age', 30)])
All return dictionary views (not lists) that reflect changes to the original dictionary. Use list() to convert if needed.
Did you get it right?
a = {1, 2, 3, 4}
b = {3, 4, 5, 6}
print(a - b)- operator (or .difference()) returns elements in a that are NOT in b. Elements 1 and 2 are only in a, so the result is {1, 2}.- operator (or .difference()) returns elements in a that are NOT in b. Elements 1 and 2 are only in a, so the result is {1, 2}.|, &, and ^ return new sets. Methods .add() and .remove() modify the set in place and return None.|, &, and ^ return new sets. Methods .add() and .remove() modify the set in place and return None..discard(x) removes element x if present, but does nothing (no error) if x doesn’t exist. .remove(x) raises KeyError if x is not in the set..discard(x) removes element x if present, but does nothing (no error) if x doesn’t exist. .remove(x) raises KeyError if x is not in the set.a | b and a & b for sets?a | b and a & b for sets?a | b (Union) → All unique elements from both sets
- Example:
{1, 2} | {2, 3}={1, 2, 3} - Can also use
a.union(b)
a & b (Intersection) → Only elements present in BOTH sets
- Example:
{1, 2} & {2, 3}={2} - Can also use
a.intersection(b)
Think: Union = everything, Intersection = common elements
Did you get it right?
items = [1, 2, 2, 3, 3, 3, 4]
print(len(set(items)))set([1, 2, 2, 3, 3, 3, 4]) = {1, 2, 3, 4}. The length is 4 unique elements.set([1, 2, 2, 3, 3, 3, 4]) = {1, 2, 3, 4}. The length is 4 unique elements.dict.fromkeys() or Python 3.7+ dict keys.dict.fromkeys() or Python 3.7+ dict keys.map() return in Python 3?map() returns an iterator (a map object), not a list. This is similar to a generator in that it uses lazy evaluation, but it is its own distinct type. You need to convert it with list() or consume it in a loop to get the actual values.map() returns an iterator (a map object), not a list. This is similar to a generator in that it uses lazy evaluation, but it is its own distinct type. You need to convert it with list() or consume it in a loop to get the actual values.names = ['alice', 'bob']
result = list(map(str.upper, names))
print(result[1])map(str.upper, names) applies .upper() to each name, creating [‘ALICE’, ‘BOB’]. result[1] accesses the second element = ‘BOB’.map(str.upper, names) applies .upper() to each name, creating [‘ALICE’, ‘BOB’]. result[1] accesses the second element = ‘BOB’.map() with list() in Python 3?map() with list() in Python 3?map() returns an iterator (lazy evaluation), not a list.
Without list():
result = map(str.upper, names)
print(result) # <map object at 0x...>With list():
result = list(map(str.upper, names))
print(result) # ['ALICE', 'BOB']Benefits of lazy evaluation:
- Memory efficient (processes one item at a time)
- Only computes when needed
- Can work with infinite sequences
When to use list(): When you need the complete result immediately or need to use it multiple times.
Did you get it right?
numbers = [1, 2, 3, 4, 5, 6]
evens = list(filter(lambda x: _____, numbers))filter() keeps elements where the function returns True. lambda x: x % 2 == 0 returns True for even numbers (divisible by 2).filter() keeps elements where the function returns True. lambda x: x % 2 == 0 returns True for even numbers (divisible by 2).zip() receives iterables of different lengths?zip() stops when the shortest iterable is exhausted. Example: zip([1, 2, 3], ['a', 'b']) produces [(1, 'a'), (2, 'b')]. The 3 is ignored.zip() stops when the shortest iterable is exhausted. Example: zip([1, 2, 3], ['a', 'b']) produces [(1, 'a'), (2, 'b')]. The 3 is ignored.pairs = [('a', 1), ('b', 2), ('c', 3)]
letters, numbers = zip(*pairs)
print(numbers)zip(*pairs) unpacks the list and ‘unzips’ it into separate tuples. letters = ('a', 'b', 'c') and numbers = (1, 2, 3). Note: zip returns tuples, not lists.zip(*pairs) unpacks the list and ‘unzips’ it into separate tuples. letters = ('a', 'b', 'c') and numbers = (1, 2, 3). Note: zip returns tuples, not lists.enumerate() are true?enumerate() returns (index, value) tuples, starts at 0 by default, supports custom start index, and works with any iterable — including strings, generators, and files. It does NOT modify the original, and does NOT require the iterable to support indexing.enumerate() returns (index, value) tuples, starts at 0 by default, supports custom start index, and works with any iterable — including strings, generators, and files. It does NOT modify the original, and does NOT require the iterable to support indexing.sorted() function returns a new sorted list. The .sort() method sorts in-place and returns None.sorted() function returns a new sorted list. The .sort() method sorts in-place and returns None.words = ['apple', 'pie', 'banana']
result = max(words, key=len)
print(result)max(words, key=len) finds the word with maximum length. ‘banana’ has 6 characters (longest), so it returns ‘banana’, not the length.max(words, key=len) finds the word with maximum length. ‘banana’ has 6 characters (longest), so it returns ‘banana’, not the length.any([False, False, False]) returns True.any() returns True if at least one element is True. Since all elements are False, it returns False. any([False, False, True]) would return True.any() returns True if at least one element is True. Since all elements are False, it returns False. any([False, False, True]) would return True.any() and all()?any() and all()?any(iterable) → True if at least one element is True
any([False, False, True])=Trueany([False, False, False])=False- Short-circuits: stops at first True
all(iterable) → True if all elements are True
all([True, True, True])=Trueall([True, False, True])=False- Short-circuits: stops at first False
Common patterns:
# Check if string has any digits
any(c.isdigit() for c in "abc3x") # True
# Check if all numbers are positive
all(n > 0 for n in [1, 2, 3]) # TrueDid you get it right?
points = [(1, 5), (3, 2), (2, 8)]
sorted_points = sorted(points, key=lambda x: _____)x[1]. This gives [(3, 2), (1, 5), (2, 8)] sorted by second values: 2, 5, 8.x[1]. This gives [(3, 2), (1, 5), (2, 8)] sorted by second values: 2, 5, 8.map()?map() is often cleaner when you already have a named function — map(str.upper, names) vs [str.upper(n) for n in names]. For lazy evaluation or infinite sequences, map() (or a generator expression) is the right choice.map() is often cleaner when you already have a named function — map(str.upper, names) vs [str.upper(n) for n in names]. For lazy evaluation or infinite sequences, map() (or a generator expression) is the right choice.numbers = [1, 2, 3, 4]
result = all(n > 0 for n in numbers)
print(result)all() checks if all elements satisfy the condition. Since all numbers (1, 2, 3, 4) are greater than 0, it returns True.all() checks if all elements satisfy the condition. Since all numbers (1, 2, 3, 4) are greater than 0, it returns True.[1, 2, 3] + [4, 5] creates a new list without modifying the original lists.+ operator for lists creates a new list containing all elements. The original lists remain unchanged. To modify in place, use .extend().+ operator for lists creates a new list containing all elements. The original lists remain unchanged. To modify in place, use .extend().numbers = [1, 2, 3, 4, 5]
# Bad approach (causes issues):
# for num in numbers:
# if num % 2 == 0:
# numbers.remove(num)
# Good approach:
numbers = [_____ for n in numbers if _____]numbers = [n for n in numbers if n % 2 != 0]. This avoids modifying the list during iteration.numbers = [n for n in numbers if n % 2 != 0]. This avoids modifying the list during iteration..append() when adding multiple items to a list?.append() when adding multiple items to a list?.append() takes exactly ONE argument!
Wrong:
my_list = []
my_list.append(1, 2, 3) # TypeError!Correct options:
- One at a time:
my_list.append(1)
my_list.append(2)- Use
.extend()for multiple items:
my_list.extend([1, 2, 3]) # [1, 2, 3]- Append a single structured item (e.g., dict):
log = []
log.append({'ip': '192.168.1.1', 'method': 'GET'})Key: append = one item, extend = multiple items
Did you get it right?
enumerate(my_list[:]) iterates over a COPY while modifying the original by index. Option 2 might work for simple cases but can fail with insertions/deletions. Option 4 creates a new list (not in-place). Option 1 has a bug (undefined i).enumerate(my_list[:]) iterates over a COPY while modifying the original by index. Option 2 might work for simple cases but can fail with insertions/deletions. Option 4 creates a new list (not in-place). Option 1 has a bug (undefined i).Given:
students = {
"student1": {"name": "Alice", "age": 16, "classes": ["Math", "Physics"]},
"student2": {"name": "Bob", "age": 17, "classes": ["Chemistry", "Biology"]}
}Which expression accesses "Math"?
students["student1"] → Alice’s dict → ["classes"] → the list ["Math", "Physics"] → [0] → first element "Math". Option 1 gets "Physics" (index 1). Option 2 tries to index a list with a string key, raising TypeError. Option 4 uses the wrong key order.students["student1"] → Alice’s dict → ["classes"] → the list ["Math", "Physics"] → [0] → first element "Math". Option 1 gets "Physics" (index 1). Option 2 tries to index a list with a string key, raising TypeError. Option 4 uses the wrong key order.Given:
students = {
"student1": {"name": "Alice", "age": 16, "classes": ["Math", "Physics"]},
"student2": {"name": "Bob", "age": 17, "classes": ["Chemistry", "Biology"]}
}"grade" in students["student1"] returns True because students["student1"] is a non-empty dictionary.
in operator checks key existence, not whether the dict is non-empty. students["student1"] has keys "name", "age", "classes" — but no "grade" key. Result is False. Compare: "classes" in students["student2"] → True.in operator checks key existence, not whether the dict is non-empty. students["student1"] has keys "name", "age", "classes" — but no "grade" key. Result is False. Compare: "classes" in students["student2"] → True.in check on a dictionary?What does this raise?
scores = {}
scores["alice"]["math"] = 95scores["alice"] raises KeyError immediately — the key doesn’t exist yet, so there’s no nested dict to assign into. Fix: initialize first with scores["alice"] = {} then scores["alice"]["math"] = 95, or in one step: scores["alice"] = {"math": 95}.scores["alice"] raises KeyError immediately — the key doesn’t exist yet, so there’s no nested dict to assign into. Fix: initialize first with scores["alice"] = {} then scores["alice"]["math"] = 95, or in one step: scores["alice"] = {"math": 95}.scores["alice"] return when "alice" isn’t in the dict yet?Scheduled and Killing timestamps per pod. Which initialization pattern is correct?pod isn’t known before the loop. Option 3 resets the dict on every log line, wiping previously recorded timestamps for that pod. Option 4 calls .get() which returns a temporary {} that gets discarded — the assignment never reaches pod_events.pod isn’t known before the loop. Option 3 resets the dict on every log line, wiping previously recorded timestamps for that pod. Option 4 calls .get() which returns a temporary {} that gets discarded — the assignment never reaches pod_events.s = "Python"
print(s[::-1])[::-1] uses step -1, which traverses the string from end to start. “Python” reversed is “nohtyP”. This is the idiomatic Python way to reverse any sequence — works on lists and tuples too.[::-1] uses step -1, which traverses the string from end to start. “Python” reversed is “nohtyP”. This is the idiomatic Python way to reverse any sequence — works on lists and tuples too.name = "Alice"
score = 95.5
print(f"{name} scored {score:.2f}"){} at runtime. {score:.2f} formats the float with exactly 2 decimal places, so 95.5 becomes "95.50" — the trailing zero is always included. Without a format spec, {score} would produce "95.5".{} at runtime. {score:.2f} formats the float with exactly 2 decimal places, so 95.5 becomes "95.50" — the trailing zero is always included. Without a format spec, {score} would produce "95.5"..2f format spec means ‘fixed-point notation, 2 decimal places’."hello".find("x") returns None when the substring is not found..find() returns -1 (not None) when the substring isn’t found. This allows safe checks like if s.find('x') != -1. In contrast, .index() raises ValueError for a missing substring. Neither method returns None — that’s a common mix-up with .get() on dicts..find() returns -1 (not None) when the substring isn’t found. This allows safe checks like if s.find('x') != -1. In contrast, .index() raises ValueError for a missing substring. Neither method returns None — that’s a common mix-up with .get() on dicts.s = " hello world "
print(len(s.split())).split() splits on any whitespace and automatically discards the empty strings that leading, trailing, and consecutive spaces would produce. " hello world ".split() → ['hello', 'world'] — length 2. By contrast, .split(' ') with an explicit space would give ['', '', 'hello', '', '', 'world', '', ''] — length 8..split() splits on any whitespace and automatically discards the empty strings that leading, trailing, and consecutive spaces would produce. " hello world ".split() → ['hello', 'world'] — length 2. By contrast, .split(' ') with an explicit space would give ['', '', 'hello', '', '', 'world', '', ''] — length 8.^ operator)?.symmetric_difference(other) returns elements that belong to exactly one of the two sets. Example: {1, 2, 3}.symmetric_difference({2, 3, 4}) = {1, 4}. The operator shorthand is ^. Contrast with difference (-), which is directional: a - b gives elements in a not in b, while symmetric difference goes both ways..symmetric_difference(other) returns elements that belong to exactly one of the two sets. Example: {1, 2, 3}.symmetric_difference({2, 3, 4}) = {1, 4}. The operator shorthand is ^. Contrast with difference (-), which is directional: a - b gives elements in a not in b, while symmetric difference goes both ways.return keyword. The expression’s value is automatically returned. lambda x: x**2 is valid; lambda x: y = x**2; y is a SyntaxError. If you need multiple steps, use a regular def function.return keyword. The expression’s value is automatically returned. lambda x: x**2 is valid; lambda x: y = x**2; y is a SyntaxError. If you need multiple steps, use a regular def function.s = {} creates an empty set.{} creates an empty dictionary, not a set. The {} syntax was used for dicts before sets got their own literal form. To create an empty set, you must use set(). Non-empty set literals work fine — {1, 2, 3} is a set — but {} is always a dict. This is a common silent bug when you expect set uniqueness behavior.{} creates an empty dictionary, not a set. The {} syntax was used for dicts before sets got their own literal form. To create an empty set, you must use set(). Non-empty set literals work fine — {1, 2, 3} is a set — but {} is always a dict. This is a common silent bug when you expect set uniqueness behavior.type({}) return? Try it.a = [1, 2, 3]
b = a
b.append(4)
print(len(a))b = a creates a reference to the same list object — not a copy. Both a and b point to the same memory. Appending to b modifies the shared list, so a has 4 elements too. To create an independent copy, use a[:], a.copy(), or list(a). This is the same gotcha as the mutable default argument — Python never implicitly copies objects.b = a creates a reference to the same list object — not a copy. Both a and b point to the same memory. Appending to b modifies the shared list, so a has 4 elements too. To create an independent copy, use a[:], a.copy(), or list(a). This is the same gotcha as the mutable default argument — Python never implicitly copies objects.max([]) raises a ValueError.max() or min() on an empty iterable raises ValueError: max() arg is an empty sequence. This surprises many developers because sum([]) returns 0 and len([]) returns 0 without errors. To guard against empty input, pass a default: max([], default=0) returns 0 instead of raising.max() or min() on an empty iterable raises ValueError: max() arg is an empty sequence. This surprises many developers because sum([]) returns 0 and len([]) returns 0 without errors. To guard against empty input, pass a default: max([], default=0) returns 0 instead of raising.items = ["a", "b", "a", "c", "a"]
counts = {}
for item in items:
counts[item] = counts.get(item, 0) + 1
print(counts["a"])counts.get('a', 0) returns 0 (missing), sets counts['a'] = 1; second — returns 1, sets 2; third — returns 2, sets 3. The pattern counts.get(key, 0) + 1 is the idiomatic one-liner for frequency counting: it handles the ‘first occurrence’ case without a pre-check or setdefault.counts.get('a', 0) returns 0 (missing), sets counts['a'] = 1; second — returns 1, sets 2; third — returns 2, sets 3. The pattern counts.get(key, 0) + 1 is the idiomatic one-liner for frequency counting: it handles the ‘first occurrence’ case without a pre-check or setdefault.config["db"]["timeout"] and returns 30 if either "db" or "timeout" is missing?config.get("db", {}) returns an empty dict if "db" is missing, giving the chained .get("timeout", 30) a safe target. Option B fails with AttributeError when "db" is absent — None has no .get(). Option A raises KeyError if "db" doesn’t exist at all. Option D works but is misleading — burying the default inside the first .get() obscures intent and is easy to mis-read.config.get("db", {}) returns an empty dict if "db" is missing, giving the chained .get("timeout", 30) a safe target. Option B fails with AttributeError when "db" is absent — None has no .get(). Option A raises KeyError if "db" doesn’t exist at all. Option D works but is misleading — burying the default inside the first .get() obscures intent and is easy to mis-read.scores = {"Alice": 85, "Bob": 92, "Charlie": 78}
top = sorted(scores.items(), key=lambda x: x[1], reverse=True)[0]
print(top[0])scores.items() yields [('Alice', 85), ('Bob', 92), ('Charlie', 78)]. Sorting by x[1] (the score) descending gives [('Bob', 92), ('Alice', 85), ('Charlie', 78)]. [0] selects the first tuple ('Bob', 92), then [0] on that tuple yields the key 'Bob' — not the score 92. This is the idiomatic pattern for ‘find the key with the highest value’.scores.items() yields [('Alice', 85), ('Bob', 92), ('Charlie', 78)]. Sorting by x[1] (the score) descending gives [('Bob', 92), ('Alice', 85), ('Charlie', 78)]. [0] selects the first tuple ('Bob', 92), then [0] on that tuple yields the key 'Bob' — not the score 92. This is the idiomatic pattern for ‘find the key with the highest value’.