Skip to content
← All Python articles

Python explainer

Fix Disagreements Between mypy and Pyright

About 4 min to read. Allow extra time to work through examples.

Your editor and CI can disagree about the same async code. Give both checkers the same settings, catch a forgotten await in each, and narrow a real difference down with reveal_type.

This article uses Python. You can make it your practice mode to browse related content across the site.

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

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.

step 51 of 56

Concurrency with Threads and asyncio

Up next Choose the Right Concurrency Tool for Each Job