Type hints rule!

Introduction, with an obligatory nod towards Ada

Python®hellip; Python®… I had fun using ๐Ÿคฉ Ada ๐Ÿคฉ at work a while, especially when I started writing some Serious Software™. However, someone else might have to work on the same software one day, so they “asked” me to rewrite what I’d started in Python and proceed from there.The irony of this situation goes many levels deep, but with a Herculean effort we’ll stay on topic. Instead, I’ll admit how it’s actually convenient: one library I need to use is not readily available in Ada, whereas Python is one of its two default API’s. pip install unusually_useful_library and we’re off to the races, nice! Technically, then, I can’t complain, but I can still grumble, at least a little.

๐Ÿ˜ญ
Oh come on, it’s not that bad.

I’ve been using Python on and off for something like 15 years, mostly because other people compel me to, but the language has its appeal. Take, for instance, its unusual approach to scope: neither unsightly curly braces, nor beautiful begin/end pairs, but indentation itself:
for each in range(10):
   print(each)
I thought it was crazy when I first heard of it, but I kind of like it now.

Kind of.

The one thing I didn’t like

One thing I did dislike about Python was the lack of static type-checking.If you don’t understand why assert(isinstance(x, MyClass)) doesn’t cut it, keep reading. That’s burned me far too many times, and I know I’m not alone. I tried Cython, which promises speed improvements in exchange for type annotations on variables, but it didn’t help at all, because the real problem was with the data structures.

Neverthemore, static type-checking can be useful. Even Python’s Powers-That-Be recognize that, allowing the programmer first to annotate code with types. Alas, that’s all they did. Python by itself doesn’t actually do anything with the types, so the annotation is mostly useless unless you think the reader will pay attention. Sadly, most Python programmers I’ve met aren’t aware type hints exist.To be fair, it is an extremely small sample size.

Enter the mypy

Fortunately, tools exist that let you do more than merely annotate: Comes now mypy to save the day! Write yourself a program, or even a whole set of modules, pass it to mypy, and voilà! Lines and lines of errors alerting you to (some of) the mistakes that will crash your program at runtime. Better yet, the error messages are brief and comprehensible!If only the C++ compiler devs would realize that error messages have more purpose in life than intimidating the weak. Then again, a legible C++ program is a contradiction in terms, so it’s hardly a surprise that the compiler developers took their cues from the languaeg designers.

An example of questionable merit

Here’s a relatively simple example that shows (a) what a terrible programmer I am, and (b) how mypy makes me less terrible. Consider the following file, my_class.py:
class MyClass:
   def __init__(self, value: float) -> None:

      self._field: int = value
   
   def get_value(self) -> float:
      return self._field
Let’s ๐Ÿง mypy ๐Ÿง that bad boy:
$ mypy my_class.py
my_class.py:4: error: Incompatible types in assignment (expression has type "float", variable has type "int")  [assignment]
Look at that! I didn’t have to run the program to find and fix a mistake; mypy found it for me!

Sure, that’s a stupid mistake, and sure, someone who makes it in a program as simple as this should perhaps reconsider his career, but it happens a lot, especially when a class has many fields, and as a program evolves. You start off thinking you need a float, but it turns out you want to use the field in a library that only accepts ints, or vice versa. Go ahead and try to index that array with a float and see what happens.

A fix

Let’s fix that. Change the field so that it is an integer:
   def __init__(self, value: int) -> None:
Run ๐Ÿง mypy ๐Ÿง again:
$ mypy my_class.py
Success: no issues found in 1 source file
What?!?

That disappoints me a little: the function get_value promises a float but returns an int. My Ada- and Rust-accustomed brain sees that as an error, but in Python it’s the correct answer, because Python automatically promotes ints to floats.

An example of unquestionable merit

Let’s illustrate one more error that’s bitten me more times than I care to admit. Consider the following file, use_my_class.py.
from my_class import MyClass

my_instance: MyClass = MyClass()
print(my_instance.get_value + 1)

First error

Throw mypy at it, and an error occurs right off:
$ mypy use_my_class.py 
use_my_class.py:3: error: Missing positional argument "value" in call to "MyClass"  [call-arg]
Found 1 error in 1 file (checked 1 source file)
Again, an obvious error, easily fixed: just put any integer at all between those parentheses. I’m somewhat partial to 4 for some reason, so I’ll change it MyClass(4).

Second error

Everything’s good now, right? Nope:
$ mypy use_my_class.py 
use_my_class.py:4: error: Unsupported operand types for + ("Callable[[], float]" and "int")  [operator]
Found 1 error in 1 file (checked 1 source file)
Whoops!

Pat yourself on the back if you spotted that error before I ran mypy on it, as it’s hard to spot but easy to fix. If you’d tried to run it, it would have raised an exception:
$ python use_my_class.py 
Traceback (most recent call last):
  File "/home/cantanima/website/living_journal/use_my_class.py", line 4, in 
    print(my_instance.get_value + 1)
          ~~~~~~~~~~~~~~~~~~~~~~^~~
TypeError: unsupported operand type(s) for +: 'method' and 'int'
The corrected program should be:
from my_class import MyClass

my_instance: MyClass = MyClass(4)
print(my_instance.get_value() + 1)
mypy signs off on it this time, and it runs great in Python!
$ python use_my_class.py 
5
Again, it might seem silly in this example, but when you have a large program you may well not see it. Worse, you can be very far deep into actual work before the bug will manifest itself, crash your program by raising an exception, and waste all the work you did since you last saved.

(You did save, didn’t you? ๐Ÿคจ )