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:sessionsis a list of nonnegative integer session lengths in minutes;minimum_minutesis a nonnegative integer and the smallest length counted as long.
Returns: the number of values insessionsthat are greater than or equal tominimum_minutes.
Empty list: returns0.
Does not: print or changesessions.
Derive Tests from the Specification
The specification already suggests useful tests:
sessions = [10, 30, 45]
assert count_long_sessions(sessions, 30) == 2assert sessions == [10, 30, 45]assert count_long_sessions([30], 30) == 1assert count_long_sessions([29], 30) == 0assert count_long_sessions([], 30) == 0assert count_long_sessions([35, 45], 40) == 1assert count_long_sessions([0, 5, 0], 0) == 3Choose 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 countPlace 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
Noneto mean that no longest session exists; - return a deliberately chosen domain value, such as
0when 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 countThe 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) == 2The 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:
- Specify. Write what the function accepts, returns, prints or changes, and does on important cases.
- Choose tests. Turn the specification into representative examples and boundaries.
- Implement. Write the code yourself or use an AI assistant.
- Run the tests. Find cases where actual and expected behavior differ.
- 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.
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
0for an empty list. - Do not change
scores.
print_shortlist(finalists, total)
- Print exactly
Finalists: LISTandCombined points: TOTALon separate lines, using the two values it receives. - Leave
finalistsunchanged.
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 = 84Have 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: 258Then 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 tominimum, in the original order and with repeated scores preserved. It returns[]when no score qualifies and does not changescores.
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 finalistsThe 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
- Why can valid-looking Python code still be incorrect?
- What should a specification say about output and mutable arguments in addition to parameters and a return value?
- Why is it useful to write tests from the specification before inspecting an implementation?
- What is the purpose of a docstring in the examples above?
- 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.