Search code examples
pythonkeyword-argument

The right way to use **kwargs in Python


I took a look at this question but it doesn't exactly answer my question. As an example, I've taken a simple method to print my name.

def call_me_by_name(first_name):
    print("Your name is {}".format(first_name))

Later on, I realized that optionally, I would also like to be able to print the middle name and last name. I made the following changes to accommodate that using **kwargs fearing that in the future, I might be made to add more fields for the name itself (such as a 3rd, 4th, 5th name etc.)

I decided to use **kwargs

def call_me_by_name(first_name,**kwargs):

    middle_name = kwargs['middle_name'] if kwargs.get('middle_name') else ""
    last_name = kwargs['last_name'] if kwargs.get('last_name') else ""

    print("Your name is {} {} {}".format(first_name,middle_name,last_name))

My only concern here is that as I continue to implement support for more names, I end up writing one line of code for every single keyword argument that may or may not come my way. I'd like to find a solution that is as pythonic as possible. Is there a better way to achieve this ?

EDIT 1

I want to use keyword arguments since this is just an example program. The actual use case is to parse through a file. The keyword arguments as of now would support parsing a file from

1) A particular byte in the file.
2) A particular line number in the file.

Only one of these two conditions can be set at any given point in time (since it's not possible to read from a particular byte offset in the file and from a line number at the same time.) but there could be more such conditions in the future such as parse a file from the first occurrence of a character etc. There could be 10-20 different such conditions my method should support BUT only one of those conditions would ever be set at any time by the caller. I don't want to have 20-30 different IF conditions unless there's no other option.


Solution

  • You have two separate questions with two separate pythonic ways of answering those questions.

    1- Your first concern was that you don't want to keep adding new lines the more arguments you start supporting when formatting a string. The way to work around that is using a defaultdict so you're able to return an empty string when you don't provide a specific keyword argument and str.format_map that accepts a dict as a way to input keyword arguments to format. This way, you only have to update your string and what keyword arguments you want to print:

    from collections import defaultdict
    def call_me_by_name(**kwargs):
        default_kwargs = defaultdict(str, kwargs)
        print("Your name is {first_name} {second_name} {third_name}".format_map(default_kwargs))
    

    2- If, on the other hand and answering your second question, you want to provide different behavior depending on the keyword arguments, like changing the way a string looks or providing different file lookup functionalities, without using if statements, you have to add different functions/methods and call them from this common function/method. Here are two ways of doing that:

    OOP:

    class FileLookup:
    
        def parse(self, **kwargs):
            return getattr(self, next(iter(kwargs)))(**kwargs)
    
        def line_number(self, line_number):
            print('parsing with a line number: {}'.format(line_number))
    
        def byte_position(self, byte_position):
            print('parsing with a byte position: {}'.format(byte_position))
    
    fl = FileLookup()
    fl.parse(byte_position=10)
    fl.parse(line_number=10)
    

    Module:

    def line_number(line_number):
        print('parsing with a line number: {}'.format(line_number))
    
    def byte_position(byte_position):
        print('parsing with a byte position: {}'.format(byte_position))
    
    def parse(**kwargs):
        return globals()[next(iter(kwargs))](**kwargs)
    
    parse(byte_position=29)
    parse(line_number=29)