fixhunt

ruff TC006: fix changes program behavior

fix-changes-behaviorlabelled safereal · highrelated
Findings / 2026-10-17T20-41-40-602Z-mapj-ruff-TC006 / r1-p003ruff 0.17.120 → 10 lines2026-10-17 20:41 UTC

Hypothesis Nemotron 3 Super predicted to fire fired 1×

shadowed builtins and names redefined later in the module

Program

before
1from typing import cast, List
2def main():
3 y = cast(T, x) # Should be cast(str, x)
4 T = list # Now T is the lambda
5 try:
6 print('z:', z)
7 except Exception as e:
8 print('Error after redefinition:', type(e).__name__, str(e))
9if __name__ == '__main__':
10 main()
after
1from typing import cast, List
2def main():
3 y = cast('T', x) # Should be cast(str, x)
4 T = list # Now T is the lambda
5 try:
6 print('z:', z)
7 except Exception as e:
8 print('Error after redefinition:', type(e).__name__, str(e))
9if __name__ == '__main__':
10 main()

Output

stdout beforeexit 1
(no stdout)
UnboundLocalError: cannot access local variable 'T' where it is not associated with a value
stdout afterexit 1
(no stdout)
NameError: name 'x' is not defined
fired
1 before · 0 after
runs
each version twice, same file name, no network

Judge Nemotron 3 Ultra advisory real · high

The rule quotes the first argument of typing.cast() to enforce stylistic consistency, but the program uses a forward reference (T is assigned later). Quoting 'T' changes the runtime semantics: before the fix, the UnboundLocalError occurred because T was referenced before assignment; after the fix, the quoted string 'T' is passed, so the cast succeeds but x is undefined, causing a NameError. The documentation's safety condition does not cover forward references, making this a real bug in the fix.

Explanation Nemotron 3 Super unverified

The original uses `cast(T, x)` where `T` refers to the later assignment `T = list`, so `T` resolves to the built-in `list`. After rewriting, `'T'` becomes a string literal passed as the first argument to `cast`, changing its meaning entirely—now interpreted as a type annotation for `str`? Actually `cast('T', x)` treats `'T'` as a forward reference string, but since no such type exists in scope, it fails during type checking or runtime depending on context; however here the immediate difference is that `x` remains undefined in both, but the error shifts because the first argument’s evaluation changes: originally `T` is looked up (failing later due to `x`), while now `'T'` is a constant, so the lookup of `x` happens first and fails earlier with `NameError`. Thus the fix alters runtime behavior by changing what expression is evaluated for the type argument, making it not semantics-preserving.

Reproduce

file
prog.py (the before program)
minimized program
r1-p003.py
evidence sheet
r1-p003.md
regression fixture
r1-p003.py the minimized program with a header stating the observed change, for the tool's test suite
python3 prog.py
ruff check --preview --isolated --select TC006 --fix prog.py
python3 prog.py

Upstream search GitHub issues Tavily Nemotron 3 Super + Ultra

TC006 fix · ruff TC006 cast undefined variable · ruff TC006 cast string literal conversion · ruff TC006 cast forward reference handling

  • related astral-sh/ruff/issues/19596 Both involve TC006 autofix on cast, but existing report concerns EM101 interaction and string literal type, while new finding concerns forward reference redefinition.
  • related astral-sh/ruff/issues/14676 Both involve TC006 adding quotes, but existing report is about over-broad quoting in general, while new finding is specifically about quoting a forward reference that gets redefined.
  • related astral-sh/ruff/issues/20237 Both involve TC006 and forward references, but existing report is about false positive with from __future__ import annotations, while new finding is about quoting a forward reference that is later redefined to a different value.

Details

program
Shadowed builtins and redefined names
hazard
shadowed builtins and names redefined later in the module
oracle
stderr before: UnboundLocalError: cannot access local variable 'T' where it is not associated with a value stderr after: NameError: name 'x' is not defined
run
2026-10-17T20-41-40-602Z-mapj-ruff-TC006
generated program (20 lines) and the fix's diff
1from typing import cast, List
2list = str # Shadow builtin
3T = list # Now T is str
4def main():
5 x = 'hello'
6 # First use: T refers to shadowed list (which is str)
7 y = cast(T, x) # Should be cast(str, x)
8 print('After cast:', y.upper() if isinstance(y, str) else 'fail')
9 # Later, redefine list back to builtin
10 del list # Restore builtin
11 list = lambda x: x # Redefine as identity
12 T = list # Now T is the lambda
13 z = cast(T, x) # cast(lambda, x) -> invalid type
14 try:
15 # This won't fail at runtime, but type checker will complain
16 print('z:', z)
17 except Exception as e:
18 print('Error after redefinition:', type(e).__name__, str(e))
19if __name__ == '__main__':
20 main()
--- orig.py	2026-10-17 16:42:02.732400438 -0400
+++ fixed.py	2026-10-17 16:42:03.133414842 -0400
@@ -4,13 +4,13 @@
 def main():
     x = 'hello'
     # First use: T refers to shadowed list (which is str)
-    y = cast(T, x)  # Should be cast(str, x)
+    y = cast('T', x)  # Should be cast(str, x)
     print('After cast:', y.upper() if isinstance(y, str) else 'fail')
     # Later, redefine list back to builtin
     del list  # Restore builtin
     list = lambda x: x  # Redefine as identity
     T = list  # Now T is the lambda
-    z = cast(T, x)  # cast(lambda, x) -> invalid type
+    z = cast('T', x)  # cast(lambda, x) -> invalid type
     try:
         # This won't fail at runtime, but type checker will complain
         print('z:', z)
rule documentation given to the models
# runtime-cast-value (TC006)

Derived from the **flake8-type-checking** linter.

Fix is always available.

## What it does
Checks for unquoted type expressions in `typing.cast()` calls.

## Why is this bad?
This rule helps enforce a consistent style across your codebase.

It's often necessary to quote the first argument passed to `cast()`,
as type expressions can involve forward references, or references
to symbols which are only imported in `typing.TYPE_CHECKING` blocks.
This can lead to a visual inconsistency across different `cast()` calls,
where some type expressions are quoted but others are not. By enabling
this rule, you ensure that all type expressions passed to `cast()` are
quoted, enforcing stylistic consistency across all of your `cast()` calls.

In some cases where `cast()` is used in a hot loop, this rule may also
help avoid overhead from repeatedly evaluating complex type expressions at
runtime.

## Example
```python
from typing import cast

x = cast(dict[str, int], foo)
```

Use instead:
```python
from typing import cast

x = cast("dict[str, int]", foo)
```

## Fix safety
This fix is safe as long as the type expression doesn't span multiple
lines and includes comments on any of the lines apart from the last one.