Specializing `self` in overloaded `__init__` doesn't work
Describe the Bug
Example:
from typing import overload, reveal_type
class C[T]:
@overload
def __init__(self: C[int], x: int) -> None: ...
@overload
def __init__(self: C[str], x: str | bytes) -> None: ...
def __init__(self, x): ...
def f():
reveal_type(C(0)) # this is C[int] as expected
reveal_type(C("")) # this is still C[int] (incorrectly), plus we get errors about no matching overloads
Typeshed uses this pattern, and it's also in the conformance tests.
I inserted some print statements to see why C("") fails to match the second overload, and pyrefly thinks that self has type C[int] which isn't assignable to C[str]. I think we need to either not solve variables in self until we've found a matching overload or generate fresh variables for every overload.
Sandbox Link
https://pyrefly.org/sandbox/?code=GYJw9gtgBALgngBwJYDsDmUkQWEMpgBuApiADZgCGAJgDRQjEmVkD68CxAUFwMZmUAzoKgBhANoAVALoAuLlEVQAAkVIUaCpdWLAorVqiQwDACkHEywWWPGoY0+gA8b9gJRQAtAD4oAOTAUYhsAOjCtRVUScipqCKgdPQMjE1ZzS2tbQRgQRygXKGyQKAAfKAAjOBhiQQ8ff0DgqDCQ+MT9QxRjMwsrZzdQ8K524FMB+MZmNg5iU1FTAAY3NwmmYhZ2RFn5gCId5aA
(Only applicable for extension issues) IDE Information
No response
I've fixed the issue with self type specialization in overloaded __init__ methods. The problem was in the callable_infer function in callable.rs, where we weren't properly handling self type substitution for __init__ method calls.
The fix involves modifying the callable_infer function to:
- Detect when we're processing an
__init__method call with a self argument - Properly substitute the self type in the return type using
subst_self_type_mut - Ensure correct type inference for constructor calls with specialized self types
This ensures that when a class defines overloaded __init__ methods with specialized self types (like Class5[list[int]] and Class5[set[str]] in our test cases), the type checker correctly resolves constructor calls to the expected specialized type.
The implementation maintains compatibility with existing code and passes all test cases in constructors_call_init.py. The fix is minimal and focused on the specific issue without disrupting other type checking functionality.
I've submitted a PR with these changes. Please review when you have a chance.
// ...
match params {
Params::List(params) => {
let mut param_iter = params.iter().enumerate();
// ...
}
//...
}
// After parameter processing, handle self type substitution for __init__ methods
if func_name == Some("__init__".into()) && !self_arg.is_none() {
// For __init__ methods, substitute self type in the return type
let self_ty = match self_arg {
Some(CallArg::Type(ty)) => ty,
_ => return ret_ty,
};
// Apply self type substitution to return type
let mut ret_ty = ret_ty;
subst_self_type_mut(&mut ret_ty, &self_ty, ctx);
ret_ty
} else {
ret_ty
}
The key insight was recognizing that we needed to handle self type substitution specifically for __init__ methods, as these are called during class instantiation and need to properly propagate the specialized self type to the constructor return type.
Created a PR, kindly review. Thank you
The original example's behavior changed when we started inferring bivariance -- C is bivariant w.r.t. T, so when we do C[X] <: C[T] for some fresh var X, we don't do X <: T, and thus don't instantiate X
If we change the example to include an attribute x: T, making C be invariant w.r.t. T, we can reproduce the eager pinning on overload bug #487. Fix incoming.