# 1.4 Preconditions

One of the most important purposes of a function docstring is to let others know how to use the function.
After all, we don't just write code for ourselves, but for other members of our development team or company,
or even the world at large if we're writing a library we think is useful to anyone.

The docstring of a function describes not only what the function does---through text and examples---but also the requirements necessary to use the function.
One such requirement is the **type contract**: this requires that when someone calls
the function, they do so with arguments of a specified type.

For example, given this function docstring:

```python
def decreases_at(numbers: list[int]) -> int:
    """Return the index of the first number that is less than its predecessor.

    >>> decreases_at([3, 6, 9, 12, 2, 1, 8, 5])
    4
    """
```

We know that `decreases_at` expects to be called on a list of integers;
if we violate the type contract, say by calling it on a single integer or a
dictionary, we cannot expect it to work properly.

In practice, we often want to extend this idea beyond specifying the required type of arguments.
For example, we might want to say that "this function must be given numbers between 1 and 10" or
"the first argument must be greater than the second argument."
A **precondition** of a function is any property that the function's arguments must satisfy to ensure that the function works as described.
They are included in a function's docstring, and form a crucial part of the function's interface.

As a user of a function, preconditions are extremely important, since they tell you what you have to do to use the function properly. They *limit* how a function can be used.
On the flip side, preconditions are freeing to the implementor of a function: by specifying a certain property in a precondition, the person writing the body of the function can go ahead and *assume* that this property is satisfied, which often leads to a simpler or more efficient implementation.
Consider a method for searching a list.
Binary search is efficient, but depends on having a sorted list.
If the search method had to confirm this,
the added work would make it slower than linear search!
In this case, it makes sense to simply require the caller to provide a sorted list.

The bottom line is that specifying preconditions is part of the *design* of a function.
It is a matter of specifying precisely what service we want to provide to the users of our functions---and what restrictions we want to impose upon them.

## How can we check preconditions?

While our previous example illustrates how to document preconditions as part of a function specification, it has one drawback: it relies on whoever is calling the function to read the documentation!
Of course, reading documentation is an important skill for any computer scientist, but despite our best intentions we sometimes miss things.
It would be nice if we could turn our preconditions into executable Python code so that the Python interpreter checks them every time we call the function.

For the rest of this section, we'll use the following function as our running example.
Note that in addition to the parameter type annotation, we've included a precondition written in the function docstring.

```python
def max_length(strings: list[str]) -> int:
    """Return the maximum length of a string in the given list of strings.

    Preconditions:
      - strings != []
    """
    max_so_far = -1
    for s in strings:
        if len(s) > max_so_far:
            max_so_far = len(s)

    return max_so_far
```

## Checking preconditions with assertions

One way to do this is to use an `assert` statement.
Because we've written the precondition as a Python expression, we can convert this to an assertion by copy-and-pasting it at the top of the function body.

```python
def max_length(strings: list[str]) -> int:
    """Return the maximum length of a string in the given list of strings.

    Preconditions:
      - strings != []
    """
    assert strings != []  # Check the precondition

    max_so_far = -1
    for s in strings:
        if len(s) > max_so_far:
            max_so_far = len(s)

    return max_so_far
```

Now, the precondition is checked every time the function is called, with a meaningful error message when the precondition is violated:

```pycon
>>> empty_list = []
>>> max_length(empty_list)
Traceback (most recent call last):
  File "<input>", line 1, in <module>
  File "<input>", line 7, in max_length
AssertionError
```

We can even improve the error message we get by using an extended syntax for `assert` statements, where we include a string message to display after the boolean expression being checked:

```python
def max_length(strings: list[str]) -> int:
    """Return the maximum length of a string in the given list of strings.

    Preconditions:
      - strings != []
    """
    assert strings != [], 'Precondition violated: max_length called on an empty list.'

    max_so_far = -1
    for s in strings:
        if len(s) > max_so_far:
            max_so_far = len(s)

    return max_so_far
```

Calling `max_length` on an empty set raises the same `AssertionError` as before, but now displays a more informative error message:

```pycon
>>> empty_list = []
>>> max_length(empty_list)
Traceback (most recent call last):
  File "<input>", line 1, in <module>
  File "<input>", line 7, in max_length
AssertionError: Precondition violated: max_length called on an empty list.
```

However, this approach of copy-and-pasting preconditions into assertions is tedious and error-prone.
First, we have to duplicate the precondition in two places.
And second, we have increased the size of the function body with extra code.
And worst of all, both of these problems increase with the number of preconditions!
*There must be a better way.*

## Enter PythonTA

The `python_ta` library we use in this course has a way to automatically check preconditions for all functions in a given file.
Here is an example:

```python
from python_ta.contracts import check_contracts  # NEW


@check_contracts  # NEW
def max_length(strings: list[str]) -> int:
    """Return the maximum length of a string in the given list of strings.

    Preconditions:
      - strings != []
    """
    max_so_far = -1
    for s in strings:
        if len(s) > max_so_far:
            max_so_far = len(s)

    return max_so_far
```

Notice that we've kept the function docstring the same, but removed the assertion.
Instead, we are importing a new module (`python_ta.contracts`), and then using the `check_contracts` from that module as a... what? 🤔

The syntax `@check_contracts` is called a **decorator**, and is technically a form of syntax that is an *optional part of a function definition* that goes immediately above the function header.
We say that the line `@check_contracts` *decorates* the function `max_length`, which means that it adds additional behaviour to the function beyond what is written the function body.

So what is this "additional behaviour" added by `check_contracts`?
As you might imagine, it reads the function's type contract and the preconditions written in the function docstring, and causes the function to check these preconditions every time `max_length` is called.
Let's see what happens when we run this file in the Python console, and attempt to call `max_length` on an empty set:

```pycon
>>> empty_list = []
>>> max_length(empty_list)
Traceback (most recent call last):
  ...  # File location details omitted
AssertionError: max_length precondition "strings != []" was violated for arguments {strings: []}
```

Pretty cool!
And moreover, because all parameter type annotations are preconditions, `python_ta` will also raise an error if an argument does not match a type annotation.
Here's an example of that:

```pycon
>>> max_length(148)
Traceback (most recent call last):
  ...  # File location details omitted
AssertionError: max_length argument 148 did not match type annotation for parameter strings: list[str]
```

We'll be using PythonTA's `check_contracts` decorator throughout this course to help us make sure we're sticking to the specifications we've written in our function header and docstrings when we call our functions.
Moreover, `check_contracts` checks the return type of each function, so it'll also work as a check when we're implementing our functions to make sure the return value is of the correct type.
