EzzeddinAbdullah2022 Cleaner Python
EzzeddinAbdullah2022 Cleaner Python
Ezzeddin Abdullah
Contents
Introduction 3
Who is this ebook for? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
What’s in it for you? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
Why writing clean code in Python? . . . . . . . . . . . . . . . . . . . . . . . . 4
Conclusion 23
2
Introduction
Python is a pretty easy scripting language compared to many programming languages.
Being easy helps you increase your productivity and automate your tasks more quickly.
However, it is still a bit difficult to write clean pythonic code. Let me tell you why this
is difficult.
Rare resources on the internet teaching clean code are written in Python. This is a
problem because we rarely find a code that is pythonic.
Pythonic code is code that is simple, clean, and readable. That is not achieved in many
Python codebases out there. It could be because people translate to Python code. They
translate objected oriented language (like Java or C#) to Python. While, in fact, it will
be easier to write the same example with less code and in a more readable and even
efficient way.
Python has features that you can take advantage of to make the code much cleaner.
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.
This is where this ebook comes in. I’ll show you a better way to use features in Python
to make your code clean.
Disclaimer: This is not a book on how to master clean code in Python. This ebook shows
you 5 ways to make your code cleaner than before.
This ebook targets people who have little background writing clean code with Python.
For those who want to take that knowledge to the next level.
I’ve designed this ebook to be a practical introduction to clean code in Python. I have
tried to exclude the fluff mentioned in many Python tutorials. And rather focus on the
uncommon stuff that can make your code really clean.
If that triggers you, then this ebook is definitely for you.
3
Why writing clean code in Python?
Clean code is code that is helpful for you and other developers in the present and future.
It is a code that is easy to understand and maintain.
Python has a philosophy called The Zen of Python. It is a collection of simple rules that
help you write clear, idiomatic Python code. Code that would be easy to understand and
maintain. It simply prefers simplicity over complexity.
Reading PEP8 documentation will definitely help you write Pythonic code. It is a style
guide for best practices in Python. In this ebook, I’ll discuss some best practices men-
tioned in PEP8.
4
Virtual Environment Setup for Python 3
A virtual environment is a great way to keep your code clean. It is a tool that helps you
isolate your code from the rest of your system (if you’re working locally). It also helps
you to keep your code isolated from other projects (especially when you’re working on
production).
A virtual environment allows you to isolate the dependencies of the project you’re working
on.
You probably have a virtual environment already set up. If not, you probably would
forget how to set up a new one each time you start a new project.
I’ll show you two ways to install Python 3 in a virtual environment.
venv is a native Python module that provides support for creating lightweight virtual
environments with their own site directories.
Being native makes it easier to use without having to install it.
Now, let’s create a new virtual environment and call it myenv:
$ python3 -m venv myenv
The myenv environment is now created and you can activate it with source command:
$ source myenv/bin/activate
You’ll see now that the prompt changes to (myenv) and you can start working on your
project.
Once you finish your work, you can deactivate the environment with deactivate com-
mand:
$ deactivate
If you want to check which python version you have, you can check with the --version
option:
$ python --version
You can also check all the dependencies you have in your brand new environment. Use
pip list command which outputs the following in my case:
pip (9.0.1)
pkg-resources (0.0.0)
setuptools (39.0.1)
Now, if you’re lazy like me and want to install packages with pip instead of pip3, upgrade
pip with:
5
$ pip install --upgrade pip
To wrap up: When you have virtualenv installed already, use these commands to do
three things. To create a new environment, activate it, and upgrade pip:
python3 -m venv myenv
source myenv/bin/activate
pip install --upgrade pip
Let’s see a second way to set up your virtual environment. I recommend it if you want to
play around real quick locally. It’s a little bit more complicated to set up the first time
but it’s a lot easier to use and activate moving forward.
Conda serves as two roles. A package manager like pip so you can use it to install
packages and modules. It also serves as an environment manager like venv that you can
use to isolate dependencies. One difference between conda and venv is that you can use
the former to create multiple environments which makes it usable in multiple projects.
Use conda with command-line commands at the Anaconda Prompt for Windows, or in
a terminal window for macOS or Linux.
It comes with some modules that you use frequently for data science projects with Python.
This is good and bad. Good because it removes the hassle of installing dependencies. Bad
because it somehow makes your machine slower.
So you can use a minimal version of conda which is miniconda. A minimal virtual
environment manager that comes with some modules but way fewer than the full version.
Select the miniconda version that suits your system. In my case, I need the Linux version
‘Miniconda3 Linux 64-bit’. So I’ve downloaded the shell script installer file and would run
it:
$ bash Miniconda3-latest-Linux-x86_64.sh
Note that you’ve now created a new environment called py3.9 which has a Python 3.9
interpreter. It’s activated now and you can start working on your project. You can
deactivate it with conda deactivate.
If you want to activate it, you can do it with conda activate py3.9.
Let’s see what modules we have in our new environment:
$ conda list
# packages in environment at /home/usr/miniconda3/envs/py3.9:
#
6
# Name Version Build Channel
_libgcc_mutex 0.1 main
_openmp_mutex 4.5 1_gnu
ca-certificates 2021.7.5 h06a4308_1
certifi 2021.5.30 py39h06a4308_0
ld_impl_linux-64 2.35.1 h7274673_9
libffi 3.3 he6710b0_2
libgcc-ng 9.3.0 h5101ec6_17
libgomp 9.3.0 h5101ec6_17
libstdcxx-ng 9.3.0 hd4cf53a_17
ncurses 6.2 he6710b0_1
openssl 1.1.1l h7f8727e_0
pip 21.2.4 py37h06a4308_0
python 3.9.7 h12debd9_1
readline 8.1 h27cfd23_0
setuptools 58.0.4 py39h06a4308_0
sqlite 3.36.0 hc218d9a_0
tk 8.6.11 h1ccaba5_0
tzdata 2021a h5d7bf9c_0
wheel 0.37.0 pyhd3eb1b0_1
xz 5.2.5 h7b6447c_0
zlib 1.2.11 h7b6447c_3
If you’re too lazy to type conda activate <your-environment> every time you want
to activate your environment, you can create an alias for it in your .bashrc file (if you’re
using Linux) or bash_profile (if you’re using macOS for a bash shell):
alias cond="conda activate <environment-name>"
where cond is the alias name and <environment-name> is the name of your environment.
Replace both based on your desire.
For the 3 lines of virtualenv, you can alias them as well:
alias vir='python3 -m venv venv; . venv/bin/activate; pip install --upgrade pip'
So whenever you need to set up a new virtual environment, you can just type vir to
create the venv environment and activate it.
Note: This environment is created per subdirectory of your project. So if you’re working
on a project in /home/usr/myproject, you’ll have a virtual environment named venv in
/home/usr/myproject/venv.
7
If you desire to use conda, you can just type cond instead and you’re ready to use the
environment.
Note: The conda alias mentioned above does not create a new environment. It just
activates the environment you set in the alias.
8
Dictionaries in a Useful Use Case
Here, you’ll see how a simple implementation of the Open-Closed Principle (OCP) in
Python.
OCP is a design principle that states that software entities (modules, classes, functions,
etc.) should be open for extension, but closed for modification.
This is the second SOLID design principle (the ‘O’ in SOLID).
The open/closed principle was first proposed by Bertrand Meyer. He is the creator of the
Eiffel programming language and the idea of design by contract.
Consider a unit of code “open for extension” when its behavior can be easily changed
without modifying it. The fact that no actual modification is needed to change the
behavior of a unit of code makes it “closed” for modification.
The purpose of this principle is to be able to extend the behavior of an entity without
ever modifying its source code.
This happens when your objects are open to extension (using inheritance). But closed to
alteration (by altering methods or changing values in an object).
Let’s see an example to make it more clear.
As we can see, there is a check for the country name to calculate the tax amount for that
particular country.
9
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-prone. Plus it’s not Pythonic. Let’s
fix this.
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 Python code?
Firstly, you have a class that you don’t really need in this case.
Secondly, you have repetitive if statements that check the same variable.
Thirdly, you just have one variable that needs to be changed to reflect the tax rate for
each country.
To solve each problem, do the following:
1. Get rid of the class and make it just one function.
2. Get rid of the repetitive if statements and find a way to replace them with a lookup
table.
3. Create a 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,
}
So when you use a lu_tax_rate dictionary as a lookup table, you make 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.
10
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 coun-
tries)
• 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:
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 a method 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(f"Country {country} is not in our database.")
return tax_amount(income, deduction)
lu_tax_amount = {
"Egypt": egypt,
"Palestine": palestine,
"USA": usa
}
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
11
with the country name (e.g. usa()) and add that method to the lookup table (e.g. "USA":
usa).
At the end of the day, Python has features that are easy to take advantage of. This is
very needed to solve problems especially when you write clean code.
Follow the Zen of Python when you write Python code (i.e. Code that is Pythonic).
12
Supercharging Python Classes With Property
For a long time, I’ve not been able to use classes well enough in Python.
Rare resources on the web could help me write classes that are Pythonic. That’s because
a lot of resources trying to overengineer Python classes.
I used to write methods starting with get_ and set_ to get and set attributes in Python.
This practice was similar to the way Java developers write their getters and setters. Ugly
and not Pythonic.
Until I knew that there is a better way to do this in a Python way.
In this section, I’ll show you how to write @property decorator in Python classes to
be Pythonic. It should be easy to understand and use, especially with a step-by-step
approach.
Let’s first simulate our use case as if @property does not exist.
class Temperature:
def __init__(self, celsius=0):
self._celsius = celsius
def get_fahrenheit(self):
return (self._celsius * 9 / 5) + 32
temperature = Temperature(37)
print(temperature.get_fahrenheit())
# 98.6
In the above example, a class Temperature has a getter method called get_fahrenheit.
It returns the temperature to Fahrenheit.
This is a very typical use case of a getter method.
Now, Fahrenheit looks like a property of a class. You don’t need to call it a function like
the following:
class Temperature:
def __init__(self, celsius=0):
self._celsius = celsius
@property
def fahrenheit(self):
return (self._celsius * 9 / 5) + 32
temperature = Temperature(37)
print(temperature.fahrenheit)
# 98.6
13
We can see that we got rid of the () in the getter method call. Instead, we now have a
@property decorator before fahrenheit function. This function acts as a getter method.
As a side effect, we no longer need to prepend get_ to the function name.
Note that the signature of the getter method needs to be (self).
In the previous example, we have a property decorator. This is a decorator we use to add
a getter method to a class. We could use the property function instead like this:
class Temperature:
def __init__(self, celsius=0):
self._celsius = celsius
def get_fahrenheit(self):
return (self._celsius * 9 / 5) + 32
fahrenhei = property(fget=get_fahrenheit)
temperature = Temperature(37)
print(temperature.fahrenhei)
# 98.6
But the property is not only useful for that. We can also use it to set and delete the
value of the attribute. Let’s see how we can set a new value for the _celsius attribute:
class Temperature:
def __init__(self, celsius=0):
self._celsius = celsius
@property
def fahrenheit(self):
return (self._celsius * 9 / 5) + 32
@fahrenheit.setter
def fahrenheit(self, value):
if not isinstance(value, int) and not isinstance(value, float):
raise TypeError('Value must be a number.')
self._celsius = (value - 32) * 5 / 9
temp = Temperature(98)
print(temp._celsius)
14
# 98
temp.fahrenheit = 98.6
print(temp._celsius)
# 37.0
# calling the getter
print(temp.fahrenheit)
# 98.6
temp.fahrenheit = "98F"
print(temp._celsius)
# TypeError: Value must be a number.
The property decorator, acting as a setter method, is useful to write clean code especially
when we want to:
• Do validation to the value of the attribute.
• Do some pre-processing. before the setting happens.
In the above example, we validated the value of the _celsius attribute. We then checked
if it’s not an integer nor float, we raise a TypeError.
Now the name of the decorator (before .setter) must be identical to the method name.
In our case, it’s fahrenheit.
Also note that the signature of the setter method needs to be (self, value). The name
of the value is arbitrary.
To conclude, you’ve seen how to write a getter and a setter method in Python classes
using the property decorator; the feature that saves us time and helps write Pythonic
and cleaner code.
15
Switch Case Replacements for Python
Many programming languages have switch-case statements.
In contrast, there are no switch-case statements in Python for versions less than 3.10.
switch is useful especially if you want to return a lot of different values based on a specific
input.
When I first started programming with C, switch statements made sense. After I
switched to Python, I found there was no such thing. I needed to find a workaround
or something to check a single expression against different things.
I first used if statements as a replacement for switch cases. It was ugly and unreadable.
Until I knew there are some other ways to do this to make my code more readable and
efficient as well.
In this section, I will show you how to replace switch statements for new Python 3.10
and older versions.
If you are using a Python version less than 3.10, you can use a dictionary as a mapper.
For example, you can use a dictionary to map numbers to their associated words like the
following:
digits_mapper = {
0: "zero",
1: "one",
2: "two",
3: "three",
4: "four",
5: "five",
6: "six",
7: "seven",
8: "eight",
9: "nine",
}
print(digits_mapper.get(5, "nothing"))
# five
In the above example, we used the get method. It returns the value associated with the
key. If the key is not in the dictionary, it will return the value specified in the second
argument.
16
Let’s see a more complex example: If you want to calculate taxes based on many countries.
Consider using dictionaries. This time you can define each country’s tax rate calculation
in a separate function. Pass that function to a dictionary like the following:
def egypt(amount):
calculate_tax = amount * 0.05 # or insert the tax equation
return calculate_tax
def palestine(amount):
calculate_tax = amount * 0.02 # or insert the tax equation
return calculate_tax
country_tax_calculate = {
"egypt": egypt,
"palestine": palestine
}
print(calculate_tax("egypt", 8000))
When you used a country_tax_calculate dictionary, it made your code more elegant.
Readable. And also more efficient.
You used that dictionary in calculate_tax() function passing the country_name key
to it.
Note that the key is a function (e.g. egypt) and the argument passed to it is the amount
value.
In 2006 in Python 3.0 version, Guido von Rossum (the creator of Python) rejected a
switch/case statement.
Until in September 2020 (Python 3.10) he and Brandt Bucher (a software engineer at
Microsoft) introduced a structural pattern matching.
This was introduced using a match-case statement; the first-class implementation for a
switch case in Python.
Let’s look at the following snippet in this file match.py:
def f(digit):
match digit:
case 1:
return "one"
case 2:
return "two"
case 3:
17
return "three"
case 4:
return "four"
case 5:
return "five"
case 6:
return "six"
case 7:
return "seven"
case 8:
return "eight"
case 9:
return "nine"
case _:
return "not existing" # the default case when a digit is not found
print(f(5))
# five
print(f(10))
# not existing
Pattern matching is very efficient. In fact, it is more efficient than the workaround in the
previous section.
To test this out, create a Python 3.10 environment and run the following commands with
conda:
$ conda create -n py3.10 python=3.10
$ conda activate py3.10
$ python match.py
18
Iterators and Generators
Iterators
Iterators are objects that can be used to iterate over a sequence of items. It uses the
__next__ method to get the next item in the sequence. When you iterate over that object
through a for loop or list comprehension for example, that __next__ method is called in
the background.
Let’s see how to create an iterator:
class MultiplyByTwo:
def __init__(self, number):
self.number = number
self.count = 0
def __next__(self):
self.count += 1
return self.number * self.count
mul = MultiplyByTwo(2)
print(next(mul)) # 2
print(next(mul)) # 4
print(next(mul)) # 6
The MultiplyByTwo class is an iterator. As you can see, it uses __next__ method. This
specific object uses a counter to keep track of the number of times the __next__ method
is called.
We’ve mentioned that iterators are used in for loops. Let’s see how to iterate over that
object:
mul = MultiplyByTwo(2)
for i in mul:
print(i)
TypeError: 'MultiplyByTwo' object is not iterable
This TypeError indicates that the MultiplyByTwo object being an iterator doesn’t mean
it’s iterable. That’s because it is doesn’t implement the __iter__ method.
What’s an iterable? In Python: strings, lists, files, dictionaries, sets, tuples, and genera-
tors are iterables.
So let’s modify the MultiplyByTwo class to implement the __iter__ method:
class MultiplyByTwo:
def __init__(self, number):
self.number = number
self.count = 0
19
def __iter__(self):
return self
def __next__(self):
self.count += 1
return self.number * self.count
mul = MultiplyByTwo(2)
for i in mul:
print(i)
When you run this loop, it will print out the numbers 2, 4, 6, 8, 10, etc. It’s unstoppable.
If you run this code from the terminal, you need to use Ctrl+C to stop the loop.
You might need that infinite loop for some purposes. But to make this iterable have a
limit, let’s modify the class to the following:
class MultiplyByTwo:
def __init__(self, number, limit=None):
self.number = number
self.limit = limit
self.count = 0
def __iter__(self):
return self
def __next__(self):
self.count += 1
value = self.number * self.count
if value > self.limit:
raise StopIteration
else:
return value
for i in mul:
print(i)
Now, the loop will stop when it reaches the limit. That’s because it raises the
StopIteration exception when the __next__ method reaches that limit. 10 in this
example. It is automatically handled by Python and exits the loop.
20
Generators
Generators are function-like things that return iterator objects, so you can use them
instead of defining classes. It’s preferred to use generators to generate a large set of data.
You can create generators using the yield keyword.
Python wiki says “Generator functions allow you to declare a function that behaves like
an iterator, i.e. it can be used in a for loop.” and “Python provides generator functions
as a convenient shortcut to building iterators.”
Generators are iterables like lists. But unlike iterables we know, generators are lazy which
means they produce items one at a time.
They are much more memory efficient when dealing with a large data set than any other
data structure.
def multiply_generator(number, limit):
count = 1
value = number * count
mul = multiply_generator(2, 4)
for i in mul:
print(i)
yield makes the function a generator. When that generator function is called, it returns
a generator object. When calling next(generator) or generator.__next__() (both
are equivalent), then that execution runs until the first yield yields (or gives) the given
value. Calling next again makes it run until the following yield and so on, until there
is no more yield.
It pauses the execution of the function until it is called again in the next iteration.
Use iterators when it comes to parsing large data in a file like CSV. Let’s see an example:
import csv
sum_data = 0
csv_data = []
with open("numbers.csv", "r") as f:
data = csv.reader(f)
csv_data.extend((data))
21
for row in csv_data[1:]:
sum_data += sum(map(int, row))
print(sum_data)
In this example you sum up the numbers in the CSV file. Here is a CSV example file:
x,y
12,2,5
2,4,5
4,9,7
So you get the result 50 which is the sum of all the numbers in that file. How did you
do that? we loaded the CSV file at once in memory and then iterated over it. That’s
not the best way to do it, especially when it comes to a large dataset. It’s very memory
intensive when you load the data in memory in one shot.
To fix that, you need to use iterators.
import csv
sum_data = 0
csv_data = []
with open("numbers.csv", "r") as f:
data = csv.reader(f)
for row in list(data)[1:]:
sum_data += sum(map(int, row))
print(sum_data)
So directly you get the data from the CSV file and then sum up the numbers row by row.
22
Conclusion
Writing clean code is an intense subject and this ebook is just scratching the surface.
These are just 5 simple examples of how to make your code cleaner when you write
Python code.
You now know how to use different virtual environments. You can use a dictionary as a
lookup table. You can use the property decorator to make your class properties. You
can use alternatives to the switch statement. You know how and when to use iterators
and generators.
I hope that my code examples are useful to you. If you have any questions, please feel
free to contact me.
Email: [email protected]
and feel free to follow me:
• Blog: https://ezzeddinabdullah.com
• Medium: https://ezzeddinabdullah.medium.com
• LinkedIn: https://linkedin.com/in/ezzeddinabdullah
• Twitter: https://twitter.com/EzzEddinAbdulah
23