Building from Pieces

Specifications, Verification, and AI


Learning Objectives

  • You can describe a function in terms of its purpose, parameters, return value, effects, and important edge cases.
  • You can write tests from a specification rather than from an implementation.
  • You can use tests and code reading to check whether an implementation, including AI-generated code, satisfies the requested behavior.
  • You can explain why making requirements precise is part of programming.

Chapter 7 introduced executable tests, and Chapter 8 separated a testable rule from interactive user-input handling. Both depend on deciding what a test should expect.

Before writing a function, you can describe what it accepts, returns, prints, or changes. This description of what the function should do is a specification.

Implementation and Requirement Are Different Things

Consider:

def summarize_sessions(sessions):
return len(sessions)

The code is valid and readable, but the name does not tell us enough to decide whether it is correct.

If the requirement is to return the number of sessions, this implementation fits. If the requirement is to return the total time spent reading, it does not: for [10, 20], the function returns 2 instead of 30.

Correctness depends on whether the implementation matches the intended behavior.

A Small Function Specification

A useful specification can answer questions such as:

  • What is the function called, and what is its purpose?
  • What parameters does it receive, and which values are accepted?
  • What does it return, and which rules determine that result?
  • Does it print anything or change a supplied list?
  • What should happen in important empty, boundary, or invalid cases?

For example:

Function: count_long_sessions(sessions, minimum_minutes)
Parameters: sessions is a list of nonnegative integer session lengths in minutes; minimum_minutes is a nonnegative integer and the smallest length counted as long.
Returns: the number of values in sessions that are greater than or equal to minimum_minutes.
Empty list: returns 0.
Does not: print or change sessions.

Derive Tests from the Specification

The specification already suggests useful tests:

sessions = [10, 30, 45]
assert count_long_sessions(sessions, 30) == 2
assert sessions == [10, 30, 45]
assert count_long_sessions([30], 30) == 1
assert count_long_sessions([29], 30) == 0
assert count_long_sessions([], 30) == 0
assert count_long_sessions([35, 45], 40) == 1
assert count_long_sessions([0, 5, 0], 0) == 3

Choose the expected results from the specification before implementing or inspecting the function. Otherwise, a test may repeat a mistake in the implementation. For example, the [30] case above expects 1 because a session exactly at the minimum qualifies.

A Specification Can Live Next to the Function

With the expected results written down, we can implement the session count and record its contract in the function itself.

A string used as the first statement inside a function is called its docstring. Triple quotation marks (""") let the string span several lines. A docstring documents the function; unlike a # comment, Python keeps it as part of the function’s documentation:

def count_long_sessions(sessions, minimum_minutes):
"""Return the number of sessions at least minimum_minutes long.
An empty sessions list returns 0. The function does not modify sessions.
"""
count = 0
for minutes in sessions:
if minutes >= minimum_minutes:
count = count + 1
return count

Place the earlier assertions below this definition and run the program to check it.

A short docstring records the contract rather than narrating each implementation step. Avoid:

"""Set count to zero, loop through sessions, add one when..."""

The docstring in the function above instead states the returned result, the empty case, and the promise not to change the supplied list.

Edge Cases Are Design Decisions

For longest_session([20, 45, 30]), a natural result is 45. An empty list has no longest item, so the specification must decide what longest_session([]) means. It could:

  • require a nonempty list, so callers must handle the empty case;
  • return None to mean that no longest session exists;
  • return a deliberately chosen domain value, such as 0 when every valid session length is positive.

For example, a reading tracker that accepts only positive session lengths can use 0 to mean that no session was recorded. That meaning belongs in the contract; an accumulator’s initial value alone does not decide the empty-case behavior.

AI Makes Vague Requirements More Visible

Suppose you ask an AI system:

Write a function that finds long study sessions.

Does “find” mean return the sessions or count them? What counts as long? An AI may fill those gaps with plausible choices and produce valid code for the wrong problem.

Use the count_long_sessions specification from earlier in this chapter in the request. It supplies those decisions, together with the accepted inputs, empty-case result, and allowed effects. The same contract gives you a way to check the proposed code.

Verify Generated Code Against the Rule

Imagine an AI returns:

def count_long_sessions(sessions, minimum_minutes):
count = 0
for minutes in sessions:
if minutes >= minimum_minutes:
count = count + minutes
return count

The variable is called count, but the loop adds each qualifying session’s length. The specification asks how many sessions qualify, not how many minutes they contain.

This test exposes the mismatch:

assert count_long_sessions([30, 45], 30) == 2

The implementation returns 75. Comparing that result with the specification shows why the accumulator should increase by one for each qualifying session.

Use each specification to decide what evidence would reveal a mistake in the following implementations:

A Practical Workflow

Use this sequence when implementing a small function yourself or with AI assistance:

  1. Specify. Write what the function accepts, returns, prints or changes, and does on important cases.
  2. Choose tests. Turn the specification into representative examples and boundaries.
  3. Implement. Write the code yourself or use an AI assistant.
  4. Run the tests. Find cases where actual and expected behavior differ.
  5. Read and revise. Explain why the code produces the expected results, and fix any mismatch. If the original requirement was unclear, clarify it and update the tests too.

Tests make selected requirements executable; the specification tells the tests what to expect.

Check the Requirements Yourself

Code, tests, and a specification proposed by the same AI can share one mistaken assumption. You remain responsible for deciding the required behavior and verifying the implementation against it.

Apply that workflow in a larger programming task:

Tournament shortlist

0 / 25 points

Implement three functions for selecting tournament finalists and displaying their combined points. Scores and the minimum are integers; they may be positive, zero, or negative. The two calculation functions return their results without printing.

select_finalists(scores, minimum)

  • Return a new list containing every score greater than or equal to minimum.
  • Preserve the original order and repeated scores.
  • Return [] when the input is empty or no score qualifies.
  • Do not change scores.
  • Practise building the result with a loop.

total_points(scores)

  • Return the total of all scores. Practise calculating it with a loop rather than sum(...).
  • Return 0 for an empty list.
  • Do not change scores.

print_shortlist(finalists, total)

  • Print exactly Finalists: LIST and Combined points: TOTAL on separate lines, using the two values it receives.
  • Leave finalists unchanged.

Write a meaningful short docstring inside each function. State what the function returns or prints rather than narrating the implementation line by line. For the two calculation functions, also mention an important empty-case or nonmutation promise.

Keep main(). It must use these values:

scores = [71, 84, 90, 84, 63]
minimum = 84

Have main() call select_finalists, pass the returned list to total_points, and pass both results to print_shortlist. The output must be:

Finalists: [84, 90, 84]
Combined points: 258

Then review and explain another generated implementation:

Review a generated shortlist

0 / 15 points

An AI assistant received this specification:

select_finalists(scores, minimum) returns a new list containing every score greater than or equal to minimum, in the original order and with repeated scores preserved. It returns [] when no score qualifies and does not change scores.

It produced:

def select_finalists(scores, minimum):
finalists = []
for score in scores:
if score > minimum:
finalists.append(score)
for score in scores:
if score == minimum:
finalists.append(score)
return finalists

The AI also suggested only these tests:

assert select_finalists([70, 90], 80) == [90]
assert select_finalists([50, 60], 80) == []

In five to seven sentences, evaluate the implementation against every important part of the specification. Explain why both suggested tests pass despite the defect, give one assertion that exposes it, and describe a correction. Include the selection rule, order, repeated values, empty/no-match behavior, and whether the input changes in your review.

Check Your Understanding

  1. Why can valid-looking Python code still be incorrect?
  2. What should a specification say about output and mutable arguments in addition to parameters and a return value?
  3. Why is it useful to write tests from the specification before inspecting an implementation?
  4. What is the purpose of a docstring in the examples above?
  5. What test would distinguish counting qualifying sessions from adding their lengths?

AI Study Aid

Create a chapter diagram

We're looking into to what extent AI-generated diagrams could help with studying.

Use this study aid to generate an AI-generated visual summary of the material you just studied. Each diagram style emphasizes the content in a different way, so you can choose the focus that feels most useful.

Using the diagram generator is voluntary and does not affect your course progress. We encourage you to try it out and see if it helps, but it's totally up to you! Your answers help us understand how to make better study aids in the future.

Diagrams tried: 0