Your editor shows no errors, but CI fails the same file with a mypy error. Or CI passes and your editor underlines a line in red. Two type checkers are disagreeing. A type checker reads your annotations without running the code and reports type errors. The two common ones are mypy and Pyright. In VS Code, the Pylance extension, which the Python extension installs by default, runs Pyright, so your editor and your CI may well use different tools.
Both catch a common async bug, a forgotten await:
import asyncio
async def save(name: str) -> None:
await asyncio.sleep(0.1) # stands in for a database write
async def handler(name: str) -> None:
save(name) # forgot await, so save never runs
asyncio.create_task(save(name)) # task created and then dropped
mypy reports the first line by default:
error: Value of type "Coroutine[Any, Any, None]" must be used [unused-coroutine]
note: Are you missing an await?
Pyright reports it too, through its reportUnusedCoroutine rule. The second
line is the dropped task from the
task-lifetime article. mypy reports it once
you turn on the optional unused-awaitable error code. In Pyright,
reportUnusedCallResult catches it, along with every other call whose result
you ignore.
Give both tools the same inputs
Most disagreements come from settings, not from the tools. Check four things first:
- the Python version each tool targets,
- the environment each tool searches for installed packages and stubs,
- the files each tool checks,
- how strict each tool is.
Run both tools in the project's virtual environment, so they see the same
packages. Set the Python version and the strictness for both in
pyproject.toml:
[tool.mypy]
python_version = "3.12"
strict = true
enable_error_code = ["unused-awaitable"]
[tool.pyright]
pythonVersion = "3.12"
typeCheckingMode = "strict"
Each tool's strict mode is its own list of checks, and the lists change
between releases. Pin both tool versions in CI, and upgrade them on purpose.
If the project also has a pyrightconfig.json file, Pyright uses that file
instead of the [tool.pyright] section.
Narrow down a real difference
If the settings match and the tools still disagree, cut the code down to the
smallest file that shows the difference. Then ask each tool what it infers,
with reveal_type:
import asyncio
from typing import reveal_type
async def main() -> None:
task = asyncio.create_task(asyncio.sleep(1, result="done"))
reveal_type(task) # Task[str]
reveal_type(await task) # str
Each checker prints the type it inferred at each reveal_type call. The first
line where the two outputs differ is where the disagreement starts. Then put
assert_type(value, Task[str]) in a test file to check the type you expect,
so a later upgrade that changes it fails the check.
Check your understanding mypy passes and Pyright reports an error on the same small file, with matching settings. What do you do next? Show answer Hide answer
Find the cause before you change the code. Use reveal_type to find the
first expression where the two inferred types differ. Then check each tool's
documentation and issue tracker. The cause may be a setting you missed, a
stub that differs, or a bug in one tool.
If the API is ambiguous, an explicit annotation usually makes both tools agree. If one tool is wrong, report the small file to that project as a bug.
Silence an error for one tool
Both tools respect # type: ignore. Add the mypy error code to keep it
narrow, as in # type: ignore[unused-coroutine]. Pyright has its own form,
# pyright: ignore[reportUnusedCoroutine], which mypy ignores.
When you run both tools with their unused-ignore checks on, each tool can flag
a comment that only the other tool needed. Pyright's
reportUnnecessaryTypeIgnoreComment flags every # type: ignore that only
mypy needed. To fix it, set enableTypeIgnoreComments = false in the
Pyright section, so each tool reads only its own comments.
Pick one tool to block merges
Making one checker a required CI check is a good default. The Python gate on
this site runs mypy --strict. Requiring both makes sense when you publish a
typed library, because each of your users may run either one. The cost is two configurations
to maintain and more disagreements to look into. Use the other tool in your
editor for early feedback, and write down which checks block a merge.
Read more in the mypy command-line options, the Pyright configuration and the typing directives.