[ty] Only prefer declared types in non-covariant positions (#22068)
Some checks are pending
CI / Determine changes (push) Waiting to run
CI / cargo fmt (push) Waiting to run
CI / cargo clippy (push) Blocked by required conditions
CI / cargo test (linux) (push) Blocked by required conditions
CI / cargo test (linux, release) (push) Blocked by required conditions
CI / cargo test (${{ github.repository == 'astral-sh/ruff' && 'depot-windows-2022-16' || 'windows-latest' }}) (push) Blocked by required conditions
CI / ty completion evaluation (push) Blocked by required conditions
CI / cargo test (macos-latest) (push) Blocked by required conditions
CI / cargo test (wasm) (push) Blocked by required conditions
CI / cargo build (msrv) (push) Blocked by required conditions
CI / cargo fuzz build (push) Blocked by required conditions
CI / fuzz parser (push) Blocked by required conditions
CI / test scripts (push) Blocked by required conditions
CI / ecosystem (push) Blocked by required conditions
CI / Fuzz for new ty panics (push) Blocked by required conditions
CI / cargo shear (push) Blocked by required conditions
CI / python package (push) Waiting to run
CI / pre-commit (push) Waiting to run
CI / mkdocs (push) Waiting to run
CI / formatter instabilities and black similarity (push) Blocked by required conditions
CI / test ruff-lsp (push) Blocked by required conditions
CI / check playground (push) Blocked by required conditions
CI / benchmarks instrumented (ruff) (push) Blocked by required conditions
CI / benchmarks instrumented (ty) (push) Blocked by required conditions
CI / benchmarks walltime (medium|multithreaded) (push) Blocked by required conditions
CI / benchmarks walltime (small|large) (push) Blocked by required conditions
[ty Playground] Release / publish (push) Waiting to run

## Summary

The following snippet currently errors because we widen the inferred
type, even though `X` is covariant over `T`. If `T` was contravariant or
invariant, this would be fine, as it would lead to an assignability
error anyways.

```python
class X[T]:
    def __init__(self: X[None]): ...

    def pop(self) -> T:
        raise NotImplementedError

# error: Argument to bound method `__init__` is incorrect: Expected `X[None]`, found `X[int | None]`
x: X[int | None] = X()
```

There are some cases where it is still helpful to prefer covariant
declared types, but this error seems hard to fix otherwise, and makes
our heuristics more consistent overall.
This commit is contained in:
Ibraheem Ahmed 2025-12-19 17:27:31 -05:00 committed by GitHub
parent 1a18ada931
commit 674d3902c6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 27 additions and 5 deletions

View file

@ -511,7 +511,7 @@ def f(self, dt: dict[str, Any], key: str):
```toml
[environment]
python-version = "3.12"
python-version = "3.14"
```
```py
@ -550,7 +550,7 @@ g: list[Any] | dict[Any, Any] = f3(1)
reveal_type(g) # revealed: list[int] | dict[int, int]
```
We currently prefer the generic declared type regardless of its variance:
We only prefer the declared type if it is in non-covariant position.
```py
class Bivariant[T]:
@ -594,12 +594,22 @@ x6: Covariant[Any] = covariant(1)
x7: Contravariant[Any] = contravariant(1)
x8: Invariant[Any] = invariant(1)
reveal_type(x5) # revealed: Bivariant[Any]
reveal_type(x6) # revealed: Covariant[Any]
reveal_type(x5) # revealed: Bivariant[Literal[1]]
reveal_type(x6) # revealed: Covariant[Literal[1]]
reveal_type(x7) # revealed: Contravariant[Any]
reveal_type(x8) # revealed: Invariant[Any]
```
```py
class X[T]:
def __init__(self: X[None]): ...
def pop(self) -> T:
raise NotImplementedError
x1: X[int | None] = X()
reveal_type(x1) # revealed: X[None]
```
## Narrow generic unions
```toml

View file

@ -3016,7 +3016,19 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> {
tcx.filter_union(self.db, |ty| ty.class_specialization(self.db).is_some())
.class_specialization(self.db)?;
builder.infer(return_ty, tcx).ok()?;
builder
.infer_map(return_ty, tcx, |(_, variance, inferred_ty)| {
// Avoid unnecessarily widening the return type based on a covariant
// type parameter from the type context, as it can lead to argument
// assignability errors if the type variable is constrained by a narrower
// parameter type.
if variance.is_covariant() {
return None;
}
Some(inferred_ty)
})
.ok()?;
Some(builder.type_mappings().clone())
});