check
How to use MyPy.
Activating MyPy
To opt-in, add pants.backend.python.typecheck.mypy
to backend_packages
in your config file.
[GLOBAL]
backend_packages.add = [
"pants.backend.python",
"pants.backend.python.typecheck.mypy",
]
This will register a new check
goal:
$ pants check helloworld/util/lang.py
$ pants check ::
MyPy determines which Python version to use based on its python_version
option. If that's undefined, MyPy uses the interpreter the tool is run with. Because you can only use one config file at a time with MyPy, you cannot normally say to use 2.7
for part of your codebase but 3.6
for the rest; you must choose a single version.
Instead, Pants will group your targets based on their interpreter constraints, and run all the Python 2 targets together and all the Python 3 targets together. It will automatically set python_version
to the minimum compatible interpreter, such as a constraint like ["==2.7.*", ">3.6"]
using 2.7
.
To turn this off, you can still set python_version
in mypy.ini
or --python-version
/--py2
in --mypy-args
; Pants will respect the value you set.
Hook up a MyPy config file
Pants will automatically include your config file if it's located at mypy.ini
, .mypy.ini
, setup.cfg
, or pyproject.toml
.
Otherwise, you must set the option [mypy].config
for Pants to include the config file in the process's sandbox and to instruct MyPy to load it.
[mypy]
config = "build-support/mypy.ini"
Change the MyPy version
Use the install_from_resolve
option in the [mypy]
scope:
[python.resolves]
mypy = "3rdparty/python/mypy.lock"
[mypy]
install_from_resolve = "mypy"
See Lockfiles for tools.
Incrementally adopt MyPy with skip_mypy=True
You can tell Pants to skip running MyPy on certain files by adding skip_mypy=True
to the relevant targets.
# Skip MyPy for all the Python files in this directory
# (both test and non-test files).
python_sources(name="lib", skip_mypy=True)
python_tests(name="tests", skip_mypy=True)
# To only skip certain files, use the `overrides` field.
python_sources(
name="lib",
overrides={
"util.py": {"skip_mypy": True},
# Use a tuple to specify multiple files.
("user.py", "admin.py"): {"skip_mypy": True},
},
)
When you run pants check ::
, Pants will skip any files belonging to skipped targets.
The skip_mypy
field only tells Pants not to provide the skipped files as direct input to MyPy. But MyPy, by default, will still try to check files that are dependencies of the direct inputs. So if your skipped files are dependencies of unskipped files, they may still be checked.
To change this behavior, use MyPy's --follow-imports
option, typically by setting it to silent
. You can do so either by adding it to the args
option in the [mypy]
section of your Pants config file, or by setting it in mypy.ini
.
First-party type stubs (.pyi
files)
You can use .pyi
files for both first-party and third-party code. Include the .pyi
files in the sources
field for python_source
/ python_sources
and python_test
/ python_tests
targets. MyPy will use these stubs rather than looking at the implementation.
Pants's dependency inference knows to infer a dependency both on the implementation and the type stub. You can verify this by running pants dependencies path/to/file.py
.
When writing stubs for third-party libraries, you may need to set up the [source].root_patterns
option so that source roots are properly stripped. For example:
- pants.toml
- mypy-stubs/colors.pyi
- mypy-stubs/BUILD
- src/python/project/app.py
- src/python/project/BUILD
[source]
root_patterns = ["mypy-stubs", "src/python"]
# Because we set `mypy-stubs` as a source root, this file will be
# stripped to be simply `colors.pyi`. MyPy will look at this file for
# imports of the `colors` module.
def red(s: str) -> str: ...
python_sources(name="lib")
from colors import red
if __name__ == "__main__":
print(red("I'm red!"))
# Pants will infer a dependency both on the `ansicolors` requirement
# and our type stub.
python_sources(name="lib")
Third-party type stubs
You can install third-party type stubs (for example, types-requests
) like normal Python requirements. Pants will infer a dependency on both the type stub and the actual dependency, for example, both types-requests
and requests
, which you can confirm by running pants dependencies path/to/f.py
.
If you install MyPy from a custom lockfile you can also add type stub requirements to that lockfile. This ensures that the stubs are only used when running MyPy and are not included when, for example, packaging a PEX.
Add a third-party plugin
Add any third-party MyPy plugins to a custom lockfile:
- pants.toml
- 3rdparty/python/mypy-requirements.txt
- 3rdparty/python/BUILD
[python.resolves]
mypy = "3rdparty/python/mypy-lock.txt"
[mypy]
install_from_resolve = "mypy"
mypy==1.3.0
pydantic==1.6.1
python_requirements(
name="mypy",
source="mypy-requirements.txt",
resolve="mypy",
)
Then update your mypy.ini
to load the plugin:
[mypy]
plugins =
pydantic.mypy
For some plugins, such as django-stubs
, you may need to always load certain source files, such as a settings.py
file. You can make sure that this source file is always used by hijacking the source_plugins
option, which allows you to specify targets whose sources
should always be used when running MyPy. See the section below for more information about source plugins.
For example, to fully use the django-stubs
plugin, your setup might look like this:
- pants.toml
- 3rdparty/python/mypy-requirements.txt
- 3rdparty/python/BUILD
- mypy.ini
- src/python/project/django_settings.py
- src/python/project/BUILD
[source]
root_patterns = ["src/python"]
[mypy]
install_from_resolve = "mypy"
source_plugins = ["src/python/project:django_settings"]
mypy==1.3.0
django-stubs==1.5.0
python_requirements(
name="mypy",
source="mypy-requirements.txt",
resolve="mypy",
)
[mypy]
plugins =
mypy_django_plugin.main
[mypy.plugins.django-stubs]
django_settings_module = project.django_settings
from django.urls import URLPattern
DEBUG = True
DEFAULT_FROM_EMAIL = "[email protected]"
SECRET_KEY = "not so secret"
MY_SETTING = URLPattern(pattern="foo", callback=lambda: None)
python_source(name="django_settings", source="django_settings.py")
Type stubs specified in the MyPy custom lockfile are not visible to the python-infer
subsystem, and cannot be referenced as explicit dependencies
. If you import
from a stubs module in your code, and it does not have a corresponding implementation python_requirement
target that provides the imported module, you may see a warning/error depending on the value you've configured for [python-infer].unowned_dependency_behavior
. Goals other than check
will also raise ImportError
s if the import
isn't conditional on the value of typing.TYPE_CHECKING
:
- pants.toml
- 3rdparty/python/mypy-requirements.txt
- 3rdparty/python/BUILD
- src/example.py
[python-infer]
unowned_dependency_behavior = "warning"
[mypy]
install_from_resolve = "mypy"
mypy==1.3.0
mypy_boto3_ec2==1.26.136
python_requirements(
name="mypy",
source="mypy-requirements.txt",
resolve="mypy",
)
from typing import TYPE_CHECKING
# Unsafe! Will fail outside of `check`
from mypy_boto3_ec2 import EC2Client
if TYPE_CHECKING:
# Safe, but will be flagged as a warning
from mypy_boto3_ec2 import EC2ServiceResource
For these reasons, it's recommended to load any type-stub libraries that require explicit imports as part of your normal third-party dependencies. Alternatively, you can set # pants: no-infer-dep
on the lines of type-stub imports "guarded" by a check of if TYPE_CHECKING
.
Add mypy_plugin = true
to the [python-protobuf]
scope. See Protobuf for more information.
Add a first-party plugin
To add a MyPy plugin you wrote, add a python_source
or python_sources
target with the plugin's Python file(s) included in the sources
field.
Then, add plugins = path.to.module
to your MyPy config file, using the name of the module without source roots. For example, if your Python file is called pants-plugins/mypy_plugins/custom_plugin.py
, and you set pants-plugins
as a source root, then set plugins = mypy_plugins.custom_plugin
. Set the config
option in the [mypy]
scope in your pants.toml
to point to your MyPy config file.
Finally, set the option source_plugins
in the [mypy]
scope to include this target's address, e.g. source_plugins = ["pants-plugins/mypy_plugins:plugin"]
. This will ensure that your plugin's sources are always included in the subprocess.
For example:
- pants.toml
- mypy.ini
- pants-plugins/mypy_plugins/BUILD
- pants-plugins/mypy_plugins/change_return_type.py
[mypy]
source_plugins = ["pants-plugins/mypy_plugins:plugin"]
plugins =
mypy_plugins.change_return_type
python_source(name="plugin", source="change_return_type.py")
"""A contrived plugin that changes the return type of any
function ending in `__overriden_by_plugin` to return None."""
from typing import Callable, Optional, Type
from mypy.plugin import FunctionContext, Plugin
from mypy.types import NoneType, Type as MyPyType
from plugins.subdir.dep import is_overridable_function
class ChangeReturnTypePlugin(Plugin):
def get_function_hook(
self, fullname: str
) -> Optional[Callable[[FunctionContext], MyPyType]]:
return hook if name.endswith("__overridden_by_plugin") else None
def hook(ctx: FunctionContext) -> MyPyType:
return NoneType()
def plugin(_version: str) -> Type[Plugin]:
return ChangeReturnTypePlugin
Because this is a python_source
or python_sources
target, Pants will treat this code like your other Python files, such as running linters on it or allowing you to write a python_distribution
target to distribute the plugin externally.
Reports
MyPy can generate various report files.
For Pants to properly preserve the reports, instruct MyPy to write to the reports/
folder by updating its config file or --mypy-args
. For example, in your pants.toml:
[mypy]
args = ["--linecount-report=reports"]
Pants will copy all reports into the folder dist/check/mypy
.
Known limitations
Performance is sometimes slower than normal
Pants 2.14 added support for leveraging MyPy's cache, making subsequent runs of MyPy extremely performant.
The support, however, requires features that were added to MyPy in version 0.700
, and requires that
python_version
isn't set in MyPy's config or in [mypy].args
.
If you're using a version of MyPy older than 0.700
, consider upgrading to unlock super-speedy subsequent runs of MyPy.
Additionally consider not providing python_version
in your config or args.
Tip: only run over changed files and their dependents
When changing type hints code, you not only need to run over the changed files, but also any code that depends on the changed files:
$ pants --changed-since=HEAD --changed-dependents=transitive check
See Advanced target selection for more information.