Skip to content

gh-161452: Fix python -m random with a non-positive integer input - #161453

Open
andresdelfino wants to merge 4 commits into
python:mainfrom
andresdelfino:random-cli-nonpositive-integer
Open

andresdelfino wants to merge 4 commits into
python:mainfrom
andresdelfino:random-cli-nonpositive-integer

Conversation

@andresdelfino

@andresdelfino andresdelfino commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

When python -m random gets a single integer input, main() called randint() inside the try block that converts the input with int(). For an integer N <= 0, the ValueError that randint() raises for an empty range was taken to mean that the input is not an integer, so python -m random 0 printed 0.0 and a negative N printed a random negative float. This keeps only the conversions inside the try blocks (the randint() and uniform() calls move to else: blocks), so an integer input now behaves like --integer N, as documented.

Run from the command line, both forms now print a one-line error and exit with status 1, instead of a traceback:

$ ./python -m random -3
error: empty range in randint(1, -3)
$ ./python -m random --integer 0
error: empty range in randint(1, 0)

main() itself still raises the ValueError; only the __main__ block catches it.

The new tests check that 0 and -5 raise the same ValueError from main() with and without --integer, and that the command line reports the short error message.

🤖 Generated with Claude Code

When given a single integer input, main() called randint() inside the
same try block that converts the input with int(). The ValueError that
randint() raises for an empty range was then taken to mean that the
input is not an integer, so "python -m random 0" printed 0.0 and a
negative N printed a random negative float, instead of failing like
"--integer N" does. Only convert the input inside the try block.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
skirpichev
skirpichev previously approved these changes Nov 5, 2026
Comment thread Lib/random.py Outdated
@@ -1076,6 +1075,8 @@ def main(arg_list: list[str] | None = None) -> int | str:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't you want to move this to an (inner) else: block too?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW, I think it's the right place: uniform() not raises (c.f. randint).

So, here we have only parsing failures for floats. In that case it's reasonable to try choice() as a fallback.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While Sergey is right, it would be easier to read if the same pattern is followed. Let's to think of. Let me know what you prefer.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with @gvanrossum, it's better to move return uniform(0, val) to an else: block as well.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Applied

@vstinner vstinner left a comment •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change is backward incompatible, but it respects the documentation: https://docs.python.org/dev/library/random.html#command-line-usage

If no options are given, the output depends on the input:
Integer: same as --integer.

Hum. Displaying a long traceback is not really user friendly. Can you catch the exception and display a (short) error message instead?

$ ./python -m random -3
Traceback (most recent call last):
  File "/home/vstinner/python/main/Lib/runpy.py", line 201, in _run_module_as_main
    return _run_code(code, main_globals, None,
                     "__main__", mod_spec)
  File "/home/vstinner/python/main/Lib/runpy.py", line 87, in _run_code
    exec(code, run_globals)
    ~~~~^^^^^^^^^^^^^^^^^^^
  File "/home/vstinner/python/main/Lib/random.py", line 1088, in <module>
    print(main())
          ~~~~^^
  File "/home/vstinner/python/main/Lib/random.py", line 1079, in main
    return randint(1, val)
  File "/home/vstinner/python/main/Lib/random.py", line 341, in randint
    raise ValueError(f"empty range in randint({a}, {b})")
ValueError: empty range in randint(1, -3)

cc @hugovk who authored this feature.

@hugovk

hugovk commented Nov 6, 2026

Copy link
Copy Markdown
Member

Yes, a short error message would be better than the traceback.

@rhettinger
rhettinger removed their request for review November 6, 2026 17:06
Move the uniform() call to an else block too, matching the integer
branch. When run from the command line, catch the ValueError for an
empty integer range and exit with a one-line error message instead of
a traceback.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@andresdelfino

Copy link
Copy Markdown
Contributor Author

@vstinner I applied your suggestion, handling the exception in the block that calls main. Changing the main function itself would involve more changes in the tests. Would it be better to go that way?

Comment thread Lib/test/test_random.py Resolved
Comment thread Lib/test/test_random.py Resolved
@vstinner

vstinner commented Nov 7, 2026

Copy link
Copy Markdown
Member

Some general notes on random CLI. I'm not sure that it's worth it to change them. I just wanted to share my findings.


./python -m random single_choice always returns single_choice. I suppose that it's a deliberate behavior. It's similar to ./python -m random 1 which returns always 1 and ./python -m random --float 0 which returns always 0.0.


There is a surprising behavior when the integer is larger than sys.get_int_max_str_digits(). int() raises ValueError: Exceeds the limit (4300 digits) for integer string conversion: value has 40096 digits and so switchs to float() which returns inf:

$ ./python -m random $(python -c "print('1'*40096)")
inf

$ ./python -m random $(python -c "print('-' + '1'*40096)")
-inf

$ ./python -m random --integer $(python -c "print('1'*40096)")
usage: python -m random [-h] [-c CHOICE [CHOICE ...] | -i N | -f N] [input ...]
python -m random: error: argument -i/--integer: invalid int value: '111111111111(...)111'

$ ./python -m random --integer $(python -c "print('-' + '1'*40096)")
usage: python -m random [-h] [-c CHOICE [CHOICE ...] | -i N | -f N] [input ...]
python -m random: error: argument -i/--integer: invalid int value: '-1111111111111(...)111'

By the way, it's possible to pass special float values:

$ ./python -m random --float inf
inf

$ ./python -m random --float nan
nan

It seems like random.uniform() always return inf when the maximum is inf:

$ ./python
>>> random.uniform(0, float('inf'))
inf

Ok, maybe it can return nan if self.random() returns 0.0 which is unlikely but possible :-)

And random.uniform(0, float('nan')) always return nan.

andresdelfino and others added 2 commits November 6, 2026 23:11
Co-authored-by: Victor Stinner <vstinner@python.org>

@vstinner vstinner left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. I just have a last suggestion about adding a "real" random.main() function.

Comment thread Lib/random.py
Comment on lines +1089 to +1093
import sys
try:
print(main())
except ValueError as exc:
sys.exit(f"error: {exc}")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You might rename main() to _main() and wrap these lines into a new main() function. So for example, you can call the new main() function in tests instead of having to spawn a new process.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if I do like gzip instead, and call sys.exit directly from main? So we get the benefit you mention, and follow an existing pattern (personally I always find _main/main confusing).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I suggest is to have a main() function which does everything: parse command line arguments, compute a random value, display the random value, call sys.exit() on error:

if __name__ == '__main__':
    main()

I don't care too much of how main() code would look like :-)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants