It is a little known fact that str, bytes and bytearray methods that take index arguments start and end (find(), rfind(), index(), rindex(), count(), startswith(), endswith()) accept also None. It is not explicitly documented.
.. method:: str.find(sub[, start[, end]])
Return the lowest index in the string where substring *sub* is found within
the slice ``s[start:end]``. Optional arguments *start* and *end* are
interpreted as in slice notation. Return ``-1`` if *sub* is not found.
Yes, it says “as in slice notation”, and None is accepted in slices (also not well known fact), but it is not obvious that analogy goes so far. The documentation of startswith() is even more subtle:
.. method:: str.startswith(prefix[, start[, end]])
Return ``True`` if string starts with the *prefix*, otherwise return ``False``.
*prefix* can also be a tuple of prefixes to look for. With optional *start*,
test string beginning at that position. With optional *end*, stop comparing
string at that position.
Nothing starts or stops at the None position (until you specify the meaning of None in this context).
To add to confusion, list.index() and other methods of other sequences do not accept None.
I do not know whether the support of None was added intentionally, or just a side effect of using the same _PyEval_SliceIndex() helper which was used in the slice notation and the slice() constructor (I later added the _PyEval_SliceIndexNotNone() helper for use in Argument Clinic when None is not accepted). In any case such inconsistency is confusing. I see two variants:
Explicitly document that None is accepted as index where it is accepted. In particularly, writing the signature as str.find(sub, start=None, end=None, /) instead of str.find(sub[, start[, end]]) will help. In future we can add supprt of None in list.index(), re.Pattern.match(), and other places.
Deprecate support of the None index in these methods. It will still be valid in slice notation and slice(). As undocumented, this feature may be very little used. Signatures will need support of multi-signatures.
IMO there’s a 3rd option, which is to just leave things as they are. None as “the thing you can use as an explicit form of omitting an optional argument” is a common convention, and it might be sometimes useful to be able to do this - but not enough to warrant explicitly documenting it.
I don’t think there’s much to choose between any of the 3 options, so for me status quo wins. Is there an actual problem being caused by this behaviour? If there is, that might provide a reason for perferring a different option.
IMO, changing the signature in the documentation from [, end] to , end=None would make things a bit more correct, with virtually no downsides.
As James pointed out, allowing an explicit value that works the same as a “missing argument” is very convenient. In addition to loops, it’s quite useful for writing wrappers and adapters – things like:
I asked, because I currently work on multi-signatures support, which allow to express signatures of all builtins which previously waere not parsabe. find(sub[, start[, end]]) is not parsable, it can be written as a union of signatures:
Since e.g. str.find() refer to slice(), and slice() documents the None behaviour, I would argue it’s an official feature. I have relied on it many times.
Why not just go all the way to find(sub, start=None, end=None)?[1] These are AC functions, which means making them support names is trivial (whereas when we started these were doing manual argument parsing and so it was not trivial), and “sub”, “start” and “end” are meaningful names.
FTR, I don’t have a problem with going to =None in this case (range is still special enough to leave alone).
That is, delete the / that makes them position only. ↩︎
I suppose that str.find() refer to slice() because negative indices are supported. Accepting None might be an unintentional implementation artifact.
This is a problem because different sequences can support or not support None in the index() method, so it is difficult to provide accurate general signature.
That is, delete the / that makes them position only.
In the past, adding keyword parameters had significant impact on argument parsing time, even if all arguments are positional. So this is fixed. Supporting keyword optional arguments looks like a good idea. The main problem is that some functions use start/stop pair, and other start/end pair. There may be some system behind this this, but while they are positional-only this doesn’t matter. If they are positional, we should make an important design decision.
It should absolutely be documented. I find it especially acute for itertools.islice, where the first argument may act like “start” or “stop”, depending on how many arguments are passed, and “None” is the only sensible way to spell “and continue until the iterable is exhausted” when a start is given (the length generally can’t be known n advance - or the iterable may even never end).
>>> r = range(10)
>>> list(islice(r, 3)) # acts like stop
[0, 1, 2]
>>> list(islice(r, 3, None)) # acts like start
[3, 4, 5, 6, 7, 8, 9]
Note too that for sequence slices, setting stop to None is the only way to explicitly say “and reverse the entire sequence”.
>>> s = "abc"
>>> s[::-1] # implicit
'cba'
>>> s[:None:-1] # explicit
'cba'
>>> s[:0:-1] # explicit but wrong
'cb'
>>> s[:-1:-1] # explicit but wildly wrong
''