## Summary
Support narrowing for a few more already-supported sites, but in the
context of a walrus, as in:
```python
def f(t: tuple[int, int] | tuple[None, None]):
if (first := t[0]) is not None:
reveal_type(first) # int
reveal_type(t) # tuple[int, int]
else:
reveal_type(first) # None
reveal_type(t) # tuple[None, None]
```
## Summary
This unifies the validation of supported Python versions between the CLI
and TOML (e.g., `environment.python-version`) by introducing a single
enum to share across them.
## Summary
Like https://github.com/astral-sh/ruff/pull/24402, we want to ignore
unsupported Python versions that come from the editor. Instead, we'll
fall back to the default version (if there's no other configuration
set).
One nuance here is that we don't actively show the user a popup if we
ignore this version; we just use `tracing::warn!("{message}")`. It seems
undesirable to show a popup at the conversion site, since we'd then be
showing it even if the fallback version were never used. Is it desirable
to show a popup _ever_?
<!--
Thank you for contributing to Ruff/ty! To help us out with reviewing,
please consider the following:
- Does this pull request include a summary of the change? (See below.)
- Does this pull request include a descriptive title? (Please prefix
with `[ty]` for ty pull
requests.)
- Does this pull request include references to any relevant issues?
- Does this PR follow our AI policy
(https://github.com/astral-sh/.github/blob/main/AI_POLICY.md)?
-->
## Summary
Fixes#12953
F-string interpolation can call `__format__`/`__str__`/`__repr__`, which
may have side effects. RUF019 was applying a safe auto-fix that
collapsed two `__str__` calls into one, changing behavior.
Added a tri-state `SideEffect` enum (`No`/`Maybe`/`Yes`) and a
`side_effect()` function that reuses the existing `any_over_expr`
traversal via a new `FnMut` variant (`any_over_expr_mut`), following the
approach suggested by @ntBre in the other closed pr tagged in the issue
In RUF019, `SideEffect::Maybe` (non-literal f-string interpolation) now
produces an unsafe fix instead of a safe one. Literal interpolations
like `f"{1}"` remain safe.
## Test Plan
- Added f-string fixture cases to `RUF019.py` (non-literal → unsafe,
literal → safe, no interpolation → safe).
- `cargo nextest run -p ruff_linter`
- Ecosystem check (stable + preview)
Closes#19158
Implements the `useless_finally` rule (`RUF072`), which detects useless
`finally` blocks that only contain `pass` or `...`.
It handles two cases:
- `try/except/finally: pass` - the `finally` clause is removed, leaving
a valid `try/except`
- bare `try/finally: pass`: the entire `try/finally` is unwrapped, the
try body is dedented to replace the whole statement
Fix is skipped when comments are present in or around the `finally`
block.
It complements with existing rules like `RUF047` (`needless-else`) and
`SIM105` (`suppressible-exception`). It case of `SIM105` it also
unblocks this rule, as currently `SIM105` got skipped if `finally` has
any body at all (even just `pass`).
## Test Plan
- `RUF072.py` - main rule test with error cases and non-error.
- `useless_finally_and_needless_else` - test function, which checks how
`RUF047` and `RUF072` work together on the same `try` statement.
- `useless_finally_and_suppressible_exception` - test function, which
checks how `RUF072` and `SIM105` work together.
## Summary
Previously, `Parameters::visit_source_order` used `ParametersIterator`
which visits in type-based order. For well-formed Python code, this
matches source order; but error recovery can produce ASTs like `def
foo(**kwargs, a):`.
## Summary
`PatternArguments::visit_source_order()` visited all positional patterns
first, then all keyword patterns. But keyword patterns can appear before
positional patterns in the source text:
```python
case ast.Attribute(value=ast.Name(id), attr)
```
The visitor would process `attr` first, then `value=ast.Name(id)`.
We now visit in source order using the pattern established via
`arguments_source_order`.
Closes https://github.com/astral-sh/ty/issues/2417.
See [this
comment](https://github.com/astral-sh/ruff/pull/22470#issuecomment-3818540650)
from #22470. Also related to #6140.
## Summary
Add support for formatting code blocks with curly brace syntax (e.g.,
\`\`\`{python}, \`\`\`{py}, \`\`\`{pyi}) in Markdown files. This syntax
is commonly used in tools like Quarto and R Markdown.
The regex pattern now matches both the standard syntax (\`\`\`python)
and the curly brace variant (\`\`\`{python}).
## Test Plan
Added test cases.
Fix#22951
---------
Co-authored-by: Amethyst Reese <amethyst@n7.gg>
This multiplication was always using only using the larger sub-ast size
value, which didn't do anything unsound but made sub-string annotations
become exhausted on files 32x smaller than expected (16k nodes instead
of 512k nodes).
Also I decided to do a cleanup pass on the diagnostics to make them more
precise and helpful.
I ultimately hunted down the core issue with my previous approach to "if
you give string annotation sub-AST nodes any kind of NodeIndex, a bunch
of random places in the code will start thinking it's ok to store info
about them, which is a problem because they all count up from index 0
which creates conflicts/crashes".
Rather than trying to play whackamole I decided to create a scheme to
give string annotation nodes a unique NodeIndex by shifting up the
parent node's index -- so 0xAB's sub-AST nodes look like 0xAB00...0000,
0xAB00..0001, etc. This scheme avoids any collisions for any reasonable
AST (most string annotations are like, a dozen sub-nodes, so they need
maybe 4 or 5 bits, which would require hundreds of MB of python code to
run out of bits...).
As a bonus, this admits an extremely simple implementation of recording
and fetching sub-AST types... they just are stored now, and you can just
pass in their NodeIndex and get back actual results.
* Fixes https://github.com/astral-sh/ty/issues/1640
* Fixes https://github.com/astral-sh/ty/issues/2028
Adds initial support for formatting Python code blocks inside Markdown
files.
- Adds `Markdown` source types/kinds
- Maps `.md` file extension to `Markdown` by default
- Uses simple regex adapted from blacken-docs to find and format fenced
python code blocks
- Dedents contents before formatting, and reapplies indent from fenced
<code>```py</code> header
- Selects `Python` vs `Stub` options based on language label on code
block
- Silently skips formatting for any code block with syntax errors or
that produce formatting errors.
- CLI tests formatting via both stdin and from filesystem
- Requires running with `--preview`, and otherwise emits formatting
error when given a markdown file
- Requires a user to `extend-include = ["**/*.md"]` if they want to
format markdown files by default
Limitations:
- Returns a formatting error if run with a range of any sort
- Ignores implicit code blocks (no code fence)
- Doesn't yet support `~~~` fences, arbitrary fence lengths, or code
blocks inside blockquotes
Issue #3792
- Adds `Tokens::split_at()` to get tokens before/after an offset.
- Updates `Suppressions::load_from_tokens` to take an `Indexer` and use
comment ranges to minimize the need for walking tokens looking for
indent/dedent.
Adapted from
https://github.com/astral-sh/ruff/pull/21441#pullrequestreview-3503773083Fixes#22087
## Summary
I should have factored this better but this includes a drive-by move of
find_node to ruff_python_ast so ty_python_semantic can use it too.
* Fixes https://github.com/astral-sh/ty/issues/2017
## Test Plan
Snapshots galore
## Summary
Fixes false positives in SIM222 and SIM223 where truthiness was
incorrectly assumed for `tuple(x)`, `list(x)`, `set(x)` when `x` is not
iterable.
Fixes#21473.
## Problem
`Truthiness::from_expr` recursively called itself on arguments to
iterable initializers (`tuple`, `list`, `set`) without checking if the
argument is iterable, causing false positives for cases like `tuple(0)
or True` and `tuple("") or True`.
## Approach
Added `is_definitely_not_iterable` helper and updated
`Truthiness::from_expr` to return `Unknown` for non-iterable arguments
(numbers, booleans, None) and string literals when called with iterable
initializers, preventing incorrect truthiness assumptions.
## Test Plan
Added test cases to `SIM222.py` and `SIM223.py` for `tuple("")`,
`tuple(0)`, `tuple(1)`, `tuple(False)`, and `tuple(None)` with `or True`
and `and False` patterns.
---------
Co-authored-by: Brent Westbrook <brentrwestbrook@gmail.com>
## Summary
Fixed RUF065 (`logging-eager-conversion`) to only flag `str()` calls
when they perform a simple conversion that can be safely removed. The
rule now ignores `str()` calls with no arguments, multiple arguments,
starred arguments, or keyword unpacking, preventing false positives.
Fixes#21315
## Problem Analysis
The RUF065 rule was incorrectly flagging all `str()` calls in logging
statements, even when `str()` was performing actual conversion work
beyond simple type coercion. Specifically, the rule flagged:
- `str()` with no arguments - which returns an empty string
- `str(b"data", "utf-8")` with multiple arguments - which performs
encoding conversion
- `str(*args)` with starred arguments - which unpacks arguments
- `str(**kwargs)` with keyword unpacking - which passes keyword
arguments
These cases cannot be safely removed because `str()` is doing meaningful
work (encoding conversion, argument unpacking, etc.), not just redundant
type conversion.
The root cause was that the rule only checked if the function was
`str()` without validating the call signature. It didn't distinguish
between simple `str(value)` conversions (which can be removed) and more
complex `str()` calls that perform actual work.
## Approach
The fix adds validation to the `str()` detection logic in
`logging_eager_conversion.rs`:
1. **Check argument count**: Only flag `str()` calls with exactly one
positional argument (`str_call_args.args.len() == 1`)
2. **Check for starred arguments**: Ensure the single argument is not
starred (`!str_call_args.args[0].is_starred_expr()`)
3. **Check for keyword arguments**: Ensure there are no keyword
arguments (`str_call_args.keywords.is_empty()`)
This ensures the rule only flags cases like `str(value)` where `str()`
is truly redundant and can be removed, while ignoring cases where
`str()` performs actual conversion work.
The fix maintains backward compatibility - all existing valid test cases
continue to be flagged correctly, while the new edge cases are properly
ignored.
---------
Co-authored-by: Brent Westbrook <brentrwestbrook@gmail.com>
## Summary
Bump the latest supported Python version of ty to 3.14 and updates some
references from 3.13 to 3.14.
This also fixes a bug with `dataclasses.field` on 3.14 (which adds a new
keyword-only parameter to that function, breaking our previously naive
matching on the parameter structure of that function).
## Test Plan
A `ty check` on a file with template strings (without any further
configuration) doesn't raise errors anymore.
Summary
--
Closes#19467 and also removes the warning about using Python 3.14
without
preview enabled.
I also bumped `PythonVersion::default` to 3.9 because it reaches EOL
this month,
but we could also defer that for now if we wanted.
The first three commits are related to the `latest` bump to 3.14; the
fourth commit
bumps the default to 3.10.
Note that this PR also bumps the default Python version for ty to 3.10
because
there was a test asserting that it stays in sync with
`ast::PythonVersion`.
Test Plan
--
Existing tests
I spot-checked the ecosystem report, and I believe these are all
expected. Inbits doesn't specify a target Python version, so I guess
we're applying the default. UP007, UP035, and UP045 all use the new
default value to emit new diagnostics.
## Summary
Modify the (external) signature of instance methods such that the first
parameter uses `Self` unless it is explicitly annotated. This allows us
to correctly type-check more code, and allows us to infer correct return
types for many functions that return `Self`. For example:
```py
from pathlib import Path
from datetime import datetime, timedelta
reveal_type(Path(".config") / ".ty") # now Path, previously Unknown
def _(dt: datetime, delta: timedelta):
reveal_type(dt - delta) # now datetime, previously Unknown
```
part of https://github.com/astral-sh/ty/issues/159
## Performance
I ran benchmarks locally on `attrs`, `freqtrade` and `colour`, the
projects with the largest regressions on CodSpeed. I see much smaller
effects locally, but can definitely reproduce the regression on `attrs`.
From looking at the profiling results (on Codspeed), it seems that we
simply do more type inference work, which seems plausible, given that we
now understand much more return types (of many stdlib functions). In
particular, whenever a function uses an implicit `self` and returns
`Self` (without mentioning `Self` anywhere else in its signature), we
will now infer the correct type, whereas we would previously return
`Unknown`. This also means that we need to invoke the generics solver in
more cases. Comparing half a million lines of log output on attrs, I can
see that we do 5% more "work" (number of lines in the log), and have a
lot more `apply_specialization` events (7108 vs 4304). On freqtrade, I
see similar numbers for `apply_specialization` (11360 vs 5138 calls).
Given these results, I'm not sure if it's generally worth doing more
performance work, especially since none of the code modifications
themselves seem to be likely candidates for regressions.
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|:---|---:|---:|---:|---:|
| `./ty_main check /home/shark/ecosystem/attrs` | 92.6 ± 3.6 | 85.9 |
102.6 | 1.00 |
| `./ty_self check /home/shark/ecosystem/attrs` | 101.7 ± 3.5 | 96.9 |
113.8 | 1.10 ± 0.06 |
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|:---|---:|---:|---:|---:|
| `./ty_main check /home/shark/ecosystem/freqtrade` | 599.0 ± 20.2 |
568.2 | 627.5 | 1.00 |
| `./ty_self check /home/shark/ecosystem/freqtrade` | 607.9 ± 11.5 |
594.9 | 626.4 | 1.01 ± 0.04 |
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|:---|---:|---:|---:|---:|
| `./ty_main check /home/shark/ecosystem/colour` | 423.9 ± 17.9 | 394.6
| 447.4 | 1.00 |
| `./ty_self check /home/shark/ecosystem/colour` | 426.9 ± 24.9 | 373.8
| 456.6 | 1.01 ± 0.07 |
## Test Plan
New Markdown tests
## Ecosystem report
* apprise: ~300 new diagnostics related to problematic stubs in apprise
😩
* attrs: a new true positive, since [this
function](https://github.com/python-attrs/attrs/blob/4e2c89c82398914dbbe24fe00b03f00a8b8efe05/tests/test_make.py#L2135)
is missing a `@staticmethod`?
* Some legitimate true positives
* sympy: lots of new `invalid-operator` false positives in [matrix
multiplication](https://github.com/sympy/sympy/blob/cf9f4b680520821b31e2baceb4245252939306be/sympy/matrices/matrixbase.py#L3267-L3269)
due to our limited understanding of [generic `Callable[[Callable[[T1,
T2], T3]], Callable[[T1, T2], T3]]` "identity"
types](https://github.com/sympy/sympy/blob/cf9f4b680520821b31e2baceb4245252939306be/sympy/core/decorators.py#L83-L84)
of decorators. This is not related to type-of-self.
## Typing conformance results
The changes are all correct, except for
```diff
+generics_self_usage.py:50:5: error[invalid-assignment] Object of type `def foo(self) -> int` is not assignable to `(typing.Self, /) -> int`
```
which is related to an assignability problem involving type variables on
both sides:
```py
class CallableAttribute:
def foo(self) -> int:
return 0
bar: Callable[[Self], int] = foo # <- we currently error on this assignment
```
---------
Co-authored-by: Shaygan Hooshyari <sh.hooshyari@gmail.com>
- Adds test cases exercising file selection by extension with
`--preview` enabled and disabled.
- Adds `INCLUDE_PREVIEW` with file patterns including `*.pyw`.
- In global preview mode, default configuration selects patterns from
`INCLUDE_PREVIEW`.
- Manually tested ruff server with local vscode for both formatting and
linting of a `.pyw` file.
Closes https://github.com/astral-sh/ruff/issues/13246
Our token-based rules and `noqa` extraction used an `Indexer` that kept
track of f-string ranges but not t-strings. We've updated the `Indexer`
and downstream uses thereof to handle both f-strings and t-strings.
Most of the diff is renaming and adding tests.
Note that much of the "new" logic gets to be naive because the lexer has
already ensured that f and t-string "starts" are paired with their
respective "ends", even amidst nesting and so on.
Finally: one could imagine wanting to know if a given interpolated
string range corresponds to an f-string or a t-string, but I didn't find
a place where we actually needed this.
Closes#20310