DEV Community

Dimitri Merejkowsky
Dimitri Merejkowsky

Posted on • Originally published at dmerej.info on

The virtue of incremental development

Originally published on my blog.

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:

import sys

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

if __name__ == " __main__":
    main()
Enter fullscreen mode Exit fullscreen mode

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:

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)
Enter fullscreen mode Exit fullscreen mode

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:
def convert(value, unit_in, unit_out):
    ...
Enter fullscreen mode Exit fullscreen mode
# Usage: convert.py value unit_in unit_out
$ python3 convert.py 2 meters miles
Enter fullscreen mode Exit fullscreen mode

Computing the coefficient

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

def get_coefficient(unit_in, unit_out):
   ...
Enter fullscreen mode Exit fullscreen mode

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:

units = {
   "km": { "miles": 1/1.609, "meters": 1/1000, ....},
   "yards": { "miles": 1/1760, "meters": ..., "km": ...}
   ...
}
Enter fullscreen mode Exit fullscreen mode

“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:

# Coefficients convert from "meters"
distances = {
    "km": 1/1000,
    "yards": 1.094,
    "miles": 1/1609,
}
Enter fullscreen mode Exit fullscreen mode

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:

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
Enter fullscreen mode Exit fullscreen mode

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

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
Enter fullscreen mode Exit fullscreen mode

“Look how readable the code is”, I said. “We have a value that’s calledreciprocal_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 was unnecessary.

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]
Enter fullscreen mode Exit fullscreen mode

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:

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
Enter fullscreen mode Exit fullscreen mode

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 for more ways to get in touch with me.


  1. I’m using mob programming during my Python classes. It works really well. 

Latest comments (1)

Collapse
 
dmerejkowsky profile image
Dimitri Merejkowsky

I think you will need help in promoting your content.

I'm curious. What makes you think that?