Ambiguities about Positional-only Parameters

I agree with your point about protocols, however I believe there’s already a special rule for protocols that the HasLower you show matches def lower() -> str. I think this came up on python-typing, and there was an agreement to treat a protocol’s ordinary method as if it were a static method. Since then, both PyRight and MyPy implement this rule.

I don’t think that should mean that for every class, ordinary methods should be treated as though their self parameter is positional-only.

I would tend to agree with this, I think it makes sense to make the default for Protocol that self is positional only, but everywhere else I would prefer if it were made explicit.

The only justification in my head for always treating it as positional only in a regular class, would be, that there’s actually no way to define a method with a positional only argument, without making self also positional only at the same time, so whether or not self is positional only will usually not be a deliberate choice, but rather incidental, so we might as well always treat it as positional only, since it might be easy to miss that it’s positional only, since it’s probably the most consistently named argument in Python.

3 Likes

Ditto.

I would still want the ability to override the default so that “self” is not positional-only if it turns out a lot of code is relying on that property. For example:

def my_function(/, self, param1): …

That’s not valid syntax, / can’t be the first token in the parameter list.

Are you talking about at the language level here, or just for type checkers? Because changing this in the language would be a breaking change, and would need to follow the normal process for language-level changes.

If you’re just talking about type checkers, then that’s fine, and I have no strong opinion on it. But any proposal should be clear on how a user with an otherwise annotated code base can say “for this method, I need the ability to pass self/cls as a keyword argument”. So basically an override, as you say, but one that’s specific to type checkers (the language itself doesn’t need an override).

To be explicit, I’m saying that there should be a way to annotate the following code:

class C:
    def __init__(self, val: int):
        self.val = val
    def get_val(self) -> int:
        return self.val

c = C(12)
result = C.get_val(self=c)

so that it doesn’t fail type checking, but it also continues to detect that result is an int.

1 Like

I’m only suggesting making self/cls positional-only for type checkers.

Thanks everyone for your feedback.

I’ve prepared a draft typing spec change that addresses one of the two questions I asked at the top of this thread. (I plan to tackle the other one separately.)

If you have minor suggestions on wording or formatting, please post comments in the PR. If you have more substantive feedback or concerns, please post here for better visibility.

The Typing Council has approved the proposed change to the typing spec.

This addresses only “Ambiguity #2” in the post at the top of this thread. We will tackle “Ambiguity #1” in a later change. There’s currently no good place in the typing spec to include this clarification. It will fit within a future to-be-written chapter on methods.

the current behavior in mypy makes overloads difficult to deal with in cases where the implementation has a decorator that changes its signature, because it turns self into a positional only argument:

from typing import Callable, Concatenate, TypeVar, overload, ParamSpec

T = TypeVar("T")
P = ParamSpec("P")
R = TypeVar("R")


def foo(fn: Callable[Concatenate[T, P], R]) -> Callable[Concatenate[T, P], R]:
    return fn


class Foo:
    @overload
    def bar(self, a: str) -> str: ...
    @overload
    def bar(self, a: int) -> int: ...

    # error: Overloaded function implementation does not accept all possible arguments of signature 1
    # error: Overloaded function implementation does not accept all possible arguments of signature 2
    @foo
    def bar(self, a: object) -> object: ...

the solution in this case is to explicitly make self positional only in the overloads, but ideally the type checker should just always treat self and cls as positional:

class Foo:
    @overload
    def bar(self, /, a: str) -> str: ...
    @overload
    def bar(self, /, a: int) -> int: ...