Mypy in Python
Preface
Python does not have the ability to explicitly specify variable types. All variable types are inferred based on assignment. This may lead to runtime error such as "str" + 1
(this only produces unintended/unexpected result in JavaScript though), it also prevents editor to offer valuable suggestions.
Mypy is a static type checker. In one of my company’s project, I noticed mypy.ini
sitting in the project directory, but not being utilised. After some research on mypy
, I decided to add its support together with pre-commit. This blog post discusses mypy
usage briefly, but focus more on the configurations of files to be checked or excluded in the context of mypy
and pre-commit
hook configuration.
Usage
Setup Demo
Demo project structure.
1 | . |
Dependency required for the demo.
1 | # partial pyproject.toml is shown |
Files
1 | [mypy] |
1 | def dummy_func(v: str) -> str: |
1 | from .coingecko_api import CoinGeckoAPI |
1 | import requests |
1 | import requests |
Run type checking
1 | At project root |
After running above command, mypy .
should complain these 3 types of errors:
- Library stubs not installed for “requests”
- Incompatible types in assignment (expression has type “int”, variable has type “str”)
- Argument 1 of “eq“ is incompatible with supertype “object”; supertype defines the argument type as “object”
Missing library stubs
For this error, mypy already shows Hint: "python3 -m pip install types-requests"
to install the missing type dependency, in this case types-requests
.
Hints offered are quite accurate in my experience, except the case for grpc
for my company’s project, which requires grpc-stubs
.
Incompatible type
For this error, mymy
is complaining for this line in ./module/coingecko_api.py
:
1 | self.dummy_str: str = dummy_int |
mypy
thinks I try to assign int
type to str
, despite I used type conversion inside setter annotated by @dummy_str.setter
. This is unfortunately not solved at the time of writting, you may watch this issue.
To make mypy
happy, we have no choice but to ignore it by:
1 | - self.dummy_str: str = dummy_int |
“__eq__()” input type
For this error, mypy
again provides a very useful hint message:
1 | def __eq__(self, other: object) -> bool: |
Configuration
After making relevant changes above, running mypy .
should succeed. Now I will discuss about the configuration files.
In order to discuss configurations for files to be checked or ignored, we need to add pre-commit
configuration to .pre-commit-config.yaml
:
1 | repos: |
Missing stubs again?
After that, running pre-commit run --all-files
should report one error about Missing library stubs
discussed earlier, despite mypy .
reports no error! (Note: You need to add files to version control for pre-commit
to work, a simple git add .
will work, there is no need to commit the changes though.)
This is because type dependencies need to be added explicitly for mypy
hook in .pre-commit-config.yaml
:
1 | repos: |
After making the changes above, pre-commit run --all-files
should report no errors.
Before moving on
For the following sections, please make some changes to break mypy
type checking, for me, I did the following evil code:
1 | if __name__ == "__main__": |
and
1 | - def __eq__(self, __o: object) -> bool: |
Running mypy .
and pre-commit run --all-files
should both report the same 2 errors now.
Files to check
For mypy
to check only module directory:
1 | [mypy] |
Running mypy
now (instead of mypy .
earlier) should only report one error from module
directory. However, running pre-commit run --all-files
still reports two errors. That is because mypy
hook in pre-commit
does not read files
configuration from mypy.ini
, we need to make the following changes in .pre-commit-config.yaml
:
1 | repos: |
Note the different syntax for files
configuration in mypy.ini
and mypy
hook in .pre-commit-config.yaml
. Refer to mypy configuration file for more information about files
configuration in mypy.ini
; as for .pre-commit-config.yaml
, it is just regex, which is much simpler.
(Remember to revert the changes for files
configuration made in this section before moving on to the next section.)
Files to exclude
For mypy
to exclude module
directory:
1 | [mypy] |
To add more directories or files to exclude, the exclude configuration looks like:
1 | [mypy] |
Since everything is excluded, now running mypy .
should check no files!
As usual, running pre-commit run --all-files
now checks all 4 Python files and report 2 errors, because mypy
hook in pre-commit run --all-files
does not read exclude
configuration in mypy.ini
. Add exclude
configuration to .pre-commit-config.yaml
:
1 | repos: |
Running pre-commit run --all-files
should only report one error from main.py
now. Again, the exclude
configuration for mypy
hook in pre-commit
is using regex ^_^
Other configurations?
From my trial and error, mypy
hook respects type check configurations like disallow_any_unimported
and per-module options under syntax like[mypy-mymodule]
.
Conclusion
As one of the comment from this issue:
i’ve got the same problem. it’s annoying to have to have two sources of truth for which files to include (mypy.ini and pre-commit config)
I also found it is annoying to have configurations for the same plugin at different places, but sadly this is the case for mypy
.
For me, I would put files
, exclude
and additional_dependencies
configurations in .pre-commit-config.yaml
, leave the rest in mypy.ini
and always run pre-commit run --all-files
to check instead of mypy
command. Add pre-commit
to git hook scripts locally so that it get executed automatically on git commit
and put it as part of the CI process if possible.
Referrence
Why does defining the argument types for __eq__ throw a MyPy type error?
What to do about setters of a different type than their property?
“files” configuration in mypy.ini is ignored for mirrors-mypy