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

4 participants