Python Enhancement Proposals

PEP 682 – Format Specifier for Signed Zero

PEP
682
Title
Format Specifier for Signed Zero
Author
John Belmonte <john at neggie.net>
Sponsor
Mark Dickinson <dickinsm at gmail.com>
PEP-Delegate
Mark Dickinson
Discussions-To
https://discuss.python.org/t/pep-682-format-specifier-for-signed-zero/13596
Status
Accepted
Type
Standards Track
Created
29-Jan-2022
Python-Version
3.11
Post-History
08-Feb-2022
Resolution
https://discuss.python.org/t/accepting-pep-682-format-specifier-for-signed-zero/14088

Contents

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.

Motivation

Here is negative zero:

>>> 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:

>>> 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:

>>> 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:

>>> 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:

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.

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:

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 any other widely-used language or library providing a formatting option for negative zero. However, the same z option syntax and semantics specified below have 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.)

When Rust developers debated whether to suppress negative zero in print output, they took a small survey of other languages. Notably, it didn’t mention any language providing an option for negative zero handling.

Specification

An optional, literal z is added to the Format Specification Mini-Language following sign:

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

where z is allowed for floating-point presentation types (f, g, etc., as defined by the format specification documentation). 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().

When z is present, negative zero (whether the original value or the result of rounding) will be normalized to positive zero.

Synopsis:

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

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

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).

C99 printf already uses the z option character for another purpose: qualifying the unsigned type (u) to match the length of size_t. However, since the signed zero option specifically disallows z for integer presentation types, it’s possible to disambiguate the two uses, should C want to adopt this new option.

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.

Reference Implementation

A reference implementation exists at pull request #30049.


Source: https://github.com/python-discord/peps/blob/main/pep-0682.rst

Last modified: 2022-03-06 15:51:26 GMT