Investigate ty type checker alongside mypy#682
Open
tony wants to merge 15 commits into
Open
Conversation
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 Report✅ All modified and coverable lines are covered by tests. 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. 🚀 New features to boost your workflow:
|
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]
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
type: ignorecomments across the codebaseWhat's included
devandlintdependency groups[tool.ty]configuration inpyproject.toml(python-version 3.10, src/tests scope)just tyandjust watch-tytargets in justfilecontinue-on-error: true)Rule suppressions
Each rule is suppressed in its own commit with a documented rationale. All are false positives from ty's beta-stage limitations.
unresolved-import_pytest.python_api.RaisesContextnot publicly exportedtoo-many-positional-argumentscmd()union-type resolution with*args— incomplete argument unpackingmissing-argument*args/**kwargsunpacking limitationunknown-argumentobject.__init__for union return typespossibly-missing-submodulemonkeypatch.setattr(libtmux.common, ...)— implicit submodule via__init__.pyunsupported-operatorinvalid-argument-type**kwargsunpacking +LiteralStringvsstrnarrowingunresolved-attributeobject, union narrowing gapsinvalid-assignmentIO[str] | Noneunionsdataclass-field-orderno-matching-overloadre.searchoverload withstr | bytesunion argsinvalid-return-typeobject, mock return typesnot-subscriptablecall-top-callableToptypeTotal: 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 issuesuv run ruff check . && uv run ruff format . --check— all checks passeduv run pytest --reruns 0— 1258 passed, 2 skippedjust build-docs— builds successfully