Do not erase a meaningful empty tuple[()] subscript in the transformer - #565
Merged
Merged
Conversation
The AST transformer erased a subscript when all its items resolved to Any, using `all(item is None for item in items)`. For `tuple[()]` the slice is an empty tuple, so `items` is empty and `all([])` is vacuously True — the transformer rewrote `tuple[()]` down to bare `tuple`, discarding the empty-tuple constraint. Both the import hook and @TypeChecked (which share the transformer) then accepted any tuple for a `tuple[()]` annotation, while check_type — which does not go through the transformer — correctly rejected it. Guard the erasure with `items and ...` so an empty subscript is preserved. Signed-off-by: chuenchen309 <48723787+chuenchen309@users.noreply.github.com>
agronholm
reviewed
Jul 20, 2026
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
The empty-tuple annotation
tuple[()]is silently erased to baretupleby the AST transformer, so the import hook and@typechecked(which share the transformer) wrongly accept any tuple for atuple[()]parameter.check_type— which does not go through the transformer — correctly rejects it. Two of typeguard's three checking paths disagree with the third, and the third is the correct one.check_type@typechecked(source annotation)Root cause
src/typeguard/_transformer.py,AnnotationTransformer.visit_Subscript:For
tuple[()]the AST slice isTuple(elts=[]), soitems == []andall([])is vacuouslyTrue— the transformer rewritestuple[()]totuple, dropping the constraint. (The instrumented source confirms it: the annotation is emitted as{'x': (x, tuple)}with the[()]gone.) typeguard's checker itself has explicit empty-tuple support (_checkers.py, "is not an empty tuple"), so the intended verdict is unambiguous.Fix
Guard the erasure so it only fires for a non-empty all-
Anysubscript:Genuine all-
Anyerasure (e.g.dict[Any, Any]) is unchanged.Verification
test_empty_tuple/test_empty_tuple_failintests/test_instrumentation.pyrun under both thetypecheckedandimporthookparametrizations. On the unfixed treetest_empty_tuple_failfails for both (a non-empty tuple is wrongly accepted); with the fix all 4 pass.test_transformer.py test_instrumentation.py test_checkers.py test_typechecked.py: 495 passed, 7 skipped, 9 xfailed, no failures. Fulltests/: 523 passed (the only 2 failures aretests/mypy/FileNotFoundError: 'mypy'— mypy not installed in my env, unrelated).docs/versionhistory.rst.This PR was authored by an AI coding agent (Claude Code) running on this account: the AI found the bug, ran the repro, wrote the tests, and wrote this description. The human account holder reviews every change and is accountable for it. The verification above is real and re-runnable from the diff. If this isn't the kind of contribution you want, say so and I'll close it.