In this post, we will go through a simple story of how optional arguments in Python will help developers write better code.


Imagine that you are developing software for a large shipping company. And you are tasked with implementing a function to calculate the fees for passing ships depending on the weight of the cargo. Easy and simple:

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 someday your program will work in another country, say, the United States. There is one problem — you have to use pounds instead of kilograms to calculate fees. No problem, there you go:

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 gets harder and harder, but here comes another requirement — based on the boolean flag, you have to raise an exception if the weight exceeds 1000 kilograms for certain 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, we have to force the end user to use keyword arguments explicitly. To do this we need to add one asterisk at the beginning of the list of keyword arguments:

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

Last updated Wed May 05 2021