Skip to content

Fix annotated assignment expressions being treated as unpacking targets - #563

Merged
agronholm merged 3 commits into
agronholm:masterfrom
dchaudhari7177:fix/annotated-walrus-instrumentation
Jul 19, 2026
Merged

Fix annotated assignment expressions being treated as unpacking targets#563
agronholm merged 3 commits into
agronholm:masterfrom
dchaudhari7177:fix/annotated-walrus-instrumentation

Conversation

@dchaudhari7177

Copy link
Copy Markdown
Contributor

Summary

Fixes #557. visit_NamedExpr wrapped its single target in an extra list, emitting

check_variable_assignment(otherfunc(), [[('x', int)]], memo)

where the plain-assignment path emits [('x', int)] for a single name. check_variable_assignment reads a list group as a tuple-unpacking target and a tuple group as a single target, so an annotated walrus took the unpacking branch and consumed the assigned value with list(value).

Two wrong outcomes, both only when the name carries an annotation:

assigned value before after
5 TypeError: 'int' object is not iterable 5
None TypeError: 'NoneType' object is not iterable None
"abc" no error, x becomes ['a', 'b', 'c'] "abc"
"not an int" (wrong type) TypeCheckError TypeCheckError (unchanged)

The third row is the dangerous one — no exception, just a silently corrupted variable.

A walrus target is always a single Name, so it now emits a bare tuple like the assignment path does. Type checking itself is unaffected: a genuinely wrong value still raises TypeCheckError.

Tests

New TestAssignmentExpression in test_typechecked.py covers the behaviour rather than the emitted source: non-iterable value, iterable value not consumed, an annotated argument reused in a walrus, an iterator not drained, and a wrong-typed value still raising.

Reverting only src/ fails 4 of the 5 (the 5th passes either way, since it asserts the error that already worked):

FAILED TestAssignmentExpression::test_non_iterable_value
FAILED TestAssignmentExpression::test_value_is_not_consumed
FAILED TestAssignmentExpression::test_annotated_argument
FAILED TestAssignmentExpression::test_iterator_value_is_not_consumed

test_transformer.py::TestAssign::test_assignment_expr and test_assignment_expr_annotated_argument asserted the old [[('x', int)]] output, i.e. they encoded the defect, so they now expect the single-target form. Worth noting those two golden tests passed throughout the bug's lifetime — pinning emitted source didn't catch it, which is why the new tests assert runtime behaviour instead.

Verification

  • Full suite (--ignore=tests/mypy) → 516 passed, 7 skipped, 9 xfailed
  • ruff check src tests → all checks passed; ruff format --check → 31 files already formatted
  • Added an UNRELEASED entry to docs/versionhistory.rst

tests/mypy is excluded above: both tests there fail with FileNotFoundError on a clean tree too (no mypy binary in my environment), unrelated to this change.

visit_NamedExpr wrapped its single target in an extra list, emitting
[[('x', int)]] where the plain assignment path emits [('x', int)] for a
single name. check_variable_assignment reads a list group as a
tuple-unpacking target, so the walrus took the unpacking branch and
consumed the assigned value with list().

That produced two wrong outcomes for a name that carries an annotation:
a non-iterable value raised TypeError ('int' object is not iterable),
and an iterable value raised nothing but was silently replaced, so
x: str = 'abc' left x as ['a', 'b', 'c'].

A walrus target is always a single Name, so emit a bare tuple like the
assignment path does. Type checking itself is unaffected: a genuinely
wrong value still raises TypeCheckError.

The two transformer tests asserting the old output encoded this defect,
so they now expect the single-target form.

Closes agronholm#557
@coveralls

coveralls commented Jul 18, 2026

Copy link
Copy Markdown

Coverage Status

coverage: 94.847%. remained the same — dchaudhari7177:fix/annotated-walrus-instrumentation into agronholm:master

Comment thread src/typeguard/_transformer.py Outdated
@dchaudhari7177

Copy link
Copy Markdown
Contributor Author

Removed, thanks — the reasoning lives in the PR description instead.

For reference, the emitted call goes from [[('x', int)]] to [('x', int)]: the inner list marked the group as an unpacking target, so check_variable_assignment consumed the value with list().

pytest tests/test_transformer.py still passes (84 passed, 1 skipped). The 2 failures in the full run are tests/mypy/ looking for a mypy binary that isn't installed here.

@agronholm
agronholm merged commit 32e7c85 into agronholm:master Jul 19, 2026
10 checks passed
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.

Mishandled of annotated walrus operator

4 participants