Exercises: tuples, sets, dictionaries, and files

Module 6: records and files

Name:

Student number:

Choose one answer for each multiple-choice question. Complete the state trace table. Present algorithms in pseudocode, a flowchart, or Python. Explain representation choices using words, tables, sketches, or pseudocode; short code examples are optional. Follow any answer-form requirement in the individual question. Show the reasoning and checks requested in each question.

M1) Number or not?

In Listing 1, we have four versions of a Python function split_values_per_type that receives a list of values and splits them into two lists: one with the numbers and another with the non-numbers. The function should return the two lists as the tuple (numbers, non_numbers).

For this question, a value is numeric exactly when type(value) in (int, float) is True. Therefore, integers and floating-point values are numeric, while strings and Boolean values are non-numeric.

Listing 1: Alternative implementations of a Python function that splits a list into numeric and non-numeric values and returns both resulting lists (Question 1).
(a) Implementation 1.
def split_values_per_type(values):
    numbers = []
    non_numbers = []

    for value in values:
        if type(value) in (int, float):
            numbers.append(value)
        else:
            non_numbers.append(value)

        return numbers, non_numbers
(b) Implementation 2.
def split_values_per_type(values):
    for value in values:
        numbers = []
        non_numbers = []

        if type(value) in (int, float):
            numbers.append(value)
        else:
            non_numbers.append(value)

    return numbers, non_numbers
(c) Implementation 3.
def split_values_per_type(values):
    numbers = []
    non_numbers = []

    for value in values:
        if type(value) in (int, float):
            numbers.append(value)
        else:
            non_numbers.append(value)

    return numbers, non_numbers
(d) Implementation 4.
def split_values_per_type(values):
    numbers = []
    non_numbers = []

    for value in values:
        if type(value) in (int, float):
            numbers.append(value)
        else:
            numbers.append(value)

    return numbers, non_numbers

Which implementation of split_values_per_type in Listing 1 correctly splits every input value and returns (numbers, non_numbers)?

  • split_values_per_type(values) must return its two result lists as a tuple. For example, input [2, "X"] should produce ([2], ["X"]); empty input should produce ([], []).
  • type(value) returns the value’s type: type(2) is int, type(2.5) is float, and type(True) is bool.
  • values.append(item) adds one item to the end of that list and returns None. For example, appending "B" to ["A"] changes it to ["A", "B"].

M2) Splitting a tuple

In Listing 2, a program creates a tuple and splits it into two tuples.

Python tuple indices are zero-based. A slice includes its starting index but excludes its stopping index. Therefore, arr1[:b] contains the items before index b, while arr1[b:] starts with the item at index b.

Listing 2: Code that calculates a split boundary, slices the tuple arr1, and prints the two resulting tuples (Question 2).
arr1 = (0, 10, 20, 30, 40, 50)

b = len(arr1) // 2
arr2 = arr1[:b]
arr3 = arr1[b:]

print("1:", arr2, "/ 2:", arr3)

What is printed when the code in Listing 2 is executed?

  1. 1: (True, False, False) / 2: (True, False, False)
  2. 1: (0, 10, 20) / 2: (30, 40, 50)
  3. 1: (0, 10, 20) / 2: (0, 10, 20)
  4. 1: (30, 40, 50) / 2: (0, 10, 20)
  • len(sequence) returns its number of items; len((4, 8, 12)) is 3.
  • For integer operands, // performs floor division: it divides and rounds the result down to an integer. For example, 7 // 2 is 3.
  • print displays its arguments separated by spaces and ends with a new line. For example, print("Value:", 8) displays Value: 8.

M3) Replacing negative tuple values

A measurement tuple contains integers. zero_negatives(values) must return a tuple with the same length and order, replacing each negative value with 0 and retaining every other value. The input tuple must remain unchanged.

For values = (1, -2, 3, -4, 5), the required result is (1, 0, 3, 0, 5). Empty input must return ().

Compare the four implementations in Listing 3.

Listing 3: Four ways to construct a tuple with negative measurements replaced by zero.
(a) Alternative A.
def zero_negatives(values):
    result = []
    for value in values:
        if value < 0:
            result.append(0)
        else:
            result.append(value)
    return tuple(result)
(b) Alternative B.
def zero_negatives(values):
    result = [0] * len(values)
    for i in range(len(values)):
        if values[i] < 0:
            values[i] = 0
    return tuple(result)
(c) Alternative C.
def zero_negatives(values):
    result = []
    size = len(result)
    result = [0] * len(values)
    for i in range(size):
        if values[i] < 0:
            result[i] = 0
        else:
            result[i] = values[i]
    return tuple(result)
(d) Alternative D.
def zero_negatives(values):
    result = [0] * len(values)
    for i in range(len(values)):
        if values[i] < 0:
            result[i] = 0
    return tuple(result)

Which implementation satisfies the stated requirements?

  • len(values) returns the number of items.
  • range(stop) visits integers from 0 up to, but excluding, stop: list(range(2)) is [0, 1]. list collects values into a new list, and range(0) is empty.
  • result.append(value) adds one value to the end of the list and returns None; appending 6 to [4] changes it to [4, 6].
  • tuple(result) creates a tuple containing the list’s items in order: tuple([4, 6]) is (4, 6).

M4) Sorted distinct values

A table contains integer measurements. The function in Listing 4 retains only values from 1 through 10, removes duplicates, and returns its result in ascending order.

Listing 4: Set membership records the retained integers; sorted returns a list in ascending order.
def bounded_distinct(table):
    found = set()
    for row in table:
        for value in row:
            if 1 <= value <= 10:
                found.add(value)
    return sorted(found)

data = [
    [7, 2, 11, 10],
    [11, 1, 2, 1],
    [9, 7, 0, -1],
]
print(bounded_distinct(data))

What is printed by Listing 4?

  • [1, 2, 7, 9, 10]
  • [7, 2, 11, 10, 1, 2, 9]
  • [2, 0, 0, 0, 0, 0, 2, 0, 1, 1]
  • [7, 2, 11, -1, 10, 1, 2, 9, 7, 0, -1]
  • bounded_distinct(table) receives a nested list of integers and returns a sorted list of distinct values in the inclusive range 1 through 10. For example, bounded_distinct([[3, 3, 12]]) returns [3], and bounded_distinct([]) returns [].
  • set() creates an empty set. found.add(value) adds a value and returns None; adding 4 twice still leaves only one 4 in the set.
  • sorted(values) returns a new list in ascending order without changing the input: sorted({4, 2}) is [2, 4].
  • print(value) displays the value and ends with a new line.

M5) Distinct labels in encounter order

In Listing 5, we have a Python function range_to_string that receives a rectangular range represented as a nested list of strings and returns a comma-separated string.

Listing 5: A Python function range_to_string that returns the unique values from a two-dimensional list as a comma-separated string. The join method concatenates the strings in unique_values with commas (Question 5).
def range_to_string(cell_range):
    seen = set()
    unique_values = []

    for row in cell_range:
        for value in row:
            if value not in seen:
                seen.add(value)
                unique_values.append(value)

    return ",".join(unique_values)
Table 1: Example range represented by [["A", "D"], ["1", "1"], ["C", "A"]].
A B
1 A D
2 1 1
3 C A

What is returned by range_to_string when it receives the nested list represented in Table 1?

  1. A,1,C,D,1,A
  2. A,1,C,D
  3. A,D,1,C
  4. A,D,1,1,C,A
  • range_to_string(cell_range) must return distinct labels in the order they are first encountered, one row at a time. For example, range_to_string([["X", "X"], ["Y", "X"]]) should return "X,Y".
  • set() creates an empty set. seen.add(value) adds a value once and returns None; adding "X" again does not create another member.
  • unique_values.append(value) adds one item to the end of the list and returns None.
  • ",".join(values) returns the strings joined by commas: ",".join(["X", "Y"]) is "X,Y", and joining [] returns "".

M6) Glasses for face shape

An example glasses app recommends a frame style using a fixed rule. In this app, Round maps to Aviator, Oval to Wayfarer, and Square to Round.

Figure 1 illustrates these three rules with display mannequins. The face outline identifies the input category, and the frames show the style the app should return.

Round mannequin face wearing thin double-bridge aviator frames with teardrop lenses.
(a) Round face with Aviator frames.
Oval mannequin face wearing thick angular Wayfarer frames.
(b) Oval face with Wayfarer frames.
Square mannequin face with an angular jaw wearing circular round frames.
(c) Square face with Round frames.
Figure 1: The example app’s face-shape and frame-style matches.

The app supports English, Spanish, and Dutch. For example, the English input Round should yield Aviator, the Spanish input Redonda should yield Aviador, and the Dutch input Rond should yield Aviator.

The dictionary in Table 2 stores the greeting and recommendation message for each language.

Table 2: Messages for different languages stored in LANGUAGE_MESSAGES.
Message type Language Message
greeting EN Hello! What is your face shape? Options: Round, Oval, Square.
greeting ES ¡Hola! ¿Cuál es la forma de tu cara? Opciones: Redonda, Ovalada, Cuadrada.
greeting NL Hallo! Wat is de vorm van je gezicht? Opties: Rond, Ovaal, Vierkant.
recommendation EN Based on your face shape, we recommend the style of glasses:
recommendation ES Según la forma de tu cara, recomendamos las gafas de estilo:
recommendation NL Op basis van de vorm van je gezicht, raden we de brilstijl aan:

The list in Table 3 stores face shapes and frame styles in one three-record block per language. offset is the zero-based index of the first record in the selected block.

Table 3: Mapping stored in FRAME_ROWS. The offset is the index of the first item in the selected language block.
List index Face shape Language Glasses style
0 Round EN Aviator
1 Oval EN Wayfarer
2 Square EN Round
3 Redonda ES Aviador
4 Ovalada ES Wayfarer
5 Cuadrada ES Redonda
6 Rond NL Aviator
7 Ovaal NL Wayfarer
8 Vierkant NL Rond

The complete message dictionary and frame list are supplied in Listing 6 and Listing 7.

Listing 6: Greeting and recommendation messages by language.
LANGUAGE_MESSAGES = {
    "EN": {
        "greeting": (
            "Hello! What is your face shape? Options: "
            "Round, Oval, Square."
        ),
        "recommendation": (
            "Based on your face shape, we recommend "
            "the style of glasses:"
        ),
    },
    "ES": {
        "greeting": (
            "¡Hola! ¿Cuál es la forma de tu cara? "
            "Opciones: Redonda, Ovalada, Cuadrada."
        ),
        "recommendation": (
            "Según la forma de tu cara, recomendamos "
            "las gafas de estilo:"
        ),
    },
    "NL": {
        "greeting": (
            "Hallo! Wat is de vorm van je gezicht? "
            "Opties: Rond, Ovaal, Vierkant."
        ),
        "recommendation": (
            "Op basis van de vorm van je gezicht, "
            "raden we de brilstijl aan:"
        ),
    },
}
Listing 7: Face shapes and frame styles in three language blocks.
FRAME_ROWS = [
    {
        "face_shape": "Round",
        "language": "EN",
        "style": "Aviator",
    },
    {
        "face_shape": "Oval",
        "language": "EN",
        "style": "Wayfarer",
    },
    {
        "face_shape": "Square",
        "language": "EN",
        "style": "Round",
    },
    {
        "face_shape": "Redonda",
        "language": "ES",
        "style": "Aviador",
    },
    {
        "face_shape": "Ovalada",
        "language": "ES",
        "style": "Wayfarer",
    },
    {
        "face_shape": "Cuadrada",
        "language": "ES",
        "style": "Redonda",
    },
    {
        "face_shape": "Rond",
        "language": "NL",
        "style": "Aviator",
    },
    {
        "face_shape": "Ovaal",
        "language": "NL",
        "style": "Wayfarer",
    },
    {
        "face_shape": "Vierkant",
        "language": "NL",
        "style": "Rond",
    },
]

The function recommend_glasses in Listing 8 selects the language block and prepares two messages. Its call to find_glasses uses one of the four implementations in Listing 9. To run the example, first run both data listings, define one of those implementations, and then run the main listing.

Listing 8: The recommend_glasses function receives the language, face shape, message dictionary, and frame list, and returns two messages. It normalizes the language code, selects the start of the corresponding three-item block, and calls find_glasses.
def recommend_glasses(
    language, face, texts, frames
):
    language = language.strip().upper()

    if language == "EN":
        offset = 0
    elif language == "ES":
        offset = 3
    else:
        # Other language codes use the Dutch block.
        language = "NL"
        offset = 6

    style = find_glasses(
        face, offset, frames
    )
    greeting = texts[language]["greeting"]
    prompt = texts[language]["recommendation"]
    return greeting, f"{prompt} {style}"


greeting, result = recommend_glasses(
    "ES", "  REDONDA  ",
    LANGUAGE_MESSAGES, FRAME_ROWS,
)
print(greeting)
print(result)

Compare the four implementations of find_glasses in Listing 9. The input may include extra whitespace or use a different letter case. A shape absent from the selected block must produce "No matching glasses style found.".

Listing 9: Alternative implementations of find_glasses (Question 6).
(a) Implementation 1.
def find_glasses(
    face, offset, frames
):
    shape = face.strip().lower()

    row1 = frames[offset]
    row2 = frames[offset + 1]
    row3 = frames[offset + 2]

    if shape == (
        row1["face_shape"]
        .strip().lower()
    ):
        return row1["style"]
    elif shape == (
        row2["face_shape"]
        .strip().lower()
    ):
        return row2["style"]
    elif shape == (
        row3["face_shape"]
        .strip().lower()
    ):
        return row3["style"]
    else:
        return (
            "No matching glasses "
            "style found."
        )
(b) Implementation 2.
def find_glasses(
    face, offset, frames
):
    shape = face.strip().lower()

    for index in range(
        offset, offset + 3
    ):
        candidate = (
            frames[index]
            ["face_shape"]
            .strip().lower()
        )
        if shape == candidate:
            return (
                frames[index]
                ["style"]
            )

    return (
        "No matching glasses "
        "style found."
    )
(c) Implementation 3.
def find_glasses(
    face, offset, frames
):
    shape = face.strip().lower()

    for index in range(
        offset, offset + 3
    ):
        candidate = (
            frames[index]
            ["language"]
            .strip().lower()
        )
        if shape == candidate:
            return (
                frames[index]
                ["style"]
            )

    return (
        "No matching glasses "
        "style found."
    )
(d) Implementation 4.
def find_glasses(
    face, offset, frames
):
    shape = face.strip().lower()
    result = (
        "No matching glasses "
        "style found."
    )

    for index in range(
        offset, offset + 3
    ):
        candidate = (
            frames[index]
            ["face_shape"]
            .strip().lower()
        )
        if shape == candidate:
            result = (
                frames[index]
                ["style"]
            )
            break

Which pair of implementations correctly retrieves the frame style for the supplied face shape in its language block?

  • find_glasses(face, offset, frames) should return the frame style for face in the three records starting at offset in frames. For example, find_glasses("Oval", 0, FRAME_ROWS) should return "Wayfarer". If the shape is absent, it should return "No matching glasses style found.".
  • recommend_glasses(language, face, texts, frames) returns the greeting and completed recommendation as a tuple of two strings. It receives a language code, a face shape, the message dictionary, and the frame list. Language codes other than "EN" and "ES" select the Dutch block. With English and "Oval", the recommendation ends in "Wayfarer".
  • text.strip() removes leading and trailing whitespace; text.lower() and text.upper() return lowercase and uppercase copies. For example, " OVAL ".strip().lower() is "oval", and "es".upper() is "ES".
  • range(start, stop) visits integers from start up to, but excluding, stop. For example, list(range(2, 4)) is [2, 3].
  • list(values) collects the supplied values in a list. For example, list((2, 3)) is [2, 3].
  • print(message) displays a message and ends with a new line. For example, print("Hello!") displays Hello!.
  • An f-string inserts values into text; with style = "Round", f"Style: {style}" produces "Style: Round".

M7) Payback period calculation for warehouse design projects

You are comparing seven warehouse projects that require an investment in Material Handling Equipment (MHE). For this exercise, the company accepts a project when its payback period is at most 3.0 years, including exactly 3.0 years.

The payback period is calculated by dividing the initial investment by the annual profit generated by the equipment:

\[ \text{Payback period} = \frac{\text{Initial investment}}{\text{Annual profit}} \]

This period represents the time it takes (in years) for the project to recover the initial investment through the annual profits generated.

The supplied Python function payback_years(investment, profit) in Listing 10 takes an initial investment and annual profit in the same currency and returns the payback period in years. Both amounts are positive. For example, payback_years(50_000, 20_000) returns 2.5.

Listing 10: Python function payback_years to compute the payback period for MHE projects based on the initial investment and annual profit.
def payback_years(investment, profit):
    return investment / profit

The supplied function is_viable_project(years, limit_years) in Listing 11 takes two numbers measured in years:

  1. years: the calculated payback period for one project.
  2. limit_years: the company’s maximum acceptable payback period.

It returns the Boolean value True when the project meets this payback criterion, and False otherwise. Both inputs are nonnegative.

Listing 11: Supplied function that checks whether a payback period meets a specified limit.
def is_viable_project(years, limit_years):
    return years <= limit_years

For example, with a limit of 4.0 years, is_viable_project(2.5, 4.0) returns True, is_viable_project(4.0, 4.0) returns True, and is_viable_project(4.5, 4.0) returns False. The function checks the supplied limit; it does not calculate the payback period.

Table 4 gives the investment and annual profit for each project.

Table 4: Initial investment and annual profit for various MHE projects.
Project name Initial investment Annual profit
Automated Guided Vehicles (AGVs) 500,000 150,000
Forklift Fleet Upgrade 300,000 100,000
Automated Storage and Retrieval System (AS/RS) 1,200,000 400,000
Conveyor System Installation 400,000 120,000
Pallet Shuttle System 700,000 200,000
Voice Picking System 150,000 50,000
Robotic Palletizers 800,000 250,000

The same input is stored as a list of dictionary records in Listing 12. Each record uses the fields name, initial_investment, and annual_profit.

Listing 12: Input records for the seven warehouse projects.
projects = [
    {
        "name": "Automated Guided Vehicles (AGVs)",
        "initial_investment": 500_000,
        "annual_profit": 150_000,
    },
    {
        "name": "Forklift Fleet Upgrade",
        "initial_investment": 300_000,
        "annual_profit": 100_000,
    },
    {
        "name": (
            "Automated Storage and Retrieval "
            "System (AS/RS)"
        ),
        "initial_investment": 1_200_000,
        "annual_profit": 400_000,
    },
    {
        "name": "Conveyor System Installation",
        "initial_investment": 400_000,
        "annual_profit": 120_000,
    },
    {
        "name": "Pallet Shuttle System",
        "initial_investment": 700_000,
        "annual_profit": 200_000,
    },
    {
        "name": "Voice Picking System",
        "initial_investment": 150_000,
        "annual_profit": 50_000,
    },
    {
        "name": "Robotic Palletizers",
        "initial_investment": 800_000,
        "annual_profit": 250_000,
    },
]

Use the fixed setting in Listing 13 for every project. Both supplied functions are available to all four implementations.

Listing 13: Maximum acceptable payback period for every project, in years.
limit_years = 3.0

Compare the four implementations in Listing 14. Each starts with an empty results list. Append one dictionary per project with these fields:

  • name: the project’s name.
  • payback_period: its calculated payback period in years.
  • viable: the Boolean result returned by is_viable_project.
Listing 14: Alternatives for calculating the payback period and viability of every project (Question 7).
(a) Implementation 1.
results = []

for project in projects:
    years = payback_years(
        project[
            "initial_investment"
        ],
        project["annual_profit"],
    )
    viable = is_viable_project(
        years,
        project["annual_profit"],
    )
    results.append({
        "name": project["name"],
        "payback_period": years,
        "viable": viable,
    })
(b) Implementation 2.
results = []

for project in projects:
    years = payback_years(
        project[
            "initial_investment"
        ]
    )
    viable = is_viable_project(
        years,
        limit_years,
    )
    results.append({
        "name": project["name"],
        "payback_period": years,
        "viable": viable,
    })
(c) Implementation 3.
results = []

for project in projects:
    years = payback_years(
        project[
            "initial_investment"
        ],
        project["annual_profit"],
    )
    viable = is_viable_project(
        years,
        limit_years,
    )
    results.append({
        "name": project["name"],
        "payback_period": years,
        "viable": viable,
    })
(d) Implementation 4.
results = []

for project in projects:
    years = payback_years(
        projects[0][
            "initial_investment"
        ],
        projects[0][
            "annual_profit"
        ],
    )
    viable = is_viable_project(
        years,
        limit_years,
    )
    results.append({
        "name": project["name"],
        "payback_period": years,
        "viable": viable,
    })

Which implementation correctly processes every project using the fixed limit_years and appends its result to results?

results.append(record) adds one complete record at the list’s end and returns None. Starting with results = [], results.append({"name": "AGVs"}) leaves results as [{"name": "AGVs"}].

M8) Rating frequencies in a dictionary

A transportation service lets customers rate a trip in a five-star app. In this example, customers may submit any integer from 0 through 5, including a zero-star rating.

As Figure 2 shows, each submitted rating is added to a list in arrival order. The first three customers submit 0, 2, and 3, so the list begins [0, 2, 3, ...]. Repeated ratings remain separate entries.

Three customers submit ratings 0, 2, and 3 to a five-star app, which records them in the list [0, 2, 3, ...].
Figure 2: Customer ratings enter the app and form an ordered list; the first three submissions are 0, 2, and 3.

get_frequency(ratings) summarizes that list as a dictionary:

  • The keys are all six possible ratings, from 0 through 5.
  • Each value counts how often its rating appears, including a count of 0 for an unused rating.
  • An empty input list produces all six keys with counts of 0.

For the complete example [0, 2, 3, 1, 4, 2, 5, 3, 1, 2], the required result is {0: 1, 1: 2, 2: 3, 3: 2, 4: 1, 5: 1}. For instance, rating 2 appears three times, so its dictionary entry is 2: 3.

Compare the four implementations in Listing 15.

Listing 15: Dictionary alternatives for counting customer ratings.
(a) Alternative A.
def get_frequency(ratings):
    freqs = {}
    for rating in range(1, 6):
        freqs[rating] = 0
    for rating in ratings:
        freqs[rating] += 1
    return freqs
(b) Alternative B.
def get_frequency(ratings):
    freqs = {}
    for rating in range(6):
        freqs[rating] = 0
    for index in range(len(ratings)):
        freqs[index] += 1
    return freqs
(c) Alternative C.
def get_frequency(ratings):
    freqs = {}
    for rating in range(6):
        freqs[rating] = 0
    for rating in ratings:
        freqs[rating] += rating
    return freqs
(d) Alternative D.
def get_frequency(ratings):
    freqs = {}
    for rating in range(6):
        freqs[rating] = 0
    for rating in ratings:
        freqs[rating] += 1
    return freqs

Which implementation satisfies the stated requirements?

  • range(stop) gives integers from 0 up to, excluding, stop. range(start, stop) begins at start. range(0) is empty.
  • list(values) collects the supplied values into a list. For example, list(range(3)) is [0, 1, 2], and list(range(2, 4)) is [2, 3].
  • len(ratings) returns the number of entries, including repeats: len([2, 2, 5]) is 3.

M9) Counting missing record values

A list contains dictionary records. Apply these requirements to count_empty(records):

  • Count every None value across all records.
  • Do not count 0 or an empty string; both are recorded values.
  • Every key is present. Count missing values, not absent keys.
  • No variable named worksheet is defined.

The example in Listing 16 has three missing values. record.values() visits the values stored in one dictionary.

Listing 16: Two records with three values represented by None.
records = [
    {"A": None, "B": 6, "C": 7, "D": 8},
    {"A": 9, "B": 10, "C": None, "D": None},
]

Compare the four implementations in Listing 17.

Listing 17: Alternatives that count missing values across dictionary records.
(a) Alternative A.
def count_empty(records):
    count = 0
    for record in records:
        for value in worksheet.values():
            if value is None:
                count += 1
    return count
(b) Alternative B.
def count_empty(records):
    count = 0
    for record in records:
        for value in record.values():
            if value is not None:
                count += 1
    return count
(c) Alternative C.
def count_empty(records):
    count = 0
    for record in records:
        for value in record.values():
            if value is None:
                count += 1
    return count
(d) Alternative D.
def count_empty(records):
    count = 0
    for record in records:
        count = 0
        for value in record.values():
            if value is None:
                count += 1
    return count

Which implementation satisfies the stated requirements?

  • record.values() provides the dictionary’s stored values without its keys and leaves it unchanged. An empty dictionary has no values to visit.
  • list(values) collects visited values into a list. For record = {"X": 4, "Y": None}, list(record.values()) gives [4, None].

M10) Copying dictionary records within a threshold

A table is stored as a list of dictionaries. The first dictionary contains the column labels; every later dictionary contains an experiment ID and a numeric KPI (a measured performance value). copy_below_threshold(t, table) must return a new table containing a copied header followed by a copy of every data record whose KPI is less than or equal to t. It must preserve input order, record structure, and the input table.

The example in Listing 18 has threshold 0.7. Its required output contains the header and experiment IDs 1, 3, and 5. record.copy() creates a new dictionary with the same keys and values.

Listing 18: Header and five data records stored with named fields.
table = [
    {"id": "Experiment ID", "kpi": "KPI"},
    {"id": 1, "kpi": 0.5},
    {"id": 2, "kpi": 0.9},
    {"id": 3, "kpi": 0.7},
    {"id": 4, "kpi": 0.8},
    {"id": 5, "kpi": 0.1},
]

Compare the four implementations in Listing 19.

Listing 19: Alternatives for copying the header and qualifying dictionary records.
(a) Alternative A.
def copy_below_threshold(t, table):
    result = []
    for record in table:
        if record["kpi"] <= t:
            result.append(record.copy())
    return result
(b) Alternative B.
def copy_below_threshold(t, table):
    result = []
    for record in table[1:]:
        if record["kpi"] <= t:
            result.append(record.copy())
    return result
(c) Alternative C.
def copy_below_threshold(t, table):
    result = [table[0].copy()]
    for record in table[1:]:
        if record["kpi"] <= t:
            result.append(record.copy())
    return result
(d) Alternative D.
def copy_below_threshold(t, table):
    result = [table[0].copy()]
    for record in table[1:]:
        if record["kpi"] <= t:
            for key, value in record.items():
                result.append({key: value})
    return result

Which implementation satisfies the stated requirements?

  • record.copy() returns a new dictionary with the same keys and values. For record = {"id": 8, "kpi": 0.4}, its copy is {"id": 8, "kpi": 0.4}. The records here contain only numbers or strings.
  • record.items() visits (key, value) pairs: a record {"id": 8} yields the pair ("id", 8).
  • result.append(item) adds one item to the end of the list and returns None.
  • A slice table[1:] selects every item from position 1 onward without changing table.

M11) Finding a value in a text file

A text file named values.txt contains one integer per line, in the order shown in Listing 21. It has no header or blank lines. All file operations succeed. The supplied helper in Listing 20 reads the file; the function in Listing 22 searches the returned lines in file order.

Listing 20: Supplied helper that reads the file and returns its lines as strings.
def read_lines(path):
    with open(
        path, "r", encoding="utf-8"
    ) as file:
        lines = file.readlines()
    return lines
Listing 21: Contents of values.txt, corresponding to eleven integer entries.
1
2
0
4
5
0
0
0
0
0
0
Listing 22: Return the zero-based position of the first matching integer, or -1 if it is absent.
def find_in_file(path, value):
    lines = read_lines(path)
    for i in range(len(lines)):
        if int(lines[i]) == value:
            return i
    return -1

print(
    find_in_file("values.txt", 5)
)

What is printed by Listing 22?

  • 4
  • 5
  • -1
  • 0
  • read_lines(path) receives the path of an existing UTF-8 text file and returns its lines in file order as a list of strings. For a separate file containing 8 and 3 on two lines, it returns ["8\n", "3\n"].
  • file.readlines() reads all remaining lines. Each string retains its ending newline, when present; a final line without one remains unchanged. An empty file returns [].
  • open(path, "r", encoding="utf-8") opens the file for reading and returns a file object. The with block closes it before read_lines returns. The returned list remains available to the caller.
  • Assume every line represents an integer, with no header or blank lines, and all file operations succeed. int("8\n") returns the integer 8.
  • find_in_file(path, value) receives the file path and the integer to find. It returns the first matching zero-based position or -1 if absent. For a separate file containing 8, 3, and 8 on three lines, searching for 8 returns 0, and searching for 9 returns -1. An empty file returns -1.
  • len(lines) counts the lines; range(stop) visits 0 through stop - 1, and range(0) is empty.
  • print(value) displays the value and a new line.

M12) Writing a Markdown report

Markdown is a lightweight markup language with plain-text formatting syntax. It is often used to format README files, write messages in online discussion forums, and create rich text using a plain-text editor. Listing 23 presents a main function that tests a function matrix_to_markdown that converts a matrix and headers (see Figure 3) to a Markdown table (see Figure 3 (b)).

Listing 23: Python code that uses the matrix_to_markdown function to convert the matrix and headers shown in Figure 3 (a) into the Markdown table shown in Figure 3 (b).
def main():
    data = read_data()
    headers = [
        "OrderId",
        "OrderLine",
        "Quantity",
    ]

    with open("orders.md", "w", encoding="utf-8") as file:
        text = matrix_to_markdown(data, headers)
        file.write(text)

The main function does the following:

  1. Calls the read_data function to return the supplied nested list data.
  2. Initializes the list headers with the table headers: OrderId, OrderLine, and Quantity.
  3. Calls the matrix_to_markdown function to convert the matrix and headers to Markdown format (as shown in Figure 3 (b)) and writes the result to orders.md, replacing any existing contents. The file is closed on leaving the with block. All file operations succeed.

List headers:

\[ \begin{bmatrix} OrderId & OrderLine & Quantity \end{bmatrix}. \]

Nested list data:

\[ \begin{bmatrix} 1 & 1 & 2 & 2 & 3 & 3 \\ A & B & A & B & A & B \\ 10 & 20 & 15 & 25 & 30 & 40 \end{bmatrix}. \]

The nested list has three outer items, one for each attribute. Each inner list contains the values of that attribute for the six data points. Therefore, attributes are selected with the first index and data points with the second index.

(a) Data matrix used in the main function to test the matrix_to_markdown function (columns represent data points and rows represent attributes).
| OrderId | OrderLine | Quantity |
|:---:|:---:|:---:|
| 1 | A | 10 |
| 1 | B | 20 |
| 2 | A | 15 |
| 2 | B | 25 |
| 3 | A | 30 |
| 3 | B | 40 |
(b) Markdown table generated from the data matrix and headers shown in Figure 3 (a).
Figure 3: The matrix_to_markdown function should transform the input data and headers (as shown in Figure 3 (a)) into a Markdown table (as shown in Figure 3 (b)).

In Listing 25, we have four alternative implementations of the matrix_to_markdown function. The function receives a nested list data and a list headers with the column headers and returns a string in Markdown format representing the table. It should handle an empty matrix by returning an empty string. The header is created by the get_header_row function, which returns this string, including the newline after the alignment row:

See Listing 24.

Listing 24: Markdown header and alignment rows returned by the supplied header helper.
| OrderId | OrderLine | Quantity |
|:---:|:---:|:---:|

Which implementation of the matrix_to_markdown function in Listing 25 correctly converts the matrix and headers (e.g., as shown in Figure 3 (a)) to the complete Markdown table written to orders.md (as shown in Figure 3 (b)) when main (in Listing 23) is executed?

Listing 25: Alternative implementations of the matrix_to_markdown function. The function receives a nested list data and a list headers and returns a string in Markdown format representing the table. The function creates the header using get_header_row and then iterates over the data matrix to create the data rows. The string "\n" inserts a newline character (Question 12).
(a) Implementation 1.
def matrix_to_markdown(data, headers):
    if not data:
        return ""

    text = get_header_row(headers)

    for field in range(len(data)):
        text = "|"
        for point in range(len(data[0])):
            value = data[point][field]
            text += " " + str(value) + " |"
        text += "\n"

    return text
(b) Implementation 2.
def matrix_to_markdown(data, headers):
    if not data:
        return ""

    text = get_header_row(headers)

    for point in range(len(data[0])):
        text += "|"
        for field in range(len(data)):
            value = data[point][field]
            text += " " + str(value) + " |"
        text += "\n"

    return text
(c) Implementation 3.
def matrix_to_markdown(data, headers):
    if not data:
        return ""

    text = get_header_row(headers)

    for field in range(len(data)):
        text += "|"
        for point in range(len(data[0])):
            value = data[field][point]
            text += " " + str(value) + " |"
        text += "\n"

    return text
(d) Implementation 4.
def matrix_to_markdown(data, headers):
    if not data:
        return ""

    text = get_header_row(headers)

    for point in range(len(data[0])):
        text += "|"
        for field in range(len(data)):
            value = data[field][point]
            text += " " + str(value) + " |"
        text += "\n"

    return text
  • read_data() is a supplied helper with no arguments. It returns the complete nested list shown in Figure 3 (a); for example, its first inner list is [1, 1, 2, 2, 3, 3].
  • get_header_row(headers) returns the Markdown header and alignment rows, with a newline after each. With headers = ["Item"], it returns "| Item |\n|:---:|\n".
  • main() takes no arguments and writes the converted string to orders.md; it has no explicit return value.
  • len(sequence) returns its number of items; len([2, 4]) is 2.
  • range(stop) visits 0 up to, but excluding, stop; range(0) is empty.
  • str(4) returns the string "4".
  • open(path, "w", encoding="utf-8") opens a text file for writing, replacing its contents, and returns a file object.
  • file.write("Hello\n") writes that text and returns its character count, 6. The with block closes the file when it ends.

Evaluate the bounds of the loops and the way the data matrix is accessed.

M13) Collections as dictionary keys

A dictionary stores route labels using keys that describe city names. Listing 26 defines four candidate keys: a tuple, a list, a set, and a tuple containing a list.

Listing 26: Four collections containing city names.
pair = ("Enschede", "Utrecht")
stops = ["Enschede", "Utrecht"]
cities = {"Enschede", "Utrecht"}
group = ("Enschede", ["Utrecht"])

Which candidates can be used directly as a dictionary key, without converting them to another type? Consider each candidate separately.

  • pair and group only.
  • pair only.
  • stops and cities only.
  • All four candidates.

M14) Converting collections to distinct values

A report needs distinct priority codes in ascending order, stored as a tuple. Listing 27 starts with a list and combines type conversions with sorting.

Listing 27: Convert repeated priority codes into an ordered collection.
codes = [4, 1, 4, 3, 1]
result = tuple(sorted(set(codes)))

What are the type and value of result?

  • A list with value [1, 3, 4].
  • A tuple with value (4, 1, 4, 3, 1).
  • A set containing 1, 3, and 4.
  • A tuple with value (1, 3, 4).
  1. set(values) creates a new set containing each distinct value once; it does not preserve sequence order. For integers, set([8, 8, 6]) contains 8 and 6; an empty input gives an empty set.
  2. sorted(values) returns a new list in ascending order. It keeps duplicates: sorted([8, 6, 8]) gives [6, 8, 8]; an empty input gives [].
  3. tuple(values) collects the supplied values into a tuple in their traversal order. tuple([8, 6]) gives (8, 6); an empty input gives ().

These operations leave their input collections unchanged. All codes here are integers, so they can be placed in a set and compared when sorting.

M15) Navigating a directory tree

A program must read orders.csv from the data directory (folder) in Listing 28. You run the program from /course/exercises. This folder is its current working directory, the starting point for relative paths.

Paths use / to separate names. A leading / means the root directory, the top-level folder containing everything in this tree. All entries shown are ordinary files or directories in a fictional file system. No files need to exist on your computer.

Listing 28: Directories and files available to the program.
/                       # Root: start of an absolute path.
└── course/
    ├── data/
    │   └── orders.csv
    └── exercises/      # You are here: /course/exercises.
        ├── notes.txt
        └── reports/
            └── summary.csv

Which pair gives an absolute path and a relative path that both identify the required file? Each choice gives the absolute path first and the relative path second.

  • /course/data/orders.csv and data/orders.csv.
  • /course/data/orders.csv and ../data/orders.csv.
  • /course/exercises/data/orders.csv and ../data/orders.csv.
  • /data/orders.csv and ../../data/orders.csv.
  • A path starting with / is interpreted from the root. A path without a leading / is interpreted from the current working directory.
  • .. moves to the parent directory; a directory name moves into that child directory. For example, from /lab/tasks, ../input identifies /lab/input.
  • A filename at the end selects a file inside the preceding directory.

M16) Identifying parts of a file path

A program saves a report at the file path shown in Listing 29. Each / separates names. The first / is the root directory, the top-level folder where this path begins.

Listing 29: Complete path of the saved report.
/course/reports/orders.summary.csv

Use the definitions in Table 5 to identify the parts of the report’s path.

Table 5: Parts to identify in the report’s path.
Part What to identify
Parent directory The folder immediately containing the file, written as its complete path.
File name The name after the last /, including its extension.
Stem The file name with only its final suffix removed.
Final suffix (file extension) The last dot and everything after it.

Which choice correctly identifies all four parts?

  • Parent /course/reports; file name orders.summary.csv.
    Stem orders; suffix .summary.csv.
  • Parent /course; file name reports/orders.summary.csv.
    Stem orders.summary; suffix .csv.
  • Parent /course/reports; file name orders.summary.csv.
    Stem orders.summary; suffix .csv.
  • Parent /course/reports; file name orders.summary.
    Stem orders.summary.csv; suffix csv.
  • Directory separators and dots have different roles: / separates path components, while a filename may contain several dots.
  • For example, result.final.txt has the stem result.final and the final suffix .txt. Removing the final suffix leaves earlier dots in the stem.

M17) Using dictionary keys and values together

A workshop stores its product quantities in a dictionary named stock. Each key is a product name, and its value is the number of units available. The workshop wants at least target units of every product.

The function shortages(stock, target) identifies products that need restocking. It receives the current quantities in stock and the desired quantity per product in target. Its result must satisfy these rules:

  1. Include only products whose current quantity is below target. Exclude products already at or above the target.
  2. Use each product name as a key and the additional units needed as its value. For example, shortages({"clips": 1}, 3) must return {"clips": 2} because two more clips are needed.
  3. Return a new dictionary and leave stock unchanged.

All quantities and the target are nonnegative integers. An empty stock produces {}. The order of entries does not affect the required result.

Listing 30 supplies three products and a target of 5. The required result is {"bolts": 3, "washers": 5}: bolts need three additional units, washers need five, and nuts are already at the target.

Listing 30: Product quantities and target.
stock = {
    "bolts": 2,
    "nuts": 5,
    "washers": 0,
}
target = 5

Compare the implementations in Listing 31. In each loop, name is intended to identify a product and qty its available quantity.

Listing 31: Alternatives for stock shortages.
(a) Alternative A.
def shortages(stock, target):
    result = {}
    for qty, name in stock.items():
        if qty < target:
            need = target - qty
            result[name] = need
    return result
(b) Alternative B.
def shortages(stock, target):
    result = {}
    for name, qty in stock.items():
        if qty <= target:
            need = target - qty
            result[name] = need
    return result
(c) Alternative C.
def shortages(stock, target):
    result = {}
    for name, qty in stock.items():
        if qty < target:
            need = target - qty
            result[name] = need
    return result
(d) Alternative D.
def shortages(stock, target):
    result = {}
    for name, qty in stock.items():
        if qty < target:
            need = qty
            result[name] = need
    return result

Which implementation satisfies all the requirements?

  1. stock.items() is a dictionary method that takes no arguments. It returns a view that yields each (key, value) pair as a tuple when visited by a loop, without changing the dictionary. For example, {"red": 4, "blue": 7}.items() yields ("red", 4) and ("blue", 7); an empty dictionary yields no pairs. The view reflects changes to the dictionary, so keep its entries unchanged while this loop visits them.
  2. In for key, value in mapping.items():, unpacking assigns the first item of each pair to key and the second to value. For the pair ("red", 4), these names receive "red" and 4. Unpacking into two names requires a pair of two items.

Q1) Trace state while processing pairs

A Python program compares the integer pairs [(2, 5), (4, 1), (0, 3)], stored as tuples. In each iteration, it adds the first value to a running total and calculates first - second. It then assigns a string to status:

  • "first larger" when the difference is positive.
  • "equal" when the difference is zero.
  • "second larger" when the difference is negative.

The list and its tuples are inputs. Unpacking reads each tuple’s two values into first and second, in that order.

Trace the Python program in Listing 32. The comment marks the point at which you record the state.

Listing 32: Update the total, difference, and comparison status for each integer pair.
pairs = [(2, 5), (4, 1), (0, 3)]
running_total = 0
for first, second in pairs:
    running_total += first
    difference = first - second
    if difference > 0:
        status = "first larger"
    elif difference == 0:
        status = "equal"
    else:
        status = "second larger"
    # Record the state here.
print(running_total, difference, status)
  1. Complete Table 6 at the marked checkpoint after each iteration. The first row shows how to record the values. The Total column records running_total.
  2. Write the final line printed by the program.
  3. Explain which variables carry information between iterations, which values are replaced each time, and whether reading the pairs changes them.
Table 6: State to record after assigning the comparison status for each integer pair.
Iteration Pair first second Total Difference Status
1 (2, 5) 2 5 2 -3 "second larger"
2 (4, 1)
3 (0, 3)

print displays its arguments separated by spaces and ends with a new line. For example, print(4, -2, "second larger") displays 4 -2 second larger. It returns None and leaves the displayed values unchanged.

Q2) Represent and query origin-destination records

The origin is the warehouse where a shipment departs; the destination is the warehouse where it arrives. An origin-destination (OD) pair names these two warehouses in that order. A to B and B to A are different pairs. Table 7 contains four fictional shipment records.

Table 7: Recorded shipment counts for directed warehouse pairs. Zero is a known count; pairs absent from the table have unknown counts.
Origin Destination Shipments
A B 12
B A 7
A C 0
C A 4

Each pair occurs once. Include the recorded zero when finding or printing pairs. Work with recorded pairs, rather than every possible combination of warehouses. Pairs from a warehouse to itself are outside this task. Any result order is acceptable.

  1. Propose one data structure and show how it stores these four records. You may use a list, a dictionary, or a matrix stored as a list of lists.
  2. Present algorithms in pseudocode, a flowchart, or Python for the four queries in Table 8. Each query uses your stored records and leaves them unchanged.
Table 8: Inputs and results for queries on the stored records. For example, query b with C finds A; query c with B finds A; query d with B and A returns 7.
Query Additional input Required result
a. Print all known OD pairs None Print each recorded origin, destination, and count; with no records, print nothing
b. Find destinations of an origin An origin All destinations in its recorded pairs; an empty collection if none
c. Find origins of a destination A destination All origins in its recorded pairs; an empty collection if none
d. Find a specific OD pair An origin and destination Its shipment count, including zero, or Unknown if unrecorded

Q3) Design a reusable housing-search skill

You are creating an agentic solution to search for rental houses in the Netherlands. The orchestrator below is already defined in Listing 33.

Listing 33: Orchestrator: delegate each offer check, then select matches and send one summary.
Input: Path, Criteria, Email
Output: One email
Start
    Set Websites to Call Read Sites(Path)
    Set Matches to an empty list
    For each Website in Websites
        Set Offers to Call Get Offers(Website)
        For each Offer in Offers
            Set Accepted to Call Check Offer(
                Offer, Criteria)
            If Accepted is Yes Then
                Append Offer to Matches
            End If
        End For
    End For
    Set Selected to Call Select Three(Matches)
    Call Send Summary(Email, Selected)
End
  1. Implement the real estate agent’s Check Offer(Offer, Criteria) in pseudocode or a flowchart. Its two inputs are:

    • Offer: one house record containing its city, monthly rent in euros, area in square meters, and webpage URL.
    • Criteria: the required city, maximum monthly rent, and minimum area.

    Return Yes only when all three rules hold:

    1. The offer’s city matches the required city.
    2. Its rent is at most the maximum monthly rent.
    3. Its area is at least the minimum area.

    Otherwise, return No. Rent and area equal to their limits are accepted. Leave both inputs unchanged.

The orchestrator receives a text-file path (Path), the student’s email address (Email), and Criteria. Assume valid inputs and successful operations. Every offer is currently available and appears once across all websites.

NoteSupplied operations

These operations are already defined.

  1. Read Sites(Path) reads website addresses from the text file. It returns a list in line order, trims spaces, skips blank lines, and closes the file without changing it. An empty file returns an empty list.
  2. Get Offers(Website) retrieves today’s available offers from one website address. It returns offer records in listing order without changing the website. A website with no offers returns an empty list.
  3. Select Three(Matches) receives a list of matching offers and returns a new list of up to three offers ordered by lowest rent. Equal rents keep encounter order. It leaves the input unchanged; an empty list gives an empty list.
  4. Send Summary(Email, Selected) receives an email address and a list of selected offers. It sends one summary email containing each offer’s city, rent, area, and URL in list order. With an empty list, the summary says No matching houses found. It returns no value and leaves the list unchanged.
NoteExample

Listing 34 shows the website file sites.txt.

Listing 34: Website file with two fictional addresses, searched in line order.
https://one.example
https://two.example
  • Read Sites("sites.txt") returns the two addresses in file order.
  • Get Offers("https://one.example") returns A01, A02, and A03 from Table 9, in that order.
  • Select Three([A01, B01]) returns B01 then A01 because B01 has lower rent.
  • Send Summary("student@example.test", []) sends one email saying No matching houses found.
  1. For city Enschede, maximum monthly rent 1000 euros, and minimum area 40 m², use the offers in Table 9. Which three houses will appear in the email, and in what order? Briefly justify your selection.
Table 9: Today’s available fictional offers in website and listing order. Each URL is the site address + /houses/ + label, e.g. https://one.example/houses/A01.
Site House City Rent (EUR/month) Area (m²)
1 A01 Enschede 900 50
1 A02 Enschede 1000 40
1 A03 Enschede 850 35
2 B01 Enschede 800 45
2 B02 Utrecht 700 70
2 B03 Enschede 850 55

A skill stores reusable instructions for a task. An AI agent follows instructions and uses available tools. The orchestrator coordinates the search through nested loops over websites and offers; the real estate agent checks individual offers. Each role is a named procedure with explicit inputs and outputs.

A handoff delegates one offer check. The orchestrator passes the offer and criteria to the real estate agent and receives its Yes or No decision. The supplied version waits for that decision before checking the next offer.

In agentic programming, a developer uses an AI coding agent to draft a design, test it, inspect the results, and revise it. The resulting skill can be reused with another website file or different criteria.