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