argparse action or type for comma-separated list

Question:

I want to create a command line flag that can be used as

./prog.py --myarg=abcd,e,fg

and inside the parser have this be turned into ['abcd', 'e', 'fg'] (a tuple would be fine too).

I have done this successfully using action and type, but I feel like one is likely an abuse of the system or missing corner cases, while the other is right. However, I don’t know which is which.

With action:

import argparse

class SplitArgs(argparse.Action):
    def __call__(self, parser, namespace, values, option_string=None):
        setattr(namespace, self.dest, values.split(','))


parser = argparse.ArgumentParser()
parser.add_argument('--myarg', action=SplitArgs)
args = parser.parse_args()
print(args.myarg)

Instead with type:

import argparse

def list_str(values):
    return values.split(',')

parser = argparse.ArgumentParser()
parser.add_argument('--myarg', type=list_str)
args = parser.parse_args()
print(args.myarg)
Asked By: Ryan Haining

||

Answers:

The simplest solution is to consider your argument as a string and split.

#!/usr/bin/env python3

import argparse

parser = argparse.ArgumentParser()
parser.add_argument("--myarg", type=str)
d = vars(parser.parse_args())
if "myarg" in d.keys():
    d["myarg"] = [s.strip() for s in d["myarg"].split(",")]
print(d)

Result:

$ ./toto.py --myarg=abcd,e,fg
{'myarg': ['abcd', 'e', 'fg']}
$ ./toto.py --myarg="abcd, e, fg"
{'myarg': ['abcd', 'e', 'fg']}
Answered By: mando

I find your first solution to be the right one. The reason is that it allows you to better handle defaults:

names: List[str] = ['Jane', 'Dave', 'John']

parser = argparse.ArumentParser()
parser.add_argument('--names', default=names, action=SplitArgs)

args = parser.parse_args()
names = args.names

This doesn’t work with list_str because the default would have to be a string.

Answered By: V13

Your custom action is the closest way to how it is done internally for other argument types. IMHO there should be a _StoreCommaSeperatedAction added to argparse in the stdlib since it is a somewhat common and useful argument type,

It can be used with an added default as well.

Here is an example without using an action (no SplitArgs class):

class Test:
    def __init__(self):
        self._names: List[str] = ["Jane", "Dave", "John"]

    @property
    def names(self):
        return self._names

    @names.setter
    def names(self, value):
        self._names = [name.strip() for name in value.split(",")]


test_object = Test()
parser = ArgumentParser()
parser.add_argument(
    "-n",
    "--names",
    dest="names",
    default=",".join(test_object.names),  # Joining the default here is important.
    help="a comma separated list of names as an argument",
)
print(test_object.names)
parser.parse_args(namespace=test_object)
print(test_object.names)

Here is another example using SplitArgs class inside a class completely

    """MyClass
Demonstrates how to split and use a comma separated argument in a class with defaults
"""
import sys
from typing import List
from argparse import ArgumentParser, Action


class SplitArgs(Action):
    def __call__(self, parser, namespace, values, option_string=None):
        # Be sure to strip, maybe they have spaces where they don't belong and wrapped the arg value in quotes
        setattr(namespace, self.dest, [value.strip() for value in values.split(",")])


class MyClass:
    def __init__(self):
        self.names: List[str] = ["Jane", "Dave", "John"]
        self.parser = ArgumentParser(description=__doc__)
        self.parser.add_argument(
            "-n",
            "--names",
            dest="names",
            default=",".join(self.names),  # Joining the default here is important.
            action=SplitArgs,
            help="a comma separated list of names as an argument",
        )
        self.parser.parse_args(namespace=self)


if __name__ == "__main__":
    print(sys.argv)
    my_class = MyClass()
    print(my_class.names)
    sys.argv = [sys.argv[0], "--names", "miigotu, sickchill,github"]
    my_class = MyClass()
    print(my_class.names)

And here is how to do it in a function based situation, with a default included

class SplitArgs(Action):
    def __call__(self, parser, namespace, values, option_string=None):
        # Be sure to strip, maybe they have spaces where they don't belong and wrapped the arg value in quotes
        setattr(namespace, self.dest, [value.strip() for value in values.split(",")])

names: List[str] = ["Jane", "Dave", "John"]
parser = ArgumentParser(description=__doc__)
parser.add_argument(
    "-n",
    "--names",
    dest="names",
    default=",".join(names),  # Joining the default here is important.
    action=SplitArgs,
    help="a comma separated list of names as an argument",
)
parser.parse_args()
Answered By: miigotu

I know this post is old but I recently found myself solving this exact problem. I used functools.partial for a lightweight solution:

import argparse

from functools import partial

csv_ = partial(str.split, sep=',')

p = argparse.ArgumentParser()
p.add_argument('--stuff', type=csv_)
p.parse_args(['--stuff', 'a,b,c'])
# Namespace(stuff=['a', 'b', 'c'])

If you’re not familiar with functools.partial, it allows you to create a partially "frozen" function/method. In the above example, I created a new function (csv_) that is essentially a copy of str.split() except that the sep kwarg has been "frozen" to the comma character.

Answered By: h3rrmiller
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.