38

I'm having a small issue with argparse. I have an option xlim which is the xrange of a plot. I want to be able to pass numbers like -2e-5. However this does not work - argparse interprets this is a positional argument. If I do -0.00002 it works: argparse reads it as a negative number. Is it possible to have able to read in -2e-3?

The code is below, and an example of how I would run it is:

./blaa.py --xlim -2.e-3 1e4 

If I do the following it works:

./blaa.py --xlim -0.002 1e4 

The code:

parser.add_argument('--xlim', nargs = 2,
                  help = 'X axis limits',
                  action = 'store', type = float, 
                  default = [-1.e-3, 1.e-3])

Whilst I can get it to work this way I would really rather be able to use scientific notation. Anyone have any ideas?

Cheers

jhoepken
  • 1,842
  • 3
  • 17
  • 24
Ger
  • 958
  • 1
  • 8
  • 11
  • According to http://code.google.com/p/argparse/issues/detail?id=37 it should have been fixed. Check whether the version of argparse you have is newer or same. – favoretti Jan 26 '12 at 21:03
  • @nmichaels Hi, do you mean like "-2e-5"? It doesn't work unfortunately, I think it still interprets it as an argument. The exact error from `./blah.py -xlim "-.2e-5" 1e5` is --xlim: expected 2 argument(s). If I use \- it thinks its a string and then complains because it should be a float – Ger Jan 26 '12 at 21:05
  • Yeah, this doesn't appear to have been fixed. However, it only affects options which have more than one argument and can be negative. Irritating workaround is to use `--xlower` and `--xupper` with the quoted notation: `--xlower="-1.e-3"`. That works – Chris Jan 26 '12 at 21:08
  • @favoretti Hi - I just tried v1.2 and it still an issue. – Ger Jan 26 '12 at 21:18
  • 1
    @Chris Hi, thanks for the input. I think I am going to look at parsing the sys.argv before I give it to argparse. – Ger Jan 26 '12 at 21:21
  • 1
    So I can't answer my question because I don't have enough reputation. I am going to put it here in case I forget to come back and do it: This is the solution I've come up with. I parse sys.argv, check if xlim is found, then edit the next two entries. I turn them into floats and then back into strings. Quite crude but it works :) `for i in sys.argv[1:]: if i == '--xlim': sys.argv[sys.argv.index(i) + 1] = str(float(sys.argv[sys.argv.index(i) + 1 ])) args = parser.parse_args()` – Ger Jan 26 '12 at 21:39

7 Answers7

48

One workaround I've found is to quote the value, but adding a space. That is,

./blaa.py --xlim " -2.e-3" 1e4

This way argparse won't think -2.e-3 is an option name because the first character is not a hyphen-dash, but it will still be converted properly to a float because float(string) ignores spaces on the left.

itub
  • 1,145
  • 1
  • 10
  • 11
  • 1
    nice! Worked for me in a completely different language / environment also – Jason O'Neil Jul 18 '13 at 12:45
  • I made a little preprocessor that scans sys.argv for "xlim" and adds a space to the beginning (via your answer). I put this before the call to argparse, and it seems to be working well... `for indx in range(len(sys.argv) - 1): if 'xlim' in sys.argv[indx]: sys.argv[indx + 1] = ' {0}'.format(sys.argv[indx + 1])` – jeremiahbuddha Aug 27 '13 at 17:45
  • 3
    Clever. However, if your type is `str` (the default) and not `float`, your option value will have an extra space to strip out. A [more general workaround](https://stackoverflow.com/a/49060857/1727828) is to use an equals sign: `--xlim=-2.e-3 1e4`. – mxxk Mar 02 '18 at 00:48
17

As already pointed out by the comments, the problem is that a - prefix is parsed as an option instead of as an argument. One way to workaround this is change the prefix used for options with prefix_chars argument:

#!/usr/bin/python
import argparse

parser = argparse.ArgumentParser(prefix_chars='@')
parser.add_argument('@@xlim', nargs = 2,
                  help = 'X axis limits',
                  action = 'store', type = float,
                  default = [-1.e-3, 1.e-3])
print parser.parse_args()

Example output:

$ ./blaa.py @@xlim -2.e-3 1e4
Namespace(xlim=[-0.002, 10000.0])

Edit: Alternatively, you can keep using - as separator, pass xlim as a single value and use a function in type to implement your own parsing:

#!/usr/bin/python
import argparse

def two_floats(value):
    values = value.split()
    if len(values) != 2:
        raise argparse.ArgumentError
    values = map(float, values)
    return values

parser = argparse.ArgumentParser()
parser.add_argument('--xlim', 
                  help = 'X axis limits',
                  action = 'store', type=two_floats,
                  default = [-1.e-3, 1.e-3])
print parser.parse_args()

Example output:

$ ./blaa.py --xlim "-2e-3 1e4"
Namespace(xlim=[-0.002, 10000.0])
jcollado
  • 39,419
  • 8
  • 102
  • 133
  • I really wanted to keep the `-` as the separator so I did a crude parsing of sys.argv before I called parse_args(). The way I did it is in a comment above, but your way is much better. Thanks! – Ger Jan 27 '12 at 14:11
16

If you specify the value for your option with an equals sign, argparse will not treat it as a separate option, even if it starts with -:

./blaa.py --xlim='-0.002 1e4'
# As opposed to --xlim '-0.002 1e4'

And if the value does not have spaces in it (or other special characters given your shell), you can drop the quotes:

./blaa.py --xlim=-0.002

See: https://www.gnu.org/software/guile/manual/html_node/Command-Line-Format.html

With this, there is no need to write your own type= parser or redefine the prefix character from - to @ as the accepted answer suggests.

mxxk
  • 9,514
  • 5
  • 38
  • 46
  • This doesn't work in this particular case because ---xlim expects two arguments (nargs=2). That is, unless you want to change the convention so that your program _requires_ the invoker to encode the two values as a single string, and your program then does the splitting. – itub Mar 03 '18 at 13:43
  • Ah, good point @itub. If `nargs=2` is the requirement AND any one of the two option values starts with a dash, then there's no getting around it. – mxxk Mar 05 '18 at 03:15
10

Here is the code that I use. (It is similar to jeremiahbuddha's but it answers the question more directly since it deals with negative numbers.)

Put this before calling argparse.ArgumentParser()

for i, arg in enumerate(sys.argv):
  if (arg[0] == '-') and arg[1].isdigit(): sys.argv[i] = ' ' + arg
andrewfn
  • 111
  • 1
  • 2
8

Another workaround is to pass in the argument using '=' symbol in addition to quoting the argument - i.e., --xlim="-2.3e14"

Pokechu22
  • 4,984
  • 9
  • 37
  • 62
toes
  • 603
  • 6
  • 13
4

If you are up to modifying argparse.py itself, you could change the negative number matcher to handle scientific notation:

In class _ActionsContainer.__init__()

self._negative_number_matcher = _re.compile(r'^-(\d+\.?|\d*\.\d+)([eE][+\-]?\d+)?$')

Or after creating the parser, you could set parser._negative_number_matcher to this value. This approach might have problems if you are creating groups or subparsers, but should work with a simple parser.

hpaulj
  • 221,503
  • 14
  • 230
  • 353
3

Inspired by andrewfn's approach, I created a separate helper function to do the sys.argv fiddling:

def _tweak_neg_scinot():
    import re
    import sys
    p = re.compile('-\\d*\\.?\\d*e', re.I)
    sys.argv = [' ' + a if p.match(a) else a for a in sys.argv]

The regex looks for:

  • - : a negative sign
  • \\d* : zero or more digits (for oddly formatted values like -.5e-2 or -4354.5e-6)
  • \\.? : an optional period (e.g., -2e-5 is reasonable)
  • \\d* : another set of zero or more digits (for things like -2e-5 and -7.e-3)
  • e : to match the exponent marker

re.I makes it match both -2e-5 and -2E-5. Using p.match means that it only searches from the start of each string.

hBy2Py
  • 1,707
  • 19
  • 29