Команда Python for Devs подготовила перевод статьи о двух новых Rust-базированных анализаторах типов для Python — pyrefly и ty. Оба пока в ранней альфе, но уже демонстрируют впечатляющую скорость, разные подходы к выводу типов и новые возможности.


Примечание: я люблю ставить длинные тире в тексте! Не переживайте — это написал не ИИ. (контекст)

В начале этого месяца в центре внимания оказались два новых Python-анализатора типов, написанных на Rust: pyrefly и ty. Хотя ни один из них ещё не вышел в полноценный релиз, оба стали долгожданным глотком свежего воздуха в мире Python-типизации, которым исторически правят mypy и pyright.

Оба инструмента уже довольно давно доступны в Open source и их можно свободно скачать, однако до прошлого недели Meta и Astral официально никак не заявляли о своих совершенно новых анализаторах типов следующего поколения.

На PyCon 2025, в тихой комнате 319 на Typing Summit, мы впервые получили официальный предварительный обзор этих инструментов — команд, стоящих за ними, их целей, представлений и амбиций — а также уникальных подходов, которыми они пытаются решать проблемы типизации в Python.

 Команд�� ty выступает на Typing Summit
Команда ty выступает на Typing Summit

Этот блог — сборник набросков, сделанных на мероприятии, личных разговоров с командой и не слишком тщательных экспериментов, которые я успел провести сам. Так что какие-то детали могут быть слегка размыты.

К тому же оба инструмента всё ещё на ранней альфе!

Пожалуйста, не воспринимайте это как окончательный вердикт о том, какой из них лучше или хуже. Этот текст — просто ради интереса, чтобы посмотреть, в каком состоянии находятся оба инструмента прямо сейчас.

Ниже приведены тесты и эксперименты, проведённые на актуальных версиях 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, напротив, придерживается другой философии: принципа постепенной гарантии. Основная идея такова: в корректно типизированной программе удаление аннотации типа не должно приводить к ошибке типов. Иными словами, вам не должно требоваться добавлять новые аннотации в уже рабочий код только ради устранения проблем с типами.

 слайд про «gradual guarantee» из презентации ty
слайд про «gradual guarantee» из презентации 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

Спасибо, что прочитали!

Комментарии (3)