Skip to main content

Section 1.5 Implement, Test & Refine

Subsection 1.5.1 Overview & Motivation

So far, you’ve followed a structured experience through the Design Recipe:
  1. Data Definitions (Section 1)
  2. Method Signatures & Purpose Statements (Section 2)
  3. Examples & Tests (Section 3)
  4. Skeleton / Method Template (Section 4)
Now comes the final—and perhaps most satisfying—phase: putting everything together in a working, fully tested solution. In many beginner courses, coding is treated as the only step—people rush to “just write the function.” But you’ve learned that a clear, deliberate process leads to reliable, maintainable code.
In this section, you’ll see how to transform your skeleton (or outline) into a full implementation and how to systematically test and refine it until you’re confident it meets your design. You’ll also discover why adhering to these structured steps prevents late-night debugging disasters.
By the end, you’ll have completed the entire Design Recipe cycle and seen firsthand why every phase—from data definitions to final testing—truly matters.

Subsection 1.5.2 From Skeleton to Final Code

In Section 4, you learned the value of writing a “method skeleton” (or outline): you established the logical flow of your function, step by step, without worrying about the final code details. Now, it’s time to fill in those placeholders with real Python (or Java) statements.
Think of the skeleton as a set of clear signposts. You already decided:
  • Where to validate inputs
  • How (and if) to loop
  • What to return (and when to raise exceptions)
Because those signposts are in place, you can focus on getting each piece right, confident you won’t forget any requirement from your contract or data definition.

Subsubsection 1.5.2.1 Audrey’s Latest Project

Let’s jump back to Audrey, who’s now finalizing update_rating for her music-app dream project. Previously, she documented the function’s purpose, wrote a skeleton, and crafted some tests. After receiving feedback from her friend Mai (and re-reading her own notes), Audrey feels prepared to implement the final code.

Subsection 1.5.3 Implementing Step-by-Step

Let’s revisit the update_rating skeleton and fill it in. According to the data definitions and method signature from earlier sections:
Goal: Update the rating for a given song_title in a library, returning True if updated, False if the song isn’t found, and raising ValueError if the inputs violate our constraints.
Listing 1.5.1. Filling in the Skeleton: update_rating
def update_rating(library: list, song_title: str, new_rating: int) -> bool:
    """Updates the rating for a song in the music library.
    Args:
        library: A non-empty list of songs, each [title, rating, play_count].
        song_title: The song to look for (must be a non-empty string).
        new_rating: The new rating (must be an int in [1..5]).
    Returns:
        True if the song was found and updated; False otherwise.
    Raises:
        ValueError: If library is empty, song_title is empty, or new_rating is out of range.
    """
    # 1. Validate inputs
    if library is None or len(library) == 0:
        raise ValueError("Library cannot be None or empty.")
    if not song_title:
        raise ValueError("Song title cannot be empty.")
    if not (1 <= new_rating <= 5):
        raise ValueError("Rating must be between 1 and 5.")

    # 2. Search for matching song and update rating
    for song in library:
        if song[0] == song_title:
            song[1] = new_rating
            return True
            
    # 3. If no match was found, return False
    return False
Notice how each comment from the skeleton corresponds to a distinct code block. The final result is clear and concise—no wasted lines or hidden assumptions. Audrey didn’t need to guess whether she should raise an exception or return False for invalid inputs—she’d already settled that in the earlier design steps.

Subsection 1.5.4 Testing & Refinement

Once you have your initial implementation, the next step is to run your examples and tests—the same ones you drafted in Section 3. This “test-first” or “test-early” approach pays off now by giving you immediate confidence (or immediate warning!) about whether your code does what you promised.

Subsubsection 1.5.4.1 Running Your Tests

If you created a dedicated test function, such as:
Listing 1.5.2.
def test_update_rating():
    library = [
        ["Imagine", 5, 120],
        ["Thriller", 4, 230]
    ]
    
    # Standard case: update existing song
    assert update_rating(library, "Thriller", 3) == True
    assert library[1][1] == 3  # rating changed to 3
    
    # Not found
    assert update_rating(library, "Unknown Song", 4) == False

    print("test_update_rating PASSED")
Then you simply run test_update_rating() and watch for any AssertionError. If everything passes, you’ll see no error—just a silent, successful test. Some people like to print a “PASSED!” message at the end so they know everything worked.
But what if something fails? That’s where refinement comes in.

Subsubsection 1.5.4.2 Refining Your Code

If you see a test fail—maybe the function returned True instead of False or raised an unexpected exception—don’t panic. This is exactly why we test early and often.
You’ll check:
  • Did I misunderstand an edge case? Maybe your Data Definition needs a tweak.
  • Is the test correct? On rare occasions, your test might be the issue—if you wrote an incorrect expected value or forgot the design rules you set.
  • Am I mixing concerns? If your function tries to do too many things at once, it might become confusing. Splitting complex logic into smaller helpers can clarify where the bug lies.
After finding the root cause, you adjust your code (or your test), re-run the tests, and repeat until everything aligns. This cycle—fix, test, fix, test—is far more efficient than writing code blindly and hoping your final result works.

Subsection 1.5.5 More Practice: compute_popularity

Let’s illustrate the same process with a different function: compute_popularity, which we introduced in Section 2. This function calculates a numeric “popularity score” based on rating and play_count. By Section 4, you had written a skeleton with steps:
1. Validate inputs
2. Calculate rating * log(play_count + 1)
3. Return the result
Now, you fill it in with real code:
Listing 1.5.3.
import math
def compute_popularity(song: list) -> float:
    """Calculates a song's popularity as rating * log(play_count + 1).
    Args:
        song: [title, rating, play_count] where rating in [1..5] and play_count >= 0
    Returns:
        A float representing the popularity score.
    Raises:
        ValueError: if the song data is invalid.
    """
    # 1. Validate
    if song is None or len(song) < 3:
        raise ValueError("Expected song to be [title, rating, play_count].")
    
    title, rating, play_count = song[0], song[1], song[2]
    
    if not title:
        raise ValueError("Song title can't be empty.")
    if not (1 <= rating <= 5):
        raise ValueError("Rating must be 1..5.")
    if play_count < 0:
        raise ValueError("play_count can't be negative.")
    
    # 2. Calculate 
    popularity = rating * math.log(play_count + 1)
    
    # 3. Return
    return popularity
Next, run your test suite. For instance:
Listing 1.5.4.
def test_compute_popularity():
    # Typical case
    assert abs(compute_popularity(["Thriller", 5, 100]) - 23.0) < 0.001
    
    # Edge: zero plays => rating * log(1) => 0
    assert compute_popularity(["Imagine", 5, 0]) == 0.0

    print("test_compute_popularity PASSED")
If all these pass, you’re done! If any fail, refine the code until the behavior matches your contract.

Subsection 1.5.6 Recap & Looking Ahead

By systematically implementing each skeleton step and then testing & refining, you ensure your code truly matches the design you envisioned. No guesswork, no “Hmm, I hope this works!”—just a clear, confirmable match between your function’s stated purpose and its actual behavior.
This final stage of the Design Recipe is often the most rewarding. You get to see your functions come to life without the chaos or last-minute mysteries that plague a “just wing it” approach. You also learn to trust your own work: if your design steps and tests are thorough, you can confidently declare, “Yes, this function is correct.”
In upcoming chapters, we’ll continue building on these skills:
  • Handling more complex data: dealing with nested lists, dictionaries, or multiple constraints
  • Advanced testing strategies: using Python’s pytest or frameworks like unittest
  • Relating this to Java’s type system: seeing how typed fields in Java can automate parts of this “validation” step
For now, celebrate finishing the full Design Recipe cycle on a few example functions. As you tackle larger problems, keep applying the same steps. Over time, you’ll discover that this recipe isn’t just an academic exercise—it’s a powerful habit for developing robust, maintainable code in any language you learn.

Subsection 1.5.7 Practice

Checkpoint 1.5.5. 1. Implement find_favorite_songs & Test It.

Recall your find_favorite_songs(library) skeleton from Section 4, which returns a list of titles for songs rated at least 4. Now, write the full implementation and test it thoroughly:
  • Validate library: ensure it’s not None or empty, and each element is [title, rating, play_count].
  • Loop through songs, check if rating >= 4, and append the title to a result list.
  • Return that result list.
Write at least one test that verifies this logic, plus a test for invalid input. Refine until everything passes.

Checkpoint 1.5.6. 2. Create a remove_song Function.

Imagine Audrey wants a remove_song function that removes a song by title from her library. Define it in full (data definition, contract, skeleton, final implementation) and write tests for:
  • Removing an existing song (verify it’s gone)
  • Attempting to remove a song that doesn’t exist (should it return False or do something else?)
  • Invalid inputs (e.g., None for the library)
Apply the Design Recipe steps thoroughly, then finalize the code and confirm all tests pass.

Checkpoint 1.5.7. 3. Reflection on Confidence.

In a brief paragraph, explain how writing tests before (or during) implementation affects your confidence and workflow. Compare this to times you wrote code without any formal tests. Do you feel more in control or less? What might you do differently in future assignments?

Subsection 1.5.7.1 Implement, Test & Refine Exercises

Checkpoint 1.5.8. Parsons: From Skeleton to Final Implementation.
Audrey has a skeleton for update_rating(library, song_title, new_rating) from Section 4. Now she’s ready to fill in the details and ensure it passes the tests from Section 3. Rearrange these lines to create a coherent, final version. Recall:
  • update_rating should return True if the song is found and rating is updated, else False.
  • Raise ValueError if new_rating is out of [1..5].
Checkpoint 1.5.9. Retesting After Implementation.
After you finish implementing a function based on a skeleton, why does Section 5 stress the importance of re-running your tests?
  • Tests are only needed once, and then can be deleted after initial success.
  • No. Tests should be kept and re-run whenever the code changes, catching potential regressions.
  • Because each new change (or final code details) can introduce new bugs, so re-running ensures nothing broke.
  • Correct! Implementation steps can inadvertently break previous logic, so retesting prevents regressions.
  • Because Python won’t compile unless you have at least 5 tests.
  • Python doesn’t enforce a specific number of tests; tests are a best-practice, not a compiler requirement.
  • Because once we have an implementation, we want to auto-generate new docstrings from tests.
  • Tests don’t automatically create docstrings. They verify code correctness, not write documentation.
Checkpoint 1.5.10. All Green Tests Means Complete Coverage?
    If all existing tests pass after implementation, it guarantees we have covered every possible edge case and no bugs remain.
  • True.

  • Passing tests do not always prove total correctness. We reduce risk, but cannot guarantee we covered every edge scenario.
  • False.

  • Passing tests do not always prove total correctness. We reduce risk, but cannot guarantee we covered every edge scenario.
Checkpoint 1.5.11. Parsons: Refining Code After a Bug.
Audrey runs her tests and finds a bug: update_rating crashes if library is None. She decides to refine her code. Rearrange these lines to integrate a quick fix without breaking existing logic.
Solution.
Refined solution (bug fix for None library):
def update_rating(library, song_title, new_rating):
    # Refinement: library must be a list
    if not isinstance(library, list):
        raise ValueError("Library must be a list")

    if new_rating < 1 or new_rating > 5:
        raise ValueError("Rating must be 1..5")

    for song in library:
        if song[0] == song_title:
            song[1] = new_rating
            return True

    return False
Checkpoint 1.5.12. Short Answer: Refining count_words After a Failing Test.
Below is Audrey’s count_words function and a test that fails when the input is just whitespace (" "):
def count_words(text):
# Current implementation
words = text.split(" ")
return len(words)

def test_count_words_whitespace():
# We expect 0 words if the string is just spaces
result = count_words(" ")
# But the function returns 1 instead, causing an AssertionError:
assert result == 0, f"Expected 0, got {result}"
print("Whitespace test passed.")
When test_count_words_whitespace() runs, it raises AssertionError indicating the function returns 1 instead of 0.
In 2–3 sentences, explain how you would refine count_words to handle whitespace-only input correctly without breaking other scenarios. Reference how you’ll confirm it works (e.g., re-running the failing test and possibly others).
Solution.
Sample Response (One Possible Refinement):
  • I modify count_words to strip the input before splitting or check if text.strip() is empty, returning 0 in that case. For example:
    if text.strip() == "":
        return 0
    words = text.strip().split()
    return len(words)
    
  • I rerun test_count_words_whitespace, verifying no assertion error. I’d also run existing tests (like normal strings, empty string) to ensure no regression.
You have attempted of activities on this page.