In an earlier post , you’ve seen how to implement the Open-Closed Principle (OCP) in Python.
In this post, I’m trying to solve an issue introduced in the earlier post. You’ll see a simpler implementation of that SOLID principle to be a Pythonic solution .
Pythonic code is code that is simple, clean, and readable. That was not achieved in my last post, because it was close to Java.
The reason the code was not Pythonic is that I was literally translating the code from C# to Python. I knew that Python has features that I can take advantage of to make the code much cleaner.
But I didn’t realize I can avoid following SOLID instructions written in C#.
Especially because C#, Java, and other object-oriented languages have something in common. They force you to follow a certain, rigid style that is generally unpythonic in nature.
In this tutorial, I’ll show you a better way to use the Open/Closed principle in Python.
This is the second design principle (the ‘O’ in SOLID).
Let’s start with the definition:
Software entities (modules, classes, functions, etc.) should be open for extension, but closed for modification.
I assume you already know what the OCP is. If not, please check out the first section in the OCP post.
Refined Example: Tax Calculator
Let’s continue with the tax example discussed in the previous post, but with a little rectification. Suppose you are developing an application that includes a tax calculator.
Users can input their income as a gross salary, deduction subtracted from the gross salary, and a country of residence. Then the application is expected to return the tax payable using a mathematical equation.
Of course, this is not what a real tax application would look like but this is a simple example:
class TaxCalculator:
def calculate(income, deduction, country):
taxable_income = income - deduction
if country == "Egypt":
tax_amount = taxable_income * 0.22 # or insert the tax equation
elif country == "Palestine":
tax_amount = taxable_income * 0.15 # or insert the tax equation
return tax_amount
As we can see, there is a check for the country name to calculate the tax amount for that particular country.
But what’s the problem with this code? The issue here is that the code is not clean enough to be maintainable for the future.
Assume you want to add many countries to the class. You’ll have to
modify the code to add each country. Each country will have its own tax
equation plus a new elif
statement to check the same country
variable. Ugly and error-prune. Plus it’s not Pythonic. Let’s fix this.
How to refactor to conform to OCP
To refactor such a code, you need to look at the problem at a higher level. What would make this code more idiomatic and clean Pythonic?
First, we have a class that we don’t know why we really need.
Second, we have repetitive if statements that check the same variable.
Third, we just have one variable that needs to be changed to reflect the tax rate for each country.
To solve each problem, do the following:
- Get rid of the class and make it just one function.
- Get rid of the repetitive if statements and find a way to replace them with a lookup table.
- Make the lookup table relate to the country name and the thing that is changing (e.g. the tax rate).
lu_tax_rate = {
"Egypt": 0.22,
"Palestine": 0.15
}
def calculate_tax(country, income, deduction):
rate = lu_tax_rate.get(country, False)
if not rate:
raise ValueError("Invalid country")
return (income - deduction) * rate
print(calculate_tax("Egypt", 100000, 1000))
So when you used a lu_tax_rate
dictionary as a lookup table, you
made your code Pythonic. Elegant. Idiomatic Python and readable.
Then you use that dictionary to get the tax rate inside the calculate_tax()
function. Based on the country
argument passed to
that function, you will get the associated tax rate for that country.
Finally, you use the other values passed to the function ( income
,
and deduction
) in the tax equation.
Now, the code is much simpler and can be easily refactored. Moreover, it conforms to OCP:
- open for extension (when we need to extend the functionality and add more countries)
- and also closed for modification (no actual modification you need to change the behavior of a unit of code).
That way, when you add a new country, you don’t need to add a new elif
statement as before. You can simply add a new key-value pair to the
lookup table as below:
def __init__(self):
self.lu_tax_rate = {
"Egypt": 0.22,
"Palestine": 0.15,
"USA": 0.37
}
Note: This can be more complicated than just changing the tax rate. You can replace the tax rates with methods for each country like the following:
def calculate_tax(country, income, deduction):
tax_amount = lu_tax_amount.get(country, False)
if not tax_amount:
raise ValueError("Invalid country")
return tax_amount(income, deduction)
def egypt(income, deduction):
# calculate the exact tax equation for Egypt
print(f"Calculating tax for Egypt.\nIncome: {income}\nDeduction: {deduction}")
pass
def palestine(income, deduction):
# calculate the exact tax equation for Egypt
print(f"Calculating tax for Palestine.\nIncome: {income}\nDeduction: {deduction}")
pass
def usa(income, deduction):
# calculate the exact tax equation for USA
print(f"Calculating tax for USA.\nIncome: {income}\nDeduction: {deduction}")
pass
lu_tax_amount = {
"Egypt": egypt,
"Palestine": palestine,
"USA": usa
}
print(calculate_tax("Egypt", 100000, 1000))
So whenever you need to extend the code. To add a new country with a
more complex tax equation that requires more than changing the tax rate.
Consider using a method with the country name (e.g. usa()
) and add
that method to the lookup table (e.g. "USA": self.usa
).
Final Thoughts
The purpose of writing this post is to prove that Python has features that are easy to take advantage of. This is very needed to solve problems especially when you write clean code.
This is an amendment to my previous post. I received some critiques on my approach that I was over-engineering OCP. So I thank whoever makes constructive criticism.
Finally, follow the Zen of Python when you write Python code (i.e. Code that is Pythonic).
Credit
This post is influenced by u/codemonkey14 who proposed a better approach than my previous blog post . So thank you!