Skip to content

Callable intersection aliases do not contribute constraints to generic call inference #4518

Description

@carljm

Summary

A PEP 695 alias of an intersection of callables does not contribute constraints when passed to a generic function. This can produce a different inferred return type and a false-positive argument error compared with the equivalent inline annotation.

Because today our handling of constraints from intersections on main is not good, this can look like the alias gives a better result from ignoring the intersection evidence. But after that is fixed in astral-sh/ruff#28303, we can more clearly see the problem caused by losing evidence from the alias:

from typing import Callable, reveal_type

type Both = "Callable[[int], int] & Callable[[str], str]"

def call[T](f: Callable[[T], T], x: T) -> T:
    return f(x)

def example(
    named: Both,
    inline: "Callable[[int], int] & Callable[[str], str]",
    x: bool,
):
    reveal_type(call(named, x))   # bool; invalid-argument-type
    reveal_type(call(inline, x))  # int; accepted

The named call reports:

Argument to function `call` is incorrect: Expected `(bool, /) -> bool`, found `Both`

Both calls should be accepted and reveal int: bool is a subtype of int, and the callback's integer signature accepts an int and returns an int. Matching that signature requires T = int, even though the other argument has type bool. Naming the intersection with an alias should not affect inference.

Cause

In SpecializationBuilder::infer_map_impl, the formal Callable arm runs before the arm that expands aliases in the actual type. It calls try_upcast_to_callable, which expands the alias but cannot convert the resulting general intersection into a callable. The early return Ok(()) skips inference from that argument entirely.

Consequently, only x contributes to inference in the named case, producing T = bool. Argument checking then rejects the callback because its integer signature returns int, not necessarily bool.

An inline intersection reaches the actual-intersection inference arm instead. astral-sh/ruff#28303 fixes that arm's handling of alternative specializations, but leaves this alias-handling gap. This issue specifically tracks preserving the same inference evidence through the alias; it is related to #3557.

Version

Reproduced on Ruff commit 0ecefedd582415888c39275e09ecae05465c10ef (astral-sh/ruff#28303):

ty ruff/0.16.6+20 (0ecefedd5 2026-09-03)

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingcallablesgenericsBugs or features relating to ty's generics implementationtype aliases

Type

No type

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions