diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 262580dd24c..32e84bd29b3 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -558,6 +558,7 @@ pep-0678.rst @iritkatriel pep-0679.rst @pablogsal pep-0680.rst @encukou pep-0681.rst @jellezijlstra +pep-0682.rst @mdickinson # ... # pep-0754.txt # ... diff --git a/pep-0682.rst b/pep-0682.rst new file mode 100644 index 00000000000..45b2402ef29 --- /dev/null +++ b/pep-0682.rst @@ -0,0 +1,217 @@ +PEP: 682 +Title: Format Specifier for Signed Zero +Author: John Belmonte +Sponsor: Mark Dickinson +Discussions-To: +Status: Draft +Type: Standards Track +Content-Type: text/x-rst +Created: 29-Jan-2022 +Python-Version: 3.11 +Post-History: + + +Abstract +======== + +Though ``float`` and ``Decimal`` types can represent `signed zero`_, in many +fields of mathematics negative zero is surprising or unwanted -- especially +in the context of displaying an (often rounded) numerical result. This PEP +proposes an extension to the `string format specification`_ allowing negative +zero to be normalized to positive zero. + +.. _`signed zero`: https://en.wikipedia.org/wiki/Signed_zero +.. _`string format specification`: https://docs.python.org/3/library/string.html#formatstrings + + +Motivation +========== + +Here is negative zero: + +.. code-block:: pycon + + >>> x = -0. + >>> x + -0.0 + +When formatting a number, negative zero can result from rounding. Assuming +the user's intention is truly to discard precision, the distinction between +negative and positive zero of the rounded result might be considered an +unwanted artifact: + +.. code-block:: pycon + + >>> for x in (.002, -.001, .060): + ... print(f'{x: .1f}') + 0.0 + -0.0 + 0.1 + +There are various approaches to clearing the sign of a negative zero. It +can be achieved without a conditional by adding positive zero: + +.. code-block:: pycon + + >>> x = -0. + >>> x + 0. + 0.0 + +To normalize negative zero when formatting, it is necessary to perform +a redundant (and error-prone) pre-rounding of the input: + +.. code-block:: pycon + + >>> for x in (.002, -.001, .060): + ... print(f'{round(x, 1) + 0.: .1f}') + 0.0 + 0.0 + 0.1 + +There is ample evidence that, regardless of the language, programmers are +often looking for a way to suppress negative zero, and landing on a +variety of workarounds (pre-round, post-regex, etc.). A sampling: + +* `How to have negative zero always formatted as positive zero in a + python string?`_ (Python, post-regex) +* `(Iron)Python formatting issue with modulo operator & "negative zero"`_ + (Python, pre-round) +* `Negative sign in case of zero in java`_ (Java, post-regex) +* `Prevent small negative numbers printing as "-0"`_ (Objective-C, custom + number formatter) + +What we would like instead is a first-class option to normalize negative +zero, on top of everything else that numerical string formatting already +offers. + +.. _`How to have negative zero always formatted as positive zero in a python string?`: https://stackoverflow.com/questions/11010683/how-to-have-negative-zero-always-formatted-as-positive-zero-in-a-python-string/36604981#36604981 +.. _`(Iron)Python formatting issue with modulo operator & "negative zero"`: https://stackoverflow.com/questions/41564311/ironpython-formatting-issue-with-modulo-operator-negative-zero/41564834#41564834 +.. _`Negative sign in case of zero in java`: https://stackoverflow.com/questions/11929096/negative-sign-in-case-of-zero-in-java +.. _`Prevent small negative numbers printing as "-0"`: https://stackoverflow.com/questions/10969399/prevent-small-negative-numbers-printing-as-0 + + +Rationale +========= + +There are use cases where negative zero is unwanted in formatted number +output -- arguably, not wanting it is more common. Expanding the format +specification is the best way to support this because number formatting +already incorporates rounding, and the normalization of negative zero must +happen after rounding. + +While it is possible to pre-round and normalize a number before formatting, +it's tedious and prone to error if the rounding doesn't precisely match +that of the format spec. Furthermore, functions that wrap formatting would +find themselves having to parse format specs to extract the precision +information. For example, consider how this utility for formatting +one-dimensional numerical arrays would be complicated by such pre-rounding: + +.. code-block:: python + + def format_vector(v, format_spec='8.2f'): + """Format a vector (any iterable) using given per-term format string.""" + return f"[{','.join(f'{term:{format_spec}}' for term in v)}]" + +To date, there doesn't appear to be other widely-used languages or libraries +providing such a formatting option for negative zero. However, the same +``z`` option syntax and semantics has been `proposed for C++ std::format()`_. +While the proposal was withdrawn for C++20, a consensus proposal is promised +for C++23. (The original `feature request`_ prompting this PEP was argued +without knowledge of the C++ proposal.) + +.. _`proposed for C++ std::format()`: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2020/p1496r2.pdf +.. _`feature request`: https://bugs.python.org/issue45995 + + +Specification +============= + +An optional, literal ``z`` is added to the +`Format Specification Mini-Language`_ following ``sign``: + +.. code-block:: text + + [[fill]align][sign][z][#][0][width][grouping_option][.precision][type] + +where ``z`` is allowed for numerical types other than integer. Support for +``z`` is provided by the ``.__format__()`` method of each numeric type, +allowing the specifier to be used in f-strings, built-in ``format()``, and +``str.format()``. The %-formatting style will not support the new option. + +Synopsis: + +.. code-block:: pycon + + >>> x = -.00001 + >>> f'{x:z.1f}' + '0.0' + + >>> x = decimal.Decimal('-.00001') + >>> '{:+z.1f}'.format(x) + '+0.0' + +.. _`Format Specification Mini-Language`: https://docs.python.org/3/library/string.html#format-specification-mini-language + + +Design Notes +------------ +The solution must be opt-in, because we can't change the behavior of +programs that may be expecting or relying on negative zero when formatting +numbers. + +The proposed extension is intentionally ``[sign][z]`` rather than +``[sign[z]]``. The default for ``sign`` (``-``) is not widely known or +explicitly written, so this avoids everyone having to learn it just to use +the ``z`` option. + +While f-strings, built-in ``format()``, and ``str.format()`` can access +the new option, %-formatting cannot. There is already precedent for not +extending %-formatting with new options, as was the case for the +``,`` option (:pep:`378`). + + +Backwards Compatibility +======================= + +The new formatting behavior is opt-in, so numerical formatting of existing +programs will not be affected. + + +How to Teach This +================= +A typical introductory Python course will not cover string formatting +in full detail. For such a course, no adjustments would need to be made. +For a course that does go into details of the string format specification, +a single example demonstrating the effect of the `z` option on a negative +value that's rounded to zero by the formatting should be enough. For an +independent developer encountering the feature in someone else's code, +reference to the `Format Specification Mini-Language`_ section of the +library reference manual should suffice. + +.. _`Format Specification Mini-Language`: https://docs.python.org/3/library/string.html#format-specification-mini-language + + +Reference Implementation +======================== + +A reference implementation exists at `pull request #30049`_. + +.. _`pull request #30049`: https://github.com/python/cpython/pull/30049 + + +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: