Skip to content

Make abstract/dunder methods positional-only. #135312

Open

Description

@randolf-scholz

Feature or enhancement

It would be nice if certain abstract methods / dunder methods would be defined with positional-only arguments, so that when one subclasses them an appropriate name can be chosen, or they can be defined positional-only in the subclass.1

Code sample in pyright playground1

from typing import overload
from collections.abc import Sequence

class MySequence[T](Sequence[T]):
    def __len__(self): return 0
    @overload
    def __getitem__(self, idx: int, /) -> T: ...
    @overload
    def __getitem__(self, idx: slice, /) -> Sequence[T]: ...
    def __getitem__(self, idx: int | slice, /) -> T | Sequence[T]:  # ❌ override
        return MySequence[T]()

In some cases, the stubs already fake that the argument is positional-only, such as for Container.__contains__:

Personally, I think only changing the stubs would be a pragmatic choice, but I was told by the maintainers that they believe this change should be done in Cpython first.

Backward Compatibility

  1. Changing non-abstract methods can potentially break user code. For example, this is currently legal code at runtime

    from collections.abc import Container.
    Container.__subclasshook__(C=list)  # True
  2. Changing the signature of abstract methods can introduce type-checker errors Code sample in pyright playground1

    from collections.abc import Sequence
    def get(x: Sequence[float], index: int) -> float:
        return x.__getitem__(index=index)

With this in mind, I believe changing abstrac dunder methods to use positional only arguments should be relatively safe, as calling them directly with keyword is very uncommon, and at worst it will introduce type errors.

Personally, I'd also like so see methods like Container.__subclasshook__ use positional-only arguments, but I also understand if this is not feasible due to bc-conerns.

Has this already been discussed elsewhere?

python/typeshed#14071

Linked PRs

Footnotes

  1. Note that mypy special cases certain dunder methods, and yields different results than pyright on these examples ↩ ↩2 ↩3

Activity

  1. changed the title [-]Make dunder methods positional-only in abstract classes.[/-] [+]Make abstract methods positional-only.[/+] on Jun 9, 2025
  2. changed the title [-]Make abstract methods positional-only.[/-] [+]Make abstract dunder methods positional-only.[/+] on Jun 9, 2025
  3. changed the title [-]Make abstract dunder methods positional-only.[/-] [+]Make abstract/dunder methods positional-only.[/+] on Jun 9, 2025
  4. randolf-scholz commented on Jun 9, 2025

    @randolf-scholz
    ContributorAuthor

    I did a scan of the collections.abc module to see how often certain methods are called with keyword arguments, here are the results.

    EDIT: index/count had some false positive matches due to github search not defaulting to case-sensistive.
    EDIT-2: Added non-abstract Mapping/MutableMapping methods with different signatures.

  5. JelleZijlstra commented on Jun 9, 2025

    @JelleZijlstra
    Member

    This is probably fine for the dunder methods in 3.15; it technically breaks compatibility but any affected code (if any exists) is extremely questionable.

    The non-dunder methods should probably also use positional-only args if we add them today, but making them positional-only now seems a bit riskier.

  6. serhiy-storchaka commented on Jun 10, 2025

    @serhiy-storchaka
    Member

    Abstract methods define protocol, but accepting keyword arguments was not intentional. For example, list.index() does not accept the keyword argument value. Either list support of the Sequence protocol is incorrect, or the Sequence protocol is defined incorrectly. The same is true for all other builtin and extension collection types.

    If the abstract class provides non-abstract implementation of the method, then such change can break user code. But if it is an abstract method, making its arguments positional-only should be safe.

  7. JelleZijlstra commented on Jul 5, 2025

    @JelleZijlstra
    Member

    Let's make the parameters positional-only on all the dunder methods and on all the abstract non-dunder methods.

    If I was creating these from scratch, in your list above I think I'd only allow keyword arguments for the default= arguments on Mapping/MutableMapping methods, but there's a few others we probably need to maintain as positional-or-keyword for compatibility.

  8. randolf-scholz commented on Jul 5, 2025

    @randolf-scholz
    ContributorAuthor

    @JelleZijlstra I opened a PR (#136324) for collections.abc

    What about other builtin libraries? For example, abc.ABCMeta.__isinstancecheck__.

  9. rhettinger commented on Jul 6, 2025

    @rhettinger
    Contributor

    In general, you can't add any constraints to existing methods without possibly breaking some code that currently works.

    And while dunder methods are normally called indirectly from operators, keywords, and builtin functions, they can be called directly.

    If I was creating these from scratch ...

    There's the rub. We're not creating these from scratch. They have been in the wild for a very long time.

  10. JelleZijlstra commented on Jul 6, 2025

    @JelleZijlstra
    Member

    Making the parameters positional-only doesn't add constraints; it removes them. For example, Sequence.__getitem__ is currently declared as def __getitem__(self, index):, which seems to imply that any conforming implementation must accept index as a keyword argument. Yet list (probably the most prominent Sequence) does not accept keyword arguments in its __getitem__ implementation. If we make the parameter to __getitem__ (an abstract method) positional-only, we fix this small wart.

  11. rhettinger commented on Jul 7, 2025

    @rhettinger
    Contributor

    Making the parameters positional-only doesn't add constraints; it removes them. For example, Sequence.getitem is currently declared as def getitem(self, index):, which seems to imply that any conforming implementation must accept index as a keyword argument.

    Yes, I get why this makes sense from the point of view of typing and from a specification point of view. For the abstract methods, I don't see any harm.

    However, the PR also changes concrete methods which can currently be called with keywords. That part of the PR is a breaking change.

    This snippet is based on an example in the docs (though that one uses an operator and not a dunder method call with a keyword argument).

    import collections.abc
    
    class ListBasedSet(collections.abc.Set):
        ''' Alternate set implementation favoring space over speed
            and not requiring the set elements to be hashable. '''
        def __init__(self, iterable):
            self.elements = lst = []
            for value in iterable:
                if value not in lst:
                    lst.append(value)
    
        def __iter__(self):
            return iter(self.elements)
    
        def __contains__(self, value):
            return value in self.elements
    
        def __len__(self):
            return len(self.elements)
    
    s1 = ListBasedSet('abcdef')
    s2 = ListBasedSet('defghi')
    overlap = s1.__and__(other=s2)   # <-- This currently works
    print(vars(overlap))

    Stylistically, that call is unattractive, but it does currently work and would be broken.

    Every single class that has ever been written that uses these concrete mixin methods is vulnerable to being broken by the PR. The person writing ListBasedSet isn't the one who would be affected. Instead, it would be all of the users of that class who have relied on the public API which has been stable for a very long time.

  12. serhiy-storchaka commented on Jul 8, 2025

    @serhiy-storchaka
    Member

    This is not documented behavior, and not what we want to encoruage. The name "other" is an implementation detail.

    On other hand, if leave support of keyword arguments, it will make list not conforming the Sequence protocol.

  13. randolf-scholz commented on Jul 8, 2025

    @randolf-scholz
    ContributorAuthor

    I completely get that there is BC-issue; hence why I pointed it already out in the OP. But do we even have a single, meaningful example where this happens? I did a lot of regex-based searches on GitHub in my comment #135312 (comment), which already shows that dunder usage with keyword arguments is very rare. Randomly inspecting some of the search results, I did not find a single case where the class in question inherited from collections.abc.

  14. randolf-scholz commented on Jul 8, 2025

    @randolf-scholz
    ContributorAuthor

    mypy-primer did not find any violations either. python/typeshed#14380 (comment)

  15. JoBeGaming commented on Aug 26, 2025

    @JoBeGaming

    Perhaps we should first make all methods (dunder and normal) have a temporary different name for value..., and an optional argument (kw only) with that name, which is transformed to the original one, but throws a warning.

    def m(self, arg): 
        ...

    will be

    def m(self, _arg=_sentinel, /, *, arg=_sentinel):
        if arg is not _sentinel:
            warnings.warn("Using arg as a positional argument is deprecated and will be removed in ...", DeprecationWarning)
        elif _arg is _sentinel:
            raise TypeError("m() missing 1 required positional argument: 'arg'")
        ... # Update code var names
  16. randolf-scholz commented on Feb 24, 2026

    @randolf-scholz
    ContributorAuthor

    OK, so at least in typeshed we now mark these methods as positional-only, even if the collections.abc member technically accepts keyword arguments, to avoid bad scenarios like

    from collections.abc import Sequence
    
    def null_count(seq: Sequence[int | None]) -> int:
        return seq.count(value=None)
    
    null_count([1, None, 2])  # TypeError at runtime. (false negative)

    I think @JoBeGaming 's proposal is quite reasonable and maybe could be done over the course of 2 versions, but maybe this would require a PEP?

  17. serhiy-storchaka commented on Feb 24, 2026

    @serhiy-storchaka
    Member

    As a first step, we could make abstract dunder methods positional-only. There should not be any objections.

    @randolf-scholz, could you please create a new PR for methods which are both abstract and dunder? There are many such methods not only in _collections_abc.py, but also in abc.py, numbers.py, os.py...

  18. JoBeGaming commented on Feb 24, 2026

    @JoBeGaming

    @serhiy-storchaka perhaps i could try and make a PR, at least for some of the mentioned modules (e.g. _collections_abc and abc). I'll try and work on this today, maybe I could send in a PR (partially) fixing this for some modules by tomorrow.

  19. serhiy-storchaka commented on Feb 24, 2026

    @serhiy-storchaka
    Member

    I could do this myself, but since @randolf-scholz already worked on this, and he is currently active, it is more polity to ask him first. I also prefer to see all non-controversial changes in a single PR.

  20. JoBeGaming commented on Feb 24, 2026

    @JoBeGaming

    @serhiy-storchaka I guess that makes sense, so we'll have to see what he thinks about this I guess.

  21. randolf-scholz commented on Feb 24, 2026

    @randolf-scholz
    ContributorAuthor

    @JoBeGaming Feel free to go right ahead.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    stdlibStandard Library Python modules in the Lib/ directorytopic-typingtype-featureA feature request or enhancement

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions