Here is simply how I went about integrating mypy into a few projects. This post assumes you are using git and have a test system in place.
First Step: Scaffolding
This step consists first of installing mypy in the project and configuring it. Then run it, counting the number of lines and saving them to a file. For example:
mypy src | wc -l > mypy_err_count.txt
Technically this counts more than the number of errors, but for what we want to do with it it, it is good enough.
Then add a step to your tests to make sure the new error count is not higher than it was. For example, in bash:
[ $(mypy src | wc -l) -lte $(cat mypy_err_count.txt) ]
Now the error count can only go in one direction: towards zero.
Second Step: Systematic Iterations
This is where we start adding types everywhere. The idea is to use the previous step to do it continuously. That is, every now and then put some time into it and open a merge request to the main branch. Each time, we must take care to update the
mypy_err_count.txt file and open a pull request. The other contributors will have no choice but to maintain the status quo or improve the situation, otherwise the error count would increase and the tests would fail. Obviously, you have to reject merge requests to the trunk that increase the error count. Everyone has to contribute.
During this stage, there are three important tools:
reveal_type: Function that prints the type that mypy infers. This is handy when types become complex and you are not sure of what’s going on. It can also help you determine if a type is incorrect because of an error on your part or an error by mypy.
# type: ignore: Marker to put at the end of a line to ignore errors detected on it. Obviously, you should aim to use it as little as possible. However, mypy is not yet perfect and sometimes makes mistakes. When this is the case, it is a good time to use
# type: ignore. The beauty: With mypy updates, if a
# type: ignorebecomes useless because the new version now infers the correct type, you will get a new error that tells you that the
# type: ignoreis useless and can be removed.
cast: Sometimes it is logically impossible for mypy to detect the correct type. A classic example:
json.load. What is the type that comes out of it? Dict? List? What kind of list? If you know, it’s time to use
Third Step: Remove the Scaffolding
You have reached zero errors? Congratulations! This is now the easiest step. We remove the
mypy_err_count.txt file and replace the test based on it with a direct execution of mypy.
To Type or not to Type the Tests?
Good question. The idea behind mypy is to type the code to facilitate the discovery of bugs. So typing the tests will mainly help you to find bugs… in your tests. But since they are tests, then they will crash while you are testing. A bit circular, isn’t it?
Although typing tests can be useful, it’s better to focus on the application itself first. It’s fine, mypy can validate directories or not, so if your tests are in a directory (e.g. test), just exclude it from mypy validation.
Then it’s up to you. I think it’s worth trying on a few tests. Then, decide based on that test if it’s worth pursuing or not.