Writing Function Contracts

Introduction: A Function's Written Contract

Welcome to the final unit of Returning Values and Understanding Scope in Python! We have covered a lot of ground together: Unit 1 showed how return sends a value out of a function, Unit 2 explained that for the pure calculation functions in this course, parameters move values in and return explicitly provides each result, and Unit 3 connected functions into pipelines where one stage's result feeds the next.

There is one gap left. In Unit 3, the only way to learn what a stage took in and handed back was to read its body. That does not scale: a caller should be able to trust a function without opening it. The fix is the docstring, a written contract stating what a function does, what it takes in, what it hands back, and any side effects.

Our program this time is small: one documented celsius_to_fahrenheit function, its printed result 212.0, and the very same documentation read back while the program runs in two different formats. Four ideas carry us there:

  1. A docstring is a triple-quoted string placed as the first statement in the body.
  2. It documents behavior, arguments, return value, and side effects.
  3. Python stores it and hands it back at runtime through __doc__.
  4. help() renders it together with the function's signature.

The Undocumented Function We Start From

Let's begin where most code begins: with no documentation at all. Here is the conversion function in its bare form, plus the line that calls it.

def celsius_to_fahrenheit(celsius):
    return celsius * 9 / 5 + 32


print(celsius_to_fahrenheit(100))

Tracing the call: celsius binds to 100, then 100 * 9 / 5 gives 180.0, and + 32 gives 212.0. The / operator is true division, which always produces a float, so the printed result is 212.0 rather than 212, matching the float results we saw in earlier units.

212.0

Now the honest question: what does a reader learn from the def line alone? Only that there is one parameter named celsius. Nothing says whether the function returns its result or prints it, or which unit the number comes back in. The reader has to decode the arithmetic to find out. As always, the file keeps our established style: no imports, no main() wrapper, no __main__ guard, and the definition sits above every line that uses it.

Writing the Docstring: Summary, Args, Returns

Let's write the contract that answers those questions directly, placing it inside the function body.

def celsius_to_fahrenheit(celsius):
    """Convert a Celsius temperature to Fahrenheit.

    Args:
        celsius: Temperature in degrees Celsius.

    Returns:
        The equivalent temperature in degrees Fahrenheit.
    """
    return celsius * 9 / 5 + 32

The contract has three parts: the one-line summary states what the function does; the Args: block names each parameter and explains what it means; the Returns: block describes the value handed back. Mechanically, the triple quotes """...""" let the string span multiple lines, and the whole string must be the first statement in the body, sitting between the def line and the first executable line.

Notice what did not change: re-running the program still prints exactly 212.0. Documentation describes behavior; it never alters it. This is also where a docstring differs from a # comment: a comment explains how the code works to someone reading the source, while a docstring is a promise to the caller, who may never open the source at all.

Documenting Side Effects (and Their Absence)

Look closely at what our contract deliberately does not claim: it never mentions printing, writing files, or modifying anything. That silence is meaningful because celsius_to_fahrenheit is a pure value-producing function in the Unit 1 style; its only effect is the value it hands back. A function that behaves differently must say so, like this aside, which is not part of our final program:

def scale(amount):
    """Return the amount doubled.

    Also prints the incoming amount before scaling.

    Args:
        amount: The number to double.

    Returns:
        The amount multiplied by two.
    """
    print("scaling:", amount)
    return amount * 2

The rule is plain: anything a caller could not predict from the return value alone, such as printing or changing something outside the function, belongs in the docstring. The test of a good contract is simple: we never had to read celsius * 9 / 5 + 32 to know what to pass in or what we would get back.

Reading Documentation Back with `__doc__`

Here comes the part that surprises many learners. Unlike a comment, which the parser throws away, a docstring is kept: Python stores it on the function object itself. That means we can print it while the program runs.

# Read the documentation back at runtime
print(celsius_to_fahrenheit.__doc__)

Note the shape of this expression carefully: __doc__ is an attribute of the function object, so there are no parentheses after the function name. We are inspecting the function, not calling it. The printed text is the raw string from the source, preserving the blank lines and the leading indentation of the Args: and Returns: entries exactly as we typed them.

Convert a Celsius temperature to Fahrenheit.

    Args:
        celsius: Temperature in degrees Celsius.

    Returns:
        The equivalent temperature in degrees Fahrenheit.
    

Had we skipped the docstring, __doc__ would simply be None: the same None we met back in Unit 1 when a function returned nothing.

A Friendlier View with `help()`

The raw text is faithful but a little rough. Python offers a nicer view of the same information with one more line, which completes our program.

help(celsius_to_fahrenheit)

help() is a builtin, so no import is needed, and it is a genuine function call, which is why it takes parentheses (unlike __doc__). It formats our docstring together with information Python already knows about the function, adding the signature line above a re-indented copy of the same text.

Help on function celsius_to_fahrenheit in module __main__:

celsius_to_fahrenheit(celsius)
    Convert a Celsius temperature to Fahrenheit.

    Args:
        celsius: Temperature in degrees Celsius.

    Returns:
        The equivalent temperature in degrees Fahrenheit.

Both views draw from the one docstring in the body: change the summary line, and both outputs change; delete the docstring, and help() shows only a bare signature. Placement matters for the same reason: a triple-quoted string moved below the return is not documentation at all, just unreachable text, and both views go empty.

When a Contract Goes Stale

A written promise carries a risk: Python never checks a docstring against the code, so a contract that was once true can quietly become a lie. Consider this drifted example, again just an aside:

def scale(amount):
    """Print the scaled amount.

    Args:
        amount: The number to scale.
        factor: How much to multiply by.

    Returns:
        None.
    """
    return amount * 2

Three specific lies live in that docstring:

  • The summary claims a side effect, printing, that never happens.
  • The Args: block names a factor parameter the signature does not accept.
  • The Returns: block claims None when the function hands back a number.

The failure mode is what makes this dangerous: print(scale(5)) happily prints 10, nothing crashes, and no warning appears. Only the humans who trusted the documentation are misled. So we adopt a working habit: changing behavior and changing the contract are one edit, never two. If we remove a print, we remove the sentence describing it in the same change.

The Complete Program

Putting every piece together, here is the finished file in its exact final shape:

def celsius_to_fahrenheit(celsius):
    """Convert a Celsius temperature to Fahrenheit.

    Args:
        celsius: Temperature in degrees Celsius.

    Returns:
        The equivalent temperature in degrees Fahrenheit.
    """
    return celsius * 9 / 5 + 32


print(celsius_to_fahrenheit(100))

# Read the documentation back at runtime
print(celsius_to_fahrenheit.__doc__)
help(celsius_to_fahrenheit)

The output arrives in three parts: the converted value 212.0, the raw docstring, and finally the help() rendering with the signature on top; the same documentation shown twice in two formats. Every choice is deliberate: no imports, no main() wrapper, no __main__ guard, no module-level docstring, no print inside the function body, and every definition above the lines that call or inspect it. There is also a nice payoff for Unit 3: once contracts are written down, a pipeline's stage order becomes checkable on paper because one function's documented return is exactly what the next one's documented parameter expects.

Conclusion and Next Steps

In one sentence: a docstring is a triple-quoted string placed as the first statement of a function body that promises callers what the function does, what it takes in, what it hands back, and what it changes, and Python keeps that promise readable at runtime through __doc__ and help(). Around it sit four rules: placement decides whether the text counts as documentation; side effects must be stated explicitly; both runtime views share a single source; and a stale contract fails silently, so code and docs change together.

Congratulations on reaching the end of Returning Values and Understanding Scope in Python! We started with a first return and arrived at documented, composable functions. Next up, Writing Complex Python Functions puts conditionals, loops, and lists inside these functions, where a clear contract matters even more.

In the practices ahead, we will read a finished contract, document a bare function, print its documentation back at runtime, repair a docstring that no longer matches its function, and write a second documented function in the same style. Let's head into the editor and write our first contracts!

Sign up

Join the 1M+ learners on CodeSignal

Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal