Skip to content

Investigate ty type checker alongside mypy #681

Open

Description

@tony

Summary

ty is Astral's Rust-based Python type checker (beta). It's 10-100x faster than mypy, checks unannotated function bodies by default, and supports advanced features like intersection types and reachability analysis.

libtmux is well-positioned for ty adoption:

  • No mypy plugins in use (ty's biggest gap — no plugin system)
  • Only 9 type: ignore comments across the codebase
  • Already uses Astral's ruff and uv tooling
  • Strict mypy configuration with full type annotations

Goals

  1. Add ty as a dev dependency alongside mypy (not replacing it)
  2. Add ty configuration in pyproject.toml and justfile targets
  3. Add ty to CI as a non-blocking step
  4. Compare diagnostics: what ty catches vs mypy, false positives, speed
  5. Determine if ty can eventually displace mypy

Approach

  • Gradual adoption: run both checkers in parallel
  • Start with known-noisy rules suppressed, re-enable incrementally
  • CI runs ty with continue-on-error: true so it doesn't block builds
  • Document findings in the PR for future reference

References

Activity

  1. tony commented on May 24, 2026

    @tony
    MemberAuthor

    Analysis: ty vs mypy — rule coverage and displacement feasibility

    Versions analyzed: ty 0.0.39 (rules implemented in ruff@a69a3d9), mypy master@c0cced3 (ahead of v1.20.2)


    Rule count

    Checker Total rules Default error Default warn Default ignore
    ty 115 (111 diagnostic + 4 suppression) ~90 ~20 5
    mypy 78 (63 default-on + 15 opt-in) 63 — 15 opt-in

    Raw rule count favors ty (115 vs 78), but they slice the problem differently — ty splits mypy's broad codes into more specific rules (e.g., mypy's call-arg → ty's too-many-positional-arguments, missing-argument, unknown-argument, parameter-already-assigned, positional-only-parameter-as-kwarg, conflicting-argument-forms).

    How much of mypy does ty cover?

    Mapping mypy's 78 error codes to ty equivalents:

    Coverage Count % Examples
    ✅ Fully covered 36 46% arg-type → invalid-argument-type, operator → unsupported-operator, override → invalid-method-override
    ⚠️ Partially covered 7 9% union-attr (ty uses broader unresolved-attribute), no-overload-impl (ty's invalid-overload covers some cases)
    ❌ Not covered 35 45% See breakdown below
    Full mapping table (click to expand)

    ✅ Fully covered (36):

    mypy code ty rule
    attr-defined unresolved-attribute
    name-defined unresolved-reference
    call-arg too-many-positional-arguments + missing-argument + unknown-argument + parameter-already-assigned + positional-only-parameter-as-kwarg + conflicting-argument-forms
    arg-type invalid-argument-type
    call-overload no-matching-overload
    valid-type invalid-type-form
    override invalid-method-override + invalid-attribute-override
    return invalid-return-type (partial — ty doesn't flag missing return statements in all branches)
    return-value invalid-return-type
    assignment invalid-assignment
    type-arg invalid-type-arguments
    type-var invalid-legacy-type-variable + invalid-type-variable-bound + invalid-type-variable-constraints + invalid-type-variable-default
    index not-subscriptable + invalid-key + index-out-of-bounds
    operator unsupported-operator
    typeddict-item invalid-typed-dict-field + missing-typed-dict-key
    import unresolved-import
    no-redef conflicting-declarations
    abstract call-abstract-method
    valid-newtype invalid-newtype
    empty-body empty-body
    redundant-cast redundant-cast
    assert-type type-assertion-failure
    possibly-undefined possibly-unresolved-reference
    name-match mismatched-type-name
    unused-awaitable unused-awaitable
    used-before-def unresolved-reference + possibly-unresolved-reference
    unused-ignore unused-type-ignore-comment + unused-ignore-comment
    explicit-override invalid-explicit-override
    unimported-reveal undefined-reveal
    metaclass invalid-metaclass + conflicting-metaclass
    syntax Built into ruff parser
    narrowed-type-not-subtype invalid-type-guard-definition
    deprecated deprecated
    unused-coroutine unused-awaitable (similar scope)
    no-overload-impl invalid-overload (covers this case)
    await-not-async invalid-await

    ⚠️ Partially covered (7):

    mypy code ty status
    union-attr Covered by broader unresolved-attribute — no union-specific messaging
    import-not-found / import-untyped unresolved-import covers import-not-found; no import-untyped equivalent
    ignore-without-code ignore-comment-unknown-rule is related but different scope
    mutable-override invalid-attribute-override covers some but not all mutable override cases
    overload-cannot-match / overload-overlap invalid-overload is broader but may not catch all overlap/unreachable cases

    ❌ Not covered (35):

    mypy code What's missing
    var-annotated ty has no "require annotations" mode
    method-assign No check for assigning to methods
    list-item / dict-item No container item type checking
    typeddict-unknown-key No specific unknown key check
    has-type No "cannot determine type" check
    func-returns-value No check for using void function return
    type-abstract No check requiring concrete classes for Type[...]
    str-format / str-bytes-safe No string formatting type safety
    exit-return No __exit__ return type check
    literal-required No literal value enforcement
    safe-super No abstract method call warning
    top-level-await No top-level await check
    no-untyped-def / no-untyped-call No strict mode — ty checks all code by default but doesn't require annotations
    no-any-unimported / no-any-return No Any-rejection checks
    unreachable ty has reachability analysis but no warning rule
    annotation-unchecked No unchecked annotation notification
    typeddict-readonly-mutated No ReadOnly TypedDict mutation check
    redundant-expr No redundant expression warning
    truthy-bool / truthy-function / truthy-iterable No truthy analysis
    str-unpack No string unpacking warning
    redundant-self No redundant Self type warning
    exhaustive-match No exhaustive match checking
    misc Catch-all (not really mappable)
    prop-decorator No property decorator stacking check
    untyped-decorator No untyped decorator check
    explicit-any No explicit Any warning
    comparison-overlap No comparison overlap check

    The strict mode gap

    libtmux uses strict = true, which enables 13 mypy flags. ty has no strict mode equivalent:

    mypy strict flag ty equivalent Status
    check-untyped-defs Default behavior ✅ ty does this by default
    warn-redundant-casts redundant-cast ✅
    warn-unused-ignores unused-type-ignore-comment ✅
    disallow-any-generics — ❌
    disallow-subclassing-any — ❌
    disallow-untyped-calls — ❌
    disallow-untyped-defs — ❌
    disallow-incomplete-defs — ❌
    disallow-untyped-decorators — ❌
    warn-return-any — ❌
    no-implicit-reexport — ❌
    strict-equality — ❌
    extra-checks — ❌

    3 of 13 strict flags are covered. The missing 10 are all "annotation enforcement" checks — they ensure code stays fully typed. ty's philosophy is different: it checks whatever types exist but doesn't mandate their presence.

    What ty catches that mypy doesn't

    ty has ~30 rules with no mypy equivalent, including:

    ty-only rule What it catches
    index-out-of-bounds Static bounds checking on tuple/list indexing
    dataclass-field-order Required fields after defaulted fields
    zero-stepsize-in-slice x[::0] slice with zero step
    invalid-context-manager Using non-context-manager in with
    invalid-exception-caught Catching non-exception types
    invalid-raise Raising non-exception types
    duplicate-base Duplicate base classes
    cyclic-class-definition Circular class inheritance
    instance-layout-conflict Multiple __slots__ base conflict
    invalid-total-ordering Missing methods for @total_ordering
    invalid-frozen-dataclass-subclass Non-frozen subclass of frozen dataclass
    isinstance-against-protocol isinstance() with non-runtime Protocol
    possibly-missing-submodule Implicit submodule access
    call-top-callable Calling with unknown signature
    unsupported-bool-conversion __bool__ returning non-bool

    What libtmux specifically gains and loses

    libtmux's type: ignore codes in use (7 unique mypy codes across 9 comments):

    mypy code ty equivalent Covered?
    no-redef (×3) conflicting-declarations ✅
    attr-defined (×3) unresolved-attribute ✅
    union-attr (×1) unresolved-attribute ✅
    return-value (×1) invalid-return-type ✅
    dict-item (×1) — ❌
    assignment (×1) invalid-assignment ✅
    arg-type (×1) invalid-argument-type ✅

    6 of 7 codes used in libtmux are covered by ty. The missing one (dict-item) is a container item type check used in one test.

    Speed

    Cold-cache benchmarks on libtmux (69 source files):

    Checker Time Notes
    ty 0.12s Multi-threaded Rust (687% CPU)
    mypy (cold) 3.4s Single-threaded Python
    mypy (warm) 0.14s With .mypy_cache/

    28× faster cold, comparable warm. The cold-cache gap matters most in CI.

    Verdict: can ty displace mypy today?

    No. The critical blocker is the strict mode gap:

    1. No annotation enforcement. libtmux uses strict = true specifically to prevent untyped code from entering the codebase. ty has no equivalent — it checks whatever types exist but won't catch a new function missing annotations. Dropping mypy would silently allow untyped code to regress.

    2. 45% of mypy's error codes have no ty equivalent. Many are niche (str-format, truthy-bool), but some matter: dict-item, list-item, comparison-overlap, exhaustive-match.

    3. Argument unpacking is broken. 86 of 147 diagnostics in this PR were false positives from ty's incomplete *args/**kwargs support (ty#404). The cmd() method — libtmux's core API — triggers this on nearly every call.

    4. Beta stability. Rule names and defaults can change between versions. Not suitable as a sole gatekeeper.

    Recommendation

    Keep both, with ty as a non-blocking companion. This is what the current PR implements.

    • mypy remains the CI gate: strict mode enforcement, stable error codes, mature ecosystem
    • ty runs alongside for speed (just ty for local dev), catching things mypy misses (dataclass ordering, context manager validation, protocol isinstance checks), and preparing for eventual migration

    Re-evaluate when:

    • ty ships strict mode or annotation enforcement (tracked in ty roadmap)
    • ty#404 (argument unpacking) is resolved — this alone would eliminate 60% of current false positives
    • ty reaches 1.0 with stable rule names

    The libtmux codebase is already ty-ready: fully annotated, no mypy plugins, compatible type: ignore comments. When ty closes the strict mode gap, switching will be straightforward.

  2. 24 remaining items

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions