`Callable` isn't a special form

we should update the typing spec to not refer to Callable as a special form, because unlike Union or Literal, it is just a normal class

type checkers might choose to apply some special casing to this type like saying it has a __get__ method, but i don’t think that necessitates it being a special form

currently the spec says that type[Callable] is invalid because Callable is regarded as a special form. i believe this provides no safety or clarity, and prevents valid typing patterns

Callable is no more of a special form than Iterable or any other class is

here is a proposed change to typeshed to correct the currently incorrect definition of typing.Callable:

here are some issues that are a result of this incorrect typing:

from collections.abc import Callable
from typing import _SpecialForm, assert_type

class C(Callable): ...  # expect no error

a: Callable
a.__call__  # expect no error

s: _SpecialForm = Callable  # expect an error
Callable._name  # expect an error

assert_type(Callable, type[Callable])  # expect no error

class NotCallable: ...

_: type[Callable] = NotCallable  # expect an error
3 Likes

I think this makes a lot of sense. As I’ve said before: we should avoid special cases and aim for simplicity. So this is a +1 for me.


Ideally, we would be able to Callable ourselves as

@runtime_checkable
class Callable[**InTs, OutT](Protocol):
    @staticmethod
    def __call__(*args: InTs.args, **kwargs: InTs.kwargs) -> OutT: ...

But then it’d differ with typing.Callable in two ways:

  1. @runtime_checkable isn’t powerful enough, so isinstance(_, Callable) could lead to false positives, for example for some user-defined callable class CallMe, both isinstance(CallMe, Callable) and isinstance(call_me_instance, Callable) would be true. In some niche cases where CallMe cannot be constructed as CallMe, this would be incorrect. I also expect it to be a bit slower this way.
  2. InTs is a ParamSpec, but as far as I’m aware, ParamSpec is always invariant, and cannot be defined contravariant.

Personally, I’d be able to live with 1., but 2. can be pretty problematic. So unless ParamSpec can be defined as contravariant, I think that the better approach would indeed be to, as you suggested, to make the typing.Callable special form a bit less special, so that it’ll behave as if it’s the Callable protocol above, but without its two issues.

3 Likes

There’s also a 3rd difference:

  1. Currently, ParamSpec cannot be parametrized using an argument list containing a TypeVarTuple.
    Parametrizing **P with [*Ts]

This seems to be an underspecification on ParamSpec rather than Callable, but it impacts user-defined Callable protocols and aliases.

1 Like

The specific section you’re changing in your PR needs a bigger rework, as I mentioned in Spec change: Clarify that `tuple` should not be prohibited as an argument to `type` - #7 by Jelle . Certainly type[Literal] is more obviously nonsensical than type[Callable].

Callable is in fact currently a special form because it is listed as such in Type annotations — typing documentation . Whether it should be a special form is a different question. It would be nice if Callable can just be a regular Protocol with a __call__ method, but as discussed in this thread, we’re not quite there yet.

6 Likes