ruff TC006: fix changes program behavior
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
Output
(no stdout) NameError: name 'MyType' is not defined
(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
--- 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.