Files
ruff/crates/ty_python_semantic/resources/mdtest/narrow/callable.md
2026-01-14 00:56:47 +00:00

3.5 KiB

Narrowing for callable()

Basic narrowing

The callable() builtin returns TypeIs[Callable[..., object]], which narrows the type to the intersection with Top[Callable[..., object]]. The Top[...] wrapper indicates this is a fully static type representing the top materialization of a gradual callable.

Since all callable types are subtypes of Top[Callable[..., object]], intersections with Top[...] simplify to just the original callable type.

from typing import Any, Callable

def f(x: Callable[..., Any] | None):
    if callable(x):
        # The intersection simplifies because `(...) -> Any` is a subtype of
        # `Top[(...) -> object]` - all callables are subtypes of the top materialization.
        reveal_type(x)  # revealed: (...) -> Any
    else:
        # Since `(...) -> Any` is a subtype of `Top[(...) -> object]`, the intersection
        # with the negation is empty (Never), leaving just None.
        reveal_type(x)  # revealed: None

Narrowing with other callable types

from typing import Any, Callable

def g(x: Callable[[int], str] | None):
    if callable(x):
        # All callables are subtypes of `Top[(...) -> object]`, so the intersection simplifies.
        reveal_type(x)  # revealed: (int, /) -> str
    else:
        reveal_type(x)  # revealed: None

def h(x: Callable[..., int] | None):
    if callable(x):
        reveal_type(x)  # revealed: (...) -> int
    else:
        reveal_type(x)  # revealed: None

Narrowing from object

def f(x: object):
    if callable(x):
        reveal_type(x)  # revealed: Top[(...) -> object]
    else:
        reveal_type(x)  # revealed: ~Top[(...) -> object]

Calling narrowed callables

The narrowed type Top[Callable[..., object]] represents the set of all possible callable types (including, e.g., functions that take no arguments and functions that require arguments). While such objects are callable (they pass callable()), no specific set of arguments can be guaranteed to be valid.

import typing as t

def call_with_args(y: object, a: int, b: str) -> object:
    if isinstance(y, t.Callable):
        # error: [call-top-callable]
        return y(a, b)
    return None

Narrowing with named expressions (walrus operator)

When callable() is used with a named expression, the target of the named expression should be narrowed.

from typing import Any

class Foo:
    func: Any | None

def f(foo: Foo):
    first = getattr(foo, "func", None)
    if callable(first):
        reveal_type(first)  # revealed: Any & Top[(...) -> object]
    else:
        reveal_type(first)  # revealed: (Any & ~Top[(...) -> object]) | None

    if callable(second := getattr(foo, "func", None)):
        reveal_type(second)  # revealed: Any & Top[(...) -> object]
    else:
        reveal_type(second)  # revealed: (Any & ~Top[(...) -> object]) | None

Assignability of narrowed callables

A narrowed callable Top[Callable[..., object]] should be assignable to Callable[..., Any]. This is important for decorators and other patterns where we need to pass the narrowed callable to functions expecting gradual callables.

from typing import Any, Callable, TypeVar
from ty_extensions import static_assert, Top, is_assignable_to

static_assert(is_assignable_to(Top[Callable[..., bool]], Callable[..., int]))

F = TypeVar("F", bound=Callable[..., Any])

def wrap(f: F) -> F:
    return f

def f(x: object):
    if callable(x):
        # x has type `Top[(...) -> object]`, which should be assignable to `Callable[..., Any]`
        wrap(x)