4

I would like to understand why this works fine:

>>> test_string = 'long brown fox jump over a lazy python'
>>> 'formatted "{test_string[0]}"'.format(test_string=test_string)
'formatted "l"'

Yet this fails:

>>> 'formatted "{test_string[-1]}"'.format(test_string=test_string)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: string indices must be integers
>>> 'formatted "{test_string[11:14]}"'.format(test_string=test_string)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: string indices must be integers

I know this could be used:

'formatted "{test_string}"'.format(test_string=test_string[11:14])

...but that is not possible in my situation.

I am dealing with a sandbox-like environment where a list of variables is passed to str.format() as dictionary of kwargs. These variables are outside of my control. I know the names and types of variables in advance and can only pass formatter string. The formatter string is my only input. It all works fine when I need to combine a few strings or manipulate numbers and their precision. But it all falls apart when I need to extract a substring.

2
  • 2
    I removed the part which basically asked "why does string slicing work" to focus more on the different cases which sometimes work and sometimes don't, even though the structure is the same.
    – mkrieger1
    Commented Sep 18, 2024 at 9:56
  • 1
    For f-strings it works: f'formatted "{test_string[-1]}"'
    – makukha
    Commented Sep 18, 2024 at 10:51

2 Answers 2

4

Why it doesn't work

This is explained in the spec of str.format():

The arg_name can be followed by any number of index or attribute expressions. An expression of the form '.name' selects the named attribute using getattr(), while an expression of the form '[index]' does an index lookup using __getitem__().

That is, you can index the string using bracket notation, and the index you put inside the brackets will be the argument of the __getitem__() method of the string. This is indexing, not slicing. The bottom line is that str.format() simply doesn't support slicing of the replacement field (= the part between {}), as this functionality isn't part of spec.

Regarding negative indices, the grammar specifies:

element_index     ::=  digit+ | index_string

This means that the index can either be a sequence of digits (digit+) or a string. Since any negative index such as -1 is not a sequence of digits, it will be parsed as index_string. However, str.__getitem__() only supports arguments of type integer. Hence the error TypeError: string indices must be integers, not 'str'.

Solutions to the problem

Use f-strings

>>> test_string = 'long brown fox jump over a lazy python'
>>> f"formatted {test_string[0]}"
'formatted l'
>>> f"formatted {test_string[0:2]}"
'formatted lo'
>>> f"formatted {test_string[-1]}"
'formatted n'

Use str.format() but slice the argument of str.format() directly, rather than the replacement field

>>> test_string = 'long brown fox jump over a lazy python'
>>> 'formatted {replacement}'.format(replacement=test_string[0:2])
'formatted lo'
>>> 'formatted {replacement}'.format(replacement=test_string[-1])
'formatted n'
1
  • Thanks for a detailed explanation. Now I fully understand why it does not work. Neither of the proposed solutions help my case as .format() is called in the backend and my only input is formatter string. I hope this is useful for someone else. Commented Sep 18, 2024 at 12:05
2

The str.format() method uses different syntax than f-string literals.

With f-strings, it works as expected:

>>> test_string = 'long brown fox jump over a lazy python'
>>> f'formatted "{test_string[-1]}"'
'formatted "n"

Also, compare the syntax when using dict index:

>>> x = {'key': 'value'}
>>> f'{x["key"]}'
'value'
>>> '{x[key]}'.format(x=x)
'value'

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.