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=.