31

I need to request that an argument is >= 12 using argparse.

I cannot find a way to obtain this result using argparse, it seems there's no way to set rules to a given value but only full sets of accepted values like choices=['rock', 'paper', 'scissors'].

My code is:

import sys, argparse

parser = argparse.ArgumentParser()
parser.add_argument("-b", "--bandwidth", type=int, help="target bandwidth >=12")
args = parser.parse_args()
if args.bandwidth and args.bandwidth < 12:
    print "ERROR: minimum bandwidth is 12"
    sys.exit(1)

I wonder if there is a way to obtain this result directly with some argparse option.

Asclepius
  • 57,944
  • 17
  • 167
  • 143
giuspen
  • 1,365
  • 3
  • 20
  • 33
  • How about subclassing `argparse.Action`, and override `__call__` to create `>=12` rule? – Srinivas Reddy Thatiparthy Sep 09 '13 at 14:37
  • Can you write down a couple of lines of code? – giuspen Sep 09 '13 at 14:40
  • 3
    Under 'related' is a http://stackoverflow.com/questions/14117415/using-argparse-allow-only-positive-integers thread. The accepted answer there uses `type`, and points out that it is an adaptation of the documentation example, the one titled `perfect_square`. – hpaulj Sep 09 '13 at 18:00

4 Answers4

54

One way is to use a custom type.

def bandwidth_type(x):
    x = int(x)
    if x < 12:
        raise argparse.ArgumentTypeError("Minimum bandwidth is 12")
    return x

parser.add_argument("-b", "--bandwidth", type=bandwidth_type, help="target bandwidth >= 12")

Note: I think ArgumentTypeError is a more correct exception to raise than ArgumentError. However, ArgumentTypeError is not documented as a public class by argparse, and so it may not be considered correct to use in your own code. One option I like is to use argparse.error like alecxe does in his answer, although I would use a custom action instead of a type function to gain access to the parser object.

A more flexible option is a custom action, which provides access to the current parser and namespace object.

class BandwidthAction(argparse.Action):

    def __call__(self, parser, namespace, values, option_string=None):
        if values < 12:
            parser.error("Minimum bandwidth for {0} is 12".format(option_string))
            #raise argparse.ArgumentError("Minimum bandwidth is 12")

        setattr(namespace, self.dest, values)

parser.add_argument("-b", "--bandwidth", action=BandwidthAction, type=int,
                     help="target bandwidth >= 12")
chepner
  • 497,756
  • 71
  • 530
  • 681
  • 3
    +1 custom action looks like a better option. Using `type` looks somewhat hacky. – alecxe Sep 09 '13 at 14:44
  • 2
    I think it's valid to consider a range-restricted set of integers as a subtype of `int`, just like `N` (the natural numbers) is a subset of `Z` (the integers) in mathematics. The action is actually overkill, as there's no need to use, for example, the parser or the name of the option to type-check the value of the option. – chepner Sep 09 '13 at 14:48
  • I wanted to consider the case there's no upper limit and so the range is not a good option in my case. – giuspen Sep 09 '13 at 14:53
  • 1
    `16.4.3.6. type` of the documentation has a custom `Type` example that raises a `argparse.ArgumentTypeError(msg)`. `FileType` also raises an `ArgumentTypeError` if it can't open a file. `ArgumentTypeError` is included in the `__all__` import. The API documentation is not polished. – hpaulj Sep 09 '13 at 17:40
  • @hpaulj Thanks for the correction. I made my comment based on the module docstring, which omits `ArgumentTypeError`. Clearly, I did not read down far enough, especially since `__all__` is only a few lines below the docstring. – chepner Sep 09 '13 at 22:01
27

you can try with something you introduce in your explanation :

import sys, argparse

parser = argparse.ArgumentParser()
parser.add_argument("-b", "--bandwidth", type=int, choices=range(12,100))
args = parser.parse_args()

for example, thus , its Argparse which will raise the error itself with invalid choice

  • 4
    This introduces an artificial upper bound on the value. – chepner Sep 09 '13 at 14:50
  • 2
    its an example, we dont know the "upper" limit he needs –  Sep 09 '13 at 14:52
  • 7
    Why assume an upper limit exists at all? – chepner Sep 09 '13 at 14:59
  • Yes, I wanted to consider the case there's no upper limit – giuspen Sep 09 '13 at 15:07
  • @chepner as i said it's an example which works with the keyword "choices" he introduced himself. but thx for the downvote for something that works, i'll keep it in mind. To giuspen ok so you find your solution ;) –  Sep 09 '13 at 15:11
  • It was not me to downvote even though in the question I mentioned that I was searching for something different from choices, I anyway thank you for spending time to write your opinion/solution :) – giuspen Sep 09 '13 at 15:33
  • @giuspen yes i know that ;) but it's not important . thanks u 2 –  Sep 09 '13 at 15:34
  • 2
    In theory `choices` works with anything that accepts the `in` operator (has a `__contains__`). That would include something like `range(1, 65535)`. But as discussed in http://bugs.python.org/issue16418 , the help and error messages can be problematic, since it tries to list all values. – hpaulj Sep 09 '13 at 17:30
  • This wouldn't work with a real number constrained to be inside an interval. – a06e Mar 17 '17 at 13:34
  • 19
    This will ruin the `-h, --help` message with printing all those choices – Achilles Nov 23 '17 at 12:03
  • 2
    @Achilles nah, you just set `metavar='[1, 65535)'` or whatever, specify the `help` message and it won't print the list. – Alnitak Mar 15 '19 at 04:07
  • @Alnitak This only solves half of the issue. The help message is cleaned up, sure, but the error that gets raised when the value is out of the range can result in the outputting of a very large list that would obscure the terminal window. – Kalcifer Aug 27 '21 at 04:16
26

You can call the parser error without creating custom types or separate functions. A simple change to your code example is enough:

import argparse

parser = argparse.ArgumentParser()
parser.add_argument("-b", "--bandwidth", type=int, help="target bandwidth >=12")
args = parser.parse_args()
if args.bandwidth and args.bandwidth < 12:
    parser.error("Minimum bandwidth is 12")

This will cause the application to exit and display the parser error:

$ python test.py --bandwidth 11 
usage: test.py [-h] [-b BANDWIDTH]
test.py: error: Minimum bandwidth is 12
jonatan
  • 9,011
  • 2
  • 30
  • 34
  • This does the job, and the technique can of course also be repurposed when subclassing `argparse.ArgumentParser`. For this, see the [related answer](https://stackoverflow.com/a/39947874/832230). – Asclepius Oct 09 '16 at 19:56
1

How about this?

import sys, argparse

parser = argparse.ArgumentParser()
parser.add_argument(
    "-b", "--bandwidth", 
    type=lambda x: (int(x) > 11) and int(x) or sys.exit("Minimum bandwidth is 12"),
    help="target bandwidth >=12"
)

But plese note, I didn't try it in a real code. Or you can change sys.exit to parser.error as wrote by @jonatan.