Команда 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
Спасибо, что прочитали!