The virtue of incremental development
Tags: python, teaching
Published: 2019-04-24 13:29:01

---
title: The virtue of incremental development
published: true
tags: [python, teaching]
canonical_url: https://dmerej.info/blog/post/the-virtue-of-incremental-development/
---


*Originally published on [my blog](https://dmerej.info/blog/post/the-virtue-of-incremental-development/)*.

Here’s today challenge: can you write a command-line tool that allows converting to and from various measurements units?

For instance, you could input “3 miles in meters” and get “4828.03”.

I submitted this challenge to my Python students last weekend, asking them to write the code from scratch. [^1]

1 hour later, something miraculous happened that I never would have expected.

But let me tell you the full story.

# Getting started

I told the students that they could start by writing some “exploratory code”.

“Just hard-code anything you have to and keep everything in the `main()` function“, I said.

After a few discussions, we agreed to only write code that converted kilometers to miles, and that we’ll read the values from the command line.

Here’s what we came up with:

```python
import sys

def main():
    kilometers = float(sys.argv)
    miles = kilometers / 1.609
    print(f"{.2f}", miles)

if __name__ == " __main__":
    main()
```

I then pointed out that the code was not generic. Indeed, “kilometers”, “miles” and “1.609” are hard-coded there.

# Naming a new function

The students understood there was a three-parameters function waiting to be written. So we went to the drawing board and after a while, we decided to have a function called `convert(value, unit_in, unit_out)`.

Note that we did _not_ make any assumption about the _body_ of the function. We just wanted to see how `main()` could become more generic, and we were still allowed to hard-code parts of the code:

```python
def convert(value, unit_in, unit_out):
    coefficient = 1 / 1.609
    result = value * coefficient
    return result

def main():
    value = float(sys.argv[1])
    unit_in = sys.argv[2]
    unit_out = sys.argv[3]

    result = convert(value, unit_in, unit_out)
    print(f"{.2f}", result)
```

Some notes:

- The `main()` function is now completely _generic_, and we probably won’t need to change it.
- The signature of the `convert` function almost dictated the command-line syntax:

```python
def convert(value, unit_in, unit_out):
    ...
```

```
# Usage: convert.py value unit_in unit_out
$ python3 convert.py 2 meters miles
```

# Computing the coefficient

Now it was time to get rid of the hard-coded coefficient. This time finding a function name was easier:

```python
def get_coefficient(unit_in, unit_out):
   ...
```

Then we tried to figure out how to implement it. We knew we would be needing a dictionary, but the structure of it was unknown.

“Back to the drawing board”, I said. “Let’s write down what the dictionary should look like”.

Here’s our first attempt:

```python
units = {
   "km": { "miles": 1/1.609, "meters": 1/1000, ....},
   "yards": { "miles": 1/1760, "meters": ..., "km": ...}
   ...
}
```

“This won’t do”, I said. “Look at what happens if we add a new measurement unit, such as `feet`”.

We’ll have to:

- add a new ‘feet’ key to the `units` dictionary,
- compute all the coefficient to convert from `feet` to all the other units,
- and add a `feet` key to all the other dictionaries

There has to be a better way!

After a short brainstorming session, we decided to limit ourselves to _distance_ measurements, and to _always convert to SI units_ first.

So we draw the new structure of the `units` dictionary:

```python
# Coefficients convert from "meters"
distances = {
    "km": 1/1000,
    "yards": 1.094,
    "miles": 1/1609,
}
```

And then we thought about the algorithm. We found three possibilities:

- If we want to convert _from meters_, we just have to look up the coefficient in the dictionary
- If we want to convert _to meters_, we can look up the coefficient in the dictionary and return its inverse
- Otherwise, we combine the two procedures above and return the product of the two coefficients.

“This is looking good”, I said. “Let’s try to implement the algorithm but just for the first case and see what happens”.

# Testing the algorithm

I showed my students how they could use Python’s interpreter to check the get\_coefficient() function was working properly.

We quickly managed to get the first case working:

```python
def get_coefficient(unit_in, unit_out):
    # FIX ME: only works with distances for now
    # Coefficients to convert from "meters"
    distances = {
        "km": 1/1000,
        "yards": 1.094,
        "miles": 1/1609,
    }
    if unit_in == "m":
        return distances[unit_out]

>>> import conversion
>>> conversion.get_coefficient("m", "km")
0.001
>>> conversion.get_coefficient("m", "yards")
1.094
```

“Cool, this works”, I said. “Let’s see what happens when the input value is not in meters:”

```python
def get_coefficient(unit_in, unit_out):
    # FIX ME: only works with distances for now
    # Coefficients to convert from "meters"
    distances = {
        "km": 1/1000,
        "yards": 1.094,
        "miles": 1/1609,
    }
    if unit_in == "m":
        return distances[unit_out]
    else:
        reciprocal_coefficient = 1 / distances[unit_in]
        return reciprocal_coefficient * distances[unit_out]

>>> import conversion
>>> conversion.get_coefficient("miles", "yards")
1760
```

“Look how readable the code is”, I said. “We have a value that’s called`reciprocal_coefficient` and we get it by calling 1 over something else. Isn’t this nice?“.

# The miracle

I then pointed out that the [‘else’ after the return](https://dmerej.info/blog/post/else-after-return-yea-or-nay/) was unnecessary.

```python
def get_coefficient(unit_in, unit_out):
    # FIX ME: only works with distances for now
    # Coefficients to convert from "meters"
    distances = {
        "km": 1/1000,
        "yards": 1.094,
        "miles": 1/1609,
    }
    if unit_in == "m":
        return distances[unit_out]
    reciprocal_coefficient = 1 / distances[unit_in]
    return reciprocal_coefficient * distances[unit_out]
```

And then it happened. “Hey”, one of the students said, “what if we added meters in the distances dictionary with `1` as value? We could get rid of the first `if` too!“.

“Let’s do it”, I said:

```python
def get_coefficient(unit_in, unit_out):
    # FIX ME: only works with distances for now
    distances = {
        "m": 1,
        "km": 1/1000,
        "yards": 1.094,
        "miles": 1/1609,
    }
    reciprocal_coefficient = 1 / distances[unit_in]
    return reciprocal_coefficient * distances[unit_out]

>>> import conversion
>>> conversion.get_coefficient("m", "m")
1
>>> conversion.get_coefficient("km", "m")
1000
>>> conversion.get_coefficient("m", "yards")
1760
```

And of course, this works. When `meters` is either `unit_in` or `unit_out`, all operations will involve multiplying or dividing by 1.

That was a really nice surprise for several reasons:

- One, when I thought about the problem alone, before starting the workshop, I was pretty sure I would need a much more complex data structure.
- Two, one of the students just refused to believe the code would work, even after having seen it in action in the interpreter ;)
- Three, we killed one comment :)

# Lessons learned

We found a beautiful algorithm and a nice data structure, not by trying to solve _everything_ at once, but by slowly building up more and more generic code, getting rid of hard-coded values one after the other, and by carefully thinking about naming.

I hope you find this approach useful, and I highly suggest you try using it next time you implement a new feature.

Cheers!

---

_I'd love to hear what you have to say, so please feel free to leave a comment below, or check out [my contact page](https://dmerej.info/blog/pages/contact/) for more ways to get in touch with me._


[^1]: I’m using [mob programming](https://en.wikipedia.org/wiki/Mob_programming) during my Python classes. It works really well.