42

I want to use the standard library argparse module to parse command line arguments to my program, and have the program accept an optional argument -i (or --image) which is a dictionary.

I tried configuring the parser like this:

parser.add_argument('-i','--image', type=dict, help='Generate an image map from the input file (syntax: {\'name\': <name>, \'voids\': \'#08080808\', \'0\': \'#00ff00ff\', \'100%%\': \'#ff00ff00\'}).')

But when I try to run the script, I get an error:

$ ./script.py -i {'name': 'img.png','voids': '#00ff00ff','0': '#ff00ff00','100%': '#f80654ff'}
    
script.py: error: argument -i/--image: invalid dict value: '{name:'

Even though similar syntax would work fine inside the interpreter:

>>> a={'name': 'img.png','voids': '#00ff00ff','0': '#ff00ff00','100%': '#f80654ff'}

How should I a) write the command line and b) set up the argparse logic?

Karl Knechtel
  • 62,466
  • 11
  • 102
  • 153
user975296
  • 531
  • 1
  • 4
  • 3
  • 1
    You can read formats like JSON from external file or stdin, and then parse it. So you argparse type will be actually a file. – Yauhen Yakimovich Jun 28 '13 at 13:51
  • 1
    as @wim said in his answer the shell is processing the arguments before passing them down to python. If you prepend your command with 'echo' (`echo ./script.py -i {'name': ...`) you'll see what python is seeing (mainly it is not receiving any quotes). In your case that there is no `$` in your param (that could be interpreted by the shell as an enviroment variable) you can surround your dict with double quotes: `./script.py -i "{'name': 'img.png', ....}"` – Carlos Campderrós Aug 27 '15 at 11:13
  • Trying to pass a dict on the command line - using Python-like syntax - is really not how the command line is intended to work. It will be difficult for users to get the syntax right, because of how the shell tokenizes the command line to figure out separate arguments. It doesn't know anything about balancing brackets, only quoting and escaping - and different shells/terminals/operating systems have different rules for how that works. – Karl Knechtel Mar 10 '23 at 07:46

13 Answers13

68

Necroing this: json.loads works here, too. It doesn't seem too dirty.

import json
import argparse

test = '{"name": "img.png","voids": "#00ff00ff","0": "#ff00ff00","100%": "#f80654ff"}'

parser = argparse.ArgumentParser()
parser.add_argument('-i', '--input', type=json.loads)

args = parser.parse_args(['-i', test])

print(args.input)

Returns:

{u'0': u'#ff00ff00', u'100%': u'#f80654ff', u'voids': u'#00ff00ff', u'name': u'img.png'}

Edd
  • 1,925
  • 1
  • 17
  • 15
  • `json.loads` is a nice choice for `type`. Like `int` and `float` it takes a string, and returns a `ValueError` if it can't handle it. It is also safer than `eval`. For this purpose it may be a little too general (i.e. it can handle a list `'[1, 2]'`), but the user can deal with that after the `parse_args()`. – hpaulj Aug 02 '13 at 02:48
  • When the values are `str`ings, `int`, `float`, it works fine. For other types of values, such as `bool`, it does not (but passing `1` for `True` should work but all well-written code). – gerrit Jul 14 '17 at 14:01
  • can you add an example of the cli input? almost everything I try results in `invalid loads value`. e.g this works,. `--input="{}"` this fails `--input="{'foo': 'bar'}"` – J'e Nov 13 '20 at 18:44
  • @J'e `--input='{"foo" : "bar" }'` will work, because JSON Syntax mandates double quotes for string. – Ying Lyu Apr 26 '21 at 04:15
18

For completeness, and similarly to json.loads, you could use yaml.load (available from PyYAML in PyPI). This has the advantage over json in that there is no need to quote individual keys and values on the command line unless you are trying to, say, force integers into strings or otherwise overcome yaml conversion semantics. But obviously the whole string will need quoting as it contains spaces!

>>> import argparse
>>> import yaml
>>> parser = argparse.ArgumentParser()
>>> parser.add_argument('-fna', '--filename-arguments', type=yaml.load)
>>> data = "{location: warehouse A, site: Gloucester Business Village}"
>>> ans = parser.parse_args(['-fna', data])
>>> print ans.filename_arguments['site']
Gloucester Business Village

Although admittedly in the question given, many of the keys and values would have to be quoted or rephrased to prevent yaml from barfing. Using the following data seems to work quite nicely, if you need numeric rather than string values:

>>> parser.add_argument('-i', '--image', type=yaml.load)
>>> data = "{name: img.png, voids: 0x00ff00ff, '0%': 0xff00ff00, '100%': 0xf80654ff}"
>>> ans = parser.parse_args(['-i', data])
>>> print ans.image
{'100%': 4161164543L, 'voids': 16711935, 'name': 'img.png', '0%': 4278255360L}
Hamish
  • 193
  • 2
  • 6
10

Using simple lambda parsing is quite flexible:

parser.add_argument(
    '--fieldMap',
    type=lambda x: {k:int(v) for k,v in (i.split(':') for i in x.split(','))},
    help='comma-separated field:position pairs, e.g. Date:0,Amount:2,Payee:5,Memo:9'
)
Konstantin Glukhov
  • 1,898
  • 3
  • 18
  • 25
  • That's the cleanest solution so far! Thanks, helped a lot – Rovshan Musayev Oct 09 '20 at 12:29
  • Very clean solution, Thanks. As v is used in multiple context, its creating confusion for me, below is working as well `type=lambda e: {k:int(v) for k,v in (x.split(':') for x in e.split(','))},` – BeingSachin Jul 28 '21 at 06:42
8

I’ll bet your shell is messing with the braces, since curly braces are the syntax used for brace expansion features in many shells (see here).

Passing in a complex container such as a dictionary, requiring the user to know Python syntax, seems a bad design choice in a command line interface. Instead, I’d recommend just passing options in one-by-one in the CLI within an argument group, and then build the dict programmatically from the parsed group.

wim
  • 338,267
  • 99
  • 616
  • 750
  • 1
    Changed in version 3.11: Calling add_argument_group() on an argument group is deprecated. This feature was never supported and does not always work correctly. The function exists on the API by accident through inheritance and will be removed in the future. As per the API docs, can be referred below in more details https://docs.python.org/dev/library/argparse.html#argparse.ArgumentParser.add_argument_group – Anurag Upadhyaya Jan 19 '22 at 05:30
  • 1
    @AnuragUpadhyaya So what? This answer suggests to add an argument group on the parser, not on a returned group. The feature itself isn't being removed, only an unrelated misuse of the feature.. – wim Jan 19 '22 at 05:34
  • This seems to be the only answer that actually addresses the **reported error** (which is caused by the command line rather than by `argparse` - although the `argparse` usage is also wrong). – Karl Knechtel Mar 10 '23 at 07:51
5

Combining the type= piece from @Edd and the ast.literal_eval piece from @Bradley yields the most direct solution, IMO. It allows direct retrieval of the argval and even takes a (quoted) default value for the dict:

Code snippet

parser.add_argument('--params', '--p', help='dict of params ', type=ast.literal_eval, default="{'name': 'adam'}")
args = parser.parse_args()

Running the Code

python test.py --p "{'town': 'union'}"

note the quotes on the dict value. This quoting works on Windows and Linux (tested with [t]csh).

Retrieving the Argval

dict=args.params
frankeye
  • 51
  • 1
  • 3
3

You can definitely get in something that looks like a dictionary literal into the argument parser, but you've got to quote it so when the shell parses your command line, it comes in as

  • a single argument instead of many (the space character is the normal argument delimiter)
  • properly quoted (the shell removes quotes during parsing, because it's using them for grouping)

So something like this can get the text you wanted into your program:

python MYSCRIPT.py -i "{\"name\": \"img.png\", \"voids\": \"#00ff00ff\",\"0\": \"#ff00ff00\",\"100%\": \"#f80654ff\"}"

However, this string is not a valid argument to the dict constructor; instead, it's a valid python code snippet. You could tell your argument parser that the "type" of this argument is eval, and that will work:

import argparse

parser = argparse.ArgumentParser()
parser.add_argument('-i','--image', type=eval, help='Generate an image map...')
args = parser.parse_args()
print args

and calling it:

% python MYSCRIPT.py -i "{\"name\": \"img.png\", \"voids\": \"#00ff00ff\",\"0\": \"#ff00ff00\",\"100%\": \"#f80654ff\"}"
Namespace(image={'0': '#ff00ff00', '100%': '#f80654ff', 'voids': '#00ff00ff', 'name': 'img.png'})

But this is not safe; the input could be anything, and you're evaluating arbitrary code. It would be equally unwieldy, but the following would be much safer:

import argparse
import ast

parser = argparse.ArgumentParser()
parser.add_argument('-i','--image', type=ast.literal_eval, help='Generate an image map...')
args = parser.parse_args()
print args

This also works, but is MUCH more restrictive on what it will allow to be eval'd.

Still, it's very unwieldy to have the user type out something, properly quoted, that looks like a python dictionary on the command line. And, you'd have to do some checking after the fact to make sure they passed in a dictionary instead of something else eval-able, and had the right keys in it. Much easier to use if:

import argparse

parser = argparse.ArgumentParser()
parser.add_argument("--image-name", required=True)
parser.add_argument("--void-color", required=True)
parser.add_argument("--zero-color", required=True)
parser.add_argument("--full-color", required=True)

args = parser.parse_args()

image = {
    "name": args.image_name,
    "voids": args.void_color,
    "0%": args.zero_color,
    "100%": args.full_color
    }
print image

For:

% python MYSCRIPT.py --image-name img.png --void-color \#00ff00ff --zero-color \#ff00ff00 --full-color \#f80654ff
{'100%': '#f80654ff', 'voids': '#00ff00ff', 'name': 'img.png', '0%': '#ff00ff00'}
Matt Anderson
  • 19,311
  • 11
  • 41
  • 57
  • Wow, thanks for the possibilities overview; however, despite in the example I've put only 0 and 100%, these could actually be any value (e.g. {'46%':'#0f0e0d0c','3629','#f0e0d0c0'}), which is not contemplated in your last piece of code... – user975296 Oct 04 '11 at 12:51
2

One of the simplest ways I've found is to parse the dictionary as a list, and then convert that to a dictionary. For example using Python3:

#!/usr/bin/env python3
import argparse

parser = argparse.ArgumentParser()
parser.add_argument('-i', '--image', type=str, nargs='+')
args = parser.parse_args()
if args.image is not None:
    i = iter(args.image)
    args.image = dict(zip(i, i))
print(args)

then you can type on the command line something like:

./script.py -i name img.png voids '#00ff00ff' 0 '#ff00ff00' '100%' '#f80654ff'

to get the desired result:

Namespace(image={'name': 'img.png', '0': '#ff00ff00', 'voids': '#00ff00ff', '100%': '#f80654ff'})
Juan A. Navarro
  • 10,595
  • 6
  • 48
  • 52
1

General Advice: DO NOT USE eval.

If you really have to ... "eval" is dangerous. Use it if you are sure no one will knowingly input malicious input. Even then there can be disadvantages. I have covered one bad example.

Using eval instead of json.loads has some advantages as well though. A dict doesn't really need to be a valid json. Hence, eval can be pretty lenient in accepting "dictionaries". We can take care of the "danger" part by making sure that final result is indeed a python dictionary.

import json
import argparse

tests = [
  '{"name": "img.png","voids": "#00ff00ff","0": "#ff00ff00","100%": "#f80654ff"}',
  '{"a": 1}',
  "{'b':1}",
  "{'$abc': '$123'}",
  '{"a": "a" "b"}' # Bad dictionary but still accepted by eval
]
def eval_json(x):
  dicti = eval(x)
  assert isinstance(dicti, dict)
  return dicti

parser = argparse.ArgumentParser()
parser.add_argument('-i', '--input', type=eval_json)
for test in tests:
  args = parser.parse_args(['-i', test])
  print(args)

Output:

Namespace(input={'name': 'img.png', '0': '#ff00ff00', '100%': '#f80654ff', 'voids': '#00ff00ff'})
Namespace(input={'a': 1})
Namespace(input={'b': 1})
Namespace(input={'$abc': '$123'})
Namespace(input={'a': 'ab'})
Mayank Jaiswal
  • 12,338
  • 7
  • 39
  • 41
  • This is extremely dangerous advice. Using eval will (obviously) cause the input from the cmdline to be evaluated as python. This happens *before* it returns a value so your type checking is too little too late. Also, there is plenty of valid dangerous python which will still return a dict.... The "we can take care of the danger" statement is both inaccurate and dangerous to be spreading as advice. – Dave Rawks Nov 02 '16 at 21:15
  • Okay. Agree. I am changing the language of the answer. – Mayank Jaiswal Nov 07 '16 at 06:37
1

A minimal example to pass arguments as a dictionary from the command line:

# file.py
import argparse
import json
parser = argparse.ArgumentParser()
parser.add_argument("-par", "--parameters",
                    required=False,
                    default=None,
                    type=json.loads
                )
args = parser.parse_args()
print(args.parameters)

and in the terminal you can pass your arguments as a dictionary using a string format:

python file.py --parameters '{"a":1}'
Galuoises
  • 2,630
  • 24
  • 30
0

Here is a another solution since I had to do something similar myself. I use the ast module to convert the dictionary, which is input to the terminal as a string, to a dict. It is very simple.

Code snippet

Say the following is called test.py:

import argparse
import ast

parser = argparse.ArgumentParser()
parser.add_argument('--params', '--p', help='dict of params ',type=str)

options = parser.parse_args()

my_dict = options.params
my_dict = ast.literal_eval(my_dict)
print(my_dict)
for k in my_dict:
  print(type(my_dict[k]))
  print(k,my_dict[k])

Then in the terminal/cmd line, you would write:

Running the code

python test.py --p '{"name": "Adam", "lr": 0.001, "betas": (0.9, 0.999)}'

Output

{'name': 'Adam', 'lr': 0.001, 'betas': (0.9, 0.999)}
<class 'str'>
name Adam
<class 'float'>
lr 0.001
<class 'tuple'>
betas (0.9, 0.999)
Bradley
  • 33
  • 4
0

TLDR Solution: The simplest and quickest solution is as below:

import argparse
parser = argparse.ArgumentParser()
parser.add_argument("-par", "--parameters",
                    default={},
                    type=str)
args = parser.parse_args()

In the parser.add_argument function:

  1. Use a dictionary object for default object
  2. str as the type

Then args.parameters will automatically be converted to a dictionary without any need for ast.literal.eval or json.loads.

Motivation: The methods posted by @Galuoises and @frankeye, appear to not work when the default is set as a json encoded dictionary such as below.

parser.add_argument("-par", "--parameters",
                required=False,  default="{\"k1\":v1, \"k2\":v2}",
                type=json.loads)

This is because

Akanni
  • 924
  • 9
  • 10
0

The following works just fine:

parser = argparse.ArgumentParser()
parser.add_argument("-par", "--parameters",
                required=False,  default={"k1a":"v1a","k2a":"v2a"},
                type=json.loads)
args = parser.parse_args()
print(str(parameters))

result:
{'k1a': 'v1a', 'k2a': 'v2a'}

For default value, the type should be dict since json.loads returns a dictionary, not a string, the default object should be given as a dictionary.

import argparse,json,sys
sys.argv.extend(['-par','{"k1b":"v1b","k2b":"v2b"}'])
parser = argparse.ArgumentParser()
parser.add_argument("-par", "--parameters",
                required=False,  default={"k1":"v1","k2":"v2"},
                type=json.loads)
args = parser.parse_args()
print(str(args.parameters))

result: 
{'k1b': 'v1b', 'k2b': 'v2b'}
Quin
  • 87
  • 10
0

 You could try:

$ ./script.py -i "{'name': 'img.png','voids': '#00ff00ff','0': '#ff00ff00','100%': '#f80654ff'}"

I haven't tested this, on my phone right now.

Edit: BTW I agree with @wim, I think having each kv of the dict as an argument would be nicer for the user.

John Keyes
  • 5,479
  • 1
  • 29
  • 48