fixhunt

ruff TC008: fix changes program behavior

fix-changes-behaviorlabelled safereal · mediumno match
Findings / 2026-10-17T20-45-09-298Z-olks-ruff-TC008 / r1-p005ruff 0.17.14 → 3 lines2026-10-17 20:45 UTC

Hypothesis Nemotron 3 Super predicted not to fire fired 1×

Rule does not rewrite '"int" "|" "None"' (concatenated strings), so alias remains quoted; if developer expects it to be unquoted, they may misuse it.

Program

before
1from typing import TypeAlias
2BadAlias: TypeAlias = "int" "|" "None"
3print(repr(BadAlias))
after
1from typing import TypeAlias
2BadAlias: TypeAlias = int|None
3print(repr(BadAlias))

Output

stdout beforeexit 0
'int|None'
stdout afterexit 0
int | None
fired
1 before · 0 after
runs
each version twice, same file name, no network

Judge Nemotron 3 Ultra advisory real · medium

The rule removes quotes from a PEP 613 type alias that uses concatenated string literals ('int' '|' 'None'), rewriting it to the unquoted union int|None. At runtime, the original alias evaluates to the string 'int|None' (no spaces), while the fixed alias evaluates to the types.Union object int | None (repr shows spaces). This changes the program's observable behavior: the printed representation differs, and any code inspecting the alias value would see a string vs a union object. The documentation presents the rewrite as removing unnecessary quotes but does not warn that concatenated string literals produce a different runtime value than the unquoted union, making the fix unsafe for this construct.

Explanation Nemotron 3 Super unverified

The original string literal `"int" \| \"None\"` evaluates to the exact text `int|None`, which prints as `'int|None'`. Replacing it with the type expression `int|None` creates a union type object whose `__repr__` yields `int | None` (spaces added by Python’s formatting). Since a string and a type are distinct objects with different representations, the change alters observable output, violating semantic equivalence.

Reproduce

file
prog.py (the before program)
minimized program
r1-p005.py
evidence sheet
r1-p005.md
regression fixture
r1-p005.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 TC008 --fix prog.py
python3 prog.py

Upstream search GitHub issues Tavily Nemotron 3 Super + Ultra

TC008 fix · TC008 TypeAlias string union · TypeAlias literal pipe syntax · ruff TC008 quoted types fix

no duplicate or related report 1 hits triaged

Details

program
String Concatenation Inside Quotes Left Unchanged
hazard
Rule does not rewrite '"int" "|" "None"' (concatenated strings), so alias remains quoted; if developer expects it to be unquoted, they may misuse it.
oracle
stdout before: 'int|None' stdout after: int | None
run
2026-10-17T20-45-09-298Z-olks-ruff-TC008
generated program (4 lines) and the fix's diff
1from typing import TypeAlias
2BadAlias: TypeAlias = "int" "|" "None"
3print(type(BadAlias))
4print(repr(BadAlias))
--- orig.py	2026-10-17 16:45:19.800591576 -0400
+++ fixed.py	2026-10-17 16:45:20.001597096 -0400
@@ -1,4 +1,4 @@
 from typing import TypeAlias
-BadAlias: TypeAlias = "int" "|" "None"
+BadAlias: TypeAlias = int|None
 print(type(BadAlias))
 print(repr(BadAlias))
rule documentation given to the models
# quoted-type-alias (TC008)

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

Fix is always available.

This rule is in preview and is not stable. The `--preview` flag is required for use.

## What it does
Checks for unnecessary quotes in [PEP 613] explicit type aliases
and [PEP 695] type statements.

## Why is this bad?
Unnecessary string forward references can lead to additional overhead
in runtime libraries making use of type hints. They can also have bad
interactions with other runtime uses like [PEP 604] type unions.

PEP-613 type aliases are only flagged by the rule if Ruff can have high
confidence that the quotes are unnecessary. Specifically, any PEP-613
type alias where the type expression on the right-hand side contains
subscripts or attribute accesses will not be flagged. This is because
type aliases can reference types that are, for example, generic in stub
files but not at runtime. That can mean that a type checker expects the
referenced type to be subscripted with type arguments despite the fact
that doing so would fail at runtime if the type alias value was not
quoted. Similarly, a type alias might need to reference a module-level
attribute that exists in a stub file but not at runtime, meaning that
the type alias value would need to be quoted to avoid a runtime error.

## Example
Given:
```python
from typing import TypeAlias

OptInt: TypeAlias = "int | None"
```

Use instead:
```python
from typing import TypeAlias

OptInt: TypeAlias = int | None
```

Given:
```python
type OptInt = "int | None"
```

Use instead:
```python
type OptInt = int | None
```

## Fix safety
This rule's fix is marked as safe, unless the type annotation contains comments.

## See also
This rule only applies to type aliases in non-stub files. For removing quotes in other
contexts or in stub files, see:

- [`quoted-annotation-in-stub`][PYI020]: A rule that
  removes all quoted annotations from stub files
- [`quoted-annotation`][UP037]: A rule that removes unnecessary quotes
  from *annotations* in runtime files.

## References
- [PEP 613 – Explicit Type Aliases](https://peps.python.org/pep-0613/)
- [PEP 695: Generic Type Alias](https://peps.python.org/pep-0695/#generic-type-alias)
- [PEP 604 – Allow writing union types as `X | Y`](https://peps.python.org/pep-0604/)

[PEP 604]: https://peps.python.org/pep-0604/
[PEP 613]: https://peps.python.org/pep-0613/
[PEP 695]: https://peps.python.org/pep-0695/#generic-type-alias
[PYI020]: https://docs.astral.sh/ruff/rules/quoted-annotation-in-stub/
[UP037]: https://docs.astral.sh/ruff/rules/quoted-annotation/