Mypy vs pyright in practice

I currently use mypy for checking the correctness of my type hints. There’s some advantage you have seen in practice using pyright instead, or both?

It’s best practice in other languages, to make sure your code compiles with multiple compilers. Multiple typecheckers is the Python equivalent.

It can rapidly get messier, if lots of both # mypy: ignore and # pyright: ignores are added. And the two tools can have differences of opinion.

1 Like

Depending on how simple the types are in your code you may not notice much difference. I see many differences of opinion between the two when adding annotations in sympy which is a large, mostly unannotated codebase that makes extensive use of Python’s dynamic typing capabilities. Usually I prefer pyright’s opinion about the types over mypy’s. Once you get to the point where everything is annotated and neither checker reports an error they usually tend to agree about the types but mypy’s inference seems to be weaker and its reveal_type often comes up with Any in situations where pyright can infer the correct types.

Some differences are irreconcilable though e.g. things like this mypy bug mean that I think it would be impossible for sympy to use mypy as an internal type checker if everything was fully annotated. As far as I can tell pyright is the only Python type checker that actually understands __new__.

Both checkers can be configured to some extent so often the differences between them are just differences in the default configuration. For example mypy doesn’t check the bodies of functions that don’t have annotations in the signature whereas pyright does but both can be configured to do the opposite.

In my experience mypy is more likely to blur the line between a type checker and a linter. Some people probably like that but I prefer these things to be separate. Here is an example:

def func(x: int) -> int | None:
    if x > 0:
        return x

So pyright accepts this but mypy does not:

$ pyright t.py
0 errors, 0 warnings, 0 informations 
$ mypy t.py
t.py:1: error: Missing return statement  [return]
Found 1 error in 1 file (checked 1 source file)

The function is correctly typed: it returns None implicitly. You might dislike that style and prefer an explicit return None. I consider that to be the job of an opinionated linter rather than a type checker though. Probably there is a way to configure this behaviour in each checker.

A common point of disagreement between the two that I find is:

data = ['123', '456']
data = [int(s) for s in data]

If you don’t annotate data then pyright is happy for you to change the type when rebinding and will simply infer the new type from the code:

$ pyright t.py 
0 errors, 0 warnings, 0 informations 
$ mypy t.py
t.py:2: error: List comprehension has incompatible type List[int]; expected List[str]  [misc]
Found 1 error in 1 file (checked 1 source file)

The only way to get mypy to accept this is if you use a different variable name:

data_int = [int(s) for s in data]

This sort of thing comes up a lot when adding annotations to a pre-typing codebase. In my experience satisfying pyright is more likely to be a case of just adding annotations in lots of places whereas satisfying mypy often requires making pointless changes like renaming data to data_int.

In practice I think a big difference is that I use pyright as an interactive editor plugin whereas mypy is a command line tool. That is not a strict difference and pyright can be used from the command line and presumably mypy can be used in an editor but I think that is not how each is more commonly used. As I see it from sympy’s perspective there are two types of downstream consumer of the annotations:

  • Downstream libraries that all seem to use mypy to check their annotations.
  • End users who mostly don’t use annotations themselves but may have pyright/pylance running in their editors.

I have explained to some downstream library maintainers that although we are adding type hints in sympy it will not be possible for mypy to fully understand them unless things like the bug mentioned above are fixed. My recommendation therefore is for them to use pyright rather than mypy. Some of them have said that pyright is too slow though.

Certainly if you run pyright over the whole sympy codebase it is much slower than mypy. Partly that is I think because it is checking more unannotated code and partly it is to do with caching but ultimately I think pyright is just slower if run from the command line. It is much faster though when running in an editor which is its primary mode of use. In any case some people have told me that they don’t want to use pyright on other codebases because it is slower than mypy.

Probably the end result for sympy will be that pyright is used as a checker in CI but downstream libraries will still use mypy. That means we need mypy to be able to understand the public API but we won’t use it for checking internal code. We will want pyright’s inference to work as well as possible because that is what makes it work for end users writing code with pyright running in their editors. It would be nice if mypy’s inference could work better but it is more important just that mypy can consume the basic public API and then allow downstream libraries to check their own codebases.

2 Likes

Personally no. I find an explicit “return None” unnecessarily verbose.

Well, in compiled languages you are forced to do this. It’s annoying, but I feel it better.

This is really interesting. Maybe pyright could add an option to disable caching. Don’t know if they have a motivation to do so.

You’re probably right, but my real life time is short :slight_smile:

Not in all compiled languages. For example Rust allows changing the type with let:

let x = 1;
let x = "asd";
1 Like

It’s a double-edged sword. There may be incorrect code that is missed by one but caught by the other, but there is also code that may only be accepted by one and not the other.

Mypy has a plugin system; pyright doesn’t. Correspondingly, some advanced attrs features can only be supported on Mypy. Maybe other libraries are in a similar situation?