LEARN PYTHON | 03 STRINGS AND COLLECTIONS
Build it. Test it. Explain it.
Choose the right collection and turn its data into useful results. Work independently before opening a hint, then compare your tested solution with one possible approach.
YOUR CHALLENGE ROUTE
Complete these tasks in your Python editor. The page is a challenge guide, not a general Python runner. Use fictional details where helpful. Work in order or choose a task matching an area you need to practise.
These challenges combine earlier decisions and loops with Section 3 strings and collections. The page does not execute arbitrary Python: run the sample programs in your editor. State input assumptions and test both ordinary and awkward cases.
TRACK YOUR TESTED SOLUTIONS
Tick a task only after you have run it, checked at least two relevant cases and explained one line. These are self-recorded checks, not an automatic assessment of your source code.
Progress lasts while this page is open. Save your actual Python files in your project folder.
01 | FOUNDATION
INSPECT A CHARACTER SAFELY
Ask for a word and an integer index. Read one character only if the index is valid. Include negative indexing.
YOUR REQUIREMENTS
- Store text without stripping meaningful characters.
- Convert suitable integer text to an index.
- Check -len(word) <= index < len(word).
- Print a controlled message for invalid positions.
- Test empty text and both valid endpoints.
Hint: where to begin
An empty string has no valid single index. The negative endpoint is -len(word), not -len(word)-1.
TEST YOUR PROGRAM
| Test input or action | Expected result |
|---|---|
| ROBOT; 0 | Character R |
| ROBOT; -1 | Character T |
| ROBOT; 5 | Controlled invalid-position message |
| Empty word; 0 | Controlled invalid-position message |
Reveal one possible solution and explanation
word = input("Word: ")
index = int(input("Integer index: "))
if -len(word) <= index < len(word):
print("Character:", repr(word[index]))
else:
print("No character at that index")The check protects the indexing operation. It validates position, not the integer conversion: this version assumes suitable integer text. repr makes a space or empty-looking character easier to inspect.
A different implementation can also be correct if it meets the requirements and tests. Compare meaning and behaviour, rather than only copying the source.
Extension: Add targeted ValueError handling for unsuitable index text.
02 | STRING METHODS
EXTRACT AND CLEAN A CODE
Clean a club code using strip and upper, then extract its first three characters with a slice.
YOUR REQUIREMENTS
- Ask for a code.
- Store the returned cleaned and uppercase string.
- Slice with stop three.
- Report the original and cleaned strings using repr.
- Explain what happens with a short code.
Hint: where to begin
String methods return new text. Assign their result. A slice stops before its stop position and can safely return fewer characters.
TEST YOUR PROGRAM
| Test input or action | Expected result |
|---|---|
| robot7 | Cleaned ROBOT7; prefix ROB |
| ab | Cleaned AB; prefix AB |
| Spaces only | Cleaned and prefix are empty |
Reveal one possible solution and explanation
original = input("Club code: ")
cleaned = original.strip().upper()
prefix = cleaned[:3]
print("Original:", repr(original))
print("Cleaned:", repr(cleaned))
print("Prefix:", repr(prefix))The original name remains associated with the input text. The returned strings are stored separately. A short slice returns available characters; it does not guarantee a code meets a length or format rule.
A different implementation can also be correct if it meets the requirements and tests. Compare meaning and behaviour, rather than only copying the source.
Extension: Reject a cleaned code shorter than three characters using a separate length condition.
03 | MUTABLE LISTS
EDIT A SCORE LIST
Begin with three scores, replace the middle score, append a new one and remove the last item while reporting it.
YOUR REQUIREMENTS
- Begin with [8, 5, 12].
- Replace the item at index one with 9.
- Append 7 as one new item.
- Use pop() and store its returned value.
- Show the list after every change.
Hint: where to begin
Index one is the second item. pop() without an argument removes the last item and returns it.
TEST YOUR PROGRAM
| Test input or action | Expected result |
|---|---|
| Replacement | [8, 9, 12] |
| Append | [8, 9, 12, 7] |
| Pop | Returns 7; remaining [8, 9, 12] |
Reveal one possible solution and explanation
scores = [8, 5, 12]
scores[1] = 9
print("Replaced:", scores)
scores.append(7)
print("Appended:", scores)
removed = scores.pop()
print("Removed:", removed)
print("Remaining:", scores)The list keeps its identity while its contents change. append changes the list and returns None; pop changes the list and returns the removed item. Do not assign scores = scores.append(7).
A different implementation can also be correct if it meets the requirements and tests. Compare meaning and behaviour, rather than only copying the source.
Extension: Create a slice containing the first two items and verify that changing an integer item in the slice does not change scores.
04 | ALIASING INVESTIGATION
REPAIR A SHARED LIST
A backup unexpectedly changes when the working list is edited. Repair the backup for a flat list of integer scores.
YOUR REQUIREMENTS
- Explain what alias = original does.
- Create a separate outer list with copy().
- Change an integer item in the working list.
- Print both lists.
- State the limit of this shallow-copy solution.
SOURCE TO REPAIR
working = [8, 5, 12] backup = working working[0] = 99 print(backup)
Hint: where to begin
Assignment shares the same list. copy creates a new outer list, but it does not recursively copy nested mutable objects.
TEST YOUR PROGRAM
| Test input or action | Expected result |
|---|---|
| Original broken assignment | Both names see [99, 5, 12] |
| Repaired copy | Working [99, 5, 12]; backup [8, 5, 12] |
| Nested-list thought experiment | Inner lists would still be shared by a shallow copy |
Reveal one possible solution and explanation
working = [8, 5, 12]
backup = working.copy()
working[0] = 99
print("Working:", working)
print("Backup:", backup)Reassigning an item of the new outer list does not replace an item of the backup. This is suitable for these immutable integer items. A nested-list case requires separate attention to inner objects.
A different implementation can also be correct if it meets the requirements and tests. Compare meaning and behaviour, rather than only copying the source.
Extension: Show the difference between changing one nested row and replacing the entire row.
05 | TUPLES
UNPACK A COORDINATE
Store a fixed two-value coordinate and unpack it into x and y. Then make a one-item tuple correctly.
YOUR REQUIREMENTS
- Use (4, 7) for the coordinate.
- Unpack into exactly two names.
- Print labelled coordinates.
- Store a single number in a one-item tuple using a comma.
- Compare its type with (4).
Hint: where to begin
The comma makes the one-item tuple. Parentheses alone only group the expression.
TEST YOUR PROGRAM
| Test input or action | Expected result |
|---|---|
| Unpack (4, 7) | x 4; y 7 |
| (4,) | tuple |
| (4) | int |
Reveal one possible solution and explanation
position = (4, 7)
x, y = position
single = (4,)
grouped = (4)
print("x:", x, "y:", y)
print("One-item type:", type(single))
print("Grouped type:", type(grouped))Tuple item associations cannot be replaced. Unpacking assigns existing items to names and must match the number of targets in this example. Parentheses do not independently create a tuple.
A different implementation can also be correct if it meets the requirements and tests. Compare meaning and behaviour, rather than only copying the source.
Extension: Explain why a tuple containing a list is not a deeply frozen record.
06 | SETS
COMPARE CLUB MEMBERS
Compare club memberships where a name might be listed more than once. Report distinct memberships, overlap and coding-only members.
YOUR REQUIREMENTS
- Create sets from the provided lists.
- Use union, intersection and directional difference.
- Sort values only for readable output.
- Explain why duplicates are removed.
- Do not index a set.
Hint: where to begin
Use | for union, & for intersection and - for members in the left set but not the right. Sorting returns an ordered list for display.
TEST YOUR PROGRAM
| Test input or action | Expected result |
|---|---|
| Union | Kai, Maya, Noor |
| Intersection | Kai |
| Coding minus robotics | Maya |
| Reverse difference | Noor |
Reveal one possible solution and explanation
coding = set(["Maya", "Kai", "Maya"])
robotics = set(["Kai", "Noor"])
print("Either club:", sorted(coding | robotics))
print("Both clubs:", sorted(coding & robotics))
print("Coding only:", sorted(coding - robotics))Set membership is distinct and case-sensitive. Difference is directional. The alphabetical output comes from sorted(), rather than any promised iteration order of the sets.
A different implementation can also be correct if it meets the requirements and tests. Compare meaning and behaviour, rather than only copying the source.
Extension: Normalise first names to lowercase before comparison and explain the consequences of choosing case-insensitive identity.
07 | DICTIONARIES
DISTINGUISH MISSING FROM ZERO
Repair a score report that treats a genuine zero as missing.
YOUR REQUIREMENTS
- Use a dictionary record with score zero.
- Check key membership, not truthiness of its value.
- Report the stored score when present.
- Use a separate missing-key message otherwise.
- Test zero, a positive score and no score key.
SOURCE TO REPAIR
record = {"name": "Maya", "score": 0}
if record.get("score"):
print(record["score"])
else:
print("Score missing")Hint: where to begin
if record.get("score") treats zero as false. "score" in record asks whether the key exists.
TEST YOUR PROGRAM
| Test input or action | Expected result |
|---|---|
| score: 0 | Stored score: 0 |
| score: 8 | Stored score: 8 |
| No score key | Score missing |
Reveal one possible solution and explanation
record = {"name": "Maya", "score": 0}
if "score" in record:
print("Stored score:", record["score"])
else:
print("Score missing")Existence and value are separate questions. The guarded direct access is safe for a present key and preserves zero as valid data. This does not independently establish that the stored value has a permitted type or range.
A different implementation can also be correct if it meets the requirements and tests. Compare meaning and behaviour, rather than only copying the source.
Extension: Add explicit score-range validation after confirming the key exists.
08 | KEY-VALUE DATA
UPDATE AN INVENTORY RECORD
Track equipment counts by name and add a delivery to an existing or new key.
YOUR REQUIREMENTS
- Begin with three motors and no sensor key.
- Add two motors.
- Add one sensor using get with numeric default zero.
- Print each key and value using items().
- Keep keys as labels rather than positional indexes.
Hint: where to begin
inventory.get(name, 0) reads zero for an absent key without inserting it. The assignment then stores the new total.
TEST YOUR PROGRAM
| Test input or action | Expected result |
|---|---|
| motor delivery 2 | motor 5 |
| sensor delivery 1 | sensor 1 |
| Delivery 0 to an absent key | Stores key with zero; get alone would not insert it |
Reveal one possible solution and explanation
inventory = {"motor": 3}
for item, amount in [("motor", 2), ("sensor", 1)]:
inventory[item] = inventory.get(item, 0) + amount
for item, count in inventory.items():
print(item, count)Each loop tuple is unpacked into item and amount. The dictionary stores one current value for each key. Adding an existing key replaces its value rather than creating a second identical key.
A different implementation can also be correct if it meets the requirements and tests. Compare meaning and behaviour, rather than only copying the source.
Extension: Add a non-negative amount check before accepting a delivery.
09 | NESTED COLLECTIONS
TOTAL ROWS AND THE WHOLE TABLE
Calculate each team’s score total and one grand total from a nested list.
YOUR REQUIREMENTS
- Use [[8, 5], [12, 4, 3], []].
- Reset row_total for each new row.
- Initialise grand_total once before all rows.
- Print each row number and total.
- Allow rows of different lengths, including empty rows.
Hint: where to begin
The outer loop selects a row. The inner loop visits that row’s values. An empty row performs no inner iterations.
TEST YOUR PROGRAM
| Test input or action | Expected result |
|---|---|
| Row 1 | 13 |
| Row 2 | 19 |
| Empty row 3 | 0 |
| Grand total | 32 |
Reveal one possible solution and explanation
rows = [[8, 5], [12, 4, 3], []]
grand_total = 0
for row_number, row in enumerate(rows, start=1):
row_total = 0
for value in row:
row_total += value
grand_total += row_total
print("Row", row_number, "total:", row_total)
print("Grand total:", grand_total)The row accumulator is reset once per row so it does not include earlier teams. The grand accumulator is maintained across rows. enumerate gives display row numbers; it does not change the actual list indexes.
A different implementation can also be correct if it meets the requirements and tests. Compare meaning and behaviour, rather than only copying the source.
Extension: Calculate a mean for each non-empty row and report no values for an empty row.
10 | COMPREHENSIONS
FILTER AND TRANSFORM WITHOUT SHARED ROWS
Make a list of doubled values that are at least five in the original data, then create independent zero-filled rows.
YOUR REQUIREMENTS
- Use source [2, 5, 8, 4].
- First write the filtering and doubling with a loop.
- Write the equivalent list comprehension.
- Create two independent three-item rows.
- Change one cell and verify the other row is unaffected.
Hint: where to begin
In [number * 2 for number in numbers if number >= 5], the filter tests the original number. A fresh row expression must execute for each outer iteration.
TEST YOUR PROGRAM
| Test input or action | Expected result |
|---|---|
| Both result lists | [10, 16] |
| After cell update | [[0, 9, 0], [0, 0, 0]] |
| Source numbers | Remain [2, 5, 8, 4] |
Reveal one possible solution and explanation
numbers = [2, 5, 8, 4]
loop_result = []
for number in numbers:
if number >= 5:
loop_result.append(number * 2)
compact_result = [number * 2 for number in numbers if number >= 5]
print("Loop:", loop_result)
print("Comprehension:", compact_result)
rows = [[0] * 3 for _ in range(2)]
rows[0][1] = 9
print("Rows:", rows)The final if filters visits; the leading expression transforms included values. Constructing a new row inside the comprehension avoids the shared-inner-list problem of [[0] * 3] * 2. _ is an ordinary name used by convention when its value is not needed.
A different implementation can also be correct if it meets the requirements and tests. Compare meaning and behaviour, rather than only copying the source.
Extension: Try the shared-row version separately and explain the different output without claiming that all nested lists are shared.
11 | CAPSTONE
BUILD THE QUIZ AND SCOREBOARD
Build the complete quiz from the lesson and test its control flow before adding new questions.
YOUR REQUIREMENTS
- Use the three authored question dictionaries.
- Accept only an offered answer code.
- Retry invalid codes without advancing or changing score.
- Complete multiple player rounds.
- Sort completed (score, name) tuples in reverse order.
- State the descending-name tie display policy.
- Explain that the scoreboard is temporary.
Hint: where to begin
Keep the player loop, question loop and invalid-answer retry loop distinct. Append a completed result once per finished round.
TEST YOUR PROGRAM
| Test input or action | Expected result |
|---|---|
| Maya; 2, 1, 3; quit | Maya score 3/3; 100.0% |
| Kai; 1, 1, 2; quit | Kai score 1/3 |
| Maya; 9, 2, 1, 3; quit | Retry 9; final score 3/3 |
| quit immediately | No completed players |
Reveal one possible solution and explanation
# Session quiz: authored question bank must be non-empty and consistent.
questions = [
{"prompt": "Which Python type stores text?",
"options": ["int", "str", "bool"], "answer": "2"},
{"prompt": "What is the first non-negative list index?",
"options": ["0", "1", "-1"], "answer": "1"},
{"prompt": "Which collection keeps distinct members?",
"options": ["list", "tuple", "set"], "answer": "3"}
]
scoreboard = []
print("Python collections quiz")
while True:
name = input("Player first name, or quit to finish: ").strip()
if name == "quit":
break
if not name:
print("Enter a non-empty name")
continue
score = 0
for question in questions:
print()
print(question["prompt"])
for number, option in enumerate(question["options"], start=1):
print(f"{number}. {option}")
valid_choices = [str(number) for number in range(1, len(question["options"]) + 1)]
response = input("Option number: ").strip()
while response not in valid_choices:
print("Choose one of the displayed option numbers")
response = input("Option number: ").strip()
if response == question["answer"]:
score += 1
print("Correct")
else:
print(f"Not correct. Answer code: {question['answer']}")
percentage = score / len(questions) * 100
print(f"{name}: {score}/{len(questions)} ({percentage:.1f}%)")
scoreboard.append((score, name))
print()
print("Session scoreboard: score descending, then name descending for ties")
if scoreboard:
ranked = sorted(scoreboard, reverse=True)
for position, result in enumerate(ranked, start=1):
score, name = result
print(f"Display position {position}: {name} - {score}/{len(questions)}")
else:
print("No completed players")Each dictionary stores one question’s prompt, options and correct answer code. Invalid answers do not reach the scoring check. The result tuple captures a finished score and name; reverse tuple sorting orders scores first and names second. Positions are display positions, not competitive tie ranks.
A different implementation can also be correct if it meets the requirements and tests. Compare meaning and behaviour, rather than only copying the source.
Extension: Add two authored questions and update test answers. The percentage and valid-choice ranges already use actual collection lengths.
12 | TEST AND REFLECT
TEST TIES, LIMITS AND DATA QUALITY
Test the complete project with multiple players. Explain what the program guarantees and what its author must still check.
YOUR REQUIREMENTS
- Run Maya and Noor with equal scores and predict descending-name order.
- Run the same name twice and confirm two completed entries.
- Check that blank names are rejected.
- Check that 02 is not accepted as answer code 2.
- Review every correct-answer code against its options.
- Explain the need for persistent storage before claiming saved results.
Hint: where to begin
The question bank is trusted authored data. Input validation does not prove the question or answer is factually correct.
TEST YOUR PROGRAM
| Test input or action | Expected result |
|---|---|
| Maya 3/3, Noor 3/3 | Noor before Maya; both scores retained |
| Maya plays twice | Two result entries, not a unique-player lookup |
| 02, then 2 at question one | 02 rejected; same question retried |
| Close and restart program | Previous in-memory scores are not restored |
Reveal one possible solution and explanation
# Session quiz: authored question bank must be non-empty and consistent.
questions = [
{"prompt": "Which Python type stores text?",
"options": ["int", "str", "bool"], "answer": "2"},
{"prompt": "What is the first non-negative list index?",
"options": ["0", "1", "-1"], "answer": "1"},
{"prompt": "Which collection keeps distinct members?",
"options": ["list", "tuple", "set"], "answer": "3"}
]
scoreboard = []
print("Python collections quiz")
while True:
name = input("Player first name, or quit to finish: ").strip()
if name == "quit":
break
if not name:
print("Enter a non-empty name")
continue
score = 0
for question in questions:
print()
print(question["prompt"])
for number, option in enumerate(question["options"], start=1):
print(f"{number}. {option}")
valid_choices = [str(number) for number in range(1, len(question["options"]) + 1)]
response = input("Option number: ").strip()
while response not in valid_choices:
print("Choose one of the displayed option numbers")
response = input("Option number: ").strip()
if response == question["answer"]:
score += 1
print("Correct")
else:
print(f"Not correct. Answer code: {question['answer']}")
percentage = score / len(questions) * 100
print(f"{name}: {score}/{len(questions)} ({percentage:.1f}%)")
scoreboard.append((score, name))
print()
print("Session scoreboard: score descending, then name descending for ties")
if scoreboard:
ranked = sorted(scoreboard, reverse=True)
for position, result in enumerate(ranked, start=1):
score, name = result
print(f"Display position {position}: {name} - {score}/{len(questions)}")
else:
print("No completed players")Sorting provides a reproducible display policy, not a fairness judgement between equal scores. Names are display labels and not guaranteed unique identities. The program has no file persistence and assumes a non-empty, correctly authored question bank.
A different implementation can also be correct if it meets the requirements and tests. Compare meaning and behaviour, rather than only copying the source.
Extension: Write a short user guide and a future-improvement plan for random question order, functions and saved scores. Keep changes separate and rerun core tests.
KEEP A SMALL TEST RECORD
Record inputs, expected results, actual results and a conclusion. Include an empty collection, an endpoint index, a missing key, a valid zero and a quiz retry where relevant.
For a collection change, record the before and after values. For the quiz, record whether an invalid response leaves the question and score unchanged. A data-quality check is separate from testing program control flow.
READY FOR THE NEXT SECTION?
You are ready to move on when you can explain zero-based indexing, select a suitable collection, distinguish copying from sharing, access nested data and test a quiz scoreboard.
If one skill is uncertain, revisit its lesson and change a worked example before repeating the challenge. The Section 3 workbook is coming soon.
Next: functions and returning values Python course navigation