gvgramazio
gvgramazio

Reputation: 1154

Managing keyword arguments in python strings: how to format only some arguments and keep the others unformatted

I have some troubles understanding the way the format() method of string works.

Suppose that I set a string variable with keywords arguments:

s = '{hello} {person_name}'

I could either assign this value to another variable or print it. In the latter case, the result would be {hello} {person_name}.

I could also use the format() method while printing s and assign some values to the keywords:

print(s.format(hello='hello', person_name='Alice'))

In this case, the result is hello Alice. Of course, I could also assign it to a new variable.

My problem arises when I want to use format only on one keyword:

print(s.format(hello='hello'))

or

a = s.format(hello='hello')

Both of them throw an error:

KeyError: 'person_name'


I want to be able to run something like :

s = '{hello} {person_name}'
a = s.format(hello='hello')
if something:
  b = a.format(person_name='Alice')
else:
  b = a.format(person_name='Bob')
print(b)

Is something like this possible or should I set all keywords when I use format()?

Upvotes: 2

Views: 869

Answers (4)

Lenka Vraná
Lenka Vraná

Reputation: 1706

I think you have to define all the keywords while using format().

I would suggest a different approach using *args:

def printHello(*args):
    print(' '.join([arg for arg in args]))

printHello('hello', 'Alice')
# hello Alice
printHello('hello')
# hello

You can send any number of words into this function.

Upvotes: 1

r.ook
r.ook

Reputation: 13898

In your use case, you might consider escaping the {person} in the string:

# double brace the person_name to escape it for the first format
s = '{hello} {{person_name}}'
a = s.format(hello='hello')

# a = 'hello {person_name}'

if something:
  b = a.format(person_name='Alice')
  # b = 'hello Alice'
else:
  b = a.format(person_name='Bob')
  # b = 'hello Bob'

print(b)

With this method however you will need to follow the explicit order in which you escaped your variables. i.e. you must assign hello first and then person_name. If you need to be flexible about the order of things, I would suggest using a dict to construct the variables before passing it altogether:

# dict approach
s = '{hello} {person_name}'

# determine the first variable
d = {'hello':'hello'}
... do something
d.update({'person': 'Alice'})

# unpack the dictionary as kwargs into your format method
b = s.format(**d)

# b = 'hello Alice'

This gives you a bit more flexibility on the order of things. But you must only call .format() once all the variables are provided in your dict (at least it must have a default value), otherwise it'll still raise an error.

If you want to be more fancy and want the ability to print the field names at the absence of the variable, you can make your own wrapper function as well:

# wrapper approach
# We'll make use of regex to keep things simple and versatile
import re

def my_format(message, **kwargs):

    # build a regex pattern to catch words+digits within the braces {}
    pat = re.compile('{[\w\d]+}')

    # build a dictionary based on the identified variables within the message provided
    msg_args = {v.strip('{}'): v for v in pat.findall(message)}

    # update the dictionary with provided keyword args
    msg_args.update(kwargs)

    # ... and of course, print it
    print(message.format(**msg_args))

s = 'Why {hello} there {person}'
my_format(s, hello='hey')
# Why hey there {person}

my_format(s, person='Alice') 
# Why {hello} there Alice

You can determine the default display (at the absence of a variable) you want by modifying the v in dictionary comprehension.

Upvotes: 3

user459872
user459872

Reputation: 24974

As per PEP 3101 If the index or keyword refers to an item that does not exist, then an IndexError/KeyError should be raised.

But you can create your own custom formatter class like this.

from string import Formatter


class MyStringFormatter(Formatter):
    def get_value(self, key, args, kwds):
        try:
            return super().get_value(key, args, kwds)
        except KeyError:
            return "{%s}" % key

fmt = MyStringFormatter()

DEMO

s = "{hello} {person_name}"

keywords = {'hello': 'hello'}
a = fmt.format(s, **keywords)
print(a) 
# This will print hello {person_name}

something = False

if something:
    person_name = {'person_name': 'Alice'}
else:
    person_name = {'person_name': 'Bob'}
b = fmt.format(a, **person_name)

print(b)   
# This will print `hello Bob` if `something` is False, 'hello Alice' otherwise.

Upvotes: 2

knoight
knoight

Reputation: 479

Is something like this possible or should I set all keywords when I use format()?

PEP-3101 says:

If the index or keyword refers to an item that does not exist, then an IndexError/KeyError should be raised.

So yes, if you are going to use keywords you would have to specify them all.

Upvotes: 1

Related Questions