When inferring the variance of a tuple type, we currently go through
the`Tuple::tuple_class_type` helper, which unions all elements of the
tuple. This can lead to type variables getting simplified out of the
target type, e.g., in the case of `tuple[T, object]`, leading to
incorrect variance results. This PR adds a separate path that infers
variance from the elements of the tuple directly.
## Summary
Built-in literal intersections intern a `TypePair` and enter a tracked
Salsa query even when the two literal values fully determine the result.
We now simplify concrete `int`, `bool`, `str`, and `bytes` literal pairs
directly, preserving promotability and recursive-definition semantics.
These relations have no inference dependencies, so they cannot
participate in inference cycles.
line-length is a global option, not under `lint.`
This was introduced in #25013, after which #25389 attempted to fix it.
However, that change was made in autogenerated docs only (instead of
fixing it at the source), and thus the fix was undone again in #25396.
This instead fixes the issue in the source code, followed by a `cargo
dev generate-all`.
This fixes#26450.
TID254 now reports policy violations without offering a fix when an
import statement contains multiple names. Single-name imports remain
fixable. This prevents conflicting lazy-import requirements from
repeatedly adding and removing `lazy`, and uses the same single-member
check as TID255.
The tests cover both `import` and `from ... import ...`, including
conflicting policies and fixes in both directions.
btw: cuz i not the natvie English speaker, i will use ai to imporve my
txt, which may let it look like a AI generate....
---------
Co-authored-by: Brent Westbrook <brentrwestbrook@gmail.com>
There is quite a lot of code that assumes that an unbounded typevar has
an implicit upper bound of `object`. But this is only true for regular
`TypeVar`s. `ParamSpec`s, for instance, have the top `paramspec_value`
callable as their implicit upper bound.
This patch adds a new `TypeVarDomain` type (also needed for #28249) that
describes what kind of values the typevar takes on in a specialization,
and a method that returns the correct top type for each domain.
Most callers should use `require_bound_or_constraints` to get the upper
bound or constraints of a typevar, without having to worry about whether
the upper bound is implicit or declared. It now returns the correct
implicit upper bound for the particular kind of typevar. There were many
places that were calling `bound_or_constraints`, trying to add a
fast-path for `object` where possible. But since this is only valid for
plain `TypeVar`s, I've updated most of those callers to just call
`require_bound_or_constraints` and not over-think the optimization.
(Most of the time there's a recursive call that will return quickly for
`object` anyway.)
Similar to #28315, we should ignore the removed return type of a
paramspec value when calculating its top and bottom materializations.
(We actually go one step further and skip it for all `TypeMapping`s)
(This tells me we might want to consider a different representation for
paramspec values, but that's not "drop everything and do it now"
urgent.)
## Summary
`MultiBindingsByUse` currently builds and sorts a temporary `Vec`, then
allocates again when collecting into a `ThinVec`. For tables with one to
three entries, that final collection also retains four slots.
We now build the `ThinVec` directly with capacity reserved to the map's
length and sort it in place, removing the intermediate allocation and
excess capacity.
Delegate `Name` equality with borrowed strings to `CharStr`, including
Salsa lookups, so these callers use the compact string's comparison
implementation.
This connects Ruff's name comparisons to the short-string fast paths in
https://github.com/astral-sh/char_str/pull/40. Those fast paths require
a later `char_str` release; this change is compatible with the current
dependency.
Use hashbrown's borrowed-entry insertion for long parser names so a
missing name reuses its lookup when it is inserted. Construct the
heap-backed `Name` only on a miss, while preserving the inline bypass
and shared allocations for repeated long names.
When synthesizing constructor parameters for dataclasses and named
tuples, we rebuild each unaliased field name from `&str`. Names longer
than 16 bytes therefore allocate again even though the field already
owns shared storage.
Clone the existing `Name` when no alias is present so the generated
parameter shares its allocation with the field. Aliased parameters
retain their existing names.
## Summary
We now finish inferring nested unannotated dictionaries such as this
example, which previously exceeded a 30-second timeout:
```python
def f(flag: bool):
x = {}
y = {}
return {"a": x, "b": {"c": y} if flag else {"d": {"e": y}}}
```
The saved collection-use constraints feed back into the empty
dictionaries' initializers. Without normalization, each cycle iteration
adds another layer of dictionary types. We now apply the same
recursive-type normalization used for expression and binding types to
these constraints, allowing inference to converge.
Concretely, for this example, we:
- Infer `y = {}`
- Inspect constraints from the `return` statement
- Infer the `return` statement, which reads `y`
- Re-enter inference for `y = {}`
Within that `return`, we see `{"c": y} if flag else {"d": {"e": y}}`. If
`Y` is our current estimate of `y`'s type:
- The false branch has type `dict[str, dict[str, Y]]`
- We use that branch as contextual guidance for the true branch
- Therefore, the value `y` in `{"c": y}` gets an expected type of
`dict[str, Y]`
- We save that expectation as a constraint on `y`’s initializer
Inferring `y` from that saved constraint feeds the additional dictionary
layer back into the next iteration. If we start with `Y₀ =
dict[Divergent, Divergent]`, the saved constraints grow like:
- `dict[str, Y₀]`
- `dict[str, dict[str, Y₀]]`
- `dict[str, dict[str, dict[str, Y₀]]]`
And they're stored in `extra.collection_use_constraints`. We already
normalize expression and binding types during cycle recovery, but
weren’t normalizing this field, even though it also feeds back into
inference.
The fix applies that same normalization to the saved constraints,
reducing these expanding forms to `dict[str, Divergent]`. The next
iteration produces the same normalized constraint, allowing inference to
converge.
Closes https://github.com/astral-sh/ty/issues/4466.
## Summary
Skip `Generic[T]` bases when discovering Pydantic model configuration so
legacy generic classes preserve inherited settings.
closes https://github.com/astral-sh/ty/issues/4483
## Test plan
Regression tests
## Summary
When comparing an "actual" typevar against a "formal" union, we now use
the new constraint-set based solver instead of the legacy solver. The
previous implementation did not respect (co)variance correctly.
```py
class Contravariant[T]:
def __init__(self: Contravariant[int | str]) -> None: ...
# members to make it contravariant in `T`
reveal_type(Contravariant()) # revealed: previously an error, now Contravariant[int | str]
```
closes https://github.com/astral-sh/ty/issues/3990
## Ecosystem
The two new diagnostics are correct; mypy and pyright agree with that
behavior.
<details>
Reproducer:
```py
from typing import Any, Generic
from typing_extensions import TypeVar
T = TypeVar("T", default=str, covariant=True)
class KDE(Generic[T]):
def __init__(self: "KDE[str | Any]") -> None: ...
reveal_type(KDE())
```
- Base: KDE[str]
- PR: KDE[str | Any]
- Pyright and mypy agree with the PR.
</details>
## Test Plan
New Markdown tests
## Summary
Fix non-strict `TypeIs` and `isinstance` narrowing of gradual
implementations with fully static protocols:
```py
@runtime_checkable
class IntReader(Protocol):
def read(self) -> int: ...
class GradualReader:
def read(self) -> Any: ...
def _(obj: GradualReader):
if isinstance(obj, IntReader):
reveal_type(obj) # revealed: GradualReader & IntReader, previously GradualReader
reveal_type(obj.read()) # revealed: Any & int, previously `Any`
```
We now preserve the original type (`GradualReader`) only when it is a
subtype of the protocol's top materialization. Otherwise, we intersect
with `IntReader`.
closes https://github.com/astral-sh/ty/issues/4479.
## Test Plan
New regression test. I chose not to include the exact example from
https://github.com/astral-sh/ty/issues/4479 (but that example *does*
pass now, and `x.read("value")` is inferred as `int`), since I find that
harder to understand and because of
https://github.com/astral-sh/ty/issues/4480
## Summary
Collecting call-argument contexts computes a generic specialization for
every candidate overload before inspecting its arguments, even when no
parameter uses that specialization. We now compute it on the first
request and share the result across arguments for that overload.
The cache lives within one context-collection pass. Empty calls,
unmatched arguments, and parameters whose context comes directly from a
type-variable bound can avoid the computation entirely. Argument
traversal order and the specialization calculation are unchanged.
## Summary
This PR aligns `TypeIs` and `isinstance` narrowing for unspecialized
generics and gradual protocols in both strict and non-strict narrowing
modes:
- if `analysis.strict-generic-narrowing = true`, we now intersect with
the top materialization of the generic/gradual type, i.e. for a type
guard returning `TypeIs[list]`, we now narrow with the (typically
unspellable) `Top[list[Unknown]]`. Previously, we did not do this
consistently for `isinstance` narrowing either when narrowing to a
gradual protocol.
- if `analysis.strict-generic-narrowing = false` (default), we now use
the same "union filtering" approach that we also use for `isinstance`
narrowing (note that the [`TypeIs`
PEP](https://peps.python.org/pep-0742/) specifies the narrowing behavior
in terms of intersections, but allows "practical approximations of these
types"). Using the union filtering approach allows us to avoid overly
permissive gradual unions that would result from plain intersections
with a type like `list[Unknown]`. We also extend the union filtering
approach here to properly handle gradual protocols.
In both modes, the negative branch still uses the top materialization to
exclude all specializations.
The new behavior can be summarized like this:
```py
class Invariant[T]:
value: T
def is_invariant(x: object) -> TypeIs[Invariant]: ...
@final
class Unrelated: ...
```
<table>
<thead>
<tr>
<th colspan="3"><code>is_invariant(x)</code> / <code>isinstance(x,
Invariant)</code></th>
</tr>
<tr>
<th>Before narrowing</th>
<th>Non-strict narrowing</th>
<th>Strict narrowing</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>object</code></td>
<td><code>Invariant[Unknown]</code></td>
<td><code>Top[Invariant[Unknown]]</code></td>
</tr>
<tr>
<td><code>Invariant[int] | Unrelated</code></td>
<td><code>Invariant[int]</code></td>
<td><code>Invariant[int]</code></td>
</tr>
</tbody>
</table>
```py
@runtime_checkable
class Reader(Protocol):
def read(self) -> Any: ...
class SpecificReader:
def read(self) -> str: ...
def is_reader(arg: object) -> TypeIs[Reader]: ...
```
<table>
<thead>
<tr>
<th colspan="3"><code>is_reader(x)</code> / <code>isinstance(x,
Reader)</code></th>
</tr>
<tr>
<th>Before narrowing</th>
<th>Non-strict narrowing</th>
<th>Strict narrowing</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>object</code></td>
<td><code>Reader</code></td>
<td><code>Top[Reader]</code></td>
</tr>
<tr>
<td><code>SpecificReader | Unrelated</code></td>
<td><code>SpecificReader</code></td>
<td><code>SpecificReader</code></td>
</tr>
</tbody>
</table>
closes https://github.com/astral-sh/ty/issues/4394 (Fixed in both
non-strict and strict mode; Before: reveals `(list[Unknown] &
~list[Unknown]) | int` and reports `invalid-return-type`, After: reveals
`int`; no errors)
## Ecosystem impact
Looks great. Lots of removed diagnostics in non-strict-mode projects,
and only very few new diagnostics in strict-mode projects, all of which
are expected with these changes here.
## Test strategy
New and updated Markdown tests
## Summary
Every `Signature` currently stores optional source overload position and
receiver constraints inline, even though most signatures need neither.
Store those two fields together in an optional boxed payload, reducing
the inline size of the common case. Signatures that need either field
retain its value through mapping, receiver binding, and overload
handling.
## Summary
Repeated statement-call reachability checks reconstruct the `(callable,
call_expr, is_await)` key for `analyze_non_terminal_call`. Intern that
information when constructing the predicate and pass its ID directly to
the tracked query, avoiding repeated argument interning. The same fields
continue to identify each call, including awaited calls.
## Performance
Median CPU time across four runs of profiling CLI builds on macOS arm64,
using the existing microbenchmark inputs:
| Benchmark | Before | After | Reduction |
| --- | ---: | ---: | ---: |
| `repeated_statement_calls` | 29.7 ms | 28.0 ms | 5.8% |
| `repeated_statement_calls_pre_narrowed` | 30.4 ms | 28.4 ms | 6.6% |
| `repeated_statement_calls_fixed_reachability` | 29.5 ms | 27.1 ms |
8.1% |
| `repeated_statement_calls_in_try_with_if_branches` | 82.0 ms | 73.6 ms
| 10.3% |
| `repeated_suppressing_context_managers_interleaved_calls` | 68.7 ms |
59.5 ms | 13.4% |
For `repeated_statement_calls`, the retained-memory report falls from
10.36 MB to 8.84 MB (14.6%).
## Summary
Branch merges repeat reachability work for consecutive places that share
the same last-applied reachability node. We now reuse the most recently
merged path constraint within each merge, where both branch tips remain
fixed. We also skip the member-state merge when there are no member
states.
## Summary
Writing the packed AST node index checks and extends its word buffer for
every entry. We now size and zero each chunk's payload once, using its
known entry count and bit width, then write entries through a mutable
slice. Both relative-pointer and wide-pointer layouts retain the same
encoding.
## Summary
Filling missing generic arguments first constructs fallback types for
every parameter, then overwrites supplied arguments and defaults. We now
fill arguments in one pass, constructing fallbacks only when needed.
Defaults still see the completed prefix of earlier arguments, so
dependent defaults retain their substitution order.
We also skip expanding-cycle detection when there is at most one pending
mapping: self references are not followed, so an expanding cycle
requires at least two mappings.
[CodSpeed
Simulation](https://github.com/astral-sh/ruff/runs/101546864573) reports
+5.37% efficiency for `inherited_recursive_protocol_constructor` and
+5.32% for `inherited_recursive_protocol_diagnostic`, compared with
`0587598`.
## Summary
Top and bottom materialization currently enter a cached query even when
the type already determines the result. We now handle dynamic types,
divergent cycle markers, simple static types, and nongeneric nominal
instances directly before entering the query.
Types that require traversal continue through the existing cached
materialization and cycle handling. Divergent markers retain their cycle
identity and record the requested materialization.
## Summary
We retain two `SmallVec` payloads for every narrowing constraint,
including the common case of a single intersection or replacement
operation. Store single operations inline and allocate the
disjunction-of-conjunctions payload only for combined constraints,
reducing the memory retained by cached narrowing results.
The compact representation preserves the existing merge semantics for
intersections, generic filtering, and `TypeGuard` replacements,
including type-alias normalization when evaluating a constraint.
## Summary
Successful MRO queries retain a result layout large enough to hold the
full error kind and fallback MRO. We now box `StaticMroError` in the
cached `Result`, reducing the retained size of the cached result.
## Summary
Add unsafe fixes to diagnostics that suggest `Literal`: wrap integer,
bytes, and boolean literals and strings that cannot be parsed as forward
annotations in `Literal[...]`, and replace parameterized `LiteralString`
with `Literal` when its arguments are valid literal parameters.
The fixes preserve literal spelling and reuse unshadowed import aliases.
They import from `typing` on Python 3.8+, or from `typing_extensions` on
Python 3.7 when it is a declared direct dependency and the installed
module exports `Literal`. The typing-module availability checks are
shared with the `reveal_type` and `assert_never` fixes.
When the final `elif` condition is always true but ty cannot add an
`assert_never` call, offer an unsafe fix for
`redundant-condition-strict` that replaces the `elif` with `else` and
moves the condition into a defensive assertion.
For example, on Python 3.10 in a project without a direct dependency on
`typing_extensions`, the fix makes this change:
```diff
def handle(value: str | int):
if isinstance(value, str):
print(value)
- elif isinstance(value, int):
+ else:
+ assert isinstance(value, int)
print(value)
```
After the `str` branch, `value` must be an `int`, so the final
`isinstance` check is statically always true. The assertion retains that
check at runtime while making the exhaustive branching explicit.
The fallback also applies when the tested value cannot be reused as an
`assert_never` argument or import-name conflicts prevent a usable
import. It preserves condition parentheses, comments, indentation, and
line endings, and parenthesizes bare assignment expressions. No edit is
offered when the body cannot accommodate an indented assertion or
rewriting the header would discard a comment.
The fix is unsafe because an incorrect static assumption can make the
assertion fail at runtime.
Stacked on #28178.
## Summary
Converting a bound method to a callable retains its signatures in both
the `bound_signatures` query and the interned `CallableType`. We now
compute the bound signatures in `into_callable_type` and have
`bound_signatures` borrow the interned signatures, removing the extra
query and retained signature copy.
## Summary
We currently clone an updated signature out of `FunctionType` and retain
a second copy in the Salsa cache whenever it is requested. Return the
stored signature directly, avoiding the duplicate allocation and memo
entry.
Signatures derived from the function literal still use a tracked query,
so changes to the defining AST only invalidate callers when the computed
signature changes.