Pyrefly vs. ty: Comparing Python’s Two New Rust-Based Type Checkers

39 points by hoistbypetard


dcreager

[ty developer here]

We are happy with the attention that ty is starting to receive, but it’s important to call out that both ty and pyrefly are still incomplete! (OP mentions this, but it’s worth emphasizing again here.)

There are definitely examples cropping up that hit features that are not yet implemented. So when you encounter something where you think what we’re doing is daft, please recognize that we might have just not gotten around to that yet. Python is a big language!

ubernostrum

pyrefly, mypy, and pyright all assume that my_list.append("foo") is a typing error, even though it is technically allowed (Python collections can have multiple types of objects!) If this is the intended behavior, ty is the only checker that implicitly allows this without requiring additional explicit typing on my_list.

This is where we have to step back and talk about goals. What the Python type-checking community has been doing is, effectively, defining a parallel language – call it “Typethon” – which just happens to be syntactically compatible with Python (or, mostly is, and when it isn’t, Python changes to accommodate), despite having different semantics. So the question for any given Python type checker is: are you a “type checker for Python”, or are you a type checker for Typethon?

The quoted passage above highlights one of the differences: a Python list is a heterogeneous container. “List of what?” is not a useful question to ask of a Python list, because any given list might contain items all of the same type, or might contain items of many different types, etc. But in Typethon, lists are homogeneous and there effectively is no such thing as standalone list, only list[T]. Something similar is true of mapping types: in Python the type dict is a mapping of nearly arbitrary keys and values (I say “nearly” only because hashability requirements do exist), while in Typethon there is, again, no dict, only dict[K, V] (if you must have a dict whose values are not all of one type V, you have to use the TypedDict construct to specify the exact set of literal keys and types of values you expect).

This also leads to “fun” times for people porting existing code to use type annotations, because a lot of common Python things don’t work in Typethon. For example, if you have a list[str | None] (say, obtained from reading a bunch of possibly-missing configuration values), you cannot use type narrowing operations to get to a list[str] no matter what you do, because Typethon defines list as an invariant type. You have to first go back and explicitly change the annotation to something like Sequence[str | None] which – because Sequence is covariant – does let you narrow (but then gets into yet more difficulty when you realize that in Typethon Sequence is immutable).

Some of the above will be familiar to people who’ve read my previous comments on experiences with Typethon.