Skip to content

Unification loss when wrapping generic callback with functools.partial yields GenericResidual@_T #3546

@QEDady

Description

@QEDady

Describe the Bug

When passing a generic function (carrying a scoped TypeVar in its return type) into functools.partial using keyword arguments, Pyrefly's constraint solver fails to unify the generic return type with the caller's type parameter.

Instead, Pyrefly generates a deferred type placeholder: GenericResidual@_T. Passing this partial callback downstream results in an incorrect false positive downstram e.g. this causes the following to produce a false positive bad-return:

import functools
from typing import Callable, Generic, TypeVar

_S = TypeVar('_S')

class Box(Generic[_S]):
  pass

def build(x: int, factory: Callable[[], _S]) -> _S:
  return factory()

def run(f: Callable[[int], _S]) -> Box[_S]:
  return Box()

def test(factory: Callable[[], _S]) -> Box[_S]:
  partial_fn = functools.partial(build, factory=factory) # Annotating with functools.partial[_S] fixes the issue. 
  reveal_type(partial_fn) # partial[GenericResidual@_T] 
  reveal_type(run(partial_fn)) # Box[GenericResidual@_T]
  return run(partial_fn) # Returned type `Box[GenericResidual@_T]` is not assignable to declared return type `Box[_S]` [[bad-return]

My guess is that Pyrefly is unable to propagate generic unification constraints through the generic constructor of functools.partial. Since build is not invoked directly, its return TypeVar _S remains free and unbound at the partial assignment site, leaking a deferred GenericResidual@_T downstream rather than mapping it to the outer scope's _S type. It is worth noting that annotating partial_fn: functools.partial[_S] makes the above type checks as Pyrefly is not able to unify the GenericResidual@_T.

Sandbox Link

https://pyrefly.org/sandbox/?project=N4IgZglgNgpgziAXKOBDAdgEwEYHsAeAdAA4CeSIEAtsbgE4AuABGAK7oDGDuuUcAOujB1cVJg1LEI6AOZNqtRkwDCqKFFTZYAGiYBxGOhh0IHXQBVJMAGqo6gwQH0AykwC8TS8Rt2AFAHIXfwBKB04NODgmACECXwMjEw4AbRcAXWDEQSYmYlRIsMwYMCZsVmhMX3xEeXQGXTBULnpSGtV1TVhk5LTddOCmAFoAPiYXLPQcuhgGVjpJxua6Ul9Q9EEikrp2XzA2tQ0tGG7pBl6x5wyh0dj8VMuJqZm5ydvVwuLxeAZdpu5l-YdI7dc79a4xAj3NKPXJ2BgQNSOMCTDxsTjcXhwEhwhFQXxlCoNP4tNyLf6kAYAYiYAEF0OhcAxUPDZEwAO4QBgACxY7GamOxjFxUJYEHw8HEXJg8kirBghCY2SY0wAbjBERJvL48kLEci1k81RqrL5tuhtTi9ehggblc95sqdjr4VbgiBtCAyNMwFBSIRuFQoBRqQAFUje31MNBYPD4JgcXBCCAyObMiCJwiCanOGDSrkMBjEOCIAD0Ja9xV9hHoMhLhhLmFwHDgJYTSZTdDTiZLLHoTFQKtQ0E60rbkA7XcmuGI8MTWMEZG5icGarocHTKMVIAAzIQAIwAJn4IEEyWMIjXaUE7AU9AYMEwg0wEGmXAgavcTH8yYZ038ggZBhBmmABHcppkfABrGBSEGJoOHgKIPH8Nk7HQf90BAABfD0-nfGAADFoBgChoxwAgSHILCgA

(Only applicable for extension issues) IDE Information

No response

Metadata

Metadata

Assignees

Labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions