Positional-only parameters are discussed in PEP 570, which introduced the /
separator to demarcate positional-only parameters from those that can be specified by keyword.
Prior to PEP 570, a convention was established that a parameter whose name starts with a double underscore was to be considered positional-only. Here is the relevant part of the typing spec.
As I’ve added better enforcement of positional-only parameters in pyright, I’ve discovered a couple of ambiguities in PEP 570 and the typing spec. I’d like to clarify the desired behavior and update the typing spec accordingly. I think it’s important for library authors to see consistent behavior across type checkers here.
Ambiguity #1: Should the self
parameter in an instance method and the cls
parameter in a class method be considered implicitly positional-only even if it is not explicitly marked as such? In other words, should the following signatures all be considered identical for purposes of type checking?
class A:
def method1(self, x: int) -> None: ...
def method2(__self, x: int) -> None: ...
def method3(whatever, x: int) -> None: ...
def method4(self, /, x: int) -> None: ...
PEP 570 mentions in its background section “…it is undesirable that a caller can bind by keyword to the name self
when calling the method from the class”, but the specification section doesn’t indicate whether all self
and cls
parameters should be implicitly positional-only.
I think the desired behavior here is to treat self
and cls
as implicitly positional-only in instance methods and class methods. Can anyone think of a reason not to do this?
If we decide not to make self
and cls
implicitly positional-only, then it creates more ambiguities that we will need to discuss.
Ambiguity #2: When using the backward-compatibility convention for positional-only parameters, how should a type checker interpret the situation where a parameter with a double-underscored name follows a parameter without a double-underscored name?
def func(count: int, __mode: str) -> None: ...
In the example above, should count
be considered positional-only because it is followed by a positional-only parameter __mode
? Or should __mode
be considered not positional-only because it follows a parameter that is not positional-only? Or should this be considered an error on the part of the developer and flagged as such by a type checker (and the resulting behavior unspecified)?
This situation came up in a recent bug report for pyright. The issue involves a callback protocol definition in the pydantic library:
class ModelBeforeValidatorWithoutInfo(Protocol):
def __call__(self, cls: Any, __value: Any) -> Any:
...