Argparse: Allow combining type
and choices
(original) (raw)
Currently, combining type
and choices
in an argparse Action
gives weird usage strings.
As a toy example:
import argparse
parser = argparse.ArgumentParser()
names = ["mo", "tu", "we", "th", "fr", "sa", "su"]
days = [1,2,3,4,5,6,7]
def name_day(value):
return names.index(value) + 1
parser.add_argument(
'--days',
type=name_day,
choices=days,
default=["mo"],
)
parser.parse_args()
This will result in the following usage message:
usage: example.py [-h] [--days {1,2,3,4,5,6,7}]
This is because the check against choices
is performed after type conversion.
The problem is also present for other type conversions, for instance when the target types are locations on a map, or dates on a calendar.
Proposal:
Allow the developer to specify choices in the same language as the users inputs arguments. This will allow the developer to rewrite the above example to the following:
day_names = ["mo", "tu", "we", "th", "fr", "sa", "su"]
def name_day(value):
return day_names.index(value) + 1
parser.add_argument(
'--days',
type=name_day,
choices=day_names,
)
parser.parse_args()
Instead of validating the converted value against the available choices, validate the user supplied value against the available choices. Because this can break existing code, the feature will only be enabled by a parser option: convert_choices
.
Motivation:
The reworked example is slightly more concise and (at least in my mind) more intuitive. It is also consistent with how the default
value is specified, which is also before conversion.
By implementing the proposed behavior, we also get better error
and usage
messages. We don’t break existing code, because the behavior is only enabled by a feature flag.
gerardw (Gerardwx) April 17, 2025, 7:19pm 2
Your type is “name_day” which isn’t a type and your list is strings.
This works.
#!/usr/bin/env python3
import argparse
import logging
class MyType:
def __init__(self, v):
self.v = v
self._svalue = str(v)
def __eq__(self, other):
if isinstance(other, MyType):
return self.v == other.v or self._svalue == other.v
return False
def __hash__(self):
return hash(self.v)
def __repr__(self):
return f"MyType({self.v})"
def main():
logging.basicConfig()
parser = argparse.ArgumentParser()
parser.add_argument( 'value', type=MyType, choices=[MyType(1), MyType("2")])
args = parser.parse_args()
print(f"Got type: {type(args.value)}")
print(f"Got value: {args.value}")
print(f"Internal type: {type(args.value.v)}")
if __name__ == "__main__":
main()
hansthen (Hans Then) April 17, 2025, 7:30pm 3
Sure, for some values of type
this will work. Especially if you override __str__
and __repr__
. However, it will not work for other type
conversions. Suppose we have a parser that accepts as options today
and tomorrow
and converts them to actual date
objects.
sayandipdutta (Sayandip Dutta) April 17, 2025, 7:49pm 4
Does a custom action class meet your needs?
hansthen (Hans Then) April 17, 2025, 8:20pm 5
Maybe. But I am not looking for a workaround. For instance, the solution proposed by Gerardwx will work (in some cases), but it requires a lot of extra code. Similarly, creating a custom Action
class will also require more code.
My proposal to change the argparse
library would make the most natural implementation (as in the example) simply work.
jamestwebber (James Webber) April 18, 2025, 1:40am 6
I think the most natural implementation would just do this translation from choice to int in the code after parsing. And I’d use a dictionary rather than index
into a list.
But that’s just me. “Most natural” is pretty subjective.
jtwaleson (Jouke Waleson) April 21, 2025, 7:16pm 7
I like the (edited) example. It seems like a bug to do the type conversion when rendering the help section. It is now suggesting that the user types 1,2,3
while they should be typing mo,tu,we
. In fact, typing 1,2,3
would be an error.
pulkin (Artem Pulkin) April 22, 2025, 7:29pm 8
Sounds like a perfect use case for enum.
I am not a fan of this change otherwise: you clearly expect the argument to be converted independent of parsing. Yes, the help message does not make sense but I would say it is mostly because you misuse type=
.