Organizing Code with Functions

Specifying What You Want -- Turning Intent into Prompts


Learning Objectives

  • You can write a precise function specification covering name, parameters, return value, and behavior on edge cases.
  • You can recognize when an AI prompt is too vague to produce reliable output.
  • You can use a function specification as the basis for an AI prompt, and verify the result against the specification.

In the first part of the course, the AI-collaboration skill was reading: understanding what a piece of code actually does. In the second part of the course, the skill was verifying: confirming that what it does is what it should do, on every input it might see. Both of those skills are about evaluating code that already exists.

In this part, the skill is specifying: writing a precise description of what you want before the code exists. This is a different skill, because it is about being precise before the code is written, rather than after. It is also a more fundamental skill, because it is the skill of making design decisions and communicating them clearly, which is the foundation for writing code that matches your intent, whether you write it yourself or ask an AI to write it for you.

Writing Versus Specifying

When you write code yourself with a vague idea in mind, you fill in the gaps as you go. You decide that 15 minutes should be “short” instead of “solid” because that is what feels right when you reach the line. You decide that an empty list should return None rather than 0 or raising an exception because the moment of writing reminds you that one of them might crash later. The vagueness is fine, because you are the one resolving it, in your head, as you’re working on the code.

When an AI writes code from a vague idea, it fills in the gaps. The AI decides that 15 is solid because the most common style across its training data leans one way. It decides that empty input returns 0 because that was the most common pattern in similar examples. None of these decisions are yours. None of them are even explicit.

This is fine if you happen to want what the AI happened to choose. It is not fine if you wanted something specific. And in the typical case — where you have not really thought about the choice — it is even worse than not fine, because you do not realize a choice was made until something goes wrong.

The fix is to make the choices before the code exists. A specification is a document that outlines those choices in writing.

What a Function Specification Looks Like

A function specification has five parts. The last one is optional for functions that never raise, but for any function with input validation it should be present.

PartQuestion it answers
NameWhat is this function called?
ParametersWhat does it take, in what order, with what types?
Return valueWhat does it produce? What type?
Behavior on edge casesWhat does it do on the empty input, the boundary value, the unusual case?
Raises (when applicable)Which exceptions does it raise, and on what inputs?

The first three are familiar territory. The fourth is where most of the work happens, and it is also where most function specifications fall short. A spec that says “returns the maximum” and does not say what happens on an empty list is a spec that has only done half its job. The fifth is the natural home for any input-validation decisions you made using Exceptions and Input Validation’s exception-raising pattern: if the function raises ValueError on bad input, the spec needs to say so.

Here is a complete specification for classify_session:

classify_session(minutes: int) -> str
Returns a one-word description of a session length.
"short" for minutes < 15
"solid" for 15 <= minutes <= 90
"long" for minutes > 90
Edge cases:
- Negative minutes: treated as short.
- Zero minutes: treated as short.
- Exactly 15: solid.
- Exactly 90: solid.
- Very large minutes (e.g., 10000): treated as long.

This is what a precise spec looks like. There are no places where two reasonable programmers would disagree about what the function should do, because the spec has pinned everything down.

Now compare the above to a more informal description: “a function that classifies study sessions by length.”

The informal version does not say what the thresholds are. It does not say what happens at the boundaries. It does not say what the return value looks like — is it "short", or "Short", or 1, or something else? Two programmers reading the informal description would produce two different functions, and neither would technically be wrong, as the description is vague.

Edge Cases Matter

Look at the spec for classify_session again. The edge case section is the longest part. That is on purpose.

The “normal” behavior of a function is usually indicated by the name of the function. Almost any reasonable programmer or AI would write something like if minutes < 15: return "short" for the short case. There is no real disagreement to resolve.

The disagreements are at the edges. Is 15 short or solid? Is 90 solid or long? What does the function do when minutes is 0, or negative, or absurdly large? These are the choices that affect behavior in ways that the obvious tests do not catch. They are also the choices an AI will make for you if you do not make them yourself.

This is the same checklist from the chapter Verifying Behavior and Finding the Edge Cases AI Misses from the previous part, applied at a different point in the process:

CaseWhere it shows up
EmptySpec section for empty list or empty string
Single elementSpec section for a list with exactly one item
Boundary valuesSpec section for exact threshold values and < vs <=
All equalSpec section for a list where every item is the same
No matchSpec section for what happens when a search finds nothing
ZeroSpec section for 0 as a numeric input
NegativesSpec section for negative numeric inputs
Unusual input formatSpec section for Done vs done, whitespace, etc.

In the previous part, you asked: “what inputs would break this code?” Now you ask: “what should this code do on each of those inputs?”

Vague vs. Specific Prompt

To make the difference concrete, here are two prompts you might give to an AI assistant.

Vague prompt:

Write a Python function that classifies study sessions by length.

What might the AI produce?

def classify_session(minutes):
if minutes < 30:
return "Short"
elif minutes < 60:
return "Medium"
else:
return "Long"

That runs. It looks fine. Is it what you wanted? Almost certainly not. Compare with your intended thresholds:

  • The AI used 30 and 60 as thresholds. You wanted 15 and 90.
  • The AI used three labels: Short, Medium, Long. You wanted short, solid, long (capitalization included).
  • The AI picked < everywhere, so 30 is medium and 60 is long. Without a spec, this is no better or worse than any other choice — but it is a choice that you did not get to make.

Specific prompt:

Write a Python function classify_session(minutes: int) -> str that returns "short" for minutes < 15, "solid" for 15 <= minutes <= 90, and "long" for minutes > 90. Treat negative or zero minutes as short. Exactly 15 should be solid; exactly 90 should be solid.

What might the AI produce now?

def classify_session(minutes):
if minutes < 15:
return "short"
elif minutes <= 90:
return "solid"
else:
return "long"

This is exactly what you wanted, because there was nowhere for the AI to make decisions. The specification did the design work, and the AI did the typing.

Prompt a precise reading classifier

0 / 10 points

Write a prompt that asks a large language model to create Python code only.

The generated program must define a function named reading_label(minutes) with this exact behavior:

  • return "warmup" for minutes less than 20,
  • return "focused" for minutes from 20 through 60,
  • return "marathon" for minutes greater than 60,
  • treat negative minutes and zero minutes as "warmup",
  • and include no input(...) calls.

The generated code will be tested by calling:

reading_label(-5) # should return "warmup"
reading_label(0) # should return "warmup"
reading_label(19) # should return "warmup"
reading_label(20) # should return "focused"
reading_label(60) # should return "focused"
reading_label(61) # should return "marathon"

Your prompt should explicitly include the exact function name, exact returned strings, exact boundary values, and the instruction to avoid input(...).

Ask for Python code only, with no explanation.

Specification as a Design Activity

Look back at what just happened. To write the specific prompt above, you had to know the thresholds. You had to decide what to do at the boundaries. You had to pick the exact strings to return. By the time the spec was written, the important design decisions were already made. The code that followed was almost mechanical.

Creating specifications is programming. The act of writing a spec is the act of making the decisions that the program embodies.

This is also true when you are not using an AI. A function with a clear spec in your head — or, perhaps, in a comment above the function — is faster to write, easier to test, and less likely to be wrong. The discipline that makes AI prompts effective is the same discipline that makes your own code better. The AI just happens to make the difference visible.

Explain specification as design

0 / 10 points

In your own words, explain why writing a function specification is part of programming.

Use this function idea as your example: a function that returns the average of a list of minutes.

In 3-6 sentences, mention at least two design decisions a specification would need to make, such as empty-list behavior, return type, invalid inputs, or exceptions.

A Workflow

Putting the ideas together, here is a workflow for using specifications, with or without an AI:

  1. Write the spec. Five parts: name, parameters, return, edge cases, and raises (when applicable). The edge case section should explicitly cover the rows of the checklist that apply.
  2. Decide cases from the spec. The cases you would test for already came out of the spec. Write them down before writing the function.
  3. Implement. Either you write the function, or you ask an AI to write it. The spec is the input to either.
  4. Test against the spec. Write the test assertions from the cases in step 2. These tests check that the function matches the spec, not that it matches itself. This is the key distinction.
  5. Compare. Run the tests. If they pass, the function matches what you wanted. If they fail, you know which case it failed on.

Take a moment to re-read step 4. There is a temptation to write tests that match whatever the implementation happens to do — read the code, then write tests that match. That is testing the implementation against itself. Tests derived from the spec catch the case where the implementation drifted from the intent. That is the case you most want to catch. Pay explicit attention to testing the edge cases, because those are the cases where the behavior of the function should be tested.

Tests from the spec

0 / 5 points

A spec says:

average_score(scores: list of int) -> float or None
Returns the arithmetic average of the scores.
Edge cases:
- Empty list: returns None.
- Single-element list: returns that element.
- Negative scores are included in the average.

Which group of assertions best tests the function against this spec?

A Worked Example: longest_session

To put it all together, here is a complete spec for the longest_session function from the previous chapter.

longest_session(minutes_list: list of int) -> int or None
Returns the largest value in `minutes_list`.
Edge cases:
- Empty list: returns None.
- Single-element list: returns that element.
- All elements equal: returns that element.
- List contains negative numbers: returns the largest (closest to zero).
- List of length 1 with value 0: returns 0.

Notice what the spec is doing. It is explicitly choosing to return None for the empty case, rather than crashing, returning 0, or raising an error. Each of those would be defensible; the spec picks one. It is explicitly saying that negatives are handled (a programmer or AI might otherwise assume non-negative input and write a function that breaks on negatives). It is being clear about the return type: an integer, or None.

Now derive the tests from the spec:

assert longest_session([]) is None
assert longest_session([45]) == 45
assert longest_session([60, 60, 60]) == 60
assert longest_session([-5, -10, -3]) == -3
assert longest_session([0]) == 0

Five tests, each corresponding to a line of the spec. If the function passes all five, it matches the spec.

Now, an AI prompt derived from the spec:

Write a Python function longest_session(minutes_list) that returns the largest value in minutes_list. It must return None for an empty list. Single-element list returns that element. The function must handle negative numbers and zero correctly.

The AI’s job here is constrained. There is no room to guess at the empty case, because the spec said it. There is no room to assume non-negative input, because the spec said negatives are handled. The function the AI produces is verifiable directly against the tests above. If it passes them, it is correct by your definition of correct, which is the only definition that matters.

Alternative with Raise Instead of Return None

The spec above chose to return None for the empty case. The same function could be specified differently, using the exception-raising approach from the chapter on Exceptions and Input Validation:

longest_session(minutes_list: list of int) -> int
Returns the largest value in `minutes_list`.
Edge cases:
- Single-element list: returns that element.
- All elements equal: returns that element.
- List contains negative numbers: returns the largest (closest to zero).
Raises:
- ValueError if minutes_list is empty.

Two changes from the first version. The return type is now just int, not int or None, because the function no longer returns a sentinel value for the empty case. And there is a fifth section, Raises, which says exactly what exception the function raises and when.

Both versions are valid specs. The first asks the caller to remember to check for None. The second forces the caller to either ensure the list is not empty or catch the ValueError. The decision between them is a design choice, and the spec is where that choice is recorded — explicitly, not by accident.

Prompt shortest_session from a full spec

0 / 10 points

Write a prompt that asks a large language model to create Python code only.

The generated program must define a function named shortest_session(minutes_list) with this exact behavior:

  • return the smallest value in minutes_list,
  • return None if the list is empty,
  • return the only value for a single-element list,
  • handle all-equal lists,
  • handle zero,
  • and handle negative numbers by returning the smallest value, e.g. -10 from [-5, -10, -3].

The generated code will be tested by calling:

shortest_session([]) # should return None
shortest_session([45]) # should return 45
shortest_session([60, 60, 60]) # should return 60
shortest_session([-5, -10, -3]) # should return -10
shortest_session([0]) # should return 0

Your prompt should explicitly include the exact function name, the empty-list behavior, the single-element case, all-equal lists, zero, and negative numbers.

Ask for Python code only, with no explanation.

Where You Are in the Nine-Skill Arc

Three skills are now in place:

PartAI skillWhat it gives you
1Reading code you didn’t writeKnowing what a piece of code does
2Verifying behavior and finding edge casesKnowing whether what it does is right
3Specifying what you want — turning intent into promptsKnowing what to ask for in the first place
4Detecting plausible-but-wrong code(next)
5Comparing approaches — yours and the AI’s(later)
6Critiquing data designs
7Working with AI on libraries you’re still learning
8Critiquing AI-suggested visualizations
9Iterating with AI on a real project, and recognizing when to stop

Reading + verifying + specifying = the three foundational skills. Reading tells you what something does. Verifying tells you whether what it does is right. Specifying tells you what to want in the first place. With all three, the AI relationship has shape: you decide what you want (specify), you ask for it precisely (prompt), and you check what you got (read, verify).

Summarize the AI skill from this part

0 / 10 points

Summarize the AI-collaboration skill from this part: specifying what you want.

Explain how it builds on reading code and verifying behavior from the earlier parts. In 3-6 sentences, use one example of a function specification in your answer.

A Note on Where the Specification Lives

In a small project, your specifications can live in comments above the function:

def longest_session(minutes_list):
"""Return the largest value in minutes_list, or None if empty.
Single-element list returns that element.
Negative numbers are handled.
"""
...

This kind of comment, written with triple quotes immediately below a function definition, is called a docstring. Python attaches it to the function and makes it available through help(longest_session) and various editor features. The docstring serves as a permanent home for the function’s spec. You write it once when the function is created, and any reader — yourself, a colleague, an AI — can refer to it.

For larger projects, specs sometimes live in separate documents or design files. For this course, docstrings on the functions are enough. The point is that the spec is written down somewhere and visible to anyone reading the code.

Write a docstring specification

0 / 10 points

Define a pure function named average_temperature(readings).

Write a docstring immediately under the function definition that acts as a short specification. The docstring must mention:

  • that the function returns the average of the readings,
  • that an empty list returns None,
  • that negative and zero readings are included in the average.

The function must:

  • return None for an empty list,
  • return the arithmetic average otherwise,
  • handle one value, zero, and negative values correctly,
  • not ask for input,
  • and not print.

Example calls and return values:

average_temperature([]) -> None
average_temperature([10]) -> 10
average_temperature([-5, 0, 10]) -> 1.6666666666666667

The tests will load your program and inspect both the function behavior and the docstring.

Check Your Understanding

  1. What are the five parts of a function specification, and which one is optional?
  2. Why does writing tests from the spec rather than from the implementation matter?
  3. Look at the vague vs. specific prompts in this chapter. Pick one decision the vague prompt left to the AI, and explain how it could have gone wrong.
  4. Suppose you write a spec that just says “returns the average of the list”. What is missing from that spec, based on this chapter’s checklist?

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