Programming with PythonControl Flow and Collections › Day 52

Hands-on lab — Day 52: Lists in Depth

Commands

Setup

cd labs/sections/programming-with-python/day-052-lists-in-depth
python3 --version

Run

python3 examples/toolkit.py
python3 starter/toolkit.py

Test

bash tests/run_tests.sh

File tree

examples/toolkit.py
expected-output/FIELDS.md
expected-output/sample-run.txt
expected-output/test-run.txt
metadata.yml
README.md
requirements/README.md
security.md
starter/toolkit.py
tests/run_tests.sh
troubleshooting.md

Lab README

Day 052 lab — List Toolkit

Lesson

  • Lesson title: Lists in Depth
  • Day number: 52 of 365
  • Lesson article: https://ai-roadmap-365.github.io/day-052-lists-in-depth
  • Lab files: everything you need is in this directory — follow “How to run” below.
  • Browse the course locally: from the repository root, this lab also appears in the course website at /labs/day-052-lists-in-depth when the site is running.

Purpose

Day 52's lesson teaches Python lists in depth: indexing and slicing, in-place methods versus functions that return new lists, and the reference model that makes list mutation surprising. This lab makes those ideas concrete. You build a List Toolkit — five small, idiomatic functions that slice a list, sort it with a key, remove duplicates while preserving order, flatten a nested list, and safely copy a list before changing it — and you run a test suite that proves the difference between mutating a list in place and returning a new one. This is the exact skill that keeps datasets, batches, and token sequences from being silently corrupted in the data and machine-learning code later in the course.

Learning objectives

  • Slice a list with start, stop, and step, including negative indices.
  • Sort with a key= function and rely on Python's stable sort.
  • Tell in-place methods (sort, append) apart from functions that return a new list (sorted, a slice) — and know which one you called.
  • Remove duplicates while preserving first-seen order, and flatten a nested list, each returning a new list.
  • Copy a list before mutating it, and explain why aliasing makes the original change otherwise.

Prerequisites

  • The Day 52 lesson (read it first — it explains every part this lab builds).
  • Days 49-51: a first real program with functions and a main guard, conditionals, and loops.
  • A text editor and a terminal. No programming experience beyond these weeks is assumed.

Supported operating systems

  • macOS — fully supported (tested on macOS with Apple Silicon, Python 3.14.0).
  • Linux — fully supported (any distribution with Python 3 and bash).
  • Windows — use WSL and follow the Linux path, or substitute python for python3 if that is how Python is exposed. The program is pure standard-library Python and behaves identically everywhere.

Hardware requirements

Any computer that runs Python 3. The program manipulates a handful of short lists; it needs no special memory, disk, or GPU.

Required software

  • python3 (3.8 or newer; tested on 3.14.0).
  • bash for the test runner (preinstalled on macOS and Linux).
  • Standard library only — the sys module ships with Python. No packages to install. See requirements/README.md.

Free and open-source options

Everything here is free and open source: Python, bash, and the standard library. No account, API key, network access, or purchase is needed.

Installation

None beyond Python itself. Move into this directory and you are ready:

cd labs/sections/programming-with-python/day-052-lists-in-depth
python3 --version   # confirm Python 3.8+ is available

File structure

day-052-lists-in-depth/
├── README.md                       ← you are here
├── metadata.yml                    ← machine-readable lab metadata
├── starter/
│   └── toolkit.py                  ← YOUR working file (5 numbered exercises)
├── examples/
│   └── toolkit.py                  ← complete reference implementation
├── tests/
│   └── run_tests.sh                ← automated checks (pipeline, imports, in-place vs new)
├── expected-output/
│   ├── sample-run.txt              ← real captured run of the reference
│   ├── test-run.txt                ← real captured run of the test suite
│   └── FIELDS.md                   ← required behaviour on every platform
├── requirements/
│   └── README.md                   ← dependency statement (Python 3 only)
├── troubleshooting.md
└── security.md

How to run

From this directory:

## 1. See the finished toolkit's demo pipeline
python3 examples/toolkit.py

## 2. Call one function directly, without running the whole program
python3 -c "import sys; sys.path.insert(0, 'examples'); import toolkit; print(toolkit.dedupe([1, 1, 2, 1, 3]))"

## 3. Your task: complete the five exercises in the starter, then run it
python3 starter/toolkit.py

## 4. Check your work
bash tests/run_tests.sh

What the commands do

  • python3 examples/toolkit.py — runs the complete reference program. It builds a list of scores, then prints the results of slicing it (top three, every other item, the last two), sorting words by length (stably), removing duplicates while keeping order, flattening a two-row matrix, and growing a copy while proving the original stays unchanged.
  • python3 -c "...toolkit.dedupe..." — imports one function from the module and calls it, without running the whole program. This works because the main guard holds main back on import.
  • python3 starter/toolkit.py — runs your version. The starter ships with five exercises stubbed out (each raising NotImplementedError until you finish it): dedupe, flatten, sort-by-length, top-n, and copy-before-mutate.
  • bash tests/run_tests.sh — runs the reference through the demo pipeline, imports every function and checks its return value, and asserts the core lesson: sorted() returns a new list while .sort() mutates in place, assignment aliases a list while a slice copies it, and none of the toolkit functions change their input. Then it checks your starter (structurally until you finish, strictly afterwards). Exits 0 only if every check passes.

Expected output

See expected-output/sample-run.txt — a real captured run:

$ python3 examples/toolkit.py
built:    [88, 72, 95, 72, 60, 95, 81]
top 3:    [95, 95, 88]
stride:   [88, 95, 60, 81]
last 2:   [95, 81]
by len:   ['fig', 'fig', 'pear', 'kiwi', 'plum', 'apple']
unique:   ['pear', 'fig', 'apple', 'kiwi', 'plum']
flat:     [1, 2, 3, 4, 5, 6]
grown:    [10, 20, 30, 40]
original: [10, 20, 30]

The last two lines are the payoff: grown gained a 40, but original did not, because with_appended copied the list before appending. The toolkit is deterministic, so your numbers will match exactly. expected-output/FIELDS.md lists the required result for every operation on every platform.

Validation steps

  1. Run python3 examples/toolkit.py — the original: line must still read [10, 20, 30], proving the copy protected it.
  2. Run the import one-liner — it must print [1, 2, 3].
  3. Complete the five exercises in starter/toolkit.py, then run it and confirm its output matches the reference.
  4. Run the tests (next section) — every check must pass.

Tests

bash tests/run_tests.sh

Expected final line while the starter is unfinished: 13 checks, 0 failure(s). Once you complete all five starter exercises, seven more checks run your version through the same pipeline plus the copy-safety check, giving 20 checks, 0 failure(s). The command exits 0 on success and non-zero on any failure, so it can run in CI. A full captured run is in expected-output/test-run.txt.

Cleanup

Nothing to clean up: the program and tests read no input and write nothing outside their own console output (no files, no network, no settings). To reset your work, restore the starter from git: git checkout -- starter/toolkit.py. If a __pycache__ folder appears after an import, remove it with rm -rf examples/__pycache__.

Troubleshooting

See troubleshooting.md for the full list: python vs python3, the deliberate NotImplementedError stubs, importing vs running, the with_appended mutation trap, and why .sort() returns None.

Security notes

See security.md. Short version: the program makes no network calls, writes no files, and needs no privileges. Its central lesson is to copy before you mutate — return a new list rather than changing the one you were given, and reach for a deep copy when the data is nested.

Extension exercises

  1. Add a deep_flatten(nested) that flattens arbitrarily nested lists (a list inside a list inside a list), then compare it with the one-level flatten.
  2. Add a chunk(items, size) that splits a list into a list of smaller lists of length size — the shape of turning a dataset into training batches.
  3. Write a demonstration that uses copy.deepcopy on a matrix, mutates one inner list, and shows the deep copy is unaffected while a shallow [:] copy is not.
  • Previous day: Day 51 — Loops: for, while, and Iteration Patterns (labs/sections/programming-with-python/day-051-loops-for-while-and-iteration-patterns/).
  • Next day: Day 53 — Dictionaries in Depth (labs/sections/programming-with-python/day-053-dictionaries-in-depth/).
  • Week 8 project: the Terminal Task Manager, a to-do CLI that stores tasks in a list (and dictionaries) — built directly on the list skills in this lab.

Expected output

FIELDS.md

# Expected output — Day 052 lab

This directory holds real captured runs from the authoring machine
(macOS, Apple Silicon, Python 3.14.0, 2026-07-13). Your numbers will match
exactly, because the toolkit is deterministic — the same input always
produces the same output on every platform.

## Files

- `sample-run.txt` — the reference program's demo pipeline, plus two
  `python3 -c` import checks calling `dedupe` and `flatten`.
- `test-run.txt` — a full run of `bash tests/run_tests.sh` with the starter
  still unfinished (13 checks, 0 failures). The repository path is shown as
  `<repo>` so nothing machine-specific leaks in.

## Required behaviour on every platform

A correct toolkit must, for these inputs, produce exactly:

| Operation | Input | Result |
| --------- | ----- | ------ |
| `top_n(scores, 3)` | `[88, 72, 95, 72, 60, 95, 81]` | `[95, 95, 88]` |
| slice `scores[::2]` | `[88, 72, 95, 72, 60, 95, 81]` | `[88, 95, 60, 81]` |
| slice `scores[-2:]` | `[88, 72, 95, 72, 60, 95, 81]` | `[95, 81]` |
| `sort_by_length(words)` | `['pear','fig','apple','kiwi','plum','fig']` | `['fig','fig','pear','kiwi','plum','apple']` |
| `dedupe(words)` | `['pear','fig','apple','kiwi','plum','fig']` | `['pear','fig','apple','kiwi','plum']` |
| `flatten(matrix)` | `[[1,2,3],[4,5,6]]` | `[1, 2, 3, 4, 5, 6]` |
| `with_appended([10,20,30], 40)` | original stays | `[10, 20, 30]` (unchanged) |

The only platform difference is the shell prompt shown before each command
(`$` here); the program's own output is identical everywhere Python 3 runs.
Python guarantees a stable sort, so `sort_by_length` produces the same order
on every platform and version.

## Test-suite counts

- With the starter unfinished: `13 checks, 0 failure(s).`
- Once you complete all five starter exercises: `20 checks, 0 failure(s).`
  (the eight extra checks run your starter through the same demo pipeline as
  the reference, plus a check that it copies before mutating).

sample-run.txt

$ python3 examples/toolkit.py
built:    [88, 72, 95, 72, 60, 95, 81]
top 3:    [95, 95, 88]
stride:   [88, 95, 60, 81]
last 2:   [95, 81]
by len:   ['fig', 'fig', 'pear', 'kiwi', 'plum', 'apple']
unique:   ['pear', 'fig', 'apple', 'kiwi', 'plum']
flat:     [1, 2, 3, 4, 5, 6]
grown:    [10, 20, 30, 40]
original: [10, 20, 30]

$ python3 -c "import sys; sys.path.insert(0, 'examples'); import toolkit; print(toolkit.dedupe([1, 1, 2, 1, 3]))"
[1, 2, 3]

$ python3 -c "import sys; sys.path.insert(0, 'examples'); import toolkit; print(toolkit.flatten([[1, 2, 3], [4, 5, 6]]))"
[1, 2, 3, 4, 5, 6]

test-run.txt

Testing <repo>/labs/sections/programming-with-python/day-052-lists-in-depth/examples/toolkit.py ...
  ok: pipeline runs, exit 0
  ok: top 3 via sort+slice
  ok: stride slice [::2]
  ok: negative-index slice [-2:]
  ok: stable sort by length
  ok: order-preserving dedupe
  ok: flatten a matrix
  ok: copy stayed safe
Testing importability and return values of examples/toolkit.py ...
  ok: each function returns the expected list
Testing in-place vs new-list behaviour ...
all in-place/new-list assertions passed
  ok: sorted vs .sort(), aliasing vs copy, no input mutation
Testing starter/toolkit.py ...
  ok: starter is valid Python
Note: starter/toolkit.py still has unfinished exercises — testing structure only.
  ok: starter defines dedupe
  ok: starter defines with_appended

13 checks, 0 failure(s).

Source files

examples/toolkit.py (3426 bytes)
#!/usr/bin/env python3
"""List Toolkit — five idiomatic list operations, plus a demo pipeline.

This is a complete, small, real program: it exposes a handful of pure list
functions and a `main` that runs them over a sample dataset, printing each
step so you can watch a list being built, sliced, sorted, and transformed.

Every function here returns a NEW list and never changes the list it is
given — that discipline (copy before you mutate) is the whole point of the
lesson, because silent list mutation is one of the most common bugs in
data and machine-learning code.

Usage:
    python3 toolkit.py            # run the demo pipeline

Import and reuse any function:
    from toolkit import dedupe, flatten, sort_by_length, top_n, with_appended
"""
import sys


def dedupe(items):
    """Return a new list with duplicates removed, preserving first-seen order.

    Uses list membership (`in`), which scans the growing result — clear and
    correct. A set would make the membership test faster; you meet sets on
    Day 54.
    """
    result = []
    for item in items:
        if item not in result:      # membership test on a list is O(n)
            result.append(item)
    return result


def flatten(matrix):
    """Flatten one level of nesting: a list of lists into a single new list.

    flatten([[1, 2], [3, 4]]) -> [1, 2, 3, 4]
    """
    result = []
    for row in matrix:
        for item in row:            # iterate the inner list
            result.append(item)
    return result


def sort_by_length(words):
    """Return a NEW list of words sorted by length, shortest first.

    `sorted` returns a new list and leaves the input untouched. The sort is
    stable, so words of equal length keep their original relative order.
    """
    return sorted(words, key=len)


def top_n(numbers, n):
    """Return the n largest numbers, highest first, as a new list.

    Sort a copy in descending order, then slice off the first n.
    """
    return sorted(numbers, reverse=True)[:n]


def with_appended(items, value):
    """Return a NEW list equal to items plus value; the input is unchanged.

    `items[:]` makes a shallow copy first, so appending touches only the copy
    — the caller's original list is protected from surprise mutation.
    """
    result = items[:]               # shallow copy protects the caller's list
    result.append(value)
    return result


def main(argv):
    """Run the demo pipeline over a sample dataset. Returns exit code 0."""
    # BUILD a list of exam scores.
    scores = [88, 72, 95, 72, 60, 95, 81]
    print(f"built:    {scores}")

    # SLICE: top three (sort then slice), every other item, the last two.
    print(f"top 3:    {top_n(scores, 3)}")
    print(f"stride:   {scores[::2]}")
    print(f"last 2:   {scores[-2:]}")

    # SORT with a key: order words by length, stably.
    words = ["pear", "fig", "apple", "kiwi", "plum", "fig"]
    print(f"by len:   {sort_by_length(words)}")

    # TRANSFORM: remove duplicates (order kept) and flatten a matrix.
    print(f"unique:   {dedupe(words)}")
    matrix = [[1, 2, 3], [4, 5, 6]]
    print(f"flat:     {flatten(matrix)}")

    # SAFE COPY: grow a copy, then prove the original was not touched.
    original = [10, 20, 30]
    grown = with_appended(original, 40)
    print(f"grown:    {grown}")
    print(f"original: {original}")
    return 0


if __name__ == "__main__":
    sys.exit(main(sys.argv))
metadata.yml (590 bytes)
lesson_id: D052
day: 52
kind: python-program
languages: [python]
setup_commands:
  - cd labs/sections/programming-with-python/day-052-lists-in-depth
  - python3 --version
run_commands:
  - python3 examples/toolkit.py
  - python3 starter/toolkit.py
test_commands:
  - bash tests/run_tests.sh
cleanup_commands:
  - 'git checkout -- starter/toolkit.py  # optional: reset your work'
requires_network: false
requires_api_key: false
estimated_minutes: 30
last_executed: '2026-07-13'
executed_on: 'macOS (Apple Silicon), Python 3.14.0, bash tests/run_tests.sh → 13 checks, 0 failure(s), exit 0'
requirements/README.md (946 bytes)
# Dependencies — Day 052 lab

**Python 3 only. No third-party packages.**

- `python3` (3.8 or newer; tested on 3.14.0). Preinstalled on most Linux
  distributions and installable on macOS; you set this up on Day 43.
- `bash` for the test runner (preinstalled on macOS and Linux).
- Only the Python standard library is used — specifically the `sys` module,
  which ships with Python. There is deliberately no `requirements.txt`: lists
  are a built-in type, so this lab runs on a plain Python install with nothing
  to install first.

Check your Python is present and new enough:

```bash
python3 --version
```

If that prints `Python 3.8` or higher, you are ready. Windows users: run the
commands inside WSL, or use `python` in place of `python3` if that is how
Python is exposed on your system. The stable-sort behaviour this lab relies on
has been guaranteed by Python since version 3.7, so any supported Python
produces identical output.
starter/toolkit.py (3135 bytes)
#!/usr/bin/env python3
"""List Toolkit — YOUR working file.

Build this program one exercise at a time. Each numbered exercise below
names exactly what to write. The finished reference is in
examples/toolkit.py — try each exercise yourself before peeking.

The golden rule for every function here: return a NEW list, and never change
the list you were given. Copy before you mutate.

When all five exercises are done, run:
    python3 starter/toolkit.py
    bash tests/run_tests.sh
"""
import sys


def dedupe(items):
    """Return a new list with duplicates removed, preserving first-seen order."""
    # Exercise 1: DEDUPE.
    # Build an empty result list. Loop over items; append an item only if it
    # is not already `in` result. Return result.
    # Verify by hand: dedupe([1, 1, 2, 1, 3]) must be [1, 2, 3].
    raise NotImplementedError("Exercise 1: implement dedupe")


def flatten(matrix):
    """Flatten one level of nesting: a list of lists into a single new list."""
    # Exercise 2: FLATTEN.
    # Build an empty result list. Loop over each row in matrix; loop over each
    # item in that row; append the item to result. Return result.
    # Verify by hand: flatten([[1, 2], [3, 4]]) must be [1, 2, 3, 4].
    raise NotImplementedError("Exercise 2: implement flatten")


def sort_by_length(words):
    """Return a NEW list of words sorted by length, shortest first."""
    # Exercise 3: SORT WITH A KEY.
    # Return sorted(words, key=len). `sorted` returns a new list and leaves
    # `words` untouched; the sort is stable, so equal-length words keep order.
    raise NotImplementedError("Exercise 3: implement sort_by_length")


def top_n(numbers, n):
    """Return the n largest numbers, highest first, as a new list."""
    # Exercise 4: SORT THEN SLICE.
    # Sort a copy in descending order (sorted(numbers, reverse=True)), then
    # slice off the first n with [:n]. Return that slice.
    raise NotImplementedError("Exercise 4: implement top_n")


def with_appended(items, value):
    """Return a NEW list equal to items plus value; the input is unchanged."""
    # Exercise 5: COPY BEFORE YOU MUTATE.
    # Make a shallow copy first: result = items[:]. Append value to result.
    # Return result. Do NOT append to `items` directly — that would change the
    # caller's list, the exact bug this lesson is about.
    raise NotImplementedError("Exercise 5: implement with_appended")


def main(argv):
    """Run the demo pipeline over a sample dataset. Returns exit code 0."""
    scores = [88, 72, 95, 72, 60, 95, 81]
    print(f"built:    {scores}")
    print(f"top 3:    {top_n(scores, 3)}")
    print(f"stride:   {scores[::2]}")
    print(f"last 2:   {scores[-2:]}")
    words = ["pear", "fig", "apple", "kiwi", "plum", "fig"]
    print(f"by len:   {sort_by_length(words)}")
    print(f"unique:   {dedupe(words)}")
    matrix = [[1, 2, 3], [4, 5, 6]]
    print(f"flat:     {flatten(matrix)}")
    original = [10, 20, 30]
    grown = with_appended(original, 40)
    print(f"grown:    {grown}")
    print(f"original: {original}")
    return 0


if __name__ == "__main__":
    sys.exit(main(sys.argv))
tests/run_tests.sh (5527 bytes)
#!/usr/bin/env bash
# Tests for the Day 052 lab. Run from the lab directory:
#   bash tests/run_tests.sh
#
# Exercises the complete reference program (examples/toolkit.py) on known
# inputs, imports each toolkit function and checks its return value, and — the
# heart of this lab — proves the difference between in-place mutation and
# returning a new list (sorted vs .sort(), aliasing vs copy). Finally it checks
# the learner's starter: structurally while exercises are unfinished, and to the
# same strict standard once they are complete.
# No network, non-interactive. Exits 0 only if every check passes.
set -u

# Keep the working tree clean: do not let imported modules write __pycache__.
export PYTHONDONTWRITEBYTECODE=1

lab_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
ref="${lab_dir}/examples/toolkit.py"
starter="${lab_dir}/starter/toolkit.py"
failures=0
checks=0

check() {
  local label="$1" ok="$2"
  checks=$((checks + 1))
  if [ "${ok}" = "yes" ]; then
    echo "  ok: ${label}"
  else
    echo "  FAIL: ${label}"
    failures=$((failures + 1))
  fi
}

# check_run <label> <script> <expect_exit> <needle> <arg...>
# Runs the program, checks its exit code equals expect_exit and that its
# combined output contains needle.
check_run() {
  local label="$1" script="$2" expect_exit="$3" needle="$4"
  shift 4
  local out code
  out="$(python3 "${script}" "$@" 2>&1)"
  code=$?
  if [ "${code}" -eq "${expect_exit}" ] && printf '%s' "${out}" | grep -qF "${needle}"; then
    check "${label}" "yes"
  else
    check "${label}" "no"
    echo "    (exit ${code}, expected ${expect_exit}; output: ${out})"
  fi
}

run_program_checks() {
  local script="$1"
  echo "Testing ${script} ..."
  check_run "pipeline runs, exit 0"     "${script}" 0 "built:    [88, 72, 95, 72, 60, 95, 81]"
  check_run "top 3 via sort+slice"      "${script}" 0 "top 3:    [95, 95, 88]"
  check_run "stride slice [::2]"        "${script}" 0 "stride:   [88, 95, 60, 81]"
  check_run "negative-index slice [-2:]" "${script}" 0 "last 2:   [95, 81]"
  check_run "stable sort by length"     "${script}" 0 "by len:   ['fig', 'fig', 'pear', 'kiwi', 'plum', 'apple']"
  check_run "order-preserving dedupe"   "${script}" 0 "unique:   ['pear', 'fig', 'apple', 'kiwi', 'plum']"
  check_run "flatten a matrix"          "${script}" 0 "flat:     [1, 2, 3, 4, 5, 6]"
  check_run "copy stayed safe"          "${script}" 0 "original: [10, 20, 30]"
}

# --- Reference program: always tested strictly ---
run_program_checks "${ref}"

# --- Import each function and check its return value ---
echo "Testing importability and return values of examples/toolkit.py ..."
if python3 -c "
import sys; sys.path.insert(0, '${lab_dir}/examples'); import toolkit
assert toolkit.dedupe([1, 1, 2, 1, 3]) == [1, 2, 3]
assert toolkit.flatten([[1, 2], [3, 4]]) == [1, 2, 3, 4]
assert toolkit.sort_by_length(['bbb', 'a', 'cc']) == ['a', 'cc', 'bbb']
assert toolkit.top_n([3, 9, 1, 7, 4], 2) == [9, 7]
assert toolkit.with_appended([1, 2], 3) == [1, 2, 3]
"; then
  check "each function returns the expected list" "yes"
else
  check "each function returns the expected list" "no"
fi

# --- The core lesson: in-place mutation vs returning a new list ---
echo "Testing in-place vs new-list behaviour ..."
if python3 -c "
import sys; sys.path.insert(0, '${lab_dir}/examples'); import toolkit
# sorted() returns a NEW list; the original is unchanged.
xs = [3, 1, 2]; ys = sorted(xs)
assert ys == [1, 2, 3] and xs == [3, 1, 2]
# .sort() mutates IN PLACE and returns None.
zs = [3, 1, 2]; ret = zs.sort()
assert ret is None and zs == [1, 2, 3]
# Aliasing: assignment shares one list, so a change shows through both names.
p = [1]; q = p; q.append(2)
assert p == [1, 2]
# A slice copy breaks the alias: the original is protected.
m = [1]; n = m[:]; n.append(2)
assert m == [1] and n == [1, 2]
# with_appended must NOT mutate its input (copy-before-mutate).
a = [1, 2]; b = toolkit.with_appended(a, 3)
assert b == [1, 2, 3] and a == [1, 2]
# dedupe/flatten/sort_by_length/top_n leave their inputs unchanged.
src = [1, 1, 2]; toolkit.dedupe(src); assert src == [1, 1, 2]
nested = [[1], [2]]; toolkit.flatten(nested); assert nested == [[1], [2]]
print('all in-place/new-list assertions passed')
"; then
  check "sorted vs .sort(), aliasing vs copy, no input mutation" "yes"
else
  check "sorted vs .sort(), aliasing vs copy, no input mutation" "no"
fi

# --- Learner starter ---
echo "Testing starter/toolkit.py ..."
if python3 -c "compile(open('${starter}').read(), '${starter}', 'exec')" 2>/dev/null; then
  check "starter is valid Python" "yes"
else
  check "starter is valid Python" "no"
fi

if grep -q 'NotImplementedError' "${starter}"; then
  echo "Note: starter/toolkit.py still has unfinished exercises — testing structure only."
  grep -q 'def dedupe' "${starter}" && check "starter defines dedupe" "yes" || check "starter defines dedupe" "no"
  grep -q 'def with_appended' "${starter}" && check "starter defines with_appended" "yes" || check "starter defines with_appended" "no"
else
  run_program_checks "${starter}"
  if python3 -c "
import sys; sys.path.insert(0, '${lab_dir}/starter'); import toolkit
a = [1, 2]; b = toolkit.with_appended(a, 3)
assert b == [1, 2, 3] and a == [1, 2]
assert toolkit.dedupe([1, 1, 2]) == [1, 2]
"; then
    check "starter copies before mutating (input unchanged)" "yes"
  else
    check "starter copies before mutating (input unchanged)" "no"
  fi
fi

echo
echo "${checks} checks, ${failures} failure(s)."
[ "${failures}" -eq 0 ]

Troubleshooting

Troubleshooting — Day 052 lab

python: command not found

Use python3 explicitly, as every command in this lab does. On macOS and most Linux systems, bare python may be missing or point to an old version. Check with python3 --version.

The starter raises NotImplementedError when I run it

That is expected until you finish the exercises. Each unfinished function raises NotImplementedError on purpose so you cannot accidentally think an empty function "works." Replace each raise NotImplementedError(...) line with the real body described in the comment above it. Once all five exercises are done, python3 starter/toolkit.py prints the same pipeline as the reference.

ModuleNotFoundError: No module named 'toolkit'

Python imports a module by looking on its search path (sys.path), which does not include the examples/ subfolder by default. The import one-liners in this lab add it first:

python3 -c "import sys; sys.path.insert(0, 'examples'); import toolkit; print(toolkit.dedupe([1, 1, 2, 1, 3]))"

Run this from the lab directory (the folder that contains examples/), not from inside examples/ itself.

My with_appended changed the original list

You appended to the input list directly instead of to a copy. The whole point of the function is to leave the caller's list untouched: make a shallow copy first with result = items[:] (or list(items)), append to result, and return result. If you write items.append(value) you have created the exact aliasing bug this lesson warns about — the caller's list grows behind their back.

.sort() returned None

.sort() sorts the list in place and returns None — that is correct Python behaviour, not a bug. If you want a sorted result to assign to a new name, use sorted(items), which returns a new list and leaves the original alone. Writing items = items.sort() is a classic mistake: it throws away your list and replaces it with None.

My dedupe or flatten result is in a surprising order

dedupe must keep the first occurrence of each item and drop later repeats, so the order follows the input. flatten reads the matrix row by row, left to right, so [[1, 2], [3, 4]] becomes [1, 2, 3, 4]. If your order is wrong, check that you append inside the loops in reading order and never sort.

bash: tests/run_tests.sh: Permission denied

Run it through bash explicitly (as the README shows) rather than executing it directly: bash tests/run_tests.sh. You do not need to chmod +x anything.

The test suite writes __pycache__

It should not: the runner sets PYTHONDONTWRITEBYTECODE=1 before importing. If you import the module yourself and see a __pycache__ folder appear, it is harmless cached bytecode; delete it with rm -rf examples/__pycache__ or set the same variable in your shell.

Security notes

Security notes — Day 052 lab

  • What the program does: builds a few lists in memory, slices, sorts, and transforms them, and prints the results. It makes no network connections, reads and writes no files, and changes no settings. The test runner is equally self-contained and non-interactive.

  • Copy before you mutate — the safety habit of this lab. The functions here take a list and return a new list, never changing the caller's data. That discipline is a correctness and safety property, not just style: a function that quietly mutates a list it was handed can corrupt data another part of the program still depends on, and such bugs are silent — nothing crashes, the numbers are just wrong. When you need to change a list you were given, copy it first (items[:], list(items), or copy.deepcopy for nested lists) and change the copy.

  • Shallow vs deep copy is a real trap. A shallow copy (items[:]) duplicates the outer list but shares the inner lists. If your data is nested — a matrix, a batch of records — mutating an inner list still shows through both copies. Reach for copy.deepcopy when the nested contents must be independent. Knowing which copy you have prevents a class of hard-to-find data-corruption bugs.

  • Validate input; never execute it. This lab does not read untrusted input, but the rule from Day 49 still stands: to turn text into data, parse it with safe converters (int(), float()), never with eval() or exec(), which run their argument as code.

  • Privileges: everything runs as your normal user. Nothing here needs sudo. As always in this course, if a script ever asks you to run it with elevated privileges, stop and read it first.

  • Reading before running: every file in this lab is short and commented. Read examples/toolkit.py and tests/run_tests.sh before running them. Running unread scripts is one of the most common ways developers get compromised; the course's rule is that every lab script is small enough to read and understand first.