You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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
Add ty as a dev dependency alongside mypy (not replacing it)
Add ty configuration in pyproject.toml and justfile targets
Add ty to CI as a non-blocking step
Compare diagnostics: what ty catches vs mypy, false positives, speed
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
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).
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:
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:
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.
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.
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.
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
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.
Activity
ty(rules) Suppress invalid-argument-type (#681)
ty(rules) Suppress unresolved-attribute (#681)
ty(rules) Suppress invalid-assignment (#681)
ty(rules) Suppress dataclass-field-order (#681)
ty(rules) Suppress no-matching-overload (#681)
ty(rules) Suppress invalid-return-type (#681)
ty(rules) Suppress not-subscriptable (#681)
ty(rules) Suppress call-top-callable (#681)
tony commentedon May 24, 2026
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
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'stoo-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:
arg-type→invalid-argument-type,operator→unsupported-operator,override→invalid-method-overrideunion-attr(ty uses broaderunresolved-attribute),no-overload-impl(ty'sinvalid-overloadcovers some cases)Full mapping table (click to expand)
✅ Fully covered (36):
attr-definedunresolved-attributename-definedunresolved-referencecall-argtoo-many-positional-arguments+missing-argument+unknown-argument+parameter-already-assigned+positional-only-parameter-as-kwarg+conflicting-argument-formsarg-typeinvalid-argument-typecall-overloadno-matching-overloadvalid-typeinvalid-type-formoverrideinvalid-method-override+invalid-attribute-overridereturninvalid-return-type(partial — ty doesn't flag missing return statements in all branches)return-valueinvalid-return-typeassignmentinvalid-assignmenttype-arginvalid-type-argumentstype-varinvalid-legacy-type-variable+invalid-type-variable-bound+invalid-type-variable-constraints+invalid-type-variable-defaultindexnot-subscriptable+invalid-key+index-out-of-boundsoperatorunsupported-operatortypeddict-iteminvalid-typed-dict-field+missing-typed-dict-keyimportunresolved-importno-redefconflicting-declarationsabstractcall-abstract-methodvalid-newtypeinvalid-newtypeempty-bodyempty-bodyredundant-castredundant-castassert-typetype-assertion-failurepossibly-undefinedpossibly-unresolved-referencename-matchmismatched-type-nameunused-awaitableunused-awaitableused-before-defunresolved-reference+possibly-unresolved-referenceunused-ignoreunused-type-ignore-comment+unused-ignore-commentexplicit-overrideinvalid-explicit-overrideunimported-revealundefined-revealmetaclassinvalid-metaclass+conflicting-metaclasssyntaxnarrowed-type-not-subtypeinvalid-type-guard-definitiondeprecateddeprecatedunused-coroutineunused-awaitable(similar scope)no-overload-implinvalid-overload(covers this case)await-not-asyncinvalid-awaitunion-attrunresolved-attribute— no union-specific messagingimport-not-found/import-untypedunresolved-importcoversimport-not-found; noimport-untypedequivalentignore-without-codeignore-comment-unknown-ruleis related but different scopemutable-overrideinvalid-attribute-overridecovers some but not all mutable override casesoverload-cannot-match/overload-overlapinvalid-overloadis broader but may not catch all overlap/unreachable cases❌ Not covered (35):
var-annotatedmethod-assignlist-item/dict-itemtypeddict-unknown-keyhas-typefunc-returns-valuetype-abstractType[...]str-format/str-bytes-safeexit-return__exit__return type checkliteral-requiredsafe-supertop-level-awaitno-untyped-def/no-untyped-callno-any-unimported/no-any-returnunreachableannotation-uncheckedtypeddict-readonly-mutatedredundant-exprtruthy-bool/truthy-function/truthy-iterablestr-unpackredundant-selfexhaustive-matchmiscprop-decoratoruntyped-decoratorexplicit-anycomparison-overlapThe strict mode gap
libtmux uses
strict = true, which enables 13 mypy flags. ty has no strict mode equivalent:check-untyped-defswarn-redundant-castsredundant-castwarn-unused-ignoresunused-type-ignore-commentdisallow-any-genericsdisallow-subclassing-anydisallow-untyped-callsdisallow-untyped-defsdisallow-incomplete-defsdisallow-untyped-decoratorswarn-return-anyno-implicit-reexportstrict-equalityextra-checks3 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:
index-out-of-boundsdataclass-field-orderzero-stepsize-in-slicex[::0]slice with zero stepinvalid-context-managerwithinvalid-exception-caughtinvalid-raiseduplicate-basecyclic-class-definitioninstance-layout-conflict__slots__base conflictinvalid-total-ordering@total_orderinginvalid-frozen-dataclass-subclassisinstance-against-protocolisinstance()with non-runtime Protocolpossibly-missing-submodulecall-top-callableunsupported-bool-conversion__bool__returning non-boolWhat libtmux specifically gains and loses
libtmux's type: ignore codes in use (7 unique mypy codes across 9 comments):
no-redef(×3)conflicting-declarationsattr-defined(×3)unresolved-attributeunion-attr(×1)unresolved-attributereturn-value(×1)invalid-return-typedict-item(×1)assignment(×1)invalid-assignmentarg-type(×1)invalid-argument-type6 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):
.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:
No annotation enforcement. libtmux uses
strict = truespecifically 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.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.Argument unpacking is broken. 86 of 147 diagnostics in this PR were false positives from ty's incomplete
*args/**kwargssupport (ty#404). Thecmd()method — libtmux's core API — triggers this on nearly every call.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.
just tyfor local dev), catching things mypy misses (dataclass ordering, context manager validation, protocol isinstance checks), and preparing for eventual migrationRe-evaluate when:
The libtmux codebase is already ty-ready: fully annotated, no mypy plugins, compatible
type: ignorecomments. When ty closes the strict mode gap, switching will be straightforward.py(deps[dev]) Add ty type checker alongside mypy (#681)
ty(rules) Suppress unresolved-import (#681)
ty(rules) Suppress too-many-positional-arguments (#681)
ty(rules) Suppress missing-argument (#681)
ty(rules) Suppress unknown-argument (#681)
ty(rules) Suppress possibly-missing-submodule (#681)
24 remaining items
py(deps[dev]) Add ty type checker alongside mypy (#681)
ty(rules) Suppress unresolved-import (#681)
ty(rules) Suppress too-many-positional-arguments (#681)
ty(rules) Suppress missing-argument (#681)
ty(rules) Suppress unknown-argument (#681)
ty(rules) Suppress possibly-missing-submodule (#681)
ty(rules) Suppress unsupported-operator (#681)
ty(rules) Suppress invalid-argument-type (#681)
ty(rules) Suppress unresolved-attribute (#681)
ty(rules) Suppress invalid-assignment (#681)
ty(rules) Suppress dataclass-field-order (#681)
ty(rules) Suppress no-matching-overload (#681)
ty(rules) Suppress invalid-return-type (#681)
ty(rules) Suppress not-subscriptable (#681)
ty(rules) Suppress call-top-callable (#681)