2020-03-20 12:21:06 -04:00
|
|
|
|
PEP: 616
|
|
|
|
|
Title: String methods to remove prefixes and suffixes
|
|
|
|
|
Author: Dennis Sweeney <sweeney.dennis650@gmail.com>
|
|
|
|
|
Sponsor: Eric V. Smith <eric@trueblade.com>
|
|
|
|
|
Status: Draft
|
|
|
|
|
Type: Standards Track
|
|
|
|
|
Content-Type: text/x-rst
|
|
|
|
|
Created: 19-Mar-2020
|
|
|
|
|
Python-Version: 3.9
|
2020-03-20 14:55:52 -04:00
|
|
|
|
Post-History: 20-Mar-2020
|
2020-03-20 12:21:06 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Abstract
|
|
|
|
|
========
|
|
|
|
|
|
2020-03-25 13:17:50 -04:00
|
|
|
|
This is a proposal to add two new methods, ``removeprefix()`` and
|
|
|
|
|
``removesuffix()``, to the APIs of Python's various string objects. These
|
2020-03-22 15:02:10 -04:00
|
|
|
|
methods would remove a prefix or suffix (respectively) from a string,
|
2020-03-25 13:17:50 -04:00
|
|
|
|
if present, and would be added to Unicode ``str`` objects, binary
|
2020-03-22 15:02:10 -04:00
|
|
|
|
``bytes`` and ``bytearray`` objects, and ``collections.UserString``.
|
2020-03-20 12:21:06 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Rationale
|
|
|
|
|
=========
|
|
|
|
|
|
2020-03-22 17:16:57 -04:00
|
|
|
|
There have been repeated issues on Python-Ideas [#pyid]_ [3]_,
|
|
|
|
|
Python-Dev [4]_ [5]_ [6]_ [7]_, the Bug Tracker, and
|
|
|
|
|
StackOverflow [#confusion]_, related to user confusion about the
|
|
|
|
|
existing ``str.lstrip`` and ``str.rstrip`` methods. These users are
|
2020-03-25 13:17:50 -04:00
|
|
|
|
typically expecting the behavior of ``removeprefix`` and ``removesuffix``,
|
2020-03-22 17:16:57 -04:00
|
|
|
|
but they are surprised that the parameter for ``lstrip`` is
|
|
|
|
|
interpreted as a set of characters, not a substring. This repeated
|
|
|
|
|
issue is evidence that these methods are useful. The new methods
|
|
|
|
|
allow a cleaner redirection of users to the desired behavior.
|
2020-03-20 12:21:06 -04:00
|
|
|
|
|
|
|
|
|
As another testimonial for the usefulness of these methods, several
|
|
|
|
|
users on Python-Ideas [#pyid]_ reported frequently including similar
|
2020-03-25 13:17:50 -04:00
|
|
|
|
functions in their code for productivity. The implementation
|
2020-03-20 12:21:06 -04:00
|
|
|
|
often contained subtle mistakes regarding the handling of the empty
|
2020-03-25 13:17:50 -04:00
|
|
|
|
string, so a well-tested built-in method would be useful.
|
|
|
|
|
|
|
|
|
|
The existing solutions for creating the desired behavior are to either
|
|
|
|
|
implement the methods as in the `Specification`_ below, or to use
|
|
|
|
|
regular expressions as in the expression
|
|
|
|
|
``re.sub('^' + re.escape(prefix), '', s)``, which is less discoverable,
|
|
|
|
|
requires a module import, and results in less readable code.
|
2020-03-20 12:21:06 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Specification
|
|
|
|
|
=============
|
|
|
|
|
|
2020-03-27 18:59:28 -04:00
|
|
|
|
The builtin ``str`` class will gain two new methods which will behave
|
|
|
|
|
as follows when ``type(self) is type(prefix) is type(suffix) is str``::
|
2020-03-20 12:21:06 -04:00
|
|
|
|
|
2020-03-25 13:17:50 -04:00
|
|
|
|
def removeprefix(self: str, prefix: str, /) -> str:
|
|
|
|
|
if self.startswith(prefix):
|
|
|
|
|
return self[len(prefix):]
|
2020-03-22 15:02:10 -04:00
|
|
|
|
else:
|
2020-03-25 13:17:50 -04:00
|
|
|
|
return self[:]
|
2020-03-22 15:02:10 -04:00
|
|
|
|
|
2020-03-25 13:17:50 -04:00
|
|
|
|
def removesuffix(self: str, suffix: str, /) -> str:
|
2020-03-27 18:59:28 -04:00
|
|
|
|
# suffix='' should not call self[:-0].
|
2020-03-25 13:17:50 -04:00
|
|
|
|
if suffix and self.endswith(suffix):
|
|
|
|
|
return self[:-len(suffix)]
|
2020-03-22 15:02:10 -04:00
|
|
|
|
else:
|
2020-03-25 13:17:50 -04:00
|
|
|
|
return self[:]
|
2020-03-22 15:02:10 -04:00
|
|
|
|
|
2020-03-28 20:18:13 -04:00
|
|
|
|
When the arguments are instances of ``str`` subclasses, the methods should
|
|
|
|
|
behave as though those arguments were first coerced to base ``str``
|
|
|
|
|
objects, and the return value should always be a base ``str``.
|
2020-03-20 12:21:06 -04:00
|
|
|
|
|
2020-03-23 20:02:46 -04:00
|
|
|
|
Methods with the corresponding semantics will be added to the builtin
|
2020-03-20 12:21:06 -04:00
|
|
|
|
``bytes`` and ``bytearray`` objects. If ``b`` is either a ``bytes``
|
2020-03-25 13:17:50 -04:00
|
|
|
|
or ``bytearray`` object, then ``b.removeprefix()`` and ``b.removesuffix()``
|
2020-03-27 18:59:28 -04:00
|
|
|
|
will accept any bytes-like object as an argument. The two methods will
|
|
|
|
|
also be added to ``collections.UserString``, with similar behavior.
|
2020-03-20 12:21:06 -04:00
|
|
|
|
|
2020-03-23 20:02:46 -04:00
|
|
|
|
|
2020-03-20 12:21:06 -04:00
|
|
|
|
Motivating examples from the Python standard library
|
|
|
|
|
====================================================
|
|
|
|
|
|
|
|
|
|
The examples below demonstrate how the proposed methods can make code
|
|
|
|
|
one or more of the following:
|
|
|
|
|
|
2020-03-22 15:02:10 -04:00
|
|
|
|
1. Less fragile:
|
2020-03-23 20:02:46 -04:00
|
|
|
|
|
2020-03-22 17:16:57 -04:00
|
|
|
|
The code will not depend on the user to count the length of a literal.
|
2020-03-20 12:21:06 -04:00
|
|
|
|
|
2020-03-22 15:02:10 -04:00
|
|
|
|
2. More performant:
|
2020-03-23 20:02:46 -04:00
|
|
|
|
|
2020-03-22 17:16:57 -04:00
|
|
|
|
The code does not require a call to the Python built-in ``len``
|
2020-03-25 13:17:50 -04:00
|
|
|
|
function nor to the more expensive ``str.replace()`` method.
|
2020-03-20 12:21:06 -04:00
|
|
|
|
|
2020-03-22 15:02:10 -04:00
|
|
|
|
3. More descriptive:
|
2020-03-23 20:02:46 -04:00
|
|
|
|
|
2020-03-28 20:18:13 -04:00
|
|
|
|
The methods give a higher-level API for code readability as
|
2020-03-22 17:16:57 -04:00
|
|
|
|
opposed to the traditional method of string slicing.
|
2020-03-20 12:21:06 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
find_recursionlimit.py
|
|
|
|
|
----------------------
|
|
|
|
|
|
|
|
|
|
- Current::
|
|
|
|
|
|
|
|
|
|
if test_func_name.startswith("test_"):
|
|
|
|
|
print(test_func_name[5:])
|
|
|
|
|
else:
|
|
|
|
|
print(test_func_name)
|
|
|
|
|
|
|
|
|
|
- Improved::
|
|
|
|
|
|
2020-03-25 13:17:50 -04:00
|
|
|
|
print(test_func_name.removeprefix("test_"))
|
2020-03-22 15:02:10 -04:00
|
|
|
|
|
2020-03-20 12:21:06 -04:00
|
|
|
|
|
|
|
|
|
deccheck.py
|
|
|
|
|
-----------
|
|
|
|
|
|
|
|
|
|
This is an interesting case because the author chose to use the
|
|
|
|
|
``str.replace`` method in a situation where only a prefix was
|
|
|
|
|
intended to be removed.
|
|
|
|
|
|
|
|
|
|
- Current::
|
|
|
|
|
|
|
|
|
|
if funcname.startswith("context."):
|
|
|
|
|
self.funcname = funcname.replace("context.", "")
|
|
|
|
|
self.contextfunc = True
|
|
|
|
|
else:
|
|
|
|
|
self.funcname = funcname
|
|
|
|
|
self.contextfunc = False
|
|
|
|
|
|
|
|
|
|
- Improved::
|
|
|
|
|
|
|
|
|
|
if funcname.startswith("context."):
|
2020-03-25 13:17:50 -04:00
|
|
|
|
self.funcname = funcname.removeprefix("context.")
|
2020-03-20 12:21:06 -04:00
|
|
|
|
self.contextfunc = True
|
|
|
|
|
else:
|
|
|
|
|
self.funcname = funcname
|
|
|
|
|
self.contextfunc = False
|
|
|
|
|
|
|
|
|
|
- Arguably further improved::
|
|
|
|
|
|
|
|
|
|
self.contextfunc = funcname.startswith("context.")
|
2020-03-25 13:17:50 -04:00
|
|
|
|
self.funcname = funcname.removeprefix("context.")
|
2020-03-20 12:21:06 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
cookiejar.py
|
|
|
|
|
------------
|
|
|
|
|
|
|
|
|
|
- Current::
|
|
|
|
|
|
|
|
|
|
def strip_quotes(text):
|
|
|
|
|
if text.startswith('"'):
|
|
|
|
|
text = text[1:]
|
|
|
|
|
if text.endswith('"'):
|
|
|
|
|
text = text[:-1]
|
|
|
|
|
return text
|
|
|
|
|
|
|
|
|
|
- Improved::
|
|
|
|
|
|
|
|
|
|
def strip_quotes(text):
|
2020-03-25 13:17:50 -04:00
|
|
|
|
return text.removeprefix('"').removesuffix('"')
|
2020-03-20 12:21:06 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
test_concurrent_futures.py
|
|
|
|
|
--------------------------
|
|
|
|
|
|
2020-03-25 13:17:50 -04:00
|
|
|
|
In the following example, the meaning of the code changes slightly,
|
|
|
|
|
but in context, it behaves the same.
|
|
|
|
|
|
2020-03-20 12:21:06 -04:00
|
|
|
|
- Current::
|
|
|
|
|
|
|
|
|
|
if name.endswith(('Mixin', 'Tests')):
|
|
|
|
|
return name[:-5]
|
|
|
|
|
elif name.endswith('Test'):
|
|
|
|
|
return name[:-4]
|
|
|
|
|
else:
|
|
|
|
|
return name
|
|
|
|
|
|
|
|
|
|
- Improved::
|
|
|
|
|
|
2020-03-25 13:17:50 -04:00
|
|
|
|
return (name.removesuffix('Mixin')
|
|
|
|
|
.removesuffix('Tests')
|
|
|
|
|
.removesuffix('Test'))
|
2020-03-20 12:21:06 -04:00
|
|
|
|
|
|
|
|
|
|
2020-03-22 15:02:10 -04:00
|
|
|
|
There were many other such examples in the stdlib.
|
2020-03-20 12:21:06 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Rejected Ideas
|
|
|
|
|
==============
|
|
|
|
|
|
|
|
|
|
Expand the lstrip and rstrip APIs
|
|
|
|
|
---------------------------------
|
|
|
|
|
|
|
|
|
|
Because ``lstrip`` takes a string as its argument, it could be viewed
|
2020-03-25 13:17:50 -04:00
|
|
|
|
as taking an iterable of length-1 strings. The API could, therefore, be
|
2020-03-23 20:02:46 -04:00
|
|
|
|
generalized to accept any iterable of strings, which would be
|
|
|
|
|
successively removed as prefixes. While this behavior would be
|
|
|
|
|
consistent, it would not be obvious for users to have to call
|
|
|
|
|
``'foobar'.lstrip(('foo',))`` for the common use case of a
|
2020-03-20 12:21:06 -04:00
|
|
|
|
single prefix.
|
|
|
|
|
|
2020-03-23 20:02:46 -04:00
|
|
|
|
|
2020-03-20 12:21:06 -04:00
|
|
|
|
Remove multiple copies of a prefix
|
|
|
|
|
----------------------------------
|
|
|
|
|
|
|
|
|
|
This is the behavior that would be consistent with the aforementioned
|
2020-03-22 15:02:10 -04:00
|
|
|
|
expansion of the ``lstrip``/``rstrip`` API -- repeatedly applying the
|
2020-03-20 12:21:06 -04:00
|
|
|
|
function until the argument is unchanged. This behavior is attainable
|
2020-03-22 15:02:10 -04:00
|
|
|
|
from the proposed behavior via by the following::
|
2020-03-23 20:02:46 -04:00
|
|
|
|
|
2020-03-25 13:17:50 -04:00
|
|
|
|
>>> s = 'Foo' * 100 + 'Bar'
|
|
|
|
|
>>> prefix = 'Foo'
|
2020-03-27 12:39:16 -04:00
|
|
|
|
>>> while s.startswith(prefix): s = s.removeprefix(prefix)
|
2020-03-22 15:02:10 -04:00
|
|
|
|
>>> s
|
2020-03-25 13:17:50 -04:00
|
|
|
|
'Bar'
|
2020-03-20 12:21:06 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Raising an exception when not found
|
|
|
|
|
-----------------------------------
|
|
|
|
|
|
2020-03-25 13:17:50 -04:00
|
|
|
|
There was a suggestion that ``s.removeprefix(pre)`` should raise an
|
2020-03-20 12:21:06 -04:00
|
|
|
|
exception if ``not s.startswith(pre)``. However, this does not match
|
|
|
|
|
with the behavior and feel of other string methods. There could be
|
|
|
|
|
``required=False`` keyword added, but this violates the KISS
|
|
|
|
|
principle.
|
|
|
|
|
|
|
|
|
|
|
2020-03-25 13:17:50 -04:00
|
|
|
|
Accepting a tuple of affixes
|
2020-03-28 20:18:13 -04:00
|
|
|
|
----------------------------
|
2020-03-25 13:17:50 -04:00
|
|
|
|
|
|
|
|
|
It could be convenient to write the ``test_concurrent_futures.py``
|
|
|
|
|
example above as ``name.removesuffix(('Mixin', 'Tests', 'Test'))``, so
|
|
|
|
|
there was a suggestion that the new methods be able to take a tuple of
|
|
|
|
|
strings as an argument, similar to the ``startswith()`` API. Within
|
|
|
|
|
the tuple, only the first matching affix would be removed. This was
|
|
|
|
|
rejected on the following grounds:
|
|
|
|
|
|
|
|
|
|
* This behavior can be surprising or visually confusing, especially
|
|
|
|
|
when one prefix is empty or is a substring of another prefix, as in
|
|
|
|
|
``'FooBar'.removeprefix(('', 'Foo')) == 'Foo'``
|
|
|
|
|
or ``'FooBar text'.removeprefix(('Foo', 'FooBar ')) == 'Bar text'``.
|
|
|
|
|
|
|
|
|
|
* The API for ``str.replace()`` only accepts a single pair of
|
|
|
|
|
replacement strings, but has stood the test of time by refusing the
|
|
|
|
|
temptation to guess in the face of ambiguous multiple replacements.
|
|
|
|
|
|
|
|
|
|
* There may be a compelling use case for such a feature in the future,
|
|
|
|
|
but generalization before the basic feature sees real-world use would
|
|
|
|
|
be easy to get permanently wrong.
|
|
|
|
|
|
|
|
|
|
|
2020-03-20 12:21:06 -04:00
|
|
|
|
Alternative Method Names
|
|
|
|
|
------------------------
|
|
|
|
|
|
|
|
|
|
Several alternatives method names have been proposed. Some are listed
|
|
|
|
|
below, along with commentary for why they should be rejected in favor
|
2020-03-25 13:17:50 -04:00
|
|
|
|
of ``removeprefix`` (the same arguments hold for ``removesuffix``).
|
2020-03-22 15:02:10 -04:00
|
|
|
|
|
2020-03-22 17:16:57 -04:00
|
|
|
|
- ``ltrim``, ``trimprefix``, etc.:
|
2020-03-22 15:02:10 -04:00
|
|
|
|
|
2020-03-22 17:16:57 -04:00
|
|
|
|
"Trim" does in other languages (e.g. JavaScript, Java, Go, PHP)
|
|
|
|
|
what ``strip`` methods do in Python.
|
2020-03-22 15:02:10 -04:00
|
|
|
|
|
|
|
|
|
- ``lstrip(string=...)``
|
|
|
|
|
|
2020-03-23 20:02:46 -04:00
|
|
|
|
This would avoid adding a new method, but for different
|
2020-03-22 17:16:57 -04:00
|
|
|
|
behavior, it's better to have two different methods than one
|
2020-03-25 13:17:50 -04:00
|
|
|
|
method with a keyword argument that selects the behavior.
|
2020-03-22 15:02:10 -04:00
|
|
|
|
|
2020-03-25 13:17:50 -04:00
|
|
|
|
- ``remove_prefix``:
|
2020-03-22 15:02:10 -04:00
|
|
|
|
|
2020-03-22 17:16:57 -04:00
|
|
|
|
All of the other methods of the string API, e.g.
|
|
|
|
|
``str.startswith()``, use ``lowercase`` rather than
|
|
|
|
|
``lower_case_with_underscores``.
|
2020-03-22 15:02:10 -04:00
|
|
|
|
|
2020-03-25 13:17:50 -04:00
|
|
|
|
- ``removeleft``, ``leftremove``, or ``lremove``:
|
2020-03-22 15:02:10 -04:00
|
|
|
|
|
2020-03-22 17:16:57 -04:00
|
|
|
|
The explicitness of "prefix" is preferred.
|
2020-03-22 15:02:10 -04:00
|
|
|
|
|
2020-03-25 13:17:50 -04:00
|
|
|
|
- ``cutprefix``, ``deleteprefix``, ``withoutprefix``, ``dropprefix``, etc.:
|
2020-03-22 15:02:10 -04:00
|
|
|
|
|
2020-03-25 13:17:50 -04:00
|
|
|
|
Many of these might have been acceptable, but "remove" is
|
|
|
|
|
unambiguous and matches how one would describe the "remove the prefix"
|
|
|
|
|
behavior in English.
|
2020-03-22 15:02:10 -04:00
|
|
|
|
|
2020-03-22 17:16:57 -04:00
|
|
|
|
- ``stripprefix``:
|
2020-03-22 15:02:10 -04:00
|
|
|
|
|
2020-03-22 17:16:57 -04:00
|
|
|
|
Users may benefit from remembering that "strip" means working
|
|
|
|
|
with sets of characters, while other methods work with
|
|
|
|
|
substrings, so re-using "strip" here should be avoided.
|
2020-03-20 12:21:06 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Reference Implementation
|
|
|
|
|
========================
|
|
|
|
|
|
2020-03-25 13:17:50 -04:00
|
|
|
|
See the pull request on GitHub [#pr]_.
|
2020-03-20 12:21:06 -04:00
|
|
|
|
|
|
|
|
|
|
2020-03-28 20:18:13 -04:00
|
|
|
|
History of Major revisions
|
|
|
|
|
==========================
|
|
|
|
|
|
|
|
|
|
* Version 3: Remove tuple behavior.
|
|
|
|
|
|
|
|
|
|
* Version 2: Changed name to ``removeprefix``/``removesuffix``;
|
|
|
|
|
added support for tuples as arguments
|
|
|
|
|
|
|
|
|
|
* Version 1: Initial draft with ``cutprefix``/``cutsuffix``
|
|
|
|
|
|
|
|
|
|
|
2020-03-20 12:21:06 -04:00
|
|
|
|
References
|
|
|
|
|
==========
|
|
|
|
|
|
|
|
|
|
.. [#pr] GitHub pull request with implementation
|
|
|
|
|
(https://github.com/python/cpython/pull/18939)
|
2020-03-22 17:16:57 -04:00
|
|
|
|
.. [#pyid] [Python-Ideas] "New explicit methods to trim strings"
|
2020-03-20 12:21:06 -04:00
|
|
|
|
(https://mail.python.org/archives/list/python-ideas@python.org/thread/RJARZSUKCXRJIP42Z2YBBAEN5XA7KEC3/)
|
2020-03-22 17:16:57 -04:00
|
|
|
|
.. [3] "Re: [Python-ideas] adding a trim convenience function"
|
|
|
|
|
(https://mail.python.org/archives/list/python-ideas@python.org/thread/SJ7CKPZSKB5RWT7H3YNXOJUQ7QLD2R3X/#C2W5T7RCFSHU5XI72HG53A6R3J3SN4MV)
|
|
|
|
|
.. [4] "Re: [Python-Dev] strip behavior provides inconsistent results with certain strings"
|
|
|
|
|
(https://mail.python.org/archives/list/python-ideas@python.org/thread/XYFQMFPUV6FR2N5BGYWPBVMZ5BE5PJ6C/#XYFQMFPUV6FR2N5BGYWPBVMZ5BE5PJ6C)
|
|
|
|
|
.. [5] [Python-Dev] "correction of a bug"
|
|
|
|
|
(https://mail.python.org/archives/list/python-dev@python.org/thread/AOZ7RFQTQLCZCTVNKESZI67PB3PSS72X/#AOZ7RFQTQLCZCTVNKESZI67PB3PSS72X)
|
|
|
|
|
.. [6] [Python-Dev] "str.lstrip bug?"
|
|
|
|
|
(https://mail.python.org/archives/list/python-dev@python.org/thread/OJDKRIESKGTQFNLX6KZSGKU57UXNZYAN/#CYZUFFJ2Q5ZZKMJIQBZVZR4NSLK5ZPIH)
|
|
|
|
|
.. [7] [Python-Dev] "strip behavior provides inconsistent results with certain strings"
|
|
|
|
|
(https://mail.python.org/archives/list/python-dev@python.org/thread/ZWRGCGANHGVDPP44VQKRIYOYX7LNVDVG/#ZWRGCGANHGVDPP44VQKRIYOYX7LNVDVG)
|
2020-03-23 20:02:46 -04:00
|
|
|
|
.. [#confusion] Comment listing Bug Tracker and StackOverflow issues
|
2020-03-20 12:21:06 -04:00
|
|
|
|
(https://mail.python.org/archives/list/python-ideas@python.org/message/GRGAFIII3AX22K3N3KT7RB4DPBY3LPVG/)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Copyright
|
|
|
|
|
=========
|
|
|
|
|
|
|
|
|
|
This document is placed in the public domain or under the
|
|
|
|
|
CC0-1.0-Universal license, whichever is more permissive.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
..
|
|
|
|
|
Local Variables:
|
|
|
|
|
mode: indented-text
|
|
|
|
|
indent-tabs-mode: nil
|
|
|
|
|
sentence-end-double-space: t
|
|
|
|
|
fill-column: 70
|
|
|
|
|
coding: utf-8
|
|
|
|
|
End:
|