6

I'm trying to enable a user to pass in a function name. For some reason it seems that argparse performs the type check/conversion BEFORE it checks the choices. Is this a bug? Best thing to do?

import argparse

def foo():
  return 'foo'

def bar():
  return 'bar'

parser = argparse.ArgumentParser()
functions = {f.__name__:f for f in [foo, bar]}
parser.add_argument("function", type=lambda f: functions.get(f), help="which function", choices=functions)
args = parser.parse_args()
print(args.function())

This throws:

$ python blah.py foo
usage: blah.py [-h] {foo,bar}
blah.py: error: argument function: invalid choice: <function foo at 0x7f65746dd848> (choose from 'foo', 'bar')
1
  • 2
    From argparse documentation: Note that inclusion in the choices container is checked after any type conversions have been performed, so the type of the objects in the choices container should match the type specified: Commented Apr 7, 2016 at 15:02

2 Answers 2

3

Yes, during parsing the type then choices order is clear and intentional (and not just incidental). When preparing to assign arg_strings to the namespace it calls _get_values, which does:

  def _get_values(self, action, arg_strings)
        .... (various nargs tests)
        value = self._get_value(action, arg_string)
        self._check_value(action, value)
        return value

where _get_value applies the action.type function, and _check_value tests

value not in action.choices  

For parsing choices only has to respond to the in (__contains__) expression.

So choices have to reflect values after conversion. If type is int, then choices=[1,2,3] is correct, ['1','2','3'] is not.

There are some (largely unresolved) bug issues over the display of the choices. Long lists, e.g. range(100) work in parsing, but don't display nicely. And display also requires that choices be iterable (e.g. a list, tuple, dictionary). This display issue affects the usage, the help and the error messages (each formats choices slightly differently).

metavar is your most powerful tool for replacing an undesirable choices list. I'd have to run a test case to see whether it solves things for all 3 situations.

Sign up to request clarification or add additional context in comments.

Comments

2

Apparently so, however you can work around this simply by using functions.keys() as your choices, e.g.

import argparse

def foo():
  return 'foo'

def bar():
  return 'bar'

parser = argparse.ArgumentParser()
functions = {f.__name__:f for f in [foo, bar]}
parser.add_argument("function", type=lambda f: functions.get(f), help="which function", choices=functions.values())
args = parser.parse_args()
print(args.function())

However, if you want to provide this kind of interface (mapping functions to command line arguments) you might want to take a look at click.

3 Comments

Yeah, there are several obvious workarounds. I can also just do type=str, then call functions[args.function](). Mostly, I was grumpy that --help displays the choices BEFORE "type"ing, but the parser checks the choices AFTER "type"ing. Was hoping there was a generic way to deal with that situation.
Ha, understood. For what it's worth I would suggest using the str names for commands (type=str, choices=functions.keys()) to make the error message when entering the wrong value nicer than invalid choice: None (choose from <function foo at 0x0000000000927F28>)
Apparently you can suppress the "choices" in the help by supplying a "metavar = 'FUNCTION'", then add the choices back into the help the way you want them with simple string manipulation. That's what I'm gonna do, I think... along with your .values() suggestion. Thx.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.