A short example of how types can make your life easier
Darren Burns
Posted on June 29, 2019
What follows is an example covering of some of the issues I encounter frequently when writing code in dynamic programming languages, and how rewriting the same code using type annotations (and more specifically, enums) makes my life easier. The code snippets below are written in Python, but apply to any language that supports enums, such as TypeScript, Java, etc.
Imagine you work for an online book store, and you've been tasked with writing some code to predict and store a user's book format preferences. In the codebase you're working on, you find a method that sounds useful:
def save_preferred_book_format(format):
...
You want to make use of this method somewhere in your code, but at a glance, you can't tell how to correctly call it. It clearly takes a single argument, format
, but there's no indication as to what format
is. Is it a string, a number, or maybe an object of some kind?
You dig into the method body and see code like this buried in the middle of it, which checks that the argument is valid.
if format not in VALID_BOOK_FORMATS:
raise ValueError(f"format must be one of {VALID_BOOK_FORMATS}")
You jump through to the VALID_BOOK_FORMATS
variable, and see that it is defined as follows:
VALID_BOOK_FORMATS = ["AUDIOBOOK", "EBOOK", "PAPERBACK", "HARDBACK"]
Given that this is a list of strings, and our method checks that the format
argument is contained within this list, we can deduce that when we call save_preferred_book_format
, we must pass it a string, and if it's not one of the strings defined in this list, our program will crash!
If we hadn't read the full implementation of this method, we wouldn't have known this. The signature of the method does not contain enough information in order to safely use it. In this case, the original developer was kind enough to include a reference to the list of VALID_BOOK_FORMATS
, so we were able to use our detective skills to work out how to call the method.
This is a common ritual in code bases written in dynamically typed languages such as JavaScript and Python, and it quickly becomes tiresome if you have to perform it every time you want to call a function. If the function has multiple parameters, the complexity can vastly increase the amount of time required to understand it.
This is problematic. If the one of the key ideas behind clean code is reusability, why should you have to jump through so many hoops just to understand how to reuse a function? Well designed code lets you spend more calling functions and less time trying to work out how to use them.
You shouldn't have to read the body of a function to learn how to call it.
How could the method above have been written to avoid this problem?
We know that a copy of a book be an EBOOK
or a PAPERBACK
, but it can't be both. Many programming languages come with a handy way of modelling this: enumeration types (enums)!
Here's how we could rewrite VALID_BOOK_FORMATS
using an enum instead of a list of strings:
from enum import Enum
class BookFormat(Enum):
AUDIOBOOK = 1
EBOOK = 2
PAPERBACK = 3
HARDBACK = 4
How you use an enum is similar to how you interact with radio buttons. There's a defined set of options, but you can only select one of them at a time.
Now we can update the original method signature to annotate the previously confusing parameter:
def save_preferred_book_format(format: BookFormat):
...
Just by adding this type annotation, we've told the reader of our code that the format
parameter has type BookFormat
, and so the method expects any of the 4 possible variations: AUDIOBOOK
, EBOOK
, PAPERBACK
, or HARDBACK
. We've decreased the size of the set of possible input values from infinity to four, and you no longer need to look beyond the signature of the method to understand how to use it.
save_preferred_book_format(BookFormat.EBOOK)
Not only that, but if we pass an invalid value to the function, our static analyser, editor, or compiler (e.g. tsc
, PyCharm, mypy
) will tell us about it before we even run our code. If we were to make a typo when calling the method, we'd be told about that before running our code too!
Enums are just one benefit of using a type system. They make code easier to read, save you from having to jump around your codebase to discover how to call functions, and your code becomes far less error prone.
This post is not even close to being an exhaustive exploration of the benefits of refactoring your code to use types, but hopefully it has given some readers food for thought.
A note on Python 3.8
Python 3.8 offers "literal types", which let us specify that our code must be called with a specific literal. We could potentially rewrite our method to inform readers of our code that it will only accept specific strings using literal types as follows:
_FormatType = Literal["AUDIOBOOK", "EBOOK", "PAPERBACK", "HARDBACK"]
...
def save_preferred_book_format(format: _FormatType):
...
Now, if we call our code with any literal string that isn't contained within the list, we'll be informed of it before we run it!
Posted on June 29, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.