Skip to content

Investigate ty type checker alongside mypy#682

Open
tony wants to merge 15 commits into
masterfrom
add-ty-type-checker
Open

Investigate ty type checker alongside mypy#682
tony wants to merge 15 commits into
masterfrom
add-ty-type-checker

Conversation

@tony
Copy link
Copy Markdown
Member

@tony tony commented May 24, 2026

Summary

Closes #681.

Adds ty (Astral's Rust-based Python type checker, beta v0.0.38) as a companion to mypy for evaluation. ty is 10-100x faster than mypy, checks unannotated function bodies by default, and supports 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

What's included

  • ty added to dev and lint dependency groups
  • [tool.ty] configuration in pyproject.toml (python-version 3.10, src/tests scope)
  • just ty and just watch-ty targets in justfile
  • Non-blocking ty CI step (continue-on-error: true)
  • 14 rule suppressions (one per commit, each documented with rationale and upstream issue links)

Rule suppressions

Each rule is suppressed in its own commit with a documented rationale. All are false positives from ty's beta-stage limitations.

Rule Count Root cause ty issue
unresolved-import 3 Private _pytest.python_api.RaisesContext not publicly exported #1276
too-many-positional-arguments 48 cmd() union-type resolution with *args — incomplete argument unpacking #404
missing-argument 26 Same *args/**kwargs unpacking limitation #785
unknown-argument 12 ty falls back to object.__init__ for union return types #2369
possibly-missing-submodule 14 monkeypatch.setattr(libtmux.common, ...) — implicit submodule via __init__.py #133
unsupported-operator 6 Vendored version.py tuple comparison with mixed types #1202
invalid-argument-type 20 **kwargs unpacking + LiteralString vs str narrowing #785
unresolved-attribute 5 Dict value iteration types as object, union narrowing gaps
invalid-assignment 4 TypeVar narrowing through isinstance, IO[str] | None unions
dataclass-field-order 3 Required field after defaults in inherited dataclasses
no-matching-overload 2 re.search overload with str | bytes union args
invalid-return-type 2 Dict subscript resolved as object, mock return types
not-subscriptable 1 Object subscript from narrowing path ty can't follow
call-top-callable 1 Intersection type produces uncallable Top type

Total: 147 diagnostics → 0 after all suppressions.

Speed comparison

ty completes in under 2 seconds on the full codebase — mypy takes ~10-15s.

Test plan

  • uv run ty check src tests — 0 diagnostics (all checks passed)
  • uv run mypy . — 69 files, no issues
  • uv run ruff check . && uv run ruff format . --check — all checks passed
  • uv run pytest --reruns 0 — 1258 passed, 2 skipped
  • just build-docs — builds successfully

tony added 7 commits May 24, 2026 11:01
why: Evaluate Astral's Rust-based ty type checker (beta) as a companion
to mypy. ty is 10-100x faster, checks unannotated function bodies by
default, and supports intersection types and reachability analysis.
libtmux is well-positioned: no mypy plugins, minimal type: ignore
comments, and already uses Astral's ruff and uv tooling.

what:
- Add ty to dev and lint dependency groups
- Add [tool.ty] configuration (python-version 3.10, src/tests scope)
- Add justfile targets: ty, watch-ty
- Add non-blocking ty CI step (continue-on-error: true)
why: All 3 unresolved-import errors target _pytest.python_api.RaisesContext,
a private pytest API that is not publicly exported. mypy flags these as
attr-defined (already suppressed with type: ignore comments); ty categorizes
them differently as unresolved-import. This is a known ty limitation with
private/internal module members.
what:
- Add unresolved-import = "ignore" to [tool.ty.rules]
- Reference: astral-sh/ty#1276
why: 48 false positives. ty resolves the cmd() method as a union type
that includes a (Any, Any, /) -> tmux_cmd variant from CmdProtocol,
limiting it to 2 positional args. The real signature accepts *args via
cmd(cmd: str, *args: Any, *, target=None). ty does not yet fully
support argument unpacking for *args methods (astral-sh/ty#404).
what:
- Add too-many-positional-arguments = "ignore" to [tool.ty.rules]
- Reference: astral-sh/ty#404
why: 26 false positives from the same *args/**kwargs unpacking
limitation. When calling with **kwargs, ty cannot statically verify
that required parameters are present and reports them as missing.
mypy and pyright handle **kwargs unpacking correctly.
what:
- Add missing-argument = "ignore" to [tool.ty.rules]
- Reference: astral-sh/ty#785
why: 12 false positives. ty falls back to object.__init__ when
resolving calls through union return types or dataclass-transform
decorators, then flags all keyword arguments as unknown. This affects
the cmd() method pattern used throughout libtmux.
what:
- Add unknown-argument = "ignore" to [tool.ty.rules]
- Reference: astral-sh/ty#2369
why: 14 warnings in tests using monkeypatch.setattr(libtmux.common, ...)
without an explicit import of libtmux.common. The submodule is always
available because libtmux.__init__.py re-exports it, but ty cannot
detect implicit submodule registration via __init__.py imports.
Neither mypy nor pyright flag this pattern.
what:
- Add possibly-missing-submodule = "ignore" to [tool.ty.rules]
- Reference: astral-sh/ty#133
why: 6 false positives, primarily in vendored _vendor/version.py which
uses tuple comparison with mixed-type elements (int, str, InfinityType)
for PEP 440 version ordering. ty cannot resolve element-wise comparison
operators across union-typed tuples. Also affects `in` operator checks
in tests.
what:
- Add unsupported-operator = "ignore" to [tool.ty.rules]
- Reference: astral-sh/ty#1202
@codecov
Copy link
Copy Markdown

codecov Bot commented May 24, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 51.29%. Comparing base (f70cb62) to head (ec6c440).

Additional details and impacted files
@@           Coverage Diff           @@
##           master     #682   +/-   ##
=======================================
  Coverage   51.29%   51.29%           
=======================================
  Files          25       25           
  Lines        3488     3488           
  Branches      686      686           
=======================================
  Hits         1789     1789           
  Misses       1404     1404           
  Partials      295      295           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

tony added 8 commits May 24, 2026 11:29
why: 20 false positives. ty cannot verify argument types through
**kwargs unpacking (11 from **call_kwargs in test_pane, 4 from
**filter_expr in test_query_list) and narrows LiteralString to str
differently than mypy (2 in test_options). Same root cause as the
missing-argument and unknown-argument suppressions.
what:
- Add invalid-argument-type = "ignore" to [tool.ty.rules]
- References: astral-sh/ty#785,
  astral-sh/ty#404
why: 5 false positives. ty doesn't narrow through dict value iteration
(item.split() in options.py where item is typed as object from dict
iteration) or union access patterns (pane_id on Pane | None in
test_client.py, already suppressed for mypy with type: ignore).
what:
- Add unresolved-attribute = "ignore" to [tool.ty.rules]
why: 4 false positives. ty cannot narrow TypeVars through isinstance
guards (subscript assignment on _V@convert_values in options.py after
isinstance(value, dict)), and flags IO[str] | None -> IO[str] in
control_mode.py (already suppressed for mypy with type: ignore).
what:
- Add invalid-assignment = "ignore" to [tool.ty.rules]
why: 3 false positives in Pane, Session, and Window classes. These
inherit from Obj which defines fields with defaults; the subclasses add
a required server: Server field. Python's dataclass inheritance handles
this correctly at runtime, but ty's dataclass analysis doesn't account
for the inheritance init pattern.
what:
- Add dataclass-field-order = "ignore" to [tool.ty.rules]
why: 2 false positives in query_list.py lookup_regex/lookup_iregex.
isinstance narrows data and rhs to str | bytes, but ty cannot resolve
re.search overloads for this union — it expects either (str, str) or
(bytes, Buffer), not (str | bytes, str | bytes).
what:
- Add no-matching-overload = "ignore" to [tool.ty.rules]
why: 2 false positives. options.py:1235 returns a dict subscript on a
complex nested type that ty resolves as object instead of the declared
str | int | None. test_session.py:567 returns MockTmuxCmd where
tmux_cmd is expected (already suppressed for mypy with type: ignore).
what:
- Add invalid-return-type = "ignore" to [tool.ty.rules]
why: 1 false positive in query_list.py:505. b[key] where b is typed as
object from a type narrowing path ty cannot follow — the same context
as the unresolved-attribute false positive on b.keys() nearby.
what:
- Add not-subscriptable = "ignore" to [tool.ty.rules]
why: 1 false positive in query_list.py:553. filter_(k) where filter_
is a union of callable types including T@QueryList & Top[(...) -> object].
ty's intersection type resolution produces an uncallable Top type from
the complex QueryList generic parameter.
what:
- Add call-top-callable = "ignore" to [tool.ty.rules]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Investigate ty type checker alongside mypy

1 participant