Skip to content
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
Comment thread
belm0 marked this conversation as resolved.
Resolved
173 changes: 173 additions & 0 deletions pep-9999.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
PEP: 9999
Comment thread
belm0 marked this conversation as resolved.
Resolved
Title: Format Specifier for Signed Zero
Author: John Belmonte <john@neggie.net>
Sponsor: Mark Dickinson
Comment thread
belm0 marked this conversation as resolved.
Resolved
Discussions-To: https://bugs.python.org/issue45995
Comment thread
belm0 marked this conversation as resolved.
Resolved
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
Comment thread
belm0 marked this conversation as resolved.
Resolved
of mathematics negative zero is surprising or unwanted-- especially in the
Comment thread
belm0 marked this conversation as resolved.
Resolved
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
Comment thread
belm0 marked this conversation as resolved.
Resolved


Background
==========
Comment thread
belm0 marked this conversation as resolved.
Resolved

Here is negative zero::
Comment thread
belm0 marked this conversation as resolved.
Resolved

>>> 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::
Comment thread
belm0 marked this conversation as resolved.
Resolved

>>> for x in (.002, -.001, .060):
... print(f'{x: .1f})
Comment thread
belm0 marked this conversation as resolved.
Resolved
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::
Comment thread
belm0 marked this conversation as resolved.
Resolved

>>> x = -0.
>>> x + 0.
0.0

To normalize negative zero when formatting, it would be necessary to perform
Comment thread
belm0 marked this conversation as resolved.
Resolved
a redundant (and error-prone) pre-rounding of the input::
Comment thread
belm0 marked this conversation as resolved.
Resolved

>>> 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
Comment thread
belm0 marked this conversation as resolved.
Resolved
been nice to have a formatting option for treatment of negative zero--
Comment thread
belm0 marked this conversation as resolved.
Resolved
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.


Proposal
========
Comment thread
belm0 marked this conversation as resolved.
Resolved

An optional, literal ``z`` is added to the
`Format Specification Mini-Language`_ following ``sign``::
Comment thread
belm0 marked this conversation as resolved.
Resolved

[[fill]align][sign][z][#][0][width][grouping_option][.precision][type]

where ``z`` is allowed only for float and Decimal types. The new specifier
Comment thread
belm0 marked this conversation as resolved.
Resolved
is supported by both f-strings and ``str.format()``, but not %-formatting.

Comment thread
belm0 marked this conversation as resolved.
Resolved
Synopsis::
Comment thread
CAM-Gerlach marked this conversation as resolved.
Resolved

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

>>> x = decimal.Decimal('-.00001')
>>> '{}'.format('+z.1f')
'+0.0'
Comment thread
belm0 marked this conversation as resolved.
Resolved

>>> 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
=========
Comment thread
belm0 marked this conversation as resolved.
Resolved

There are use cases where negative zero is unwanted in formatted number
output-- arguably it's the more common case. Expanding the format
Comment thread
belm0 marked this conversation as resolved.
Resolved
specification is the best way to support this because number formatting
already incorporates rounding, and the normalization of negative zero must
happen after the rounding.
Comment thread
belm0 marked this conversation as resolved.
Resolved

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::
Comment thread
belm0 marked this conversation as resolved.
Resolved

def format_vector(v, format_spec='8.2f'):
"""Format a vector (any iterable) using given per-term format string"""
Comment thread
belm0 marked this conversation as resolved.
Resolved
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.
Comment thread
belm0 marked this conversation as resolved.
Resolved

There is a long-standing decision not to enhance the old %-formatting, so
only f-strings and ``str.format()`` support the new option.
Comment thread
belm0 marked this conversation as resolved.
Resolved


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.)
Comment thread
belm0 marked this conversation as resolved.
Resolved

.. _`proposed for C++ std::format()`: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2020/p1496r2.pdf


Reference Implementation
========================

A reference implementation exits at `pull request #30049`_.
Comment thread
belm0 marked this conversation as resolved.
Resolved

.. _`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: