Numerical dictionary keys as placeholders when printing strings?

Viewed 138

I have the following code:

board_dic = {
    0: '_',
    1: '_',
    2: '_',
    3: '_',
    4: '_',
    5: '_',
    6: '_',
    7: '_',
    8: '_',
}

print("|{0}|{1}|{2}|\n|{3}|{4}|{5}|\n|{6}|{7}|{8}|".format(**board_dic)

The output when running is:

line 28, in <module>
    print("|{0}|{1}|{2}|\n|{3}|{4}|{5}|\n|{6}|{7}|{8}|".format(**board_dic))
IndexError: Replacement index 0 out of range for positional args tuple

I can't find a solution to this anywhere. If I was to replace the keys in board_dic to strings, such as 'a', 'b', 'c', etc. and replace the placeholders in the print statement to 'a', 'b', 'c', etc. then my print statement would execute without any issues. However, it seems that the problem is specifically with numerical key dictionary values.

Why is this so, and how can I fix it?

3 Answers

When using format-strings, placeholders like {1} represent the nth argument of "format()". This is different from named keyword-arguments. Using **board_dic will explode them into keyword-arguments. This is the reason it does not work as expected.

Instead you need to use positional arguments using a single *. But then you are confronted with a different issue: Dictionary ordering has only been made part of the language spec in Python 3.7 as far as I remember. It was already stable in previous versions, but that was an implementation detail and was not guaranteed in other versions (pypy, jython, ...).

So we need to sort it to be 100% sure this works.

As the keys are numerical, we can simply sort on board_dic.items(). This will give us a stable replacement.

We then need to extract the effective values. We can do this with a simple list-comprehension.

After that change we are - as long as the dictionary does not contain any missing numbers - 100% sure the output will be correct.

We can use the resulting list in .format() using a single "positional-argument-expansion".

board_dic = {
    0: '_',
    1: '_',
    2: '_',
    3: '_',
    4: '_',
    5: '_',
    6: '_',
    7: '_',
    8: '_',
}

values = [value for _, value in sorted(board_dic.items())]

print("|{0}|{1}|{2}|\n|{3}|{4}|{5}|\n|{6}|{7}|{8}|".format(*values))

If you know what to look for it is explained here:

The field_name itself begins with an arg_name that is either a number or a keyword. If it’s a number, it refers to a positional argument, and if it’s a keyword, it refers to a named keyword argument. If the numerical arg_names in a format string are 0, 1, 2, … in sequence, they can all be omitted (not just some) and the numbers 0, 1, 2, … will be automatically inserted in that order. Because arg_name is not quote-delimited, it is not possible to specify arbitrary dictionary keys (e.g., the strings '10' or ':-]') within a format string.

In simple terms, '{0} {1}'.format(...) means "replace {0} with the first positional argument and {1} with the second positional argument of format(...).

Positional arguments are those that are passed without specifying the parameter name in the function call. For example in format(1, 2, c=3), 1 and 2 are positional argument and c=3 is a keyword argument.

But the **board_dic syntax specifies keyword arguments! format(**board_dic) is equivalent to format(0='_', 1='_', 2='_', ...)1 and there are no positional arguments. Therefore, using {0}, {1}, {2}, etc., to get the first, second, third, etc., positional argument, fails.

In order to pass a dictionary with integer keys to format, you can use this:

print("|{d[0]}|{d[1]}|{d[2]}|\n|...".format(d=board_dic))

1Numbers not being allowed as keywords is another issue.

To convert the dict values to positional arguments for the str.format method using their keys, you can map a range of the length of the dict to the get method of the dict and unpack the outputting sequence as arguments:

print("|{0}|{1}|{2}|\n|{3}|{4}|{5}|\n|{6}|{7}|{8}|".format(*map(board_dic.get, range(len(board_dic)))))
Related