Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PEP 682: Format Specifier for Signed Zero #2295

Merged
merged 13 commits into from
Feb 8, 2022
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -642,3 +642,4 @@ pep-8016.rst @njsmith @dstufft
pep-8100.rst @njsmith
# pep-8101.rst
# pep-8102.rst
pep-9999.rst @mdickinson
188 changes: 188 additions & 0 deletions pep-9999.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
PEP: 9999
Title: Format Specifier for Signed Zero
Author: John Belmonte <john@neggie.net>
Sponsor: Mark Dickinson
Discussions-To: https://bugs.python.org/issue45995
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


Background
==========

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

The behavior of number formatting with regard to negative zero is not
unique to Python. The first low-level implementations of ``printf()``
and such likely wired the sign bit to ``-`` in the output without special
consideration of the zero mantissa case. In hindsight, it would have
been nice to have a formatting option for treatment of negative zero --
perhaps even coercing it to positive zero by default. However, for better
or worse, existing number formatting implementations inherit from the
original specimens, passing the zero sign through without option.


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 only for ``float`` and ``Decimal`` types. The new specifier
is supported by both f-strings and ``str.format()``, but not %-formatting.

Synopsis:

.. code-block:: pycon

>>> x = -.00001
>>> f'{x:z.1f}'
'0.0'

>>> x = decimal.Decimal('-.00001')
>>> '{}'.format('+z.1f')
'+0.0'

>>> x = 0
>>> f'{x:z.1f}' # not supported on int type
ValueError: Negative zero coercion (z) not allowed in int format specifier

>>> x = -.00001
>>> '%z.1f' % x # not supported on %-formatting
ValueError: unsupported format character 'z' (0x7a) at index 1

.. _`Format Specification Mini-Language`: https://docs.python.org/3/library/string.html#format-specification-mini-language


Rationale
=========

There are use cases where negative zero is unwanted in formatted number
output -- arguably it's the more common case. 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)}]"

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]]``, since the latter would defeat ``sign``'s default (``-``)
when using the ``z`` option. Precisely because it is the default, ``-`` is
not well known, and requiring it would increase the difficulty for those
using or encountering the new option.

There is a long-standing decision not to enhance the old %-formatting, so
only f-strings and ``str.format()`` support the new option.


Similar Work
============

The same formatting option for negative zero has been `proposed for
C++ std::format()`_. While the proposal was withdrawn for C++20, a
consensus proposal is promised for C++23. (For what it's worth, 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


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: