Starting in Python 3.6, PEP 515 introduces underscores as part of valid numbers.

Example

This code:

one_million = 1_000_000
print(one_million)

generates this output:

1000000

And it's not just for integers or underscores in certain positions.

This code:

some_float = 123_456.7
print(some_float)

some_binary = 0b1000_0000
print(some_binary)

underscores_everywhere = 1_2_3_4
print(underscores_everywhere)

generates this output:

123456.7
128
1234

Why use underscores in numbers?

Because it keeps your code readable and helps prevent bugs.

In my own case, it would have saved me plenty of headache that one time I used a RotatingFileHandler to rotate my logs every 300000000 bytes (300MB) instead of every 30000000 bytes (30MB) as I'd intended. After removing the extra zero and deploying the fix, I learned that several co-workers had copy-pasted my same logging config and deployed it in their own applications. Yikes!

Side note: in hindsight, I should have just written 30*10**6, but I think I wanted to avoid executing code in a config file. Also, 30MB of log files seems a bit much. But that's neither here nor there.

Beware

Underscores aren’t interpreted as numeric when in strings:

"1000000".isnumeric()    # True
"1_000_000".isnumeric()  # False
"_".isdigit()            # False

This seems fair since periods also evaluate those last two lines to False:

"1.2".isnumeric()  # False
".".isnumeric()    # False
A dining area closed amid the COVID-19 pandemic.
Photo by Elizabeth Kay / Unsplash

This is important to keep in mind because it'll break code that's written like this:

some_string_number = "1_000_000"

if some_string_number.isnumeric():
    value = int(some_string_number)
else:
    value = some_string_number

print(isinstance(value, int))  # False (because of the underscore)

instead of relying on a try-except:

some_string_number = "1_000_000"

try:
    value = int(some_string_number)
except ValueError:
    value = some_string_number

print(isinstance(value, int))  # True

or stripping the underscores before evaluation:

some_string_number = "1_000_000"

if some_string_number.replace("_", "").isnumeric():
    value = int(some_string_number)
else:
    value = some_string_number

print(isinstance(value, int))  # True

That said, this situation is unlikely to occur, because numbers written this way are usually hard-coded as a constant or in a config where you're already confident of their type and won't need to check if they're numeric. I guess it could happen – if you were to allow users to submit numbers with commas and your code then converted those commas to underscores for some reason – but it seems like a stretch. Still, good to be aware of potential pitfalls.