Python Doctest: Testing Examples

Let’s talk about doctests in Python! Doctests are built-in testing frameworks that allow us to test our code directly inside docstrings using documentation strings (docstring = documentation string). They make it easy for newcomers to understand how a function works by providing examples right in the docstring. Plus, they’re great for testing your code without having to write separate tests files or run external testing frameworks like unittest!

Here’s an example:

# This function adds two numbers together and returns the result.
def add_numbers(x, y):
    """Add two numbers together and return the result.
       > add_numbers(2, 3) # Example of how to use the function, with expected output
       5
       > add_numbers(-10, 7) # Another example with different inputs and expected output
       -3
       > add_numbers("hello", "world") # Example of incorrect input, which will result in a TypeError
      TypeError: unsupported operand type(s) for +: 'str' and 'str'
    """
    return x + y # This line adds the two input numbers together and returns the result.

In this example, we have a function called `add_numbers()` that takes two arguments (x and y), adds them together, and returns the result. We also added some doctests to show how the function works with different inputs. The first two examples are expected to return 5 and 3 respectively, while the third example is meant to raise a `TypeError`.

To run these tests, simply call the `doctest` module in your Python script:

# Import the doctest module
import doctest

# Define a function called "add" that takes in two parameters, "x" and "y"
def add(x, y):
    """
    This function takes in two numbers and returns their sum.
    
    Parameters:
        x (int or float): First number to be added
        y (int or float): Second number to be added
        
    Returns:
        int or float: Sum of x and y
        
    Examples:
        >>> add(2, 3)
        5
        >>> add(5.5, -2.5)
        3.0
        >>> add("2", 3)
        Traceback (most recent call last):
            ...
        TypeError: unsupported operand type(s) for +: 'str' and 'int'
    """
    # Check if both parameters are either integers or floats
    if isinstance(x, (int, float)) and isinstance(y, (int, float)):
        # If so, return the sum of x and y
        return x + y
    else:
        # If not, raise a TypeError
        raise TypeError("unsupported operand type(s) for +: '{}' and '{}'".format(type(x).__name__, type(y).__name__))

# Run the doctest module to test the function
doctest.testmod()

This will execute all of the examples inside our docstrings and check if they match what we expect them to be. If any test fails, it’ll print out an error message with the line number where the failure occurred.

Doctests can also help us catch bugs in our code by providing a simple way to test edge cases or unexpected behavior. For example:

# Define a function to calculate the factorial of a given number using recursion
def calculate_factorial(n):
    """Calculate the factorial of a given number using recursion.
    
    Args:
        n (int): The number to calculate the factorial of.
        
    Returns:
        int: The factorial of the given number.
        
    Raises:
        ValueError: If the given number is negative.
        
    Examples:
        >>> calculate_factorial(5)
        120
        >>> calculate_factorial(-3)
        ValueError: Factorial not defined for negative numbers
    """
    # Check if the given number is negative
    if n < 0:
        # If so, raise a ValueError
        raise ValueError("Factorial not defined for negative numbers")
    # Check if the given number is 0 or 1
    elif n == 0 or n == 1:
        # If so, return 1 as the factorial
        return 1
    else:
        # If not, use recursion to calculate the factorial
        return n * calculate_factorial(n-1)

In this example, we have a function called `calculate_factorial()` that calculates the factorial of a given number using recursion. We also added some doctests to show how the function works with different inputs. The first test is expected to return 120 for n=5, while the second test should raise a `ValueError`.

However, let’s say we accidentally wrote `calculate_factorial(-3)` instead of `calculate_negative_factorial(-3)`, which would have raised an error. If we run our doctests now, it will catch this mistake and print out the following error message:

# This script is used to calculate the factorial of a given number and includes doctests to ensure the function works correctly.

def calculate_factorial(n):
    """
    Calculates the factorial of a given number.

    Args:
        n (int): The number to calculate the factorial of.

    Returns:
        int: The factorial of the given number.

    Raises:
        ValueError: If the given number is negative.

    Examples:
        >>> calculate_factorial(5)
        120
        >>> calculate_factorial(0)
        1
        >>> calculate_factorial(-3)
        Traceback (most recent call last):
            ...
        ValueError: Factorial not defined for negative numbers
    """
    if n < 0:
        raise ValueError("Factorial not defined for negative numbers") # Raise a ValueError if the given number is negative.
    elif n == 0:
        return 1 # Return 1 if the given number is 0.
    else:
        result = 1
        for i in range(1, n+1):
            result *= i # Calculate the factorial by multiplying the result by each number from 1 to n.
        return result

if __name__ == "__main__":
    import doctest
    doctest.testmod() # Run the doctests to ensure the function works correctly.

As you can see, the doctest caught our mistake and printed out an error message with the line number where it occurred. This helps us catch bugs early on in development and saves us time debugging later on.

Technical Detail: Interestingly, assret is a special misspelling of assert. If you try to access an attribute that starts with assret (or assert), Mock will automatically raise an AttributeError.

SICORPS