Expanded typing spec chapter for callables

As a thought experiment, let’s assume for the moment that the backward compatibility concern for mypy makes it impractical to eliminate this special case.

In some ways, special-casing Any for (*args: Any, **kwargs: Any) is similar to special-casing tuple[Any, ...]. You may recall that we updated the typing spec several months ago to special case tuple[Any, ...] and treat it as a gradual type. The spec now indicates that this form is “bidirectionally compatible with all tuples of any length”. We opted for this interpretation to preserve backward compatibility with mypy even though some of us were uneasy about the inconsistency it introduced. That case feels similar to the problem at hand where we again find ourselves asking whether we should codify a special case because of a choice made in the past by mypy developers.

If we were to adopt this special case, how would we word it such that the specification is clear? Here’s an attempt…

If the input signature in a def statement includes both a *args and **kwargs parameter and both are typed as Any (explicitly or implicitly by virtue of having no annotation), a type checker should treat this as the equivalent of .... Any other parameters in the signature are unaffected and are retained as part of the signature.

def func1(*args: Any, **kwargs: Any) -> None:
    pass

def func2(a: int, /, *args, **kwargs) -> None:
    pass

def func3(a: int, *args: Any, **kwargs: Any) -> None:
    pass

class Proto1[**P](Protocol):
    def __call__(self, a: int, *args: P.args, **kwargs: P.kwargs) -> None: ...

assert_type(func1, Callable[..., None])  # OK
assert_type(func2, Callable[Concatenate[int, ...], None])  # OK
assert_type(func2, Callable[..., None])  # Error
assert_type(func3, Proto1[...])  # OK

class A:
    def method(self, a: int, /, *args: Any, k: str, **kwargs: Any) -> None:
        pass

class B(A):
    # This override is OK because it is consistent with the parent's method.
    def method(self, a: float, /, b: int, *, k: str, m: str) -> None:
        pass

Thoughts?

My preference is to avoid creating a special case here, but if there is consensus that such a special case is needed for mypy backward compatibility, I could live with this compromise. As I said above, it feels similar to the compromise we made for tuple[Any, ...].