I just had a simple idea that I think would nicely fit in with the existing language, but wanted to get some community feedback before potentially formally pursuing this as my first proper PEP. So, here’s my PEPP (Python Enhancement Proposal Proposal) ![]()
Thoughts/feedback/support appreciated!
Abstract:
For container types, we already have the membership test operators in and not in, which test for membership using equality or identity with the container’s members: x in y is equivalent to any(x is e or x == e for e in y).
However, there may be situations in which one might want to test for membership strictly by identity. We propose new membership test operators is in and is not in which will strictly check identity for container types: x is in y would be idiomatically equivalent to any(x is e for e in y). There will also be a new dunder method __contains_id__() which allows the is in operator to be implemented in user-defined types.
Motivation:
The combination of the is operator (as an identity test) and the in/not in operators (as membership tests) into the is in/is not in operators (as membership tests by identity) seems to be a natural merger of both the operator syntax and the abstract notion behind it, making this a near-seamless extension to the language from the programmer’s perspective.
Admittedly, however, the intended behavior of x is in y for builtin containers is already easily created using the short expression any(x is e for e in y) mentioned above. If syntactic neatness is insufficient cause to add a new feature, I should add that adding a new dunder method __contains_id__()would allow user-implemented types to override both __contains__() and __contains_id__() as they see fit. Having two separate definitions of “membership” assigned to keyword operators as needed may prove quite useful.
Backwards Compatibility:
No significant concern. The expressions use only existing hard keywords in new combinations; in current versions of the language the token sequences is in and is not in always raise syntax errors. All currently valid python code would carry the same exact same meaning with the new operators implemented, with the sole exception of code making improper use of the currently-undefined dunder method __contains_id__().
Specification:
I’m just going to mirror / extend the existing section in the language reference on the in and not in operators for now; obviously a full PEP would need much more thorough specifications (including additional modifications to the sections describing dunder methods and the full language specification, as well as other things I’m sure I’m missing).
Additionally, all builtin container types would need to have a __contains_id__() method implemented in cPython, as well as the abstract types in collections.abc (Implementing Container.__contains_id__(self, x) as return any(x is e for e in self) may be sufficient.)
6.10.2. Membership test operations¶
(Description of the in and not in operators)
6.10.2.1. Membership identity test operations¶
The operators
is inandis not intest for membership strictly by object identity.x is in sevaluates to True if x is the same object as a member of s, andFalseotherwise.x is not in sreturns the negation ofx is in s. All built-in sequences and set types support this as well as dictionary, for whichis intests whether the dictionary has a given key. For container types such as list, tuple, set, frozenset, dict, or collections.deque, the expressionx in yis equivalent toany(x is e for e in y).+++ Implementation details regarding string and bytes types is up for discussion.
For user-defined classes which define the
__contains_id__()method,x is in yreturnsTrueify.__contains_id__(x)returns a true value, andFalseotherwise.For user-defined classes which do not define
__contains_id__()but do define__iter__(),x is in yisTrueif some valuez, for which the expressionx is zis true, is produced while iterating overy. If an exception is raised during the iteration, it is as ifis inraised that exception.Lastly, the old-style iteration protocol is tried: if a class defines
__getitem__(),x is in yisTrueif and only if there is a non-negative integer index i such thatx is y[i], and no lower integer index raises theIndexErrorexception. (If any other exception is raised, it is as ifis inraised that exception).The operator
is not inis defined to have the inverse truth value ofis in.
Open Issues:
inandnot inare defined for string and bytes objects and search for substrings. However, any substring obtained by slicing, etc. would be a new object and could not have the same id as the left hand operand, meaningis inandis not in. Potential options include:- Calling
x is in stringalways returnsFalse, andx is not in stringalways returnsTrue, mirroring the fact thatx is string[slice]will (to my understanding) always be false. - Attempting to call
x is [not] in stringraisesTypeError. - Calling
x is [not] in stringreturns the same value asx [not] in string.
- Calling
- The code
if x is in y: ...more closely mirrors natural English language thanif x in y: ...; however, the latter will produce results which feel more “natural” to a beginner whereas the former may produce unexpected results to a non-advanced programmer.