Optional arguments MUST use keywords (Python3)

Optional arguments MUST use keywords (Python3)

Imagine that you are developing software for a large shipping company (why not imagine a small company). And you have the task of creating a function for calculating fees for ships based on their cargo weight. Easy peasy:

WEIGHT_RATES = [
    ( 10, 10.55),
    (  5, 5.05),
    (  2, 3.35),
    (  0, 1.25)
]

def calculate_fees(weight):
    if weight < 0:
        raise ValueError("Can't calculate shipping charge of negative weights")
    for min_weight, rate in WEIGHT_RATES:
        if weight > min_weight:
            return weight * rate

Simple enough.

But one day your program will eventually work in another country, say, the United States. There's one problem — you have to use pounds instead of kilograms to calculate fees. No problem, there you are:

def calculate_fees(weight, pnds):
    if pnds:
       weight /= 2.2
    if weight < 0:
       raise ValueError("Can't calculate shipping charge of negative weights")
    for min_weight, rate in WEIGHT_RATES:
        if weight > min_weight:
            return weight * rate

This becomes more and more complicated, but then there is another requirement — you have to raise the exception if the weight exceeds 1000 kilos for specific directions:

def calculate_fees(weight, pnds, exceed):
    if pnds:
       weight /= 2.2
    if exceed and weight > 1000:
       raise Exception("Weight can't exceed 1000 kg")
    if weight < 0:
       raise ValueError("Can't calculate shipping charge of negative weights")
    for min_weight, rate in WEIGHT_RATES:
        if weight > min_weight:
            return weight * rate

Do you see the problem? In this dummy example, you get to a function with 3 position arguments, the last two of which have the same type. The end-user or you as a developer can easily forget which one should come first and mess them up. Thanks to the same type, the Python program will not fail and you will get a logical error:

calculate_fees(2000, True, False)

or

calculate_fees(2000, False, True)

You can use keyword arguments with default values and it's a good practice:

def calculate_fees(weight, pnds=False, exceed=False):
    if pnds:
       weight /= 2.2
    if exceed and weight > 1000:
       raise Exception("Weight can't exceed 1000 kg")
    if weight < 0:
       raise ValueError("Can't calculate shipping charge of negative weights")
    for min_weight, rate in WEIGHT_RATES:
        if weight > min_weight:
            return weight * rate

But the problem is not solved. To solve the problem, you need to add one asterisk at the beginning of the keyword arguments list:

def calculate_fees(weight, *, pnds=False, exceed=False):
    if pnds:
       weight /= 2.2
    if exceed and weight > 1000:
       raise Exception("Weight can't exceed 1000 kg")
    if weight < 0:
       raise ValueError("Can't calculate shipping charge of negative weights")
    for min_weight, rate in WEIGHT_RATES:
        if weight > min_weight:
            return weight * rate

That's it, next time you will call this function you will get an error:

>>>calculate_fees(2000, True, False)
TypeError: calculate_fees() takes 1 positional argument but 3 were given

More info: PEP-3102


Daily dose of