In the generics_paramspec_specialization typing conformance test, there’s the following case:
class ClassC(Generic[P1]):
f: Callable[P1, int] = cast(Any, "")
def func30(x: ClassC[[int, str, bool]]) -> None:
x.f(0, "", True) # OK
x.f("", "", True) # E
x.f(0, "", "") # E
Writing an equivalent class with the ParamSpec substituted would mean that f isn’t callable like the test suggests, since it would be a bound method:
class ClassC:
f: Callable[[int, str, bool], int] = lambda x, y, z: 1
def func30(x: ClassC) -> None:
x.f(0, "", True) # TypeError: ... takes 3 positional arguments but 4 were given
The test case itself is a bit weird with the empty string cast, but I think any callable that’s assignable to f would be a bound method, so the current behavior doesn’t seem safe.
What determines that it’s an instance variable? The original example initializes the attribute on the class (admittedly by lying to the typechecker, but still).
It’s not a class variable because it’s not declared using the ClassVar special form.
The typing spec is a bit ambiguous about how to treat class-scoped variables that are not explicitly marked as ClassVar. (The protocols chapter provides some guidance here, but it’s not clear how this applies to non-protocol classes.) This is an area where I’d welcome additional clarity and standardization. Pyright distinguishes between three class-scoped variable classes: 1. pure class variables, 2. pure instance variables, and 3. instance variables that can be showed by class variables. For details, refer to this documentation.
The challenge here is that instance variables and class variables are treated differently by the runtime when it comes to method binding.