Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
8951088
Add support for unicode MINUS SIGN in `int`, `float` and `complex`
johnslavik Jan 21, 2026
d3f7c80
Add some test coverage
johnslavik Feb 13, 2026
3fe1b82
Fix test
johnslavik Feb 14, 2026
973e191
Add news entry
johnslavik Feb 14, 2026
2695c06
Add C API test coverage (only `test_long_fromunicodeobject`)
johnslavik Feb 14, 2026
cf0324a
Use `:func:` in news entry for better display
johnslavik Feb 14, 2026
3855919
Update `int` docs
johnslavik Feb 14, 2026
ae6cd6c
Update `float` docs
johnslavik Feb 14, 2026
4a78b3a
Update `complex` docs
johnslavik Feb 14, 2026
f233ebd
Simplify complex `complex` docs
johnslavik Feb 14, 2026
03d24b2
Fix formatting
johnslavik Feb 14, 2026
5ce17c9
Update float syntax snippet
johnslavik Feb 14, 2026
b4b9574
Update stdtypes reference
johnslavik Feb 14, 2026
ff013f5
Clarify `complex` docs
johnslavik Feb 14, 2026
ca6eca0
Slightly changed "syntax" used in `float` production list
johnslavik Feb 14, 2026
26dadc5
Fix alternative used in `complex` docs, I think it was incorrect
johnslavik Feb 14, 2026
6391d27
Parenthesize consistently
johnslavik Feb 14, 2026
f7c3fa7
Parenthesize consistently vol. 2
johnslavik Feb 14, 2026
0d519f4
Empty commit to try to skip incremental lint
johnslavik Feb 14, 2026
6c5994a
Test NaNs properly
johnslavik Feb 14, 2026
0e7110c
Fix trailing whitespace (was confused)
johnslavik Feb 14, 2026
199a879
Parenthesize in news entry too
johnslavik Feb 14, 2026
a9874e7
Use `.. versionchanged:: next`
johnslavik Feb 15, 2026
53c7e97
Add a *What's New* entry
johnslavik Feb 21, 2026
d349618
Merge branch 'main' to fix conflicts
johnslavik Feb 21, 2026
8b4d9b1
Make `versionchanged` entry for `float` more fitting
johnslavik Feb 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 25 additions & 8 deletions Doc/library/functions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -417,10 +417,10 @@ are always available. They are listed here in alphabetical order.
parts (the sign of the imaginary part is mandatory in this case).
The string can optionally be surrounded by whitespaces and the round
parentheses ``'('`` and ``')'``, which are ignored.
The string must not contain whitespace between ``'+'``, ``'-'``, the
``'j'`` or ``'J'`` suffix, and the decimal number.
For example, ``complex('1+2j')`` is fine, but ``complex('1 + 2j')`` raises
:exc:`ValueError`.
The string must not contain whitespace between ``'+'`` (ASCII plus sign),
``'-'`` (ASCII hyphen minus), ``''`` (Unicode minus sign, U+2212), the
``'j'`` or ``'J'`` suffix, and the decimal number. For example,
``complex('1+2j')`` is fine, but ``complex('1 + 2j')`` raises :exc:`ValueError`.
More precisely, the input must conform to the :token:`~float:complexvalue`
production rule in the following grammar, after parentheses and leading and
trailing whitespace characters are removed:
Expand Down Expand Up @@ -466,6 +466,11 @@ are always available. They are listed here in alphabetical order.
Passing a complex number as the *real* or *imag* argument is now
deprecated; it should only be passed as a single positional argument.

.. versionchanged:: next
``'−'`` (Unicode minus sign, U+2212) can be now used as an
alternative to ``'-'`` (ASCII hyphen minus) for denoting
negative sign.


.. function:: delattr(object, name, /)

Expand Down Expand Up @@ -781,15 +786,16 @@ are always available. They are listed here in alphabetical order.

If the argument is a string, it should contain a decimal number, optionally
preceded by a sign, and optionally embedded in whitespace. The optional
sign may be ``'+'`` or ``'-'``; a ``'+'`` sign has no effect on the value
sign may be ``'+'`` (ASCII plus sign), ``'-'`` (ASCII hyphen minus) or ``−``
(Unicode minus sign, U+2212); a ``'+'`` sign has no effect on the value
produced. The argument may also be a string representing a NaN
(not-a-number), or positive or negative infinity.
More precisely, the input must conform to the :token:`~float:floatvalue`
production rule in the following grammar, after leading and trailing
whitespace characters are removed:

.. productionlist:: float
sign: "+" | "-"
sign: "+" | "-" | "−" <Unicode minus sign (U+2212)>
infinity: "Infinity" | "inf"
nan: "nan"
digit: <a Unicode decimal digit, i.e. characters in Unicode general category Nd>
Expand Down Expand Up @@ -827,6 +833,11 @@ are always available. They are listed here in alphabetical order.
.. versionchanged:: 3.8
Falls back to :meth:`~object.__index__` if :meth:`~object.__float__` is not defined.

.. versionchanged:: next
``'−'`` (Unicode minus sign, U+2212) can be now used as an
alternative to ``'-'`` (ASCII hyphen minus) for denoting
negative sign.


.. index::
single: __format__
Expand Down Expand Up @@ -1036,8 +1047,9 @@ are always available. They are listed here in alphabetical order.

If the argument is not a number or if *base* is given, then it must be a string,
:class:`bytes`, or :class:`bytearray` instance representing an integer
in radix *base*. Optionally, the string can be preceded by ``+`` or ``-``
(with no space in between), have leading zeros, be surrounded by whitespace,
in radix *base*. Optionally, the string can be directly preceded (with no whitespaces
in between) by ``+`` (ASCII plus sign), ``-`` (ASCII hyphen minus) or ``−`` (Unicode
minus sign, U+2212), have leading zeros, be surrounded by whitespace,
and have single underscores interspersed between digits.

A base-n integer string contains digits, each representing a value from 0 to
Expand Down Expand Up @@ -1080,6 +1092,11 @@ are always available. They are listed here in alphabetical order.
.. versionchanged:: 3.14
:func:`int` no longer delegates to the :meth:`~object.__trunc__` method.

.. versionchanged:: next
:func:`int` now supports ``−`` (Unicode minus sign, U+2212)
as an alternative to ``-`` (ASCII hyphen minus) for denoting
negative integers.

.. function:: isinstance(object, classinfo, /)

Return ``True`` if the *object* argument is an instance of the *classinfo*
Expand Down
5 changes: 3 additions & 2 deletions Doc/library/stdtypes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -360,8 +360,9 @@ Notes:
alternative conversions.

(4)
float also accepts the strings "nan" and "inf" with an optional prefix "+"
or "-" for Not a Number (NaN) and positive or negative infinity.
float also accepts the strings "nan" and "inf" with an optional prefix ASCII plus
sign "+" or ASCII hyphen minus "-" or Unicode minus sign "−" (U+2212) for Not a Number
(NaN) and positive or negative infinity.

(5)
Python defines ``pow(0, 0)`` and ``0 ** 0`` to be ``1``, as is common for
Expand Down
4 changes: 4 additions & 0 deletions Doc/whatsnew/3.15.rst
Original file line number Diff line number Diff line change
Expand Up @@ -583,6 +583,10 @@ Other language changes
making it a :term:`generic type`.
(Contributed by James Hilton-Balfe in :gh:`128335`.)

* :func:`int`, :func:`float` and :func:`complex` now support strings
with ```` (Unicode minus sign, U+2212) as an alternative to ``-``
(ASCII hyphen minus, U+002D).
(Contributed by Bartosz Sławecki in :gh:`144087`.)

New modules
===========
Expand Down
2 changes: 2 additions & 0 deletions Lib/test/test_capi/test_long.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,9 @@ def test_long_fromunicodeobject(self):
self.assertEqual(fromunicodeobject('0xcafe', 0), 0xcafe)
self.assertRaises(ValueError, fromunicodeobject, 'cafe', 0)
self.assertEqual(fromunicodeobject('-123', 10), -123)
self.assertEqual(fromunicodeobject('\N{MINUS SIGN}123', 10), -123)
self.assertEqual(fromunicodeobject(' -123 ', 10), -123)
self.assertEqual(fromunicodeobject(' \N{MINUS SIGN}123 ', 10), -123)
self.assertEqual(fromunicodeobject('1_23', 10), 123)
self.assertRaises(ValueError, fromunicodeobject, '- 123', 10)
self.assertRaises(ValueError, fromunicodeobject, '', 10)
Expand Down
9 changes: 9 additions & 0 deletions Lib/test/test_complex.py
Original file line number Diff line number Diff line change
Expand Up @@ -669,18 +669,22 @@ def check(z, x, y):
check(complex("1"), 1.0, 0.0)
check(complex("1j"), 0.0, 1.0)
check(complex("-1"), -1.0, 0.0)
check(complex("\N{MINUS SIGN}1"), -1.0, 0.0)
check(complex("+1"), 1.0, 0.0)
check(complex("1+2j"), 1.0, 2.0)
check(complex("(1+2j)"), 1.0, 2.0)
check(complex("(1.5+4.25j)"), 1.5, 4.25)
check(complex("4.25+1J"), 4.25, 1.0)
check(complex(" ( +4.25-6J )"), 4.25, -6.0)
check(complex(" ( +4.25\N{MINUS SIGN}6J )"), 4.25, -6.0)
check(complex(" ( +4.25-J )"), 4.25, -1.0)
check(complex(" ( +4.25\N{MINUS SIGN}J )"), 4.25, -1.0)
check(complex(" ( +4.25+j )"), 4.25, 1.0)
check(complex("J"), 0.0, 1.0)
check(complex("( j )"), 0.0, 1.0)
check(complex("+J"), 0.0, 1.0)
check(complex("( -j)"), 0.0, -1.0)
check(complex("( \N{MINUS SIGN}j)"), 0.0, -1.0)
check(complex('1-1j'), 1.0, -1.0)
check(complex('1J'), 0.0, 1.0)

Expand All @@ -690,6 +694,7 @@ def check(z, x, y):
check(complex('-1e-500+1e-500j'), -0.0, 0.0)
check(complex('1e-500-1e-500j'), 0.0, -0.0)
check(complex('-1e-500-1e-500j'), -0.0, -0.0)
check(complex('\N{MINUS SIGN}1e\N{MINUS SIGN}500\N{MINUS SIGN}1e\N{MINUS SIGN}500j'), -0.0, -0.0)

# SF bug 543840: complex(string) accepts strings with \0
# Fixed in 2.3.
Expand All @@ -700,6 +705,8 @@ def check(z, x, y):
self.assertRaises(ValueError, complex, "1+")
self.assertRaises(ValueError, complex, "1+1j+1j")
self.assertRaises(ValueError, complex, "--")
self.assertRaises(ValueError, complex, "-\N{MINUS SIGN}")
self.assertRaises(ValueError, complex, "\N{MINUS SIGN}\N{MINUS SIGN}")
self.assertRaises(ValueError, complex, "(1+2j")
self.assertRaises(ValueError, complex, "1+2j)")
self.assertRaises(ValueError, complex, "1+(2j)")
Expand All @@ -726,7 +733,9 @@ def test_constructor_negative_nans_from_string(self):
self.assertEqual(copysign(1., complex("-nan").real), -1.)
self.assertEqual(copysign(1., complex("-nanj").imag), -1.)
self.assertEqual(copysign(1., complex("-nan-nanj").real), -1.)
self.assertEqual(copysign(1., complex("-nan\N{MINUS SIGN}nanj").real), -1.)
self.assertEqual(copysign(1., complex("-nan-nanj").imag), -1.)
self.assertEqual(copysign(1., complex("\N{MINUS SIGN}nan\N{MINUS SIGN}nanj").imag), -1.)

def test_underscores(self):
# check underscores
Expand Down
16 changes: 16 additions & 0 deletions Lib/test/test_float.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,12 +68,16 @@ def test_float(self):
self.assertRaises(ValueError, float, " +0x3.p-1 ")
self.assertRaises(ValueError, float, "++3.14")
self.assertRaises(ValueError, float, "+-3.14")
self.assertRaises(ValueError, float, "+\N{MINUS SIGN}3.14")
self.assertRaises(ValueError, float, "-+3.14")
self.assertRaises(ValueError, float, "--3.14")
self.assertRaises(ValueError, float, "-\N{MINUS SIGN}3.14")
self.assertRaises(ValueError, float, "\N{MINUS SIGN}\N{MINUS SIGN}3.14")
self.assertRaises(ValueError, float, ".nan")
self.assertRaises(ValueError, float, "+.inf")
self.assertRaises(ValueError, float, ".")
self.assertRaises(ValueError, float, "-.")
self.assertRaises(ValueError, float, "\N{MINUS SIGN}.")
self.assertRaises(TypeError, float, {})
self.assertRaisesRegex(TypeError, "not 'dict'", float, {})
# Lone surrogate
Expand Down Expand Up @@ -1110,30 +1114,40 @@ def test_nan_from_str(self):
self.assertTrue(isnan(float("nan")))
self.assertTrue(isnan(float("+nan")))
self.assertTrue(isnan(float("-nan")))
self.assertTrue(isnan(float("\N{MINUS SIGN}nan")))

self.assertEqual(repr(float("nan")), "nan")
self.assertEqual(repr(float("+nan")), "nan")
self.assertEqual(repr(float("-nan")), "nan")
self.assertEqual(repr(float("\N{MINUS SIGN}nan")), "nan")

self.assertEqual(repr(float("NAN")), "nan")
self.assertEqual(repr(float("+NAn")), "nan")
self.assertEqual(repr(float("-NaN")), "nan")
self.assertEqual(repr(float("\N{MINUS SIGN}NaN")), "nan")

self.assertEqual(str(float("nan")), "nan")
self.assertEqual(str(float("+nan")), "nan")
self.assertEqual(str(float("-nan")), "nan")
self.assertEqual(str(float("\N{MINUS SIGN}nan")), "nan")

self.assertRaises(ValueError, float, "nana")
self.assertRaises(ValueError, float, "+nana")
self.assertRaises(ValueError, float, "-nana")
self.assertRaises(ValueError, float, "\N{MINUS SIGN}nana")
self.assertRaises(ValueError, float, "na")
self.assertRaises(ValueError, float, "+na")
self.assertRaises(ValueError, float, "-na")
self.assertRaises(ValueError, float, "\N{MINUS SIGN}na")

self.assertRaises(ValueError, float, "++nan")
self.assertRaises(ValueError, float, "-+NAN")
self.assertRaises(ValueError, float, "+-NaN")
self.assertRaises(ValueError, float, "--nAn")
self.assertRaises(ValueError, float, "\N{MINUS SIGN}+NAN")
self.assertRaises(ValueError, float, "-\N{MINUS SIGN}nAn")
self.assertRaises(ValueError, float, "\N{MINUS SIGN}-nAn")
self.assertRaises(ValueError, float, "\N{MINUS SIGN}\N{MINUS SIGN}nAn")

def test_nan_as_str(self):
self.assertEqual(repr(1e300 * 1e300 * 0), "nan")
Expand All @@ -1145,11 +1159,13 @@ def test_nan_as_str(self):
def test_inf_signs(self):
self.assertEqual(copysign(1.0, float('inf')), 1.0)
self.assertEqual(copysign(1.0, float('-inf')), -1.0)
self.assertEqual(copysign(1.0, float('\N{MINUS SIGN}inf')), -1.0)

def test_nan_signs(self):
# The sign of float('nan') should be predictable.
self.assertEqual(copysign(1.0, float('nan')), 1.0)
self.assertEqual(copysign(1.0, float('-nan')), -1.0)
self.assertEqual(copysign(1.0, float('\N{MINUS SIGN}nan')), -1.0)


fromHex = float.fromhex
Expand Down
6 changes: 4 additions & 2 deletions Lib/test/test_long.py
Original file line number Diff line number Diff line change
Expand Up @@ -328,11 +328,11 @@ def test_long(self):
('1' + '0'*100, 10**100)
]
for s, v in LL:
for sign in "", "+", "-":
for sign in "", "+", "-", "\N{MINUS SIGN}":
for prefix in "", " ", "\t", " \t\t ":
ss = prefix + sign + s
vv = v
if sign == "-" and v is not ValueError:
if sign in ("-", "\N{MINUS SIGN}") and v is not ValueError:
vv = -v
try:
self.assertEqual(int(ss), vv)
Expand Down Expand Up @@ -360,9 +360,11 @@ def test_long(self):
self.assertEqual(int('0', 0), 0)
self.assertEqual(int('+0', 0), 0)
self.assertEqual(int('-0', 0), 0)
self.assertEqual(int('\N{MINUS SIGN}0', 0), 0)
self.assertEqual(int('00', 0), 0)
self.assertRaises(ValueError, int, '08', 0)
self.assertRaises(ValueError, int, '-012395', 0)
self.assertRaises(ValueError, int, '\N{MINUS SIGN}012395', 0)

# invalid bases
invalid_bases = [-909,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
:func:`int`, :func:`float` and :func:`complex` now support strings
with ``−`` (Unicode minus sign, U+2212) as an alternative to ``-``
(ASCII hyphen minus, U+002D). Contributed by Bartosz Sławecki.
3 changes: 3 additions & 0 deletions Objects/unicodeobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -9475,6 +9475,9 @@ _PyUnicode_TransformDecimalAndSpaceToASCII(PyObject *unicode)
else if (Py_UNICODE_ISSPACE(ch)) {
out[i] = ' ';
}
else if (ch == 0x2212) { /* MINUS SIGN */
out[i] = '-';
}
else {
int decimal = Py_UNICODE_TODECIMAL(ch);
if (decimal < 0) {
Expand Down
Loading