avoiding spacecraft crashes with mypy

January 1, 2021

In 1999, the Mars Client Orbiter crashed putting a melancholic end to a $125-million project. The crash happened because two teams responsible for designing two software subsystems of the spacecraft used different units of measurement.

Although a silly bug, it's not immediately clear how to avoid it in a Python program. Let's stick to the unit of measurement problem but in a lower stake situation. Assume you want to calculate someone's Body Mass Index (BMI).

The BMI formula is given by

def calculate_bmi(weight: float, height: float) -> float:
    return weight / height**2

where weight and height are measured in kilograms and meters respectively.

I am Brazilian and would probably not misuse calculate_bmi, since I am used to the metric system. If I were born in the USA, however, I might have wanted to pass weight in pounds and height in feet.

# Me
calculate_bmi(82.4, 1.83) # > 24.605094210039116

# American me
calculate_bmi(181.7, 6.00) # > 5.047222222222222

One way to avoid this kind of bug is to use mypy with typing.NewType.

# bmi.py
from typing import NewType

Kg = NewType('Kg', float)
Meter = NewType('Meter', float)

def calculate_bmi(weight: Kg, height: Meter) -> float:
    return weight / height**2

print(calculate_bmi(82.4, 1.83))

bmi.py doesn't type-check. Notice I have changed the function annotation but I am still calling it with two floats (82.4, 1.83). Mypy -- rightfully so -- yells at me.

$ mypy bmi.py
Argument 1 to "calculate_bmi" has incompatible type "float"; expected "Kg"
Argument 2 to "calculate_bmi" has incompatible type "float"; expected "Meter"

To make mypy happy, you need to wrap the numbers in the newly created types Kg and Meter.

calculate_bmi(Kg(82.4), Meter(1.83))

This style of code is not very pythonic, but it does have its place. Specially, when the same type (float) can represent two different things (pounds and kilos). At the very least, you get the correct value of your Body Mass Index by requiring the caller of calculate_bmi to explicitly declare the unit of measurement of height and weight. In more critical scenarios, you might end up savingsaved NASA a couple million dollars.