116

Is there a better way of supporting Enums as types of argparse arguments than this pattern?

class SomeEnum(Enum):
    ONE = 1
    TWO = 2

parser.add_argument('some_val', type=str, default='one',
                    choices=[i.name.lower() for i in SomeEnum])
...
args.some_val = SomeEnum[args.some_val.upper()]
Andrzej Pronobis
  • 33,828
  • 17
  • 76
  • 92
  • 1
    There was a request for `Enum` on Python bug/issues. I don't recall much enthusiasm for added special handling. Choices like yours is one way. Another would be a custom `type` function. That could both test and convert. I suspect you are more familiar with Enums than I am. – hpaulj May 15 '17 at 02:26
  • I was wondering for a moment if it would be possible to derive a custom Enum which would behave as expected when set as `type=` – Andrzej Pronobis May 15 '17 at 02:32
  • 1
    The `type` parameter is a function/callable. Write your own that takes a string, and does something with it. The common types, `int` and `float` are the standard functions that do `int("123")` or `float("12.3")`. – hpaulj May 15 '17 at 02:35

6 Answers6

169

I see this is an old question, but I just came across the same problem (Python 2.7) and here's how I solved it:

from argparse import ArgumentParser
from enum import Enum

class Color(Enum):
    red = 'red'
    blue = 'blue'
    green = 'green'

    def __str__(self):
        return self.value

parser = ArgumentParser()
parser.add_argument('color', type=Color, choices=list(Color))

opts = parser.parse_args()
print 'your color was:', opts.color

Note that defining __str__ is required to get ArgumentParser's help output to include the human readable (values) of Color.

Some sample invocations:

=> python enumtest.py blue
your color was: blue

=> python enumtest.py not-a-color
usage: enumtest.py [-h] {blue,green,red}
enumtest.py: error: argument color: invalid Color value: 'not-a-color'

=> python enumtest.py -h
usage: enumtest.py [-h] {blue,green,red}

positional arguments:
  {blue,green,red}

Since the OP's question specified integers as values, here is a slightly modified version that works in that case (using the enum names, rather than the values, as the command line args):

class Color(Enum):
    red = 1
    blue = 2
    green = 3

    def __str__(self):
        return self.name

parser = ArgumentParser()
parser.add_argument('color', type=lambda color: Color[color], choices=list(Color))

The only drawback there is that a bad parameter causes an ugly KeyError. That's easily solved by adding just a bit more code, converting the lambda into a proper function.

class Color(Enum):
    red = 1
    blue = 2
    green = 3

    def __str__(self):
        return self.name

    @staticmethod
    def from_string(s):
        try:
            return Color[s]
        except KeyError:
            raise ValueError()

parser = ArgumentParser()
parser.add_argument('color', type=Color.from_string, choices=list(Color))
ron rothman
  • 17,348
  • 7
  • 41
  • 43
  • This works and I'll be using it when possible. Unfortunately in my present case, for use-case reasons I needed the keys to begin with a numeral, ex : `5m = '5m'` which yields error `SyntaxError: invalid syntax`. If I name the key portion with quotes (`'5m' = '5m')` I get `SyntaxError: can't assign to literal`. The error makes sense, but unclear if there's a workaround for this case. – Scott Prive Aug 04 '19 at 21:32
  • You can construct an enum with a dict, e.g. define your enum class (EnumBase) with only the methods, no values, then `Color = EnumBase('Color', {'5m': 5, '10m': 10})` – M Somerville Oct 19 '19 at 07:38
  • to avoid a warning in pycharm I had to define `@classmethod def get_list(cls): return [x for x in cls]` inside the Color class and then specify `choices=Color.get_list()` instead of `choices=list(Color)` – Sergio Morstabilini Apr 07 '20 at 09:21
  • How do you do specify default or work with the Enums, as strings? – Geronimo Jan 27 '21 at 15:22
  • Specify a default the same way you always do, with the `default` keyword. Of course this only works with options, not with positional paramters. `parser.add_argument('--color', type=Color, choices=list(Color), default="red") ` – ron rothman Jan 27 '21 at 18:00
  • You can also use `parser.add_argument('--color', type=Color, choices=list(Color), default=Color.red)` – ron rothman Jan 27 '21 at 18:01
  • Turns out you can assign the choices as list(Color.__members__.values()) if you are using strings. This is a handy workaround. – F1Rumors Jan 08 '22 at 22:08
31

Just came across this issue also; however, all of the proposed solutions require adding new methods to the Enum definition.

argparse includes a way of supporting an enum cleanly using actions.

The solution using a custom Action:

import argparse
import enum


class EnumAction(argparse.Action):
    """
    Argparse action for handling Enums
    """
    def __init__(self, **kwargs):
        # Pop off the type value
        enum_type = kwargs.pop("type", None)

        # Ensure an Enum subclass is provided
        if enum_type is None:
            raise ValueError("type must be assigned an Enum when using EnumAction")
        if not issubclass(enum_type, enum.Enum):
            raise TypeError("type must be an Enum when using EnumAction")

        # Generate choices from the Enum
        kwargs.setdefault("choices", tuple(e.value for e in enum_type))

        super(EnumAction, self).__init__(**kwargs)

        self._enum = enum_type

    def __call__(self, parser, namespace, values, option_string=None):
        # Convert value back into an Enum
        value = self._enum(values)
        setattr(namespace, self.dest, value)

Usage

class Do(enum.Enum):
    Foo = "foo"
    Bar = "bar"


parser = argparse.ArgumentParser()
parser.add_argument('do', type=Do, action=EnumAction)

The advantages of this solution are that it will work with any Enum without requiring additional boilerplate code while remaining simple to use.

If you prefer to specify the enum by name change:

  • tuple(e.value for e in enum_type) to tuple(e.name for e in enum_type)
  • value = self._enum(values) to value = self._enum[values]
Tim
  • 2,510
  • 1
  • 22
  • 26
  • 1
    This is also better since it returns enum values for easy comparison. – ShnitzelKiller Nov 18 '21 at 02:56
  • this a bril solution! However it makes `mypy` unhappy! Namely, the above segment generates `error: Argument "type" to "add_argument" of "_ActionsContainer" has incompatible type "Type[Do]"; expected "Union[Callable[[str], ], FileType]"`. Do you know if there is a way to make this `mypy` compatible without resorting to type ignore? – jtimz Feb 13 '22 at 22:36
  • I don't see a type check failure for argument `type`, but I do see one for argument `action`: `Expected type 'Literal["store", "store_const", "store_true", "store_false", "append", "append_const", "count", "help", "version", "extend"]', got 'Type[EnumAction]' instead` - the possible issue is that the enum class isn't inherited from `argparse.Action`? – davidA Sep 21 '22 at 21:30
  • @davidA This is actually an issue with the type check, the `argparse` docs state "You may also specify an arbitrary action by passing an Action subclass or other object that implements the same interface." Additional actions can also be registered with argparse. – Tim Sep 24 '22 at 01:52
18

This in an improvement on ron rothman's answer. By also overriding __repr__ and changing to_string a bit, we can get a better error message from argparse when the user enters a bad value.

import argparse
import enum


class SomeEnum(enum.IntEnum):
    ONE = 1
    TWO = 2

    # magic methods for argparse compatibility

    def __str__(self):
        return self.name.lower()

    def __repr__(self):
        return str(self)

    @staticmethod
    def argparse(s):
        try:
            return SomeEnum[s.upper()]
        except KeyError:
            return s


parser = argparse.ArgumentParser()
parser.add_argument('some_val', type=SomeEnum.argparse, choices=list(SomeEnum))
args = parser.parse_args()
print('success:', type(args.some_val), args.some_val)

In ron rothman's example, if we pass the color yellow as a command line argument, we get the following error:

demo.py: error: argument color: invalid from_string value: 'yellow'

With the improved code above, if we pass three as a command line argument, we get:

demo.py: error: argument some_val: invalid choice: 'three' (choose from one, two)

IMHO, in the simple case of just converting the name of the enum members to lower case, the OP's method seems simpler. However, for more complex conversion cases, this could be useful.

David Lechner
  • 1,433
  • 17
  • 29
13

Here's the relevant bug/issue: http://bugs.python.org/issue25061

Add native enum support for argparse

I already wrote too much there. :)

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

Here's a simple way:

class Color(str, Enum):
    red = 'red'
    blue = 'blue'

parser = ArgumentParser()
parser.add_argument('color', type=Color)
args = parser.parse_args()
print('Your color was:', args.color)
Paweł Nadolski
  • 8,296
  • 2
  • 42
  • 32
0

Building on the answer by @Tim here is an extension to use enumeration names instead of values and print pretty error messages:


class EnumAction(argparse.Action):
    """
    Argparse action for handling Enums
    """

    def __init__(self, **kwargs):
        # Pop off the type value
        enum_type = kwargs.pop("type", None)

        # Ensure an Enum subclass is provided
        if enum_type is None:
            raise ValueError(
                "type must be assigned an Enum when using EnumAction")
        if not issubclass(enum_type, enum.Enum):
            raise TypeError("type must be an Enum when using EnumAction")

        # Generate choices from the Enum
        kwargs.setdefault("choices", tuple(e.name for e in enum_type))

        super(EnumAction, self).__init__(**kwargs)

        self._enum = enum_type

    def __call__(self,
                 parser: argparse.ArgumentParser,
                 namespace: argparse.Namespace,
                 value: Any,
                 option_string: str = None):
        # Convert value back into an Enum
        if isinstance(value, str):
            value = self._enum[value]
            setattr(namespace, self.dest, value)
        elif value is None:
            raise argparse.ArgumentTypeError(
                f"You need to pass a value after {option_string}!")
        else:
            # A pretty invalid choice message will be generated by argparse
            raise argparse.ArgumentTypeError()