Values, names and expressions
A variable name refers to a value. Assignment binds a name; it is not an equation that remains true forever. After x=3; y=x; x=4, y is still 3. Expressions compute values: + adds, == compares, and and/or combine conditions. Types determine which operations make sense; '3'+ '4' concatenates text, while 3+4 adds integers. Convert external text deliberately and handle conversion errors rather than hoping the type is correct.
What is y after the assignments above?
Worked solution
3; later rebinding x does not rebind y.
Selection and repetition
if selects a branch; for iterates over a collection; while repeats while its condition is true. Python uses indentation to group statements. An empty collection produces zero for iterations. A while loop needs a progress argument: while count<3 with count+=1 finishes, but forgetting the increment does not. break leaves a loop, continue advances to its next iteration, and return leaves a function. Use a trace table to distinguish these boundaries.
What happens if the increment in a true while loop is omitted?
Worked solution
Without another exit or state change, it never terminates.
Collections and mutation
A list gives ordered items; a dictionary maps keys to values. A book can be {'id':1,'title':'Dune'}. Names can refer to the same mutable object: b=a does not copy a list, so b.append changes the object seen through a. a.copy creates a new outer list, but nested records are still shared. Decide whether a function mutates an argument or returns new data, and document that contract. Avoid changing a list's length while traversing it; construct a filtered result instead.
Does copying a list copy every nested dictionary?
Worked solution
A shallow copy copies only the outer container.
Functions and interfaces
A function has parameters, a body and a result. Local names keep temporary state within a call. Return a result so callers can use it; print produces output but is not a substitute for returning data. Put catalogue search in a function, and let a separate main function parse command-line arguments and print results. This makes the same logic usable from a terminal, tests or a future web server. An if __name__ == '__main__' guard runs the interface when executed directly without running it on import.
Why return records rather than print them inside search?
Worked solution
Different callers can format, test or transmit the same result.
Errors and debugging
Syntax errors prevent parsing; runtime exceptions report a failed operation; logic errors produce an incorrect result without necessarily raising anything. Read a traceback from the failing operation back to its caller. Reproduce with the smallest input, state the expected result, inspect intermediate values, then fix the cause. Catch a specific exception only where you can respond meaningfully. Treat invalid user input as a boundary concern; assertions check programmer assumptions and can be disabled, so they are not input validation.
Should assert validate a user-supplied price?
Worked solution
No. Explicitly reject invalid input; assertions may be disabled.
Common misconceptions
- A shallow copy does not isolate nested mutable objects.
- Catching every exception and continuing can conceal corrupted state.
Lab setup
Download each script and run it in a terminal with Python 3.11 or later: python m03_catalogue.py. On Windows, py -3 is an alternative; on some systems use python3. The labs use only the standard library. Predict the result before running, then complete the variations. Run without -O so assertions remain enabled. Outputs below were captured by the builder. Code and output are identical in both language editions.
Lab 1 — A command-line catalogue
Run the default search, then pass FOUNDATION as a command-line argument. The list comprehension constructs matches without changing the input.
"""Separate reusable catalogue logic from a command-line interface."""
import argparse
def find_titles(books, query):
query = query.strip().casefold()
if not query:
raise ValueError("query must not be blank")
return [book for book in books if query in book["title"].casefold()]
def main():
parser = argparse.ArgumentParser(description="Search a small catalogue")
parser.add_argument("query", nargs="?", default="dune")
args = parser.parse_args()
books = [{"id": 1, "title": "Dune"}, {"id": 2, "title": "Foundation"}]
for book in find_titles(books, args.query):
print(f'{book["id"]}: {book["title"]}')
assert find_titles(books, "FOUND") == [books[1]]
assert find_titles(books, "missing") == []
if __name__ == "__main__":
main()
1: Dune
- Search for an absent title.
- Pass a quoted blank query.
- Add an author field and write a separate author-search function.
Worked solution
Absent prints no records. Blank input raises ValueError because an empty substring would otherwise match everything. Author search can use the same normalisation but must read book['author']; keep its validation separate from the CLI.
Lab 2 — Observe state and copies
Predict the two lists after every statement. The + expression returns a new list; append mutates an existing list.
"""Observe aliasing, copies, function return values and validation."""
def add_book(books, title):
clean = title.strip()
if not clean:
raise ValueError("blank title")
return books + [clean]
original = ["Dune"]
alias = original
alias.append("Foundation")
print("after alias append:", original)
updated = add_book(original, " Solaris ")
print("original:", original, "updated:", updated)
assert original == ["Dune", "Foundation"]
assert updated == ["Dune", "Foundation", "Solaris"]
try:
add_book(original, " ")
except ValueError:
print("blank title rejected")
else:
raise AssertionError("must reject blank input")
after alias append: ['Dune', 'Foundation']
original: ['Dune', 'Foundation'] updated: ['Dune', 'Foundation', 'Solaris']
blank title rejected
- Replace alias=original with original.copy().
- Make add_book mutate its argument; update its contract and tests.
- Explain why validation runs before the update.
Worked solution
With a copy, appending Foundation leaves original as [Dune]. A mutating version could use books.append(clean) and return None, but its tests must now expect the original to change. Validate first to avoid partially applying invalid requests.
Exercises with worked solutions
Try before opening the solution. ★ applies an idea; ★★ combines ideas; ★★★ asks for design or proof.
Trace a=2; b=a; a=a+1.
Worked solution
a=3, b=2. The final assignment rebinds only a.
For ages 17,18,19, evaluate age>=18.
Worked solution
False, True, True. Testing the boundary distinguishes > from >=.
Trace total=0 then add each value in [2,4,6].
Worked solution
States after additions: 2,6,12. An empty list leaves total at zero.
After b=a; b.append('B'), why does a change?
Worked solution
Both names refer to one list. Rebinding b to a new list would be different from mutating that shared list.
A function prints 7 but has no return. What value does its caller receive?
Worked solution
None. Printing is a side effect, not a returned value.
Why validate after strip rather than only checking the original string?
Worked solution
A string of spaces is nonempty before strip but semantically blank afterwards. Normalise first, then validate the value the function will actually use.
Design a function that filters affordable books without printing or mutation.
Worked solution
Accept books and maximum integer price_cents; validate a nonnegative limit, return [b for b in books if b['price_cents']<=limit]. State that returned dictionaries remain shared unless copied.
Search unexpectedly matches all books. Give a minimal investigation.
Worked solution
Try one book and a blank query; inspect the normalised query. The empty substring belongs to every string. Add boundary validation and a regression case.
Self-check quiz
Choose an answer for feedback; reset to retry. A text answer key is available without JavaScript.
What does = do?
Which exits a function?
What is a dictionary?
Does b=a copy a list?
No explicit return gives what?
Best use of a specific exception handler?
Answer key
- A — Use == for equality.
- B — return supplies a result and ends the call.
- C — Records can use named keys.
- A — It shares the reference.
- B — Print does not become a return.
- C — Preserve diagnostic information and handle expected failures.
Guided reading
- Python control flow — Read functions and argument passing; trace a call using local names.
- Python data structures — Read lists, dictionaries and comprehensions; rewrite the lab filter as a for loop.
Review and the next step
Explain the catalogue's input validation, list representation and return contract without looking at the code. Add one failing-input test. Module 04 introduces the mathematical language used to justify programs.
Key terms
| Term | Meaning |
|---|---|
| Mutation | Changing an existing object's state. |
| Aliasing | Multiple references to the same object. |
| Exception | A signal interrupting normal control flow. |