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