Clean up PEP-505. (#119)

* Replace emoji with C# spelling for operators.
* Remove some unnecessary alternative spellings.
* Add proper python code blocks.
* Other miscellany.

*Commit to a specific syntax*
I've removed all of the alternative syntax discussion and the
community poll. I expanded the reasoning for picking the spellings
that I did.
This commit is contained in:
Mark E. Haase 2016-10-31 11:36:47 -04:00 committed by Guido van Rossum
parent 9a70e511ad
commit b968fe97b9
1 changed files with 164 additions and 387 deletions

View File

@ -107,12 +107,12 @@ represent their respective languages' ``null``.
The C language ``null`` often bleeds into Python, too, particularly for thin
wrappers around C libraries. For example, in ``pyopenssl``, the ``X509`` class
has `a get_notBefore() method
<https://github.com/pyca/pyopenssl/blob/3257877f8846e4357b495fa6c9344d01b11cf16d
/OpenSSL/crypto.py#L1219>`_ that returns either a timestamp or ``None``. This
function is a thin wrapper around an OpenSSL function with the return type
``ASN1_TIME *``. Because this C pointer may be ``null``, the Python wrapper must
be able to represent ``null``, and ``None`` is the chosen representation.
has a ``get_notBefore()`` `method <https://github.com/pyca/pyopenssl/blob/325787
7f8846e4357b495fa6c9344d01b11cf16d/OpenSSL/crypto.py#L1219>`_ that returns
either a timestamp or ``None``. This function is a thin wrapper around an
OpenSSL function with the return type ``ASN1_TIME *``. Because this C pointer
may be ``null``, the Python wrapper must be able to represent ``null``, and
``None`` is the chosen representation.
The representation of ``null`` is particularly noticeable when Python code is
marshalling data between two systems. For example, consider a Python server that
@ -134,9 +134,7 @@ Behavior In Other Languages
---------------------------
Given that ``null``-aware operators exist in other modern languages, it may be
helpful to quickly understand how they work in those languages.
C# example::
helpful to quickly understand how they work in those languages::
/* Null-coalescing. */
@ -207,13 +205,16 @@ to surprising behavior. Consider the scenario of computing the price of some
products a customer has in his/her shopping cart::
>>> price = 100
>>> requested_quantity = 5
>>> default_quantity = 1
>>> (requested_quantity or default_quantity) * price
500
# If user didn't specify a quantity, then assume the default.
>>> requested_quantity = None
>>> (requested_quantity or default_quantity) * price
100
# The user added 5 items to the cart.
>>> requested_quantity = 5
>>> (requested_quantity or default_quantity) * price
500
# User removed 5 items from cart.
>>> requested_quantity = 0
>>> (requested_quantity or default_quantity) * price # oops!
100
@ -246,9 +247,9 @@ Ternary Operator
~~~~~~~~~~~~~~~~
Another common way to initialize default values is to use the ternary operator.
Here is an excerpt from the popular `Requests package <https://github.com/kennet
hreitz/requests/blob/14a555ac716866678bf17e43e23230d81a8149f5/requests/models.py
#L212>`_::
Here is an excerpt from the popular `Requests package
<https://github.com/kennethreitz/requests/blob/14a555ac716866678bf17e43e23230d81
a8149f5/requests/models.py#L212>`_::
data = [] if data is None else data
files = [] if files is None else files
@ -302,8 +303,8 @@ from a SQL database and formats it as JSON to send to an HTTP client::
Both ``first_seen`` and ``last_seen`` are allowed to be ``null`` in the
database, and they are also allowed to be ``null`` in the JSON response. JSON
does not have a native way to represent a ``datetime``, so the server's
contract states that any non-``null`` date is represented as an ISO-8601 string.
does not have a native way to represent a ``datetime``, so the server's contract
states that any non-``null`` date is represented as an ISO-8601 string.
Note that this code is invalid by PEP-8 standards: several lines are over the
line length limit. In fact, *including it in this document* violates the PEP
@ -369,11 +370,7 @@ aliases. These new identifiers are short enough to fit a ternary expression onto
one line, but the identifiers are also less intuitive, e.g. ``fs`` versus
``first_seen``.
As a quick preview, consider an alternative rewrite using a new operator ``💩``.
(This spelling of the operator is merely a placeholder so that the *concept* can
be debated without arguing about *spelling*. It is not intended to reflect the
public's opinion of said operator. It may, however, bring new meaning to the
phrase "code smell".)::
As a quick preview, consider an alternative rewrite using a new operator::
class SiteView(FlaskView):
@route('/site/<id_>', methods=['GET'])
@ -381,19 +378,19 @@ phrase "code smell".)::
site = db.query('site_table').find(id_)
return jsonify(
first_seen=site💩first_seen.isoformat(),
first_seen=site.first_seen?.isoformat(),
id=site.id,
is_active=site.is_active,
last_seen=site💩last_seen.isoformat(),
last_seen=site.last_seen?.isoformat(),
url=site.url.rstrip('/')
)
The ``💩`` operator behaves as a "safe navigation" operator, allowing a more
The ``?.`` operator behaves as a "safe navigation" operator, allowing a more
concise syntax where the expression ``site.first_seen`` is not duplicated.
The next example is from a trending project on GitHub called `Grab
<https://github.com/lorien/grab/blob/4c95b18dcb0fa88eeca81f5643c0ebfb114bf728/grab/upload.py>`_,
which is a Python scraping library::
<https://github.com/lorien/grab/blob/4c95b18dcb0fa88eeca81f5643c0ebfb114bf728/gr
ab/upload.py>`_, which is a Python scraping library::
class BaseUploadObject(object):
def find_content_type(self, filename):
@ -465,26 +462,24 @@ long that they must be wrapped. The overall readability is worsened, not
improved.
This code *might* be improved, though, if there was a syntactic shortcut for
this common need to supply a default value. We'll assume the fictitious
operator ``🔑`` to avoid a premature debate about the spelling of said
operator::
this common need to supply a default value::
class BaseUploadObject(object):
def find_ctype(self, filename):
ctype, encoding = mimetypes.guess_type(filename)
return ctype 🔑 'application/octet-stream'
return ctype ?? 'application/octet-stream'
class UploadContent(BaseUploadObject):
def __init__(self, content, filename=None, content_type=None):
self.content = content
self.filename = filename 🔑 self.get_random_filename()
self.content_type = content_type 🔑 self.find_ctype(self.filename)
self.filename = filename ?? self.get_random_filename()
self.content_type = content_type ?? self.find_ctype(self.filename)
class UploadFile(BaseUploadObject):
def __init__(self, path, filename=None, content_type=None):
self.path = path
self.filename = filename 🔑 os.path.split(path)[1]
self.content_type = content_type 🔑 self.find_ctype(self.filename)
self.filename = filename ?? os.path.split(path)[1]
self.content_type = content_type ?? self.find_ctype(self.filename)
This syntax has an intuitive ordering of the operands, e.g. ``ctype`` -- the
preferred value -- comes before the fallback value. The terseness of the syntax
@ -504,9 +499,9 @@ Usage Of ``None`` In The Standard Library
The previous sections show some code patterns that are claimed to be "common",
but how common are they? The attached script `find-pep505.py
<https://github.com/python/peps/blob/master/pep-0505/find-pep505.py>`_ is meant to
answer this question. It uses the ``ast`` module to search for variations of the
following patterns in any ``*.py`` file.
<https://github.com/python/peps/blob/master/pep-0505/find-pep505.py>`_ is meant
to answer this question. It uses the ``ast`` module to search for variations of
the following patterns in any ``*.py`` file::
>>> # None-coalescing if block
...
@ -577,11 +572,11 @@ The script prints out any matches it finds. Sample::
hand, we assume that ``and`` is always incorrect for safe navigation.
The script has been tested against `test.py
<https://github.com/python/peps/blob/master/pep-0505/test.py>`_ and the Python 3.4
standard library, but it should work on any arbitrary Python 3 source code. The
complete output from running it against the standard library is attached to this
proposal as
`find-pep505.out <https://github.com/python/peps/blob/master/pep-0505/find-pep505.out>`_.
<https://github.com/python/peps/blob/master/pep-0505/test.py>`_ and the Python
3.4 standard library, but it should work on any arbitrary Python 3 source code.
The complete output from running it against the standard library is attached to
this proposal as `find-pep505.out
<https://github.com/python/peps/blob/master/pep-0505/find-pep505.out>`_.
The script counts how many matches it finds and prints the totals at the
end::
@ -614,17 +609,11 @@ natural to ask if it should also apply to function invocation syntax. It might
be written as ``foo?()``, where ``foo`` is only called if it is not None. This
idea was quickly rejected, for several reasons.
First, no other mainstream language has such syntax. Moreover, it would be
difficult to discern if a function call returned ``None`` because the function
itself returned ``None`` or because it was short-circuited. Finally, Python
evaluates arguments to a function before it looks up the function itself, so
First, no other mainstream language has such syntax. Second, Python evaluates
arguments to a function before it looks up the function itself, so
``foo?(bar())`` would still call ``bar()`` even if ``foo`` is ``None``. This
behaviour is unexpected for a so-called "short-circuiting" operator.
Instead, the "``None``-severing" operator is proposed below. This operator
offers a concise form for writing ``None``-aware function expressions that is
truly short-circuiting.
``?`` Unary Postfix Operator
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -663,7 +652,7 @@ throughout the standard library and any third party library.
At the same time, the ``?`` operator may also be **too general**, in the sense
that it can be combined with any other operator. What should the following
expressions mean?
expressions mean?::
>>> x? + 1
>>> x? -= 1
@ -710,12 +699,11 @@ not nearly as powerful as support built into the language.
Specification
=============
This PEP suggests 4 new operators be added to Python:
This PEP suggests 3 new operators be added to Python:
1. ``None``-coalescing operator
2. ``None``-severing operator
3. ``None``-aware attribute access
4. ``None``-aware index access/slicing
2. ``None``-aware attribute access
3. ``None``-aware index access/slicing
We will continue to assume the same spellings as in
the previous sections in order to focus on behavior before diving into the much
@ -725,32 +713,85 @@ A generalization of these operators is also proposed below under the heading
"Generalized Coalescing".
Operator Spelling
-----------------
Despite significant support for the proposed operators, the majority of
discussion on python-ideas fixated on the spelling. Many alternative spellings
were proposed, both punctuation and keywords, but each alternative drew some
criticism. Spelling the operator as a keyword is problematic, because adding new
keywords to the language is not backwards compatible.
It is not impossible to add a new keyword, however, and we can look at several
other PEPs for inspiration. For example, `PEP-492
<https://www.python.org/dev/peps/pep-0492/>`_ introduced the new keywords
``async`` and ``await`` into Python 3.5. These new keywords are fully backwards
compatible, because that PEP also introduces a new lexical context such that
``async`` and ``await`` are only treated as keywords when used inside of an
``async def`` function. In other locations, ``async`` and ``await`` may be used
as identifiers.
It is also possible to craft a new operator out of existing keywords, as was
the case with `PEP-308 <https://www.python.org/dev/peps/pep-0308/>`_, which
created a ternary operator by cobbling together the `if` and `else` keywords
into a new operator.
In addition to the lexical acrobatics required to create a new keyword, keyword
operators are also undesirable for creating an assignment shortcut syntax. In
Dart, for example, ``x ??= y`` is an assignment shortcut that approximately
means ``x = x ?? y`` except that ``x`` is only evaluated once. If Python's
coalesce operator is a keyword, e.g. ``foo``, then the assignment shortcut would
be very ugly: ``x foo= y``.
Spelling new logical operators with punctuation is unlikely, for several
reasons. First, Python eschews punctuation for logical operators. For example,
it uses ``not`` instead of ``!``, ``or`` instead of ``||``, and ``… if … else …``
instead of ``… ? … : …``.
Second, nearly every single punctuation character on a standard keyboard already
has special meaning in Python. The only exceptions are ``$``, ``!``, ``?``, and
backtick (as of Python 3). This leaves few options for a new, single-character
operator.
Third, other projects in the Python universe assign special meaning to
punctuation. For example, `IPython
<https://ipython.org/ipython-doc/2/interactive/reference.html>`_ assigns
special meaning to ``%``, ``%%``, ``?``, ``??``, ``$``, and ``$$``, among
others. Out of deference to those projects and the large communities using them,
introducing conflicting syntax into Python is undesirable.
The spellings ``??`` and ``?.`` will be familiar to programmers who have seen
them in other popular programming languages. Any alternative punctuation will be
just as ugly but without the benefit of familiarity from other languages.
Therefore, this proposal spells the new operators using the same punctuation
that already exists in other languages.
``None``-Coalescing Operator
----------------------------
The ``None``-coalescing operator is a short-circuiting, binary operator that behaves
in the following way.
The ``None``-coalescing operator is a short-circuiting, binary operator that
behaves in the following way.
1. Evaluate the left operand first.
2. If the left operand is not ``None``, then return it immediately.
3. Else, evaluate the right operand and return the result.
Some simple examples::
Consider the following examples. We will continue to use the spelling ``??``
here, but keep in mind that alternative spellings will be discussed below::
>>> 1 🔑 2
>>> 1 ?? 2
1
>>> None 🔑 2
>>> None ?? 2
2
>>> 1 🔑 None
1
Importantly, note that the right operand is not evaluated unless the left
operand is None::
>>> def err(): raise Exception('foo')
>>> 1 🔑 err()
>>> 1 ?? err()
1
>>> None 🔑 err()
>>> None ?? err()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 1, in err
@ -762,172 +803,70 @@ this makes the operator easy to chain::
>>> timeout = None
>>> local_timeout = 60
>>> global_timeout = 300
>>> timeout 🔑 local_timeout 🔑 global_timeout
>>> timeout ?? local_timeout ?? global_timeout
60
>>> local_timeout = None
>>> timeout 🔑 local_timeout 🔑 global_timeout
>>> timeout ?? local_timeout ?? global_timeout
300
>>> import time
>>> timeout 🔑 local_timeout 🔑 global_timeout 🔑 time.sleep(10)
300
Note in the last example that ``time.sleep(10)`` represents an expensive
function call, e.g. initializing a complex data structure. In this example
``time.sleep`` is not evaluated, and the result ``300`` is returned instantly.
The operator has higher precedence than the comparison operators ``==``, ``>``,
``is``, etc., but lower precedence than any bitwise or arithmetic operators.
This precedence is chosen for making "default value" expressions intuitive to
read and write::
>>> user_flag = None
>>> default_flag = True
>>> not user_flag 🔑 default_flag # Same as next expression.
False
>>> not (user_flag 🔑 default_flag) # Same as previous.
False
>>> (not user_flag) 🔑 default_flag # Different from previous.
True
>>> not None ?? True
>>> not (None ?? True) # Same precedence
>>> user_quantity = None
>>> default_quantity = 1
>>> 1 == user_quantity 🔑 default_quantity # Same as next expression.
True
>>> 1 == (user_quantity 🔑 default_quantity) # Same as previous.
True
>>> (1 == user_quantity) 🔑 default_quantity # Different from previous.
False
>>> 1 == None ?? 1
>>> 1 == (None ?? 1) # Same precedence
>>> user_words = None
>>> default_words = ['foo', 'bar']
>>> 'foo' in user_words 🔑 default_words # Same as next expression.
True
>>> 'foo' in (user_words 🔑 default_words) # Same as previous.
True
>>> ('foo' in user_words) 🔑 default_words # Different from previous.
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: argument of type 'NoneType' is not iterable
>>> 'foo' in None ?? ['foo', 'bar']
>>> 'foo' in (None ?? ['foo', 'bar']) # Same precedence
>>> user_discount = None
>>> default_discount = 0.9
>>> price = 100
>>> price * user_discount 🔑 default_discount
>>> 1 + None ?? 2
>>> 1 + (None ?? 2) # Same precedence
Recall the example above of calculating the cost of items in a shopping cart,
and the easy-to-miss bug. This type of bug is not possible with the ``None``-
coalescing operator, because there is no implicit type coersion to ``bool``::
>>> price = 100
>>> requested_quantity = 0
>>> default_quantity = 1
>>> (requested_quantity 🔑 default_quantity) * price
>>> price = 100
>>> requested_quantity ?? default_quantity * price
0
The ``None``-coalescing operator also has a corresponding assignment shortcut.
The following assignments are semantically equivalent::
The following assignments are semantically similar, except that ```foo`` is only
looked up once when using the assignment shortcut::
>>> foo 🔑= []
>>> foo = foo 🔑 []
>>> foo ??= []
>>> foo = foo ?? []
The ``None`` coalescing operator improves readability, especially when handling
default function arguments. Consider again the example of requests, rewritten to
use ``None``-coalescing::
default function arguments. Consider again the example from the Requests
library, rewritten to use ``None``-coalescing::
def __init__(self, data=None, files=None, headers=None, params=None, hooks=None):
self.data = data 🔑 []
self.files = files 🔑 []
self.headers = headers 🔑 {}
self.params = params 🔑 {}
self.hooks = hooks 🔑 {}
self.data = data ?? []
self.files = files ?? []
self.headers = headers ?? {}
self.params = params ?? {}
self.hooks = hooks ?? {}
The operator makes the intent easier to follow (by putting operands in an
intuitive order) and is more concise than the ternary operator, while still
preserving the short circuit semantics of the code that it replaces.
``None``-Severing Operator
--------------------------
The idea of a ``None``-aware function invocation syntax was discussed on python-
ideas. The idea was not popular, so no such operator is included in this
proposal. (Justification for its exclusion is discussed in a previous section.)
Still, calling a function when it is not ``None`` is a common idiom in Python,
particularly for callback functions. Consider this hypothetical example::
import time
def delay(seconds, callback=None):
time.sleep(seconds)
if callback is not None:
callback()
With the rejected ``None``-aware function call syntax, this example might be
written more concisely as::
import time
def delay(seconds, callback=None):
time.sleep(seconds)
callback?()
Instead, consider a "``None``-severing" operator, however, which is a short-
circuiting, boolean operator similar to the ``None``-coalesing operator, except
it returns ``None`` if the left operand is ``None`` and returns the right
operand otherwise. It has short circuiting behavior that compliments the
``None``-coalescing operator: if the left operand is None, then the right
operand is not evaluated. Let's temporarily spell this operator ``✂`` and
rewrite the example accordingly::
import time
def delay(seconds, callback=None):
time.sleep(seconds)
callback ✂ callback()
At this point, you may be astonished at the mere suggestion of such a strange
operator with limited practical usefulness. It is proposed here because of the
symmetry it has with the ``None``-coalescing operator. This symmetry may be more
apparent if the two operators have complementary spellings.
In the same way that ``or`` and ``and`` go together, ``None``-coalescing and
``None``- severing might be spelled in a pleasing, symmetric way, e.g. ``or?``
and ``and?``. If such a spelling can be decided on, then this operator adds very
little cognitive load or special machinery to the language, and it's minor
utility may justify its inclusion in the language.
Note that ``None``-severing could also be used as an alternative to "safe
navigation", at the expense of some repeated expressions::
>>> from datetime import datetime
>>> d = None
>>> type(d ✂ d.isoformat())
<class 'NoneType'>
>>> d = datetime.now()
>>> d ✂ d.isoformat()
'2015-10-16T20:53:40.312135'
The repeated expression ``d`` makes this less useful than a ``None``-aware
attribute access operator, but to repeat what was said at the outset: this
proposal may be approved or rejected in whole or in part. This unlikely operator
is included in the proposal in order to be comprehensive.
The precedence and associativity of the ``None``-severing operator are the same
as the ``None``-coalescing operator.
``None``-Aware Attribute Access Operator
----------------------------------------
The ``None``-aware attribute access operator (also called "safe navigation")
checks its left operand. If the left operand is ``None``, then the operator
evaluates to ``None``. If the left operand is not ``None``, then the
operator accesses the attribute named by the right operand. As in the previous
section, we continue to use the temporary spelling ``💩``::
evaluates to ``None``. If the the left operand is not ``None``, then the
operator accesses the attribute named by the right operand::
>>> from datetime import date
>>> d = date.today()
@ -940,30 +879,31 @@ section, we continue to use the temporary spelling ``💩``::
File "<stdin>", line 1, in <module>
AttributeError: 'NoneType' object has no attribute 'year'
>>> d💩year
>>> d?.year
None
The operator has the same precedence and associativity as the plain attribute
access operator ``.``, but this operator is also short-circuiting in a unique
way: if the left operand is ``None``, then any series of attribute access, index
access, slicing, or function call operators immediately to the right of it *are
not evaluated*.
not evaluated*::
>>> name = ' The Black Knight '
>>> name.strip()[4:].upper()
'BLACK KNIGHT'
>>> name = None
>>> name💩strip()[4:].upper()
>>> name?.strip()[4:].upper()
None
If this operator did not short circuit in this way, then the second example
would partially evaluate ``name💩strip()`` to ``None()`` and then fail with
would partially evaluate ``name?.strip()`` to ``None()`` and then fail with
``TypeError: 'NoneType' object is not callable``.
To put it another way, the following expressions are semantically equivalent::
To put it another way, the following expressions are semantically similar,
except that ``name`` is only looked up once on the first line::
>>> name💩strip()[4:].upper()
>>> name?.strip()[4:].upper()
>>> name.strip()[4:].upper() if name is not None else None
.. note::
@ -975,21 +915,21 @@ To put it another way, the following expressions are semantically equivalent::
nearly useless.
This operator short circuits one or more attribute access, index access,
slicing, or function call operators that are immediately to its right, but it
slicing, or function call operators that are adjacent to its right, but it
does not short circuit any other operators (logical, bitwise, arithmetic, etc.),
nor does it escape parentheses::
>>> d = date.today()
>>> d💩year.numerator + 1
>>> d?.year.numerator + 1
2016
>>> d = None
>>> d💩year.numerator + 1
>>> d?.year.numerator + 1
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for +: 'NoneType' and 'int'
>>> (d💩year).numerator + 1
>>> (d?.year).numerator + 1
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'NoneType' object has no attribute 'numerator'
@ -1003,9 +943,10 @@ The third example fails because the operator does not escape parentheses. In
that example, the attribute access ``numerator`` is evaluated and fails because
``None`` does not have that attribute.
Finally, observe that short circuiting adjacent operators is not at all the same thing as propagating ``None`` throughout an expression::
Finally, observe that short circuiting adjacent operators is not at all the same
thing as propagating ``None`` throughout an expression::
>>> user💩first_name.upper()
>>> user?.first_name.upper()
If ``user`` is not ``None``, then ``user.first_name`` is evaluated. If
``user.first_name`` evaluates to ``None``, then ``user.first_name.upper()`` is
@ -1015,7 +956,7 @@ a value, then it must have a ``first_name``, too."
If ``first_name`` is supposed to be optional attribute, then the expression must
make that explicit::
>>> user💩first_name💩upper()
>>> user?.first_name?.upper()
The operator is not intended as an error silencing mechanism, and it would be
undesirable if its presence infected nearby operators.
@ -1039,7 +980,7 @@ the spelling of which is discussed later::
File "<stdin>", line 1, in <module>
TypeError: 'NoneType' object is not subscriptable
>>> person💩['name']
>>> person?.['name']
None
The ``None``-aware slicing operator behaves similarly::
@ -1054,7 +995,7 @@ The ``None``-aware slicing operator behaves similarly::
File "<stdin>", line 1, in <module>
TypeError: 'NoneType' object is not subscriptable
>>> name💩[4:]
>>> name?.[4:]
None
These operators have the same precedence as the plain index access and slicing
@ -1065,10 +1006,10 @@ operators. They also have the same short-circuiting behavior as the
Generalized Coalescing
----------------------
Making ``None`` a special case may seem too specialized and magical. It is
possible to generalize the behavior by making the ``None``-aware operators
invoke a dunder method, e.g. ``__coalesce__(self)`` that returns ``True`` if an
object should be coalesced and ``False`` otherwise.
Making ``None`` a special case is too specialized and magical. The behavior can
be generalized by making the ``None``-aware operators invoke a dunder method,
e.g. ``__coalesce__(self)`` that returns ``True`` if an object should be
coalesced and ``False`` otherwise.
With this generalization, ``object`` would implement a dunder method equivalent
to this::
@ -1085,23 +1026,25 @@ If this generalization is accepted, then the operators will need to be renamed
such that the term ``None`` is not used, e.g. "Coalescing Operator", "Coalesced
Member Access Operator", etc.
The coalescing operator would invoke this dunder method. The following two expressions are semantically equivalent::
The coalesce operator would invoke this dunder method. The following two
expressions are semantically similar, except `foo` is only looked up once when
using the coalesce operator::
>>> foo 🔑 bar
>>> foo ?? bar
>>> bar if foo.__coalesce__() else foo
The coalesced attribute and index access operators would invoke the same dunder
method::
>>> user💩first_name.upper()
>>> user?.first_name.upper()
>>> None if user.__coalesce__() else user.first_name.upper()
This generalization allows for domain-specific ``null`` objects to be coalesced
just like ``None``. For example, the ``pyasn1`` package has a type called
``Null`` that represents an ASN.1 ``null``.
just like ``None``. For example the ``pyasn1`` package has a type called
``Null`` that represents an ASN.1 ``null``::
>>> from pyasn1.type import univ
>>> univ.Null() 🔑 univ.Integer(123)
>>> univ.Null() ?? univ.Integer(123)
Integer(123)
In addition to making the proposed operators less specialized, this
@ -1109,179 +1052,13 @@ generalization also makes it easier to work with the Null Object Pattern, [3]_
for those developers who prefer to avoid using ``None``.
Operator Spelling
-----------------
Despite significant support for the proposed operators, the majority of
discussion on python-ideas fixated on the spelling. No consensus was achieved on
this question, for two reasons. First, Python eschews punctuation for logical
operators. For example, it uses ``not`` instead of ``!`` and ``… if … else …``
instead of ``?:``. Introducing new punctuation is a major turnoff to many
Pythonistas, including BDFL. Second, adding new keywords to the language is
not backwards compatible. Any new keyword could only be introduced in the next
major version, e.g. Python 4. (Even then, `there would be resistance
<http://opensource.com/life/14/9/why-python-4-wont-be-python-3>`_.)
Furthermore, nearly every single punctuation character on a standard keyboard
already has special meaning in Python. The only exceptions are ``$``, ``!``,
``?``, and backtick (as of Python 3). This leaves few options for a new, single-
character operator. A two character spelling is more likely, such as the ``??``
and ``?.`` spellings in other programming languages, but this decreases the
appeal of punctuation even further.
Finally, other projects in the Python universe assign special meaning to
punctuation. For example, `IPython <https://ipython.org/ipython-
doc/2/interactive/reference.html>`_ assigns special meaning to ``%``, ``%%``,
``?``, ``??``, ``$``, and ``$$``, among others. Out of deference to those
projects and the large communities using them, introducing conflicting syntax
into Python is undesirable.
This is not the first PEP to deal with this dilemma. PEP-308 [5]_, which
introduced the ternary operator, faced similar issues.
Alternative Spellings
~~~~~~~~~~~~~~~~~~~~~
In keeping with the spirit of the PEP, many alternative spellings for these
``None``-aware operators are suggested, including some that conflict with each
other. Deconfliction will be handled only if any part of this proposal is
accepted.
One caveat noted by several respondents on python-ideas: using similar spelling
for ``None`` coalescing and other ``None``-aware operators may be confusing,
because they have different short circuit semantics: coalescing short circuits
on non-``None``, while ``None``-aware attribute/index access short circuit on
``None``. This is a potential downside to spellings like ``??`` and ``?.``. This
is only a practical concern if any part of this proposal is actually accepted,
so there is no need to pontificate any further.
The following spellings are proposed candidates for the ``None``-coalescing
operator.
1. ``foo ?? bar ?? baz``
- Pros: same spelling as C# and Dart
- Cons: punctuation is ugly; possible conflict with IPython; difficult to
google to find out what it means
2. ``foo or? bar or? baz``
- Pros: similar to existing ``or`` operator
- Cons: the difference between this and ``or`` is not intuitive; punctuation
is ugly; different precedence from ``or`` may be confusing
3. ``foo ? bar ? baz``
- Pros: similar to ``??`` used in other languages
- Cons: punctuation is ugly; possible conflict with IPython; not used by any
other language
4. ``foo $$ bar $$ baz``
- Pros: pronounced "value operator" because it returns the first operand
that has a "value"
- Cons: punctuation is ugly; not used by any other language
5. ``foo else bar else baz``
- Pros: prettier than punctuation; uses an existing keyword
- Cons: difficult or impossible to implement with Python's LL(1) parser
6. ``foo or else bar or else baz``
- Pros: prettier than punctuation; use existing keywords
- Cons: difficult or impossible to implement with Python's LL(1) parser
7. ``foo def bar def baz``
- Pros: pronounced 'default'; prettier than punctuation
- Cons: difficult or impossible to implement with Python's LL(1) parser
8. ``foo then bar then baz``
- Pros: prettier than punctuation
- Cons: requires a new keyword, probably can't be implemented until Python 4
(and maybe not even then)
9. No ``None``-coalescing operator.
- (Pros and cons discussed throughout this document.)
The following spellings are proposed candidates for the ``None``-severing
operator. Each alternative has symmetry with one of the proposed spellings of
the ``None``- coalescing operator.
1. ``foo !! bar``
- Pros: symmetric with ``??``
- Cons: punctuation is ugly; possible conflict with IPython; difficult to
google to find out what it means
2. ``foo and? bar``
- Pros: symmetric with ``or?``
- Cons: punctuation is ugly; possible conflict with IPython; difficult to
google to find out what it means; different precedence from ``and`` may be
confusing
3. No ``None``-severing operator.
- (Pros and cons discussed throughout this document.)
The following spellings are proposed candidates for the ``None``-aware attribute
access operator. If you find any of these hard to read, consider that we may
adopt a convention of adding whitespace around a ``None``-aware operator to
improve readability.
1. ``foo?.bar``, ``foo ?. bar``
- Pros: same spelling as C# and Dart
- Cons: punctuation is ugly; possible conflict with IPython; difficult to
google to find out what it means; difficult to differentiate from ``.``
when reading quickly
2. ``foo$.bar``, ``foo $. bar``
- Pros: symmetry with ``$$`` operator proposed above
- Cons: punctuation is ugly; difficult to google; possible confusion because
it looks a bit like other languages' string interpolation; difficult to
google to find out what it means; difficult to differentiate from ``.``
when reading quickly
3. ``foo!bar``, ``foo ! bar``
- Pros: similar to ordinary ``.`` operator
- Cons: punctuation is ugly; possible conflict with IPython; no corresponding
spelling for index access (e.g. ``foo!['bar']`` is ambiguous)
4. ``foo->bar``, ``foo -> bar``
- Pros: easier to read than other punctuation; less likely to be confused
with ordinary attribute access
- Cons: punctuation is ugly; difficult to google; confusing because it is
spelled the same as C's dereference operator
5. ``foo try .bar``
- Pros: uses an existing keyword;
- Cons: difficult or impossible to implement in Python's LL(1) parser
6. No ``None``-aware attribute access operator.
- (Pros and cons discussed throughout this document.)
The following spellings are proposed candidates for the ``None``-aware index
access/slicing operator. The punctuation used for this operator ought to
resemble the punctuation used for the ``None``-aware attribute access.
1. ``foo?['bar']``, ``foo ? ['bar']``
- Pros: same spelling as C# and Dart
- Cons: punctuation is ugly; possible conflict with IPython; difficult to
google to find out what it means
2. ``foo$['bar']``, ``foo $ ['bar']``
- Pros: symmetry with ``$$`` operator proposed above
- Cons: punctuation is ugly; possible confusion because
it looks a bit like other languages' string interpolation
3. ``foo->['bar']``, ``foo -> ['bar']``
- Pros: easier to read than other punctuation; less likely to be confused
with ordinary attribute access
- Cons: punctuation is ugly; difficult to google; confusing because it is
spelled the same as C's dereference operator
4. ``foo try ['bar']``
- Pros: uses an existing keyword;
- Cons: difficult or impossible to implement in Python's LL(1) parser
5. No ``None``-aware index access/slicing operator.
- (Pros and cons discussed throughout this document.)
Community Poll
~~~~~~~~~~~~~~
In order to collect data about the Python community's preferences for
``None``-aware operators, and with BDFL's consent, a public poll will be
conducted, just as with PEP-308. The poll is viewed as a data-gathering
exercise, not a democratic vote.
The poll will allow respondents to rank their favorite options from the previous
section. The results will
be placed in this section of the PEP.
...TBD...
Implementation
--------------
Given that the need for ``None``-aware operators is questionable and the
spelling of said operators is almost incendiary, the implementation details for
CPython will be deferred unless and until we have a clearer idea that one (or
more) of the proposed operators will be approved.
The author of this PEP is not competent with grammars or lexers, and given the
contentiousness of this proposal, the implementation details for CPython will be
deferred until we have a clearer idea that one or more of the proposed
enhancements will be approved.
...TBD...