Repository navigation
Make abstract/dunder methods positional-only. #135312
Description
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__:
def __contains__(self, x: object, /) -> bool: ...intypeshed/stdlib/typing.pyidef __contains__(self, x):incpython/Lib/_collections_abc.py
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
-
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
-
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.
Activity
[-]Make dunder methods positional-only in abstract classes.[/-][+]Make abstract methods positional-only.[/+][-]Make abstract methods positional-only.[/-][+]Make abstract dunder methods positional-only.[/+][-]Make abstract dunder methods positional-only.[/-][+]Make abstract/dunder methods positional-only.[/+]randolf-scholz commentedon Jun 9, 2025
I did a scan of the
collections.abcmodule to see how often certain methods are called with keyword arguments, here are the results.Container:__contains__(x=...): abstract, yields 0 results on github search__subclasshook__(C=...): non-abstract, yields 0 results on github searchSequence:__getitem__(index=...): abstract, yields 165 results on github searchlistdo not even accept keywordindex.__contains__(value=...): non-abstract, yields 0 results on github searchindex(value=...): non-abstract, yields 9 results on github searchcount(value=...): non-abstract, yields 9 results on github searchMutableSequence:__setitem__(index=..., value=...): abstract, yields 0 results on github search__delitem__(index=...): abstract, yields 2 results on github searchinsert(index=..., value=...): abstract, yields 22 results on github searchappend(value=...): non-abstract, yields 56 results on github searchextend(values=...): non-abstract, yields 3 results on github searchpop(index=...): non-abstract, yields 520 results on github searchremove(value=...): non-abstract, yields 17 results on github search__iadd__(values=...): non-abstract, yields 0 results on github searchMapping:__contains__(key=...): non-abstract, yields 36 results on github search__getitem__(key=...): abstract, yields 50 results on github search__eq__(other=...): non-abstract, yields 139 results on github searchobj.get(key=...,): non-abstract, yields 7.9k results on github searchobj.get(..., default=...)yields 1.3k results on github searchobj.get(key=..., default=...)yields 164 results on github searchgoogle.cloud.datastore.client.Client.get.MutableMapping:__setitem__(key=..., value=...): non-abstract, yields 47 results on github search__delitem__(key=...): non-abstract, yields 15 results on github searchpop(key=...): non-abstract, yields 41 results on github searchpop(..., default=...): non-abstract, yields 65 results on github searchpop(key=..., default=...): non-abstract, yields 12 results on github searchos.environ, which subclassesMutableMapping.setdefault(key=...): non-abstract, yields 0 results on github searchsetdefault(..., default=...): non-abstract, yields 82 results on github searchsetdefault(key=..., default=...): non-abstract, yields 10 results on github searchSet__le__(other=..): non-abstract, yields 2 results on github search__lt__(other=...): non-abstract, yields 5 results on github search__ge__(other=...): non-abstract, yields 2 results on github search__gt__(other=...): non-abstract, yields 2 results on github search__eq__(other=...): non-abstract, yields 139 results on github search__and__(other=...): non-abstract, yields 0 results on github search__rand__(other=...): non-abstract, yields 0 results on github search__or__(other=...): non-abstract, yields 3 results on github search__sub__(other=...): non-abstract, yields 39 results on github search__rsub__(other=...): non-abstract, yields 1 results on github search__xor__(other=...): non-abstract, yields 0 results on github search__rxor__(other=...): non-abstract, yields 0 results on github searchisdisjoint(other=...): non-abstract, yields 0 results on github search_from_iterable(it=...): non-abstract, yields 0 results on github searchMutableSet:add(value=...): abstract, yields 117 results on github searchdiscard(value=...): non-abstract, yields 0 results on github searchremove(value=...): non-abstract, yields 17 results on github searchMappingView:(mapping=...): non-abstract, yields 2.4k results on github searchKeysView__contains__(key=...): non-abstract, yields 36 results on github searchItemsView__contains__(item=...): non-abstract, yields 5 results on github searchValuesView__contains__(value=...): non-abstract, yields 0 results on github searchCoroutine:send(value=...): abstract, yields 297 results on github searchthrow(typ=...): abstract, yields 0 results on github searchGenerator:send(value=...): abstract, yields 297 results on github searchthrow(typ=...): abstract, yields 0 results on github searchAsyncGenerator:asend(value=...): abstract, yields 0 results on github searchathrow(typ=...): abstract, yields 0 results on github searchEDIT:
index/counthad some false positive matches due to github search not defaulting to case-sensistive.EDIT-2: Added non-abstract
Mapping/MutableMappingmethods with different signatures.JelleZijlstra commentedon Jun 9, 2025
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.
serhiy-storchaka commentedon Jun 10, 2025
Abstract methods define protocol, but accepting keyword arguments was not intentional. For example,
list.index()does not accept the keyword argumentvalue. Eitherlistsupport of theSequenceprotocol is incorrect, or theSequenceprotocol 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.
JelleZijlstra commentedon Jul 5, 2025
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.randolf-scholz commentedon Jul 5, 2025
@JelleZijlstra I opened a PR (#136324) for
collections.abcWhat about other builtin libraries? For example,
abc.ABCMeta.__isinstancecheck__.rhettinger commentedon Jul 6, 2025
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.
There's the rub. We're not creating these from scratch. They have been in the wild for a very long time.
JelleZijlstra commentedon Jul 6, 2025
Making the parameters positional-only doesn't add constraints; it removes them. For example,
Sequence.__getitem__is currently declared asdef __getitem__(self, index):, which seems to imply that any conforming implementation must acceptindexas a keyword argument. Yetlist(probably the most prominentSequence) 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.rhettinger commentedon Jul 7, 2025
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).
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
ListBasedSetisn'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.serhiy-storchaka commentedon Jul 8, 2025
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
listnot conforming theSequenceprotocol.randolf-scholz commentedon Jul 8, 2025
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.randolf-scholz commentedon Jul 8, 2025
mypy-primerdid not find any violations either. python/typeshed#14380 (comment)JoBeGaming commentedon Aug 26, 2025
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.
will be
randolf-scholz commentedon Feb 24, 2026
OK, so at least in typeshed we now mark these methods as positional-only, even if the
collections.abcmember technically accepts keyword arguments, to avoid bad scenarios likeI 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?
serhiy-storchaka commentedon Feb 24, 2026
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 inabc.py,numbers.py,os.py...JoBeGaming commentedon Feb 24, 2026
@serhiy-storchaka perhaps i could try and make a PR, at least for some of the mentioned modules (e.g.
_collections_abcandabc). I'll try and work on this today, maybe I could send in a PR (partially) fixing this for some modules by tomorrow.serhiy-storchaka commentedon Feb 24, 2026
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.
JoBeGaming commentedon Feb 24, 2026
@serhiy-storchaka I guess that makes sense, so we'll have to see what he thinks about this I guess.
randolf-scholz commentedon Feb 24, 2026
@JoBeGaming Feel free to go right ahead.