Команда Python for Devs подготовила перевод статьи о двух новых Rust-базированных анализаторах типов для Python — pyrefly и ty. Оба пока в ранней альфе, но уже демонстрируют впечатляющую скорость, разные подходы к выводу типов и новые возможности.
Примечание: я люблю ставить длинные тире в тексте! Не переживайте — это написал не ИИ. (контекст)
В начале этого месяца в центре внимания оказались два новых Python-анализатора типов, написанных на Rust: pyrefly и ty. Хотя ни один из них ещё не вышел в полноценный релиз, оба стали долгожданным глотком свежего воздуха в мире Python-типизации, которым исторически правят mypy и pyright.
Оба инструмента уже довольно давно доступны в Open source и их можно свободно скачать, однако до прошлого недели Meta и Astral официально никак не заявляли о своих совершенно новых анализаторах типов следующего поколения.
На PyCon 2025, в тихой комнате 319 на Typing Summit, мы впервые получили официальный предварительный обзор этих инструментов — команд, стоящих за ними, их целей, представлений и амбиций — а также уникальных подходов, которыми они пытаются решать проблемы типизации в Python.

Этот блог — сборник набросков, сделанных на мероприятии, личных разговоров с командой и не слишком тщательных экспериментов, которые я успел провести сам. Так что какие-то детали могут быть слегка размыты.
К тому же оба инструмента всё ещё на ранней альфе!
Пожалуйста, не воспринимайте это как окончательный вердикт о том, какой из них лучше или хуже. Этот текст — просто ради интереса, чтобы посмотреть, в каком состоянии находятся оба инструмента прямо сейчас.
Ниже приведены тесты и эксперименты, проведённые на актуальных версиях pyrefly, ty, mypy и pyright на момент написания блога:
pyrefly 0.17.0
ty 0.0.1-alpha.7 (afb20f6fe 2025-05-26)
mypy 1.15.0 (compiled: yes)
pyright 1.1.401
Pyrefly
Pyrefly — это новый Rust-базированный Python-анализатор типов от Meta, пришедший на смену Pyre — прежнему анализатору типов Meta, написанному на OCaml. Ожидается, что Pyrefly будет быстрее, переносимее и функциональнее по сравнению с Pyre.
Одна важная мысль, которую команда Pyrefly особо подчёркивала в этом году, — они хотят быть по-настоящему Open source. Формально Pyre тоже был Open source, но скорее в духе: «мы сделали это для себя, но вот вам исходники, если хотите». В отличие от этого, одна из базовых целей Pyrefly — сильнее ориентироваться на потребности Open source-сообщества и активно взаимодействовать с ним.
ty
ty — ещё один Rust-базированный анализатор типов для Python, который сейчас разрабатывается в Astral, той самой команде, что стоит за uv и ruff. Раньше проект назывался Red-Knot, но теперь у него есть официальное имя — ty. В отличие от Meta, Astral делает всё куда тише: мягкий запуск на GitHub, короткая 30-минутная презентация и пара блог-постов и подкастов тут и там.
Сходства
И pyrefly, и ty написаны на Rust, оба работают инкрементально (хотя реализация у них немного разная — см. ниже), и оба используют Ruff под капотом для разбора AST. Оба инструмента изначально ориентированы и на проверку типов из командной строки, и на интеграцию с LSP/IDE.
Но, кроме того что это два быстрых анализатора типов для Python, на этом их сходство, по сути, заканчивается. На мой взгляд, есть четыре области, в которых эти инструменты отличаются: скорость, цели, инкрементальность и возможности. Сегодня мы и разберём их по порядку.
Скорость
Скорость, похоже, была одним из главных акцентов команды Pyrefly — об этом неоднократно говорили во вступительной презентации. По словам разработчиков, Pyrefly работает в 35 раз быстрее Pyre и в 14 раз быстрее mypy/pyright, обрабатывая до 1,8 миллиона строк кода в секунду. Достаточно быстро, чтобы «проверять типы на каждом нажатии клавиши».
Для ty скорость тоже была одним из ключевых ориентиров при проектировании, но во время презентации этому уделили меньше внимания. Единственный озвученный тезис: «в 1–2 порядка быстрее текущего поколения анализаторов типов».
Разумеется, мне сразу захотелось проверить производительность самому.
Тестирование производительности — PyTorch
Для первого теста я клонировал и переключился на последний релиз PyTorch (v2.7.0) и сравнил время проверки типов в pyrefly, ty, mypy и pyright на MacBook M4. Я провёл два замера: один — на всём репозитории PyTorch, и второй — только на поддиректории torch:
Последние версии mypy не поддерживают PyTorch. Пришлось использовать mypy 1.14.0.

Запущенные команды
Сырые данные

Запущенные команды
Сырые данные
С самого начала видно, что и для всего PyTorch, и для одной только директории torch ty работает примерно в 2–3 раза быстрее pyrefly, а оба инструмента оказываются в 10–20 раз быстрее mypy и pyright.
Есть и любопытная деталь: pyrefly нашёл больше исходных файлов, чем ty — примерно 8600 у pyrefly и около 6500 у ty в репозитории PyTorch. (Я так и не понял, откуда берётся эта разница.)
И важно помнить, что и pyrefly, и ty — всё ещё ранние альфа-версии и далеко не завершённые продукты. Это вполне может искажать результаты!
Бенчмаркинг — Django
Далее я запустил тот же бенчмарк на Django версии 5.2.1.
Примечание: во время этого теста mypy завершился с ошибкой.

Запущенные команды
Сырые данные
Мы видим ту же картину во всех тестах: ty снова оказывается самым быстрым (2900 файлов за 0.6 секунды), pyrefly идёт следом (3200 файлов за 0.9 секунды), а pyright — самый медленный (16 секунд).
Бенчмаркинг — MyPy
Напоследок я прогнал бенчмарк на самом репозитории mypy (точнее, на поддиректории mypyc). Результаты здесь аналогичные.

Запущенные команды
Сырые данные
Цели
Именно в целях pyrefly и ty, на мой взгляд, лежит главное различие между ними. Pyrefly стремится быть максимально «агрессивным» в типизации — выводить как можно больше, чтобы даже код без единой явной аннотации всё равно имел хоть какие-то типовые гарантии.
ty, напротив, придерживается другой философии: принципа постепенной гарантии. Основная идея такова: в корректно типизированной программе удаление аннотации типа не должно приводить к ошибке типов. Иными словами, вам не должно требоваться добавлять новые аннотации в уже рабочий код только ради устранения проблем с типами.

Это хорошо видно на следующем примере:
class MyClass:
attr = None
foo = MyClass()
# ➖ pyrefly | revealed type: None
# ✅ ty. | Revealed type: `Unknown | None`
# ➖ mypy. | Revealed type is "None"
# ➖ pyright | Type of "foo.attr" is "None"
reveal_type(foo.attr)
# ➖ pyrefly | ERROR: Literal[1] is not assignable to attribute attr with type None
# ✅ ty. | < No Error >
# ➖ mypy. | ERROR: Incompatible types in assignment (expression has type "int", variable has type "None")
# ➖ pyright | ERROR: Cannot assign to attribute "attr" for class "MyClass"
foo.attr = 1В этом примере pyrefly, mypy и pyright сразу же жёстко типизируют foo.attr как None и выбрасывают ошибку при присваивании 1 — тогда как ty понимает, что присваивание foo.attr = 1 само по себе не должно приводить к ошибке типов, и вместо этого рассматривает foo.attr как Unknown | None, чтобы такое присваивание было допустимо. (Unknown — это новый тип, добавленный в ty, чтобы отличать явно указанный Any от «неизвестного» Any.)
В результате pyrefly заодно способен ловить некоторые ошибки, которые другие анализаторы типов пропускают. Например, в таком случае:
my_list = [1, "b", None]
val = my_list.pop(1)
# ✅ pyrefly | revealed type: int | str | None
# ➖ ty. | Revealed type: `Unknown`
# ➖ mypy. | Revealed type is "builtins.object"
# ➖ pyright | Type of "val" is "Unknown"
reveal_type(val)
# ✅ pyrefly | ERROR: `*` is not supported between `None` and `Literal[2]`
# ➖ ty. | < No Error >
# ➖ mypy. | ERROR: Unsupported operand types for * ("object" and "int")
# ➖ pyright | < No Error >
new_val = val * 2
Формально mypy тоже выдаёт ошибку, но по неправильной причине. Например, если изменить код на
my_list = [1, "b"], программа станет корректной, но mypy всё равно будет жаловаться на несоответствие типов междуobjectиint.
Pyrefly неявно выводит для val тип int | str | None, хотя ни val, ни my_list явно не аннотированы. Благодаря этому он корректно ловит ошибку в выражении val * 2 ниже.
И это лишь один из множества примеров — остальные появятся дальше, в разделе Capabilities.
Инкрементальность
И pyrefly, и ty заявляют, что работают инкрементально — то есть изменение одного файла приводит к повторному разбору только затронутых областей, а не всей программы. У pyrefly за это отвечает собственный инкрементальный движок, встроенный прямо в анализатор типов.
У ty, наоборот, используется Salsa — тот же инкрементальный фреймворк, который лежит в основе Rust Analyzer.
Любопытно, что это означает разную степень «дробности» инкрементальности:
ty применяет тонкую инкрементализацию — изменение одной функции приводит к повторному разбору только этой функции (и зависимых от неё частей).
pyrefly использует модульный уровень — изменение одной функции заставляет перепарсить весь файл/модуль, а также зависящие от него файлы/модули.
Причина, по которой pyrefly выбрал модульный уровень вместо мелкозернистого (насколько я понял), в том, что модульная инкрементализация на Rust уже достаточно быстра, а тонкая инкрементализация заметно усложняет кодовую базу — и при этом приносит минимальный прирост производительности.
Возможности
Команды и pyrefly, и ty подчёркивают ОЧЕНЬ ЯСНО: инструменты всё ещё незавершённые, ранние альфы, с известными проблемами, багами и недостающими возможностями.Тем не менее, мне кажется полезным посмотреть, что именно уже поддерживает каждый из них — это хорошо показывает, на чём команды делали акцент и что сочли важным на данном этапе для своих анализаторов типов следующего поколения.
Неявный вывод типов
Неявный вывод типов — одна из ключевых демонстрационных возможностей pyrefly. Например, вот простой случай, когда выводится тип возвращаемого значения:
def foo(imp: Any):
return str(imp)
a = foo(123)
# ✅ pyrefly | revealed type: str
# ➖ ty. | Revealed type: `Unknown`
# ➖ mypy. | Revealed type is "Any"
# ✅ pyright | Type of "a" is "str"
reveal_type(a)
# ✅ pyrefly | ERROR: `+` is not supported between `str` and `Literal[1]`
# ➖ ty. | < No Error >
# ➖ mypy. | < No Error >
# ✅ pyright | ERROR: Operator "+" not supported for types "str" and "Literal[1]"
a + 1
Вот ещё пример, где выводятся типы у более сложных коллекций (в данном случае — dict):
from typing import reveal_type
my_dict = {
key: value * 2
for key, value in {"apple": 2, "banana": 3, "cherry": 1}.items()
if value > 1
}
# ✅ pyrefly | revealed type: dict[str, int]
# ➖ ty. | Revealed type: `@Todo`
# ✅ mypy. | Revealed type is "builtins.dict[builtins.str, builtins.int]"
# ✅ pyright | Type of "my_dict" is "dict[str, int]"
reveal_type(my_dict)Но здесь вступает в игру «gradual guarantee» из философии ty. Рассмотрим такой пример:
my_list = [1, 2, 3]
# ✅ pyrefly | revealed type: list[int]
# ➖ ty. | Revealed type: `list[Unknown]`
# ✅ mypy. | Revealed type is "builtins.list[builtins.int]"
# ✅ pyright | Type of "my_list" is "list[int]"
reveal_type(my_list)
# ➖ pyrefly | ERROR: Argument `Literal['foo']` is not assignable to parameter with type `int` in function `list.append`
# ✅ ty. | < No Error >
# ➖ mypy. | ERROR: Argument 1 to "append" of "list" has incompatible type "str"; expected "int"
# ➖ pyright | ERROR: Argument of type "Literal['foo']" cannot be assigned to parameter "object" of type "int" in function "append"
my_list.append("foo")
Pyrefly, mypy и pyright все трактуют вызов my_list.append("foo") как ошибку типизации, хотя технически это допустимо (Python-коллекции могут содержать объекты разных типов!). И если такой код и правда задуман автором, то ty — единственный анализатор, который пропускает его без необходимости добавлять явные аннотации к my_list.
Быстрое уточнение: команда ty отмечала, что такое поведение не является задумкой — оно появилось из-за неполного вывода типов для обобщённых контейнерных литералов. Подробнее об этом можно почитать в обсуждении на Hacker News.
Дженерики
Ещё одна вещь, о которой команда pyrefly упоминала в своём докладе, — что, создавая pyrefly практически с нуля, они решили сначала браться за самые сложные задачи. Это значит, что большая часть архитектуры pyrefly строилась вокруг таких трудных тем, как дженерики, перегрузки и импорт через *.
Например, вот неск��лько случаев, где и pyrefly, и ty корректно разрешают дженерики:
# === Simple Case ===
class Box[T]:
def __init__(self, val: T) -> None:
self.val = val
b: Box[int] = Box(42)
# ✅ pyrefly | revealed type: int
# ✅ ty. | Revealed type: `Unknown | int`
# ✅ mypy. | Revealed type is "builtins.int"
# ✅ pyright | Type of "b.val" is "int"
reveal_type(b.val)
# ✅ pyrefly | ERROR: Argument `Literal[100]` is not assignable to parameter `val` with type `str` in function `Box.__init__`
# ✅ ty. | ERROR: Object of type `Box[int]` is not assignable to `Box[str]`
# ✅ mypy. | ERROR: Argument 1 to "Box" has incompatible type "int"; expected "str"
# ✅ pyright | ERROR: Type "Box[int]" is not assignable to declared type "Box[str]"
b2: Box[str] = Box(100)
# === Bounded Types with Attribute ===
class A:
x: int | str
def f[T: A](x: T) -> T:
# ✅ pyrefly | revealed type: int | str
# ✅ ty. | Revealed type: `int | str`
# ✅ mypy. | Revealed type is "Union[builtins.int, builtins.str]"
# ✅ pyright | Type of "x.x" is "int | str"
reveal_type(x.x)
return xА вот примеры, где pyrefly справляется с разрешением дженериков лучше, чем ty:
from typing import Callable, TypeVar, assert_type, reveal_type
# === Generic Class Without Explicit Type Param ===
class C[T]:
x: T
c: C[int] = C()
# ✅ pyrefly | revealed type: C[int]
# ➖ ty. | `C[Unknown]`
# ✅ pypy. | Revealed type is "__main__.C[builtins.int]"
# ✅ pyright | Type of "c" is "C[int]"
reveal_type(c)
# ✅ pyrefly | revealed type: int
# ➖ ty. | Revealed type: `Unknown`
# ✅ pypy. | Revealed type is "builtins.int"
# ✅ pyright | Type of "c.x" is "int"
reveal_type(c.x)
# === Bounded Types with Callable Attribute ===
def func[T: Callable[[int], int]](a: T, b: int) -> T:
# ✅ pyrefly | revealed type: int
# ➖ ty. | ERROR: <Error: Object of type `T` is not callable>
# ✅ pypy. | Revealed type is "builtins.int"
# ✅ pyright | Type of "a(b)" is "int"
reveal_type(a(b))
return a
Любопытно, что и pyrefly, и ty испытывают трудности с разрешением ковариантных и контравариантных отношений типов. Например:
from __future__ import annotations
class A[X]:
def f(self) -> B[X]:
...
class B[Y]:
def h(self) -> B[Y]:
...
def cast_a(a: A[bool]) -> A[int]:
# ➖ pyrefly | ERROR: Return type does not match returned value: expected `A[int]`, found `A[bool]`
# ➖ ty. | ERROR: Returned type `A[bool]` is not assignable to declared return type `A[int]`
# ✅ mypy. | < No Error >
# ✅ pyright | < No Error >
return a # Allowed
Информативные сообщения об ошибках
Одной из заявленных особенностей ty является максимально понятные и лаконичные сообщения об ошибках.
Например, вот простой случай вызова функции с несовместимыми типами:

И вот как то же самое выглядит в pyrefly, mypy и pyright:

Ещё один пример — несовпадающие типы возвращаемых значений:

На мой взгляд, выглядит куда аккуратнее! Здорово видеть, что в экосистему Python приходят новые, более понятные формулировки ошибок.
Типы-пересечения и типы-исключения
И напоследок — очень классная возможность, которую показала команда Astral: поддержка intersection types (типов-пересечений) и negation types (типов-исключений). По их словам, ty — единственный Python-анализатор типов, который такое реализует. Чтобы проиллюстрировать, взгляните на пример:
class WithX:
x: int
@final
class Other:
pass
def foo(obj: WithX | Other):
if hasattr(obj, "x"):
# ➖ pyrefly | revealed type: Other | WithX
# ✅ ty. | Revealed type: `WithX`
# ➖ mypy. | Revealed type is "Union[__main__.WithX, __main__.Other]"
# ➖ pyright | Type of "obj" is "WithX | Other"
reveal_type(obj)Аннотация
@final— это новая возможность Python 3.12, запрещающая наследование от класса. Для анализатора типов это важно: он должен знать, чтоOtherне сможет получить наследника с атрибутомxкогда-нибудь в будущем.
При заданных условиях:
obj— это либоWithX, либо финальный типOther;при этом у
objдолжен быть атрибутx;единственный совместимый вывод типа для
objвreveal_type(obj)— это WithX.
Если разложить происходящее «за кулисами» по шагам:
(WithX | Other) & <Protocol with members 'x'>
=> (WithX & <Protocol with members 'x'> | (Other & <Protocol with members 'x'>)
=> WithX | Never
=> WithX
Рассмотрим ещё один пример:
class MyClass:
...
class MySubclass(MyClass):
...
def bar(obj: MyClass):
if not isinstance(obj, MySubclass):
# ➖ pyrefly | revealed type: MyClass
# ✅ ty. | Revealed type: `MyClass & ~MySubclass`
# ➖ mypy. | Revealed type is "__main__.MyClass"
# ➖ pyright | Type of "obj" is "MyClass"
reveal_type(obj)ty — единственный анализатор, который выводит тип obj в reveal_type(obj) как MyClass & ~MySubclass. Это означает, что ty вводит в систему типов Python новые парадигмы:
пересечения типов (intersections)
отрицания типов (negations)
Классно же!
Однако всё это всё ещё ранняя альфа! Например, вот такой случай:
def bar(obj: HasFoo):
if not hasattr(obj, "bar"):
reveal_type(obj)
reveal_type(obj.foo)reveal_type(obj) корректно выводит тип HasFoo & ~<Protocol with members 'bar'>, но reveal_type(obj.foo) почему-то даёт @Todo, хотя с заданными ограничениями obj.foo вполне должен быть разрешим как функция foo.
И напоследок — забавный «фокус на вечеринке»: вот как ty использует пересечения и отрицания типов, чтобы «решать» диофантовы уравнения:
# Simply provide a list of all natural numbers here ...
type Nat = Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]
def pythagorean_triples(a: Nat, b: Nat, c: Nat):
reveal_type(a**2 + b**2 == c**2)
# reveals 'bool': solutions exist (3² + 4² == 5²)
def fermats_last_theorem(a: Nat, b: Nat, c: Nat):
reveal_type(a**3 + b**3 == c**3)
# reveals 'Literal[False]': no solutions!
def catalan_conjecture(a: Nat, b: Nat):
reveal_type(a**2 - b**3 == 1)
# reveals 'bool': solutions exist (3² - 2³ == 1)Итоговые мысли
В целом, появление сразу двух новых быстрых анализаторов типов в экосистеме Python — это очень здорово! Сейчас pyrefly и ty, кажется, движутся к разным целям. ty придерживается постепенного подхода к типизации: если программа (теоретически) работает без ошибок, запуск анализатора типов не должен добавлять новых ошибок — и если они появляются, это, скорее всего, указывает на реальную проблему в коде. pyrefly, наоборот, следует подходу, который типичен для большинства современных анализаторов Python: выводить как можно больше типов, даже если это порой приводит к ошибкам там, где их не ожидали.
Как уже не раз упоминалось, и pyrefly, и ty — пока что ранние альфы. Я почти уверен, что со временем их возможности начнут сходиться, но всё равно интересно видеть, в каком состоянии находятся оба инструмента прямо сейчас и как они могут проявить себя в разных сценариях в будущем.
Попробуйте их сами!
Pyrefly можно потестировать на pyrefly.org/sandbox, а ty — на play.ty.dev. У обоих есть команды для установки через pip / uv add / poetry add / uvx, а также плагины для редакторов (VSCode, Cursor и т.д.).
А пока что ходят слухи, что Google собирается открыть свой собственный анализатор типов для Python, написанный на Go, так что будет очень любопытно посмотреть на него, когда он появится 👀
Русскоязычное сообщество про Python

Друзья! Эту статью подготовила команда Python for Devs — канала, где каждый день выходят самые свежие и полезные материалы о Python и его экосистеме. Подписывайтесь, чтобы ничего не пропустить!
Приложение
Хочу отдельно отметить: тесты в ty написаны… в Markdown! Разве это не круто?
https://github.com/astral-sh/ruff/tree/main/crates/ty_python_semantic/resources/mdtest
Спасибо, что прочитали!