Skip to main content

Section 1.2 Method Signature & Purpose Statement

Subsection 1.2.1 Overview and Objectives

In Section 1, we saw how Audrey’s lack of data definitions led to confusion when her music library needed new features. But even with clear data definitions, we still need a way to communicate what our functions or methods do with that data.
In this section, we’ll continue Audrey’s story as she learns why documenting what a function promises (and what constraints it has) is just as crucial as documenting data structure. We’ll discover how tools like docstrings in Python can make these "contracts" explicit and checkable.

Subsection 1.2.2 A Communication Breakdown

After learning about data definitions, Audrey felt more confident. Each song was now clearly defined as [title, rating, play_count] with specific rules about valid values. Excited to share her progress, she showed her code to her study group.
Listing 1.2.1.
def update_rating(library, song_title, new_rating):
    for entry in library:
        if entry[0] == song_title:
            entry[1] = new_rating
            return
Her friend Mai tried using the function:
Listing 1.2.2.
# Mai's attempt
songs = [["Hey Jude", 4, 10]]
result = update_rating(songs, "Hey Jude", 5)
print(f"Update succeeded: {result}")  # Prints: Update succeeded: None

# Later...
update_rating(None, "Hey Jude", 5)   # Crashes! 
update_rating(songs, "Hey Jude", 10)  # Sets rating to 10 (invalid!)
update_rating([], "", -1)            # Silently does nothing
"Your function is broken!" Mai said. "It returns None even when it works, crashes sometimes, and doesn’t tell me if the song wasn’t found!"
Audrey protested: "But it works fine for me! I always check the library after calling it, and I know not to pass None or invalid ratings..."
They were both right—and both wrong. The function worked exactly as Audrey intended, but she’d never explicitly stated those intentions. She’d written code that made sense to her but left others guessing about:
  • What valid values can new_rating take?
  • Can library be None or empty?
  • How do we know if the update succeeded?
  • Should it warn us if song_title isn’t found?

Subsection 1.2.3 From Confusion to Clarity

After the miscommunication, Mai and Audrey met at the campus coffee shop. Instead of arguing about who was right, they decided to write down exactly what they each thought the function should do.
Mai: "Let me write down what confused me when I tried using your function..."
Mai writes on a napkin:
  • Can I pass an empty library?
  • What happens if the song isn’t found?
  • How do I know if the update worked?
Audrey: "Oh! I see the problem. I had all these rules in my head, but I never wrote them down. Like, ratings have to be 1-5..."
Mai: "Wait, they do? That would’ve been good to know before I tried setting it to 10!"
Audrey: "Right... and the library can’t be None, and song titles can’t be empty..."
Mai: "This is exactly what I needed to know! We should document all of this."
Their conversation revealed four key parts of any function contract:
  • Preconditions: What must be true about the inputs? (e.g., rating must be 1-5)
  • Input Types: What kind of data can each parameter accept?
  • Return Value: What does success or failure look like?
  • Error Cases: What happens when things go wrong?
Mai grabbed another napkin and started organizing their thoughts:
update_rating needs:
  • Inputs:
    • library: must be a non-empty list of songs
    • song_title: must be a non-empty string
    • new_rating: must be an int from 1 to 5
  • Output: should tell us if it worked (True/False?)
  • Errors: raise ValueError for bad inputs

Subsection 1.2.4 Turning Coffee Shop Notes into Code

Back at their dorm, Audrey and Mai turned their napkin notes into Python code. They knew that Python has special "docstrings" - documentation strings that explain what functions do.
Their first attempt looked promising, but they weren’t sure about the best format:
Listing 1.2.3.
def update_rating(library, song_title, new_rating):
    """
    Updates a song's rating. The song must exist in the library.
    
    We require:
    - library is a non-None, non-empty list of songs
    - song_title is a non-empty string
    - new_rating is an int between 1 and 5
    
    Returns True if found and updated, False if song not found.
    Raises ValueError for invalid inputs.
    """
    if library is None or len(library) == 0:
        raise ValueError("Library cannot be None or empty")
    if not song_title:  # Catches empty strings
        raise ValueError("Song title cannot be empty")
    if not (1 <= new_rating <= 5):
        raise ValueError("Rating must be 1-5")
    # ... rest of implementation ...
This was much better! Now anyone reading the function would know:
  • Exactly what inputs are valid (and invalid)
  • What the function promises to return
  • When and why it might raise errors
But Mai noticed something: "This is good, but it’s still kind of... informal? Like, we’re just writing paragraphs. Is there a more standardized way to document functions in Python?"

Subsection 1.2.5 Making Documentation More Structured

Before looking at Python’s formal documentation tools, let’s practice writing a contract for another function. This will help us see what information is most important to include.

Checkpoint 1.2.4. Quick Practice: Documenting a Function.

Write informal documentation (like Mai’s napkin notes) for this function:
def find_favorite_songs(library):
    """What would you write here?"""
    result = []
    for song in library:
        if song[1] >= 4:
            result.append(song[0])
    return result

Subsection 1.2.6 Writing Clear Contracts with Python

Remember how Audrey and Mai discovered they needed to specify:
  • What kinds of inputs are valid
  • What the function promises to do
  • What happens when things go wrong
Python provides two main tools for this: method signatures with type hints and purpose statements in docstrings. Together, these form a clear contract between the function and its users.
A method signature is more than just “function name + parameters.” It’s a compact way to state the function’s inputs, outputs, and sometimes exceptions or error conditions. Combined with a purpose statement (a one- or two-sentence explanation of the function’s job), you get a “mini contract” that aligns everyone’s expectations.

Subsubsection 1.2.6.1 A Form of Living Documentation

Once written, these signatures and purpose statements become living documentation. If you decide to change the function’s behavior—like refusing to update a rating if the new value is out of [1..5]—you can update the contract accordingly. Anyone reading the code can see exactly what’s expected, without guessing or searching for hidden assumptions.

Subsubsection 1.2.6.2 Enabling Better Testing (Later Steps)

These contracts also guide test creation. Step 4 of the design recipe (Examples & Tests) becomes much clearer when we know each function’s inputs, outputs, and constraints. We can systematically test valid and invalid ratings, missing song titles, etc., rather than discovering them “by accident” during frantic debugging.

Subsection 1.2.7 From Notes to Professional Documentation

After some research, Mai discovered that professional Python developers use a combination of two tools to write clear contracts: type hints and structured docstrings.
Mai: "Look what I found! We can actually tell Python what types our function expects:"
def update_rating(library: list, song_title: str, new_rating: int) -> bool:
Audrey: "That’s cool! So anyone can see that new_rating must be an integer?"
Mai: "Exactly! And the -> bool means it returns True or False."
Excited by this discovery, they refined their documentation further:
Listing 1.2.5.
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 list of songs, each [title, rating, play_count].
                Cannot be None or empty.
        song_title: The title to search for. Must be non-empty.
        new_rating: New rating value, must be in range [1..5].

    Returns:
        True if the song was found and updated, False otherwise.

    Raises:
        ValueError: If any inputs violate their constraints.

    Example:
        >>> songs = [["Hey Jude", 3, 10]]
        >>> update_rating(songs, "Hey Jude", 4)
        True
        >>> update_rating(songs, "Unknown Song", 4)
        False
    """
    # Implementation will come later...
    pass
This version had several advantages over their coffee-shop notes:
  • Type hints (list, str, int) catch type mismatches early
  • Structured sections (Args, Returns, Raises) make the contract easy to scan
  • Example usage shows exactly how the function should behave
  • The return type (bool) makes it clear how to check for success

Subsubsection 1.2.7.1 Practice: Writing Another Contract

Mai suggested they practice by writing a contract for a new function: one that would compute a "popularity score" for each song.
Audrey: "How about something that combines rating and play count? Like rating × log(play_count + 1)?"
Mai: "Perfect! Let’s write out the contract first, before any code:"
Listing 1.2.6.
def compute_popularity(song: list) -> float:
    """
    Calculates a song's popularity score based on rating and play count.

    The score is: rating * log(play_count + 1)
    This means:
    - A 5-star song with 0 plays scores 5 * log(1) = 0
    - A 5-star song with 100 plays scores 5 * log(101) ≈ 23.0

    Args:
        song: A list [title, rating, play_count] where:
             - rating is in [1..5]
             - play_count is >= 0

    Returns:
        A float value >= 0, higher means more popular.

    Raises:
        ValueError: If rating or play_count are invalid.

    Example:
        >>> compute_popularity(["Hey Jude", 5, 100])
        23.0
    """
    # We'll implement this in later chapters
    pass
Notice how this contract:
  • Explains the mathematical formula clearly
  • Shows what different inputs would produce
  • Maintains the same constraints from our Song definition
  • Uses the same structure as update_rating for consistency

Subsection 1.2.8 Formalizing Function Contracts

Having seen how method signatures and purpose statements emerge naturally from documenting our intentions, let’s formalize what makes a good function contract. A complete contract needs:

Subsubsection 1.2.8.1 Essential Components

  • Method Signature: The function name, parameter types, and return type
  • Purpose Statement: A clear, one-sentence description of the function’s role
  • Preconditions: Requirements that inputs must satisfy
  • Postconditions: Guarantees about the output or effects
  • Error Cases: What happens when preconditions are violated

Subsubsection 1.2.8.2 Standard Documentation Format

In Python, we use docstrings with specific sections:
Listing 1.2.7.
def process_data(data: list, threshold: float) -> dict:
    """
    Brief description of function's purpose.

    Args:
        data: Description of data parameter and constraints
        threshold: Description and valid range

    Returns:
        Description of return value

    Raises:
        ValueError: When and why this might occur
        TypeError: If input types are wrong

    Examples:
        >>> process_data([1, 2, 3], 0.5)
        {'above': 2, 'below': 1}
    """

Subsection 1.2.9 A Preview: Contracts in Java

While we’re learning to write clear contracts in Python, let’s peek at how Java—a language you’ll learn soon—handles function contracts. Don’t worry if the Java syntax looks strange; you’re not expected to understand it all yet.

Subsubsection 1.2.9.1 Python vs. Java: A First Look

Listing 1.2.8.
# In Python, we document our contract in comments
def update_rating(song_title: str, new_rating: int) -> bool:
    """
    Updates a song's rating if it exists in our library.
    
    Args:
        song_title: Name of song (must not be empty)
        new_rating: New rating (must be 1-5)
    
    Returns:
        True if song was found and updated
    """
    # Implementation will come later...
    pass
// In Java, some rules are enforced by the language
public boolean updateRating(String songTitle, int newRating) {
    // Java ensures songTitle can only be text
    // and newRating can only be a whole number
    
    // Implementation will come later...
    return false;
}
Notice how Java’s type system helps enforce part of our contract:
  • You can’t accidentally pass a number where text is needed
  • You can’t return text when a true/false answer is expected
  • Java checks these rules before the program even runs

Subsubsection 1.2.9.2 Stay Focused on Python for Now

The Java example above is just a preview. For now, focus on writing clear contracts in Python:
  • Use type hints to show what kind of data functions expect
  • Write detailed docstrings explaining constraints
  • Include examples showing how to use the function correctly
We’ll learn more about Java’s type system in later chapters. For now, just notice how different languages can help us express and enforce our function contracts in different ways.

Subsection 1.2.10 The Value of Clear Contracts

Subsubsection 1.2.10.1 Immediate Benefits

  • Error Prevention: By explicitly stating constraints (like "rating must be 1-5"), we catch invalid inputs before they cause problems.
  • Documentation: The contract serves as living documentation that stays in sync with the code.
  • Testing Guide: Contracts naturally suggest test cases: valid inputs, edge cases, and error conditions.

Subsubsection 1.2.10.2 Long-term Value

  • Maintenance: Future developers (including yourself) can understand and modify code with confidence.
  • Reusability: Well-documented functions with clear contracts are easier to reuse in new contexts.
  • Reliability: When every function’s contract is clear, large systems become more reliable and easier to debug.

Subsection 1.2.11 Preparing for Implementation

With clear method signatures and purpose statements in place, we’re ready for the next step: writing examples and tests before implementation. This "test-first" approach ensures our contract is complete and unambiguous.

Subsubsection 1.2.11.1 What’s Coming Next

In Section 3, we’ll:
  • Learn how to derive test cases from contracts
  • Write examples that cover normal usage and edge cases
  • Use test frameworks to automate verification
  • Refine contracts based on test insights

Checkpoint 1.2.9. Practice: Writing Contracts.

Write a complete contract for a function that:
  • Takes a list of student names and their grades
  • Returns a list of names of students who passed (grade >= 60)
  • Handles empty lists and invalid grades appropriately
Include the method signature, purpose statement, parameters, return value, and error cases.

Subsection 1.2.12 Method Signature & Purpose Exercises

Checkpoint 1.2.10. Parsons: Audrey & Mai’s Journey to Documenting Function Constraints.

Rearrange these story beats to see how Audrey and Mai realized the importance of a function’s signature and purpose statement (docstring).

Checkpoint 1.2.11. Parsons: Chain of Reasoning for a Method Contract.

Reorder these logic steps showing why Audrey and Mai ended up writing a detailed method signature & purpose statement.

Checkpoint 1.2.12. Benefits of a Purpose Statement.

Which of the following best captures the main benefit of writing a clear purpose statement for your function based on this section’s context?
  • It ensures anyone using the function knows what it does, what inputs are valid, and what output to expect.
  • Correct! Documenting the function’s purpose and constraints is key for preventing misuse.
  • It automatically tests your code for you, eliminating the need for a test suite.
  • No, you still need to write tests. A docstring does not perform automated checks.
  • It hides the actual code so only the signature is visible in Python.
  • Python docstrings do not hide code. They just provide documentation.
  • It compresses the code size, making the program load faster.
  • Purpose statements do not optimize or compress code size.

Checkpoint 1.2.13. Signatures & Enforced Constraints.

    Specifying parameter types (e.g., song_title: str) in a Python signature will cause a runtime error if a different type is passed.
  • True.

  • Python does not enforce type hints at runtime. Type hints + docstrings help humans (and optional tools), but don’t inherently stop wrong types at runtime.
  • False.

  • Python does not enforce type hints at runtime. Type hints + docstrings help humans (and optional tools), but don’t inherently stop wrong types at runtime.

Checkpoint 1.2.14. Identify: Method Signature.

Which concept is described by: “The function name, parameters (with types), and return type—describing how the function is called and what it returns.”
  • Method Signature
  • Correct! That is precisely what a method signature describes.
  • Preconditions
  • Preconditions are about requirements that must hold before calling the function.
  • Purpose Statement
  • This explains what the function does, rather than its parameters or return type.
  • Postconditions
  • These are guaranteed outcomes once the function completes, not how it’s called.
  • Error Cases
  • Error cases describe what happens with invalid inputs, not the function’s signature details.

Checkpoint 1.2.15. Identify: Preconditions.

Which concept is described by: “Conditions that must be true or satisfied before the function runs, such as rating in [1..5].”
  • Method Signature
  • A signature states parameter types/return types, but not required input conditions.
  • Preconditions
  • Exactly! Preconditions are the assumptions that must hold before calling the function.
  • Purpose Statement
  • This describes the function’s job or role, not constraints on inputs.
  • Postconditions
  • These describe guarantees after the function completes, not the input requirements.
  • Error Cases
  • That focuses on how invalid inputs are handled, not on the valid conditions themselves.

Checkpoint 1.2.16. Identify: Purpose Statement.

Which concept is described by: “A concise explanation of what the function does, clarifying its main role or behavior.”
  • Method Signature
  • The signature is about how the function is called (parameters/return), not its main explanation.
  • Preconditions
  • These specify requirements on inputs, not the function’s overall job.
  • Purpose Statement
  • Correct! A purpose statement summarizes the function’s intended behavior.
  • Postconditions
  • Postconditions describe results after successful execution, not the overall “purpose.”
  • Error Cases
  • That deals with exceptions or invalid inputs, not the fundamental purpose.

Checkpoint 1.2.17. Identify: Postconditions.

Which concept is described by: “What the function guarantees or ensures once it completes, given valid inputs.”
  • Method Signature
  • This only specifies parameters/return type, not the guaranteed state after execution.
  • Preconditions
  • Those are the assumptions before execution, not the outcome guarantees.
  • Purpose Statement
  • This is a short description of the function’s role, not the final guaranteed state.
  • Postconditions
  • Exactly. Postconditions indicate the state or results we can rely on after a valid call.
  • Error Cases
  • Error cases handle invalid inputs or exceptions, not normal outcomes with valid data.

Checkpoint 1.2.18. Identify: Error Cases.

Which concept is described by: “Possible invalid inputs or exceptions, and how the function handles them (e.g., raises ValueError).”
  • Method Signature
  • This covers parameters and returns, not how errors are handled.
  • Preconditions
  • Preconditions specify valid input scenarios, while “error cases” detail what happens if they’re not met.
  • Purpose Statement
  • This describes the function’s main job, not how errors or invalid inputs are handled.
  • Postconditions
  • Postconditions describe normal results for valid data, not the reaction to invalid data.
  • Error Cases
  • Correct! This section explains how the function responds to invalid inputs or raises exceptions.
You have attempted of activities on this page.