fixhunt

ruff TC006: fix changes program behavior

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

Hypothesis Nemotron 3 Super predicted to fire fired 1×

If the type expression contains a forward reference that is only resolved during TYPE_CHECKING, quoting it is correct, but if the same expression appears in a runtime context elsewhere, the inconsiste

Program

before
1import typing
2y = typing.cast(MyType, [x]) # Rule triggers: MyType is not a string
after
1import typing
2y = typing.cast("MyType", [x]) # Rule triggers: MyType is not a string

Output

stdout beforeexit 1
(no stdout)
NameError: name 'MyType' is not defined
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 fix quotes the type expression 'MyType', changing it from a runtime-evaluated expression to a forward-reference string. This alters the evaluation order: originally 'MyType' was evaluated first (causing NameError on 'MyType'), but after the fix the second argument '[x]' is evaluated first (causing NameError on 'x'). The documentation only mentions safety regarding multi-line expressions and comments, not evaluation order changes, making this a silent behavior change in plausible code.

Explanation Nemotron 3 Super unverified

The original expression `typing.cast(MyType, [x])` uses the variable `MyType`, which must be defined in scope; if it isn’t, Python raises a `NameError`. After rewriting to `"MyType"`, the first argument becomes a literal string, so the cast succeeds syntactically but still attempts to evaluate `[x]`, requiring `x` to be defined—hence the error shifts from missing `MyType` to missing `x`. Since `cast` expects either a type object or a forward-reference string (used only during static analysis), replacing a required runtime identifier with a string changes when and how names are resolved, altering observable behavior despite preserving the annotation’s intent for type checkers. This violates semantic equivalence because execution now fails on a different undefined name.

Reproduce

file
prog.py (the before program)
minimized program
r2-p003.py
evidence sheet
r2-p003.md
regression fixture
r2-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 · typing cast first argument type annotation · ruff tc006 casting non-string literal · fix behavior quote string in cast

  • related astral-sh/ruff/issues/13208 Broad meta-issue about missing rules and overlapping rules for flake8-type-checking, which includes TC006 behavior.
  • related astral-sh/ruff/issues/20096 Discusses UP045 not triggering inside cast, which interacts with TC006 fixes on the same construct.
  • related astral-sh/ruff/issues/19596 Shows a chain of fixes (EM101 then TC006) on cast expressions, same rule but different trigger scenario.
  • related astral-sh/ruff/issues/16037 Infinite loop when fixing cast with format strings, same rule TC006 but different construct causing loop.
  • related astral-sh/ruff/issues/14676 TC006 being too broad and adding quotes unnecessarily, same rule but broader scope than this specific case.
  • related astral-sh/ruff/issues/1785 Original feature request to implement flake8-type-checking rules including TC006.
  • related astral-sh/ruff/issues/9573 About harmonizing TCH/TC rules with flake8-type-checking, includes TC006 scope.
  • related astral-sh/ruff/issues/14554 TC006 causing a panic/crash, same rule but different failure mode (crash vs behavior change).
  • related astral-sh/ruff/issues/26625 About TypeForm support for functions like typing.cast, same construct but forward-looking feature request.
  • related astral-sh/ruff/issues/20237 TC006 shouldn't trigger with from __future__ import annotations, same rule but different context (postponed evaluation).
  • related astral-sh/ruff/issues/4939 F401 false positive deleting import needed for cast at runtime, same construct (cast) but different rule interaction.
  • related astral-sh/ruff/issues/6650 Feature request about TYPE_CHECKING blocks and cast imports, same module context.
  • related astral-sh/ruff/pull/16669 PR stabilizing runtime-cast-value (TC006) among other things, directly addresses the rule.
  • related astral-sh/ruff/issues/18502 About adding future annotations import to help rules like TC006, same rule ecosystem.

Details

program
Forward reference in TYPE_CHECKING vs runtime
hazard
If the type expression contains a forward reference that is only resolved during TYPE_CHECKING, quoting it is correct, but if the same expression appears in a runtime context elsewhere, the inconsiste
oracle
stderr before: NameError: name 'MyType' is not defined stderr after: NameError: name 'x' is not defined
run
2026-10-17T20-41-40-602Z-mapj-ruff-TC006
generated program (8 lines) and the fix's diff
1from __future__ import annotations
2import typing
3if typing.TYPE_CHECKING:
4 from typing import List
5 MyType = List[int]
6x = 5
7y = typing.cast(MyType, [x]) # Rule triggers: MyType is not a string
8print(sum(y))
--- orig.py	2026-10-17 16:42:11.448711607 -0400
+++ fixed.py	2026-10-17 16:42:11.865726400 -0400
@@ -4,5 +4,5 @@
     from typing import List
     MyType = List[int]
 x = 5
-y = typing.cast(MyType, [x])  # Rule triggers: MyType is not a string
+y = typing.cast("MyType", [x])  # Rule triggers: MyType is not a string
 print(sum(y))
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.