Skip to content

Spurious bad-assignment error when assigning generic function results to class attributes #3215

@QEDady

Description

@QEDady

Describe the Bug

When assigning the result of a generic function to a class attribute, Pyrefly can fail with a spurious bad-assignment error if the function involves nested closures and Type Aliases.

Reproduction

import typing
from typing import Callable, TypeVar, reveal_type

T = TypeVar('T')
A = T | Callable[[], T]
B = A | list[T]

def bar(x: int) -> None:
    pass

def f(x: B[T], g: Callable[[T], None]) -> A[T]:
    def if_callable() -> T:
        r = x()  # pyrefly: ignore[not-callable]
        g(r)
        return r
    if callable(x):
        return if_callable
    elif isinstance(x, list):
        g(x[0])
        return x[0]
    else:
        g(x)
        return x

class Test:
    def __init__(self, arg: B[int]):
        self.foo = f(arg, bar) # Fails with `(() -> A) | (() -> int) | int` is not assignable to attribute `foo` with type `(() -> int) | int` [bad-assignment]
        reveal_type(self.foo)

def standalone(arg: B[int]):
    foo = f(arg, bar) # This works
    reveal_type(foo)

reveal_type diagnostics:

INFO revealed type: (() -> int) | int [reveal-type]

Details

It seems that in the class attribute case, Pyrefly goes into the function body to resolve the generics for the call. When checking the code, the call to x() (where we had to ignore the not-callable error) seems to cause some confusion. This breaks the generic resolution, making Pyrefly fall back to the raw alias A and generate the conflicting () -> A type.

It appears to be a limitation of generic solving. However, it is unclear why this wasn't reported as a bad-return-type inside the function itself instead of a bad-assignment at the use site. Additionally, it is confusing why this fails in the class attribute case but works perfectly in the standalone function case.

Sandbox Link

https://pyrefly.org/sandbox/?project=N4IgZglgNgpgziAXKOBDAdgEwEYHsAeAdAA4CeSIEAtsbgE4AuABA6cROgOYA66YduKizYdOTarUZMAwqihRU2WABomAFTYwAaqjqq6MAG4w5AfVbEYvXmqYBedZp10AFAHI1bgJS8AgvfUmAB8ZOQUlGABtSIBdVTUY3gAhAP8QqAg4BkiE6ywYMCZsXRd8RHF0Bi8mAFoAPiYAOVx0GEReJk6mYlQ4ODzMAqYwUvKknLimTnLZeUVYaITVZtaY6vqmXwn29C6mQcKIMFMAYzD5mBd1hrUdvb26APwrzoBibtIDMChScohOdD0KKAhg1M5zCKJXb3TqcFx0HzQmEGBgAVzouzoHS6RyY4PCsFKXjuMKYKPRuyOp3OEWxnRgGUOcA4WQwJ0u%2BFUGSyxLp9zh%2BEiAAY1nyHjA0RimIKRXyGXA2mKugLEaSyRKKdK8icFH11PAGCTOgcmKZTBwIAwzS4FVAwKpdNMmOMOAw1kb7rawIQwLhcAERo7VMUEUwlW8mAAxVDQOBMADuloAFkwAAYuF4bXzVEIZ64VKrBAup8RxkFMXrMgEXFj%2B1AMBh0CDYVEMGBp324EuJhgpizt9OZhqunPFpiRYqYGqV-7oKgwSpQtUGYxmfs2hnezuI3gm1lYOQtS6OsaRV3uvmdgMuINFXTVd5qJOZBP0ADW-SR6tXUHMmhc24gMoIBkF8PyEAwghQBQ7wAAqfAUPxMGgWB4PgeItJAnDovWEAtIQvDvAAyjA7ZJg2xBwIgAD01GgYhpCEPQnDUQu1GYLgJxwNRJyYf8OEMHh6DUcM9AVoYMYEu2vF8PxdC4S0TC4MQgktHABHoGQvYtDUxh0MyikONwIAAMyEAAjAATMZvCRDAdACPpUKougEj0G2U6YBABgnIJxgBG4s5Am4vAgjUBgAI6ot5MBTm%2BMCkNOJzsnqDhuPGujoCF6AgAAvsBqC%2BRAxiRtAMAUChOAECQ5C5UAA

(Only applicable for extension issues) IDE Information

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No fields configured for Bug.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions