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

Check predictions against actual results
Test input or actionExpected result
ROBOT; 0Character R
ROBOT; -1Character T
ROBOT; 5Controlled invalid-position message
Empty word; 0Controlled 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.

Revisit the related lesson

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

Check predictions against actual results
Test input or actionExpected result
robot7 Cleaned ROBOT7; prefix ROB
ab Cleaned AB; prefix AB
Spaces onlyCleaned 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.

Revisit the related lesson

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

Check predictions against actual results
Test input or actionExpected result
Replacement[8, 9, 12]
Append[8, 9, 12, 7]
PopReturns 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.

Revisit the related lesson

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

Check predictions against actual results
Test input or actionExpected result
Original broken assignmentBoth names see [99, 5, 12]
Repaired copyWorking [99, 5, 12]; backup [8, 5, 12]
Nested-list thought experimentInner 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.

Revisit the related lesson

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

Check predictions against actual results
Test input or actionExpected 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.

Revisit the related lesson

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

Check predictions against actual results
Test input or actionExpected result
UnionKai, Maya, Noor
IntersectionKai
Coding minus roboticsMaya
Reverse differenceNoor
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.

Revisit the related lesson

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

Check predictions against actual results
Test input or actionExpected result
score: 0Stored score: 0
score: 8Stored score: 8
No score keyScore 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.

Revisit the related lesson

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

Check predictions against actual results
Test input or actionExpected result
motor delivery 2motor 5
sensor delivery 1sensor 1
Delivery 0 to an absent keyStores 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.

Revisit the related lesson

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

Check predictions against actual results
Test input or actionExpected result
Row 113
Row 219
Empty row 30
Grand total32
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.

Revisit the related lesson

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

Check predictions against actual results
Test input or actionExpected result
Both result lists[10, 16]
After cell update[[0, 9, 0], [0, 0, 0]]
Source numbersRemain [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.

Revisit the related lesson

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

Check predictions against actual results
Test input or actionExpected result
Maya; 2, 1, 3; quitMaya score 3/3; 100.0%
Kai; 1, 1, 2; quitKai score 1/3
Maya; 9, 2, 1, 3; quitRetry 9; final score 3/3
quit immediatelyNo 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.

Revisit the related lesson

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

Check predictions against actual results
Test input or actionExpected result
Maya 3/3, Noor 3/3Noor before Maya; both scores retained
Maya plays twiceTwo result entries, not a unique-player lookup
02, then 2 at question one02 rejected; same question retried
Close and restart programPrevious 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.

Revisit the related lesson

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