This is a partial solution for #16412
I would still like to add something like
`runtime-evaluated-generic-subscripts`, but that will require further
consideration about how the presence of such subscripts should affect
the rest of the type expression, or in more extreme cases, the other
annotations in the function/class definition.
## Summary
For things like SQLAlchemy's `Mapped` the evaluation of the annotations
only requires a subset of all the symbols to be resolvable at runtime,
this is sometimes necessary to make forward references to other models
work without causing import cycles.
But this means we can neither assume that the annotations are
runtime-required, nor can we assume, that they are typing-only. So we
should assume the referenced binding is already in its correct execution
context, i.e. we should neither emit TC001-003, nor TC004 violations.
Instead of adding two additional settings now and potentially two more
settings later, the existing two settings for runtime-required
annotations now accept a table in addition to the list to flag
individual imports with individual runtime requirements.
For example:
```toml
[lint.flake8-type-checking.runtime-evaluated-base-classes]
"pydantic.BaseModel" = "required"
"sqlalchemy.orm.DeclarativeBase" = "ambiguous"
[lint.flake8-type-checking.runtime-evaluated-decorators]
"pydantic.validate_call" = "required"
"sqlalchemy.orm.declared_attr" = "ambiguous"
```
## Test Plan
`cargo nextest run`
Summary
--
`typing.TypeForm` was introduced in Python 3.15 and [PEP
747](https://peps.python.org/pep-0747/) as
a way to annotate the passing of type objects like `int | str`,
`list[int]`, or `MyTypeAlias`. It
can be used either as a subscripted generic, as in this example from the
PEP:
```py
from typing import TypeForm
def trycast[T](typx: TypeForm[T], value: object) -> T | None: ...
```
or as a callable, which returns its argument unchanged:
```py
from typing import TypeForm
TypeForm("list[int]")
```
This PR recognizes `typing.TypeForm` as a standard-library generic and
as a known `typing` callable
and visits its argument or subscript as a type definition.
While I was reading the [typing docs], I realized that a couple of
nearby types were also not
listed, so I've attempted to sneak them in here but could split them off
if needed. We already had
coverage for `typing.TypeGuard` but not `typing_extensions.TypeGuard`
(added in 3.10), and we were
missing both variants of `TypeIs` (added in 3.13). These are only valid
in the subscript form, so
they don't need new `Callable` entries.
[typing docs]:
https://docs.python.org/3.15/library/typing.html#typing.TypeForm
Test Plan
--
New mdtests, using `unused-import` (`F401`) to demonstrate that visiting
these arguments as type
definitions marks them as used, but other rules will be affected as
well.
Summary
--
This is a follow-up to #28459 adding simple support for conditionally
defining `__lazy_modules__`. We now track per-module laziness, which
allows us to merge definitions like:
```py
__lazy_modules__ = ["foo"]
if condition:
__lazy_modules__ = ["foo", "bar"]
```
into known laziness for `foo` and unknown laziness for `bar`. As in
#28459, that unknown laziness is still helpful and distinct from known
eagerness because it suppresses diagnostics.
The one quirk with the `branch_id` tracking used here is that it doesn't
handle exhaustive branches. For example, it would be nice if we noticed
that `foo` is always lazy here:
```py
if condition:
__lazy_modules__ = ["foo", "bar"]
else:
__lazy_modules__ = ["foo", "baz"]
```
but it's instead merged with the implicit known eagerness of all imports
before any `__lazy_modules__` assignment, producing `(foo, Unknown)`. So
this is effectively equivalent to the version without an `else` for
`foo`.
I think this is a decent tradeoff that biases us against false
positives, though, and doesn't involve any extra bookkeeping in `if`
statement traversal.
One of my three Codex reviewers also identified this as a false
negative, where the assignment and
use are in the same branch, but we've already marked the module as
`Unknown`:
```py
if condition:
__lazy_modules__ = ["json"]
import json
json.dumps({})
```
But again we're at least biased in the right direction, and we'd need
more bookkeeping to identify
this, so I was okay accepting that for now.
Test Plan
--
New mdtests for TID254 that exercise this behavior
Summary
--
For a bit of background, `__lazy_modules__` is a backwards compatibility
mechanism for lazy imports, introduced in [PEP
810](https://peps.python.org/pep-0810/) alongside the `lazy` keyword
itself. Instead of declaring an import `lazy` with the keyword, it can
also be listed in a module-level constant named `__lazy_modules__`:
```py
lazy import foo
# equivalently
__lazy_modules__ = ["foo"]
import foo
```
This can be used in projects that support older Python versions, as well
as 3.15.
It also supports more dynamic approaches (that this PR doesn't), as
noted in one of my inline comments, such as:
```py
class LazyImporter:
def __contains__(self, name): return True
__lazy_modules__ = LazyImporter()
```
As one example from the PEP for emulating wildcard lazy imports.
<hr>
The PR is pretty big in total, but it's intended to be reviewed
commit-by-commit. As usual, the very
first commit is empty for filling the PR summary, so the numbers below
refer to the remaining
non-empty commits.
The first commit adds `__lazy_modules__` tracking to the semantic model.
Because the declaration
affects later imports and can also be overwritten, we just store
whatever was assigned to it last
for later lookup when visiting other code.
The second commit expands [TID254] to recognize `__lazy_modules__` and
includes a few helper methods
on the semantic model that are used by later rules as well.
The third commit adds `__lazy_modules__` support to [TID255], which also
relies on a new
`BindingFlags::LAZY` variant. This marks bindings as originating from a
lazy import, which is easier
than trying to resolve a sequence of potentially shadowed
`__lazy_modules__` bindings to check if an
import was lazy. For example, we set the `LAZY` flag on `foo` here,
which would otherwise require
resolve multiple previous bindings:
```py
__lazy_modules__ = ["foo"]
import foo
__lazy_modules__ = []
class C(foo.Something): ...
```
The last four commits make smaller changes to four other rules. As with
TID254 and TID255, the
primary change is that we withhold a fix that involves
`__lazy_modules__` when it would change the
module's lazy status. Initially I had hoped to add fixes that modified
`__lazy_modules__` (adding to
it when we'd otherwise add `lazy`, removing from it when we'd remove
`lazy`), but this turned out to
be quite complicated. Unlike the `lazy` keyword, `__lazy_modules__` has
nonlocal effects that could
change the behavior of _any_ import from the same module in the file.
Trying to resolve all of that
to be sure the fix is reasaonable was a real mess, and reverting Codex's
attempt saved almost 700
lines for TID255 alone.
Similarly, in case this comes up in someone else's Codex review, my
Codex initially tried to handle
even more edge cases with `__lazy_modules__` definitions, such as
definitions in a loop or
redefining it with `*` imports and things like that. I tried to rein it
in toward something more
like our `__all__` and `from __future__ import annotations` handling,
which again seemed like the
better tradeoff to me.
Closes#25170.
[TID254]: https://docs.astral.sh/ruff/rules/lazy-import-mismatch/
[TID255]:
https://docs.astral.sh/ruff/rules/lazy-import-immediately-resolved/
Test Plan
--
New mdtests. The ecosystem results all look correct. They are cases
where `TID255` now fires in modules where `__lazy_modules__` is defined.
## Summary
Adds a new Ruff rule that flags method receiver parameters (`self` /
`cls`) when they are given a default value.
The rule targets functions defined directly inside a class body and uses
semantic context to ensure it only applies to methods, not standalone
functions. It determines the receiver kind, inspects the first
positional parameter, and reports a diagnostic when that receiver has a
default value.
Closes#25820
## Test Plan
Tested by adding a Ruff fixture and snapshot for RUF077, and verifying
the rule is triggered for methods whose receiver parameter has a default
value.
Files added/updated for testing include:
- `crates/ruff_linter/resources/test/fixtures/ruff/RUF077.py`
-
`crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF077_RUF077.py.snap`
I also validated the rule integration by building Ruff locally after
registering the rule and hooking it into AST statement analysis.
---------
Signed-off-by: John Kagunda <rafaeljohb@gmail.com>
Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com>
## Summary
This PR removes the unused `ClassMemberBoundness::{is_bound,
is_possibly_unbound}` predicates identified by Hawk 0.1.11. The other
APIs and CFG visualization support considered in #27339 remain
unchanged.
- 2 unused public methods removed
- 10 deletions
- 1 file changed
## Summary
This PR uses Hawk 0.1.11 to narrow internal declarations to the
visibility they actually need.
Here, I've intentionally _only_ made visibility changes; unused API
removals are split into follow-up PRs. In total, 1,201 declaration
visibility modifiers are narrowed. Public generated formatter APIs and
other declarations whose narrower visibility would activate dead-code or
API-shape lints remain unchanged.
## Summary
The linter previously inserted every version-specific builtin, magic
global, notebook builtin, and configured custom builtin into the module
scope before walking the AST, even when a file never referenced most of
those names.
This moves builtin identification into `SemanticModel` and creates
concrete builtin bindings only when a load or binding operation needs
them.
---------
Co-authored-by: Charlie Marsh <charliemarsh@openai.com>
## Summary
Publish Ruff's production Rust crates to crates.io as part of the
existing release workflow. This follows uv's setup: workspace
dependencies carry registry-compatible version pins, internal crates
receive generated READMEs and an explicit versioning policy, CI dry-runs
the full workspace publish, and release jobs publish missing crate
versions idempotently through crates.io trusted publishing.
The publishing set contains 32 packages: Ruff's CLI, WASM package,
server, linter, formatter, parser, and the internal Ruff component
crates required by them. It also includes `ty_module_resolver`,
`ty_site_packages`, and `ty_static`, because those three crates are part
of Ruff's normal dependency closure.
The publishing set deliberately excludes:
- Test-only crates: `mdtest`, `ruff_mdtest`,
`ruff_python_ast_integration_tests`, and
`ruff_python_trivia_integration_tests`.
- Benchmark and development crates: `ruff_benchmark` and `ruff_dev`.
- All remaining `ty_*` crates.
These packages retain `publish = false`, so bootstrap, version bumping,
README generation, CI dry-runs, and release publishing skip them through
Cargo metadata instead of a separate hard-coded allowlist.
The crates.io job is intentionally independent of release announcement,
so a crates.io outage cannot block the existing binary, PyPI, npm, or
GitHub release outputs. A bootstrap script reserves new crate names,
configures the release workflow as each crate's trusted publisher,
enables trusted-publishing-only releases, and records configured crates
for idempotent reruns.
The full workspace publish dry-run packages and verifies all 32 selected
crates.
Closes https://github.com/astral-sh/ruff/issues/43.
## Summary
Stacks on #25726 and enables Clippy's `iter_over_hash_type` restriction
lint across the workspace, so new hash-map and hash-set iterations must
make their ordering assumptions explicit.
Most existing loops are order-independent and use a site-local
`#[expect(clippy::iter_over_hash_type)]`. At the order-sensitive sites,
this sorts before iteration. (In practice, that should stabilize some
parser fixtures, configuration diagnostics, union normalization, etc.).
This lint is kind of annoying, but given how much we want to crack down
on non-determinism in ty, it seems worthwhile to enable IMO.
## Summary
Resolves#21494
Treat function-scope annotated variables as locals per PEP 526 ,
preventing F821 false positives.
## Test Plan
Added tests for these cases in
`crates/ruff_linter/resources/test/fixtures/pyflakes/F821_34.py`
We already skip `F821` for things like this:
```python
x = 1
if cond:
del x
print(x) # no diagnostic here
```
But when we checked for "conditional branches" we were only looking for
`match`, `if`, and `while` statements.
Here we also check for except handlers and `else` clauses. We continue
to regard `try` statements as "unconditionally executing" even though
that's not quite true.
Closes#6242
As an aside: I originally wanted to use the `BindingKind` variant
`ConditionalDeletion` introduced way back in #10415, but it turns out we
never actually constructed an instance of it! We could, if we ever
needed to track the "thing that was conditionally deleted". But since
that doesn't seem to have ever arisen in practice, I just got rid of the
variant and its references altogether.
## Summary
Fixes#19632.
When `@overload` is imported from a custom module listed in
`typing-modules` (e.g., `from std import overload`), Ruff fails to
recognize it as the `typing.overload` decorator and emits false F811
diagnostics for each overloaded function definition.
The root cause is in `SemanticModel::match_typing_qualified_name`:
`QualifiedName::from_dotted_name` splits on dots, so a single-segment
module like `std` still works, but the internal representation stores it
differently from what `QualifiedName::user_defined` produces. Use
`user_defined` instead to match the internal representation
consistently.
## Test plan
- Reproduction case: `ruff check --select F811 --config ruff.toml
temp.py` no longer emits false positives for `@overload` from custom
typing-modules
- `cargo test -p ruff_linter -- pyflakes` — all 462 tests pass
---------
Co-authored-by: Brent Westbrook <brentrwestbrook@gmail.com>
We move the logic from `Checker::in_async_context` (from when it
implements `SemanticSyntaxContext`) into the similarly named method in
`SemanticModel`. This affects the following rules:
- ASYNC105
- ASYNC210
- ASYNC212
- ASYNC220
- ASYNC221
- ASYNC222
- ASYNC230
- ASYNC240
- ASYNC250
- ASYNC251
- UP028
Closes#23425
## Summary
Fix ANN201, ANN202, ANN205, and ANN206 to not automatically suggest
`NoReturn`/`Never` return type annotations for functions that raise
`NotImplementedError`. These rules will still flag missing return type
annotations but won't provide an auto-fix, allowing developers to
manually specify the actual return type that implementations should
return.
Fixes#18886
## Problem Analysis
The bug occurred because the `auto_return_type` function in
`flake8_annotations/helpers.rs` detects when all control flow paths in a
function raise an exception (`Terminal::Raise`) and automatically
suggests `NoReturn`/`Never` as the return type. However,
`NotImplementedError` is semantically different from other exceptions -
it's used to indicate abstract methods that should be implemented by
subclasses, not methods that truly never return.
When a function raises `NotImplementedError`, it's typically an abstract
method pattern where:
- The base class defines the method signature but doesn't implement it
- Subclasses are expected to override the method with a proper
implementation
- The return type should reflect what implementations will actually
return, not `NoReturn`
For example, in Django's serializer pattern:
```py
class BaseSerializer:
def to_internal_value(self, data):
raise NotImplementedError('`to_internal_value()` must be implemented.')
```
This method should have a return type annotation like `dict` or `Any`,
not `NoReturn`, because implementations will return actual values.
## Approach
The fix modifies the `auto_return_type` function to:
1. **Added semantic analysis capability**: Modified `auto_return_type`
to accept a `SemanticModel` parameter, enabling it to analyze exception
types in raise statements.
2. **Created detection helper**: Implemented
`only_raises_not_implemented_error()` function that:
- Traverses the function body using a custom visitor to collect all
`raise` statements
- Checks if all raises are `NotImplementedError` (or `NotImplemented`)
using semantic analysis
- Returns `true` only if all raises are `NotImplementedError`
3. **Modified return type inference**: When `Terminal::Raise` is
detected, the function now:
- First checks if all raises are `NotImplementedError`
- If yes, returns `None` (no auto-fix suggested, but diagnostic still
emitted)
- If no, returns `AutoPythonType::Never` as before (suggesting
`NoReturn`/`Never`)
4. **Updated all call sites**: Updated all 4 call sites in
`definition.rs` to pass `checker.semantic()`:
- ANN201 (public functions)
- ANN202 (private functions)
- ANN205 (static methods)
- ANN206 (class methods)
The fix ensures that:
- Functions raising `NotImplementedError` still get flagged for missing
return type annotations
- But no auto-fix is suggested, allowing manual specification of the
correct return type
- Functions raising other exceptions (like `ValueError`) still correctly
get `NoReturn`/`Never` suggestions
---------
Co-authored-by: Brent Westbrook <brentrwestbrook@gmail.com>