I originally asked this question in python/typing, but I thought it would be useful to revisit it here.
The structure of typing.Callable seems a bit of a gray area at the moment. It is defined as a special-form in typeshed, so I assume that all type checkers have their own internal definition somewhere.
In particular, I would like to understand if a value of type typing.Callable
has a __call__ method,
has any attributes beyond the ones available on object.
It looks like pyright and mypy currently do not allow __call__ lookups on a typing.Callable, whereas pytype does:
A callable should probably be something that can be called and nothing else should be assumed.
Separately, I think it makes sense to have a “subtype” (in the structural sense) of Callable for functions, which then has all the function attributes:
__name__
__qualname__
(__annotations__?)
This would additionally help with determining when a callable is bound as a method. (Functions defined within a class are bound as methods, but other callables are not bound as methods.)
from typing import Function
def f(func: Function[[int, str], bool]) -> None:
print(func.__qualname__) # OK
Yes, I think it’s good to get more clarity here.
One approach could be to specify that Callable is exactly equivalent to a Protocol with only a __call__ method; i.e., the following two are the same type:
For context, pyright currently uses the “fake” (type_check_only) function class defined in builtins.pyi for all callable types. I copied that behavior from mypy, which explains why they both type checkers allow access to attributes __qualname__, etc.
I welcome additional clarity in the spec about the behavior of typing.Callable and callables in general.
One approach could be to specify that Callable is exactly equivalent to a Protocol with only a __call__ method
I like this. The protocol has a __call__ method and inherits all of object’s attributes, which matches my intuition for what a callable thing should be. I guess there’s a question of how many users are currently depending on Callable having attributes like __qualname__.
it could then be useful to have some way to express a function type, which has a few more attributes.
I occasionally access fields like qualname. There’s a problem with current definition of FunctionType. It’s not generic. I can’t do FunctionType[P, R] for paramspec P and typevar R (or concrete types).
If functiontype was generic then I’d be fine adjusting some callable annotations to it.
Making FunctionType generic as you suggest makes sense. I think we may have tried it in typeshed earlier and run into some practical issues.
types.FunctionType is the type of functions created through def at runtime, so under the principle that typing should reflect the runtime behavior, ideally type checkers should infer this type for def functions.
A complication, though, is that builtin functions (including ones created in extensions) are not instances of FunctionType, but of types.BuiltinFunctionType. However, stubs just use def for those, so type checkers can’t tell that these functions are instances of BuiltinFunctionType, not FunctionType.
Does that difference matter? Usually not, but it may matter if users are accessing attributes that exist only on one of the two.
One major issue with stripping attributes from Callable and using types.FunctionType (or some other nominal type) is how type-checkers currently infer plain functions (def) - which are just Callable. It seems like type checkers would need to rework what is inferred so that the following doesn’t issue false positives:
import types
def my_func() -> None: ...
def accepts_callback[**P, R](f: types.FunctionType[P, R]) -> types.FunctionType[P, R]:
return f
accepts_callback(my_func) # currently a typing error
But, if types.FunctionType is inferred, this doesn’t work in cases where Python source code is used to build extension modules. For example,
def functions in mypy or other modules compiled by mypyc are <built-in function>s
def functions compiled by Cython in pure python mode are <cyfunction>s
There might be some good heuristic, like avoiding inferring types.FunctionType if (1) a compiled binary with the same name exists as a Python source file or (2) if a Python source file can’t be found at all (and the type definition is taken purely from a .pyi); this could also solve issues with functions originating from Python built-in modules.
Otherwise, I agree with this,
a structural “function” type stripped down to a few commonly-accessed attributes (I frequently use __qualname__ and __module__ for introspection) seems like a simpler first step while people are changing their annotations to work with functions.
Is this indeed true? Callable only supports positional-only parameters, so I would assume any type checker will need a distinct type to model def-defined functions and lambdas.
Ah - I was only thinking of mypy, which has non-public API in mypy_extensions that you use to build a Callable type with parameters other than positional-only. Maybe other type-checkers don’t do this, but I would expect them to also use a structural type, not a nominal type.
A quick experiment indicates that pyre does something similar to mypy, though (pyre playground).
Another question with Callable is how does it behave as a descriptor. Can ClassVar[Callable]] become a bound method? If so should it be an error to assign an already bound method?
I think that’s a totally separate idea. I proposed a PartialApplication type form to solve many problems including accommodating the descriptor protocol, described here.
Speaking of bound methods and equivalence of Callable and callback protocols, that’s another thing we should iron out in the spec. See also this mypy draft PR