◐ Shell
clean mode source ↗

Issue 17336: Complex number representation round-trip doesn't work with signed zero values

When evaluating, signed zero complex numbers aren't recovered correctly.
>>> -0j
(-0-0j)
>>> (-0-0j)
0j
>>> 0j
0j

According to http://docs.python.org/dev/reference/datamodel.html#object.__repr__ the representation can be used to recreate an object with the same value. Shouldn't this also be possible with complex numbers?

When using complex('...'), round-trip works correctly. While this can be used to recover the exact number, i find it confusing that complex('...') isn't the same as eval('...').
>>> complex('-0j')
-0j
>>> complex('(-0-0j)')
(-0-0j)
>>> complex('0j')
0j
This is not easy to avoid, I'm afraid, and it's a consequence of Python's usual rules for mixed-type arithmetic:  (-0-0j) is interpreted as 0 - (0.0 + 0.0j) --- that is, the 0j is promoted to a complex instance (by giving it zero real part) before the subtraction is performed.  Then the real part of the result is computed as 0.0 - 0.0, which is 0.0.  Note that the first 0.0 comes from converting the *integer* 0 to a complex number.  If you do -0.0-0.0j you'll see a different result:

>>> -0.0-0.0j
(-0+0j)
The issue of changing the complex repr came up again in #41485, which has been closed as a duplicate of this issue.

See also https://bugs.python.org/issue23229#msg233963, where Guido says:

> BTW I don't want repr() of a complex number to use the complex(..., ...)
notation -- it's too verbose.

FWIW, I'd be +1 on changing the complex repr, but given Guido's opposition we're probably looking at a PEP to make that happen, and I don't have the bandwidth or the energy to push such a PEP through.
> BTW I don't want repr() of a complex number to use the complex(..., ...)

A compromise would be to only use this notation if signed zeros are involved.

---

Another option would be to use slightly unusual reprs for these complex numbers, which at least round-trip:

    def check(s, v):
        c = eval(s)
        # use string equality, because it's the easiest way to compare signed zeros
        cs = f"complex({c.real}, {c.imag})"
        vs = f"complex({v.real}, {v.imag})"
        assert vs == cs, f' expected {vs} got {cs}'

    check("-(0+0j)", complex(-0.0, -0.0))
    check("(-0.0-0j)", complex(-0.0, 0.0))  # non-intuitive
    check("-(-0.0-0j)", complex(0.0, -0.0))  # non-intuitive

Which I suppose would extend to complex numbers containing just one signed zero

    check("(-0.0-1j)", complex(-0.0, -1))
    check("-(0.0-1j)", complex(-0.0, 1))
    check("-(1+0j)", complex(-1, -0.0))
    check("-(-1+0j)", complex(1, -0.0))

Only two of these reprs are misleading for users who don't understand what's going on, the rest will just strike users as odd.