Investigated whether Astral's ty type checker's unused-awaitable rule catches
missing await calls that mypy strict misses.
ty isn't a dependency of this repo, but runs with no install step using
uvx ty (v0.0.65 as of 2026-07-29).
Ran uvx ty check --ignore all --error unused-awaitable, which isolates just this
rule. An unscoped ty check produces 100+ unrelated diagnostics from type-resolution
gaps with mypy/pyright and is too noisy to use as-is.
Results, scoped to pymongo/, gridfs/, bson/, test/:
- 0 hits in source (
pymongo/,gridfs/,bson/). - 16 hits, all in
test/asynchronous/(test_client.py,test_cursor.py,test_retryable_writes.py).
14 of the 16 are expected false positives. All are inside methods decorated
with @async_client_context.require_sync: plain def test methods that live in the
async source file but only run in the generated sync test file (just synchro
strips async/await). They're intentionally written in sync style and never
actually execute under the async test suite. Confirmed by checking require_sync's
implementation (test/asynchronous/__init__.py:792), which skips the test unless
_IS_SYNC is true.
2 of the 16 are real bugs, tracked in PYTHON-5975:
test/asynchronous/test_client.py(~line 1875): insidetest_reset_during_update_pool, a localResetPoolThread(threading.Thread)class definesasync def _run(self), but itsdef run(self): self._run()override never awaits it. The coroutine is created and discarded, so the thread's pool-reset loop never runs. The test still passes since it doesn't assert the reset happened concurrently, but it's silently not testing what it claims to.test/asynchronous/test_retryable_writes.py(~line 86):InsertEventListener.succeeded()(a plain sync callback) callsasync_client_context.client.admin.command(...)to set aconfigureFailPointwithout awaiting it, so the fail point is silently never configured.
Why mypy doesn't already catch these: mypy strict has an equivalent check,
unused-coroutine, enabled for test.asynchronous.* (mypy_test.ini). A minimal
repro confirmed mypy catches this bug shape when methods are fully annotated. But
mypy_test.ini disables no-untyped-def for test.*, and a second repro confirmed
that once the same methods are left unannotated (the norm for test code here), mypy
silently stops checking their bodies, missing the bug entirely. ty's
unused-awaitable check isn't blocked by missing annotations, so it catches what
mypy's config currently misses in test code.
- PYTHON-5975: fix the 2 real missing-await bugs above.
- Follow-up ticket ("Catch missing awaits in async code with ty"): wire up
ty check --ignore all --error unused-awaitableas an ongoing check, so future missing-awaits are caught automatically.
PYTHON-5975:
- Both real bugs are fixed, editing only
test/asynchronous/*(never the mirroredtest/*.py), withjust synchrore-run afterward. - Each affected test genuinely exercises its intended scenario.
Follow-up ticket:
- A
justrecipe (new, or folded intojust typing) runsty check --ignore all --error unused-awaitablescoped topymongo,gridfs,bson,test. - The 14 known
@require_syncfalse positives are suppressed (e.g.# ty: ignore[unused-awaitable]) without changing their intentionally sync-style bodies. - The check is wired into CI/pre-commit so future missing-awaits are caught automatically.
ty check --ignore all --error unused-awaitablepasses clean (0 unsuppressed diagnostics).
tyisn't a project dependency yet. Decide betweenuvx ty(no pin) or adding it to a dependency group before wiring into CI.- Keep the check scoped to
--ignore all --error unused-awaitable. A related rule,invalid-await, also fired (15 hits, including 2 in real source:pymongo/asynchronous/bulk.pyandcursor_base.py), but those overlap with existing# type: ignore[misc]suppressions and need separate, closer verification. Out of scope for both tickets. - Don't "fix" the 14
@require_synchits by addingawait. That's intentional dead code matching the generated sync test file; addingawaitwould breakjust synchrooutput. - The
ResetPoolThread.run()fix isn't just "add await":Thread.run()executes in a plain OS thread with no running event loop, so it needs a real fix (e.g. run the coroutine on a dedicated loop in that thread, or drop the rawThreadfor something loop-aware). - Once the failpoint in
InsertEventListener.succeeded()actually gets configured,test_retryable_writes.pymay exercise a new code path, so confirm it still passes and still tests what it intends. - Per AGENTS.md: never edit
pymongo/synchronous/*or mirroredtest/*.pyfiles directly. Editpymongo/asynchronous/*/test/asynchronous/*and regenerate withjust synchro.