pyrefly icon indicating copy to clipboard operation
pyrefly copied to clipboard

Specializing `self` in overloaded `__init__` doesn't work

Open rchen152 opened this issue 6 months ago • 1 comments

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

rchen152 avatar May 28 '25 05:05 rchen152

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:

  1. Detect when we're processing an __init__ method call with a self argument
  2. Properly substitute the self type in the return type using subst_self_type_mut
  3. 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

officiallyutso avatar May 28 '25 09:05 officiallyutso

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.

samwgoldman avatar Aug 01 '25 00:08 samwgoldman