How can I require my python script's argument to be a float between 0.0-1.0 using argparse?

18,436

Solution 1

The type parameter to add_argument just needs to be a callable object that takes a string and returns a converted value. You can write a wrapper around float that checks its value and raises an error if it is out of range.

def restricted_float(x):
    try:
        x = float(x)
    except ValueError:
        raise argparse.ArgumentTypeError("%r not a floating-point literal" % (x,))

    if x < 0.0 or x > 1.0:
        raise argparse.ArgumentTypeError("%r not in range [0.0, 1.0]"%(x,))
    return x

p = argparse.ArgumentParser()
p.add_argument("--arg", type=restricted_float)

Solution 2

Here is a method that uses the choices parameter to add_argument, with a custom class that is considered "equal" to any float within the specified range:

import argparse

class Range(object):
    def __init__(self, start, end):
        self.start = start
        self.end = end
    def __eq__(self, other):
        return self.start <= other <= self.end

parser = argparse.ArgumentParser()
parser.add_argument('--foo', type=float, choices=[Range(0.0, 1.0)])

Solution 3

Adding str makes that the boundaries are visuable in the help.

import argparse

class Range(object):
    def __init__(self, start, end):
        self.start = start
        self.end = end

    def __eq__(self, other):
        return self.start <= other <= self.end

    def __contains__(self, item):
        return self.__eq__(item)

    def __iter__(self):
        yield self

    def __str__(self):
        return '[{0},{1}]'.format(self.start, self.end)

parser = argparse.ArgumentParser()
parser.add_argument('--foo', type=float, choices=Range(0.0, 1.0))
parser.add_argument('--bar', type=float, choices=[Range(0.0, 1.0), Range(2.0,3.0)])

Solution 4

The argparse.add_argument call expects an iterable as 'choices' parameter. So what about adding the iterable property to the Range class above. So both scenarios could be used:

import argparse

class Range(object):
    def __init__(self, start, end):
        self.start = start
        self.end = end

    def __eq__(self, other):
        return self.start <= other <= self.end

    def __contains__(self, item):
        return self.__eq__(item)

    def __iter__(self):
        yield self

parser = argparse.ArgumentParser()
parser.add_argument('--foo', type=float, choices=Range(0.0, 1.0))
parser.add_argument('--bar', type=float, choices=[Range(0.0, 1.0), Range(2.0,3.0)])
Share:
18,436
Dolan Antenucci
Author by

Dolan Antenucci

Updated on June 06, 2022

Comments

  • Dolan Antenucci
    Dolan Antenucci about 2 years

    I'd like to use argparse on Python 2.7 to require that one of my script's parameters be between the range of 0.0 and 1.0. Does argparse.add_argument() support this?

  • Dolan Antenucci
    Dolan Antenucci almost 12 years
    I like this one because it leaves the exception raising to argparse. Thanks!
  • chepner
    chepner almost 12 years
    One suggestion: have your Range class implement the __contains__ method; then you can say choices=Range(0.0, 1.0) instead of wrapping it in a list.
  • Dolan Antenucci
    Dolan Antenucci almost 12 years
    I was originally going to choose FJ's as the accepted answer, simply because I like the "cleanness" of it (obviously arguable), but the simplicity of this won me over, and I'm using it in my code. Thanks!
  • Martin Thoma
    Martin Thoma over 9 years
    Note: restricted_float should probably be replaced by whatever it represents as it will be shown in error messages. For example, I used this pattern in a toy project geocodertools and called it longitude and latitude.
  • RickardSjogren
    RickardSjogren over 8 years
    The __contains__-approach gives ValueError: length of metavar tuple does not match nargs using Python 3.4. Otherwise it works well and I implemented __repr__ to return '{0}-{1}'.format(self.start, self.end) for prettier help text as well.
  • James McCormac
    James McCormac over 7 years
    Andrew's answer combined with @RickardSjogren comment gives a great result. Thanks!
  • PJ_Finnegan
    PJ_Finnegan about 6 years
    Even with __contains__ redefined, in Python 3.5.2 I get: TypeError: 'Range' object is not iterable if I don't put it in a single-element list. Solved redefining: def __getitem__(self, index): if index == 0: return self else: raise IndexError()
  • rasen58
    rasen58 almost 5 years
    I tried using @PJ_Finnegan __getitem__ addition, but it did not work in python 3.6.5. I still get ValueError: length of metavar tuple does not match nargs
  • chepner
    chepner over 4 years
    @Danijel What do you mean? There are a lot of different types you might want to define; argparse can't know about them all. This is how argparse supports converting the string argument to whatever value you want.
  • Danijel
    Danijel over 4 years
    I was expecting that float would be supported as int is with regards to range. Am I missing something?
  • chepner
    chepner over 4 years
    @Danijel A range of floats wouldn't be terribly useful. What's the next float after 0.5? 0.6, or 0.51, or 0.501, or...? While technically discrete, float isn't enumerable in any useful way.
  • chepner
    chepner over 4 years
    An interval type that only supported inclusion, rather than enumeration, might be useful, but Python doesn't provide one.
  • Danijel
    Danijel over 4 years
    @chepner Thanks. I was thinking about a from-to range of floats, not individual values.
  • Chris
    Chris over 3 years
    For me the argparse error message seems to show the class's repr so I just changed your __str__ method to __repr__ to get a nice error message.