Pythonprogrammingquickguide
Pythonprogrammingquickguide
Pythonprogrammingquickguide
Fatos Morina
© 2022 Fatos Morina
Contents
Introduction to Python . . . . . . . . . . . . . . . . . . . . . 1
The Basics . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
Comments . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
print() method . . . . . . . . . . . . . . . . . . . . . . . . 3
Operators . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
Arithmetic operators . . . . . . . . . . . . . . . . . . . . . 4
Assignment operators . . . . . . . . . . . . . . . . . . . . 5
Comparison operators . . . . . . . . . . . . . . . . . . . . 9
Data types . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
Variables . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
Numbers . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
Strings . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
Lists . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
Tuples . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
Dictionaries: Key-Value Data Structures . . . . . . . . . . 45
Sets . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52
Type Conversions . . . . . . . . . . . . . . . . . . . . . . . . 57
Conversions between primitive types . . . . . . . . . . . 57
Other Conversions . . . . . . . . . . . . . . . . . . . . . . 58
Wrap Up . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63
Control Flow . . . . . . . . . . . . . . . . . . . . . . . . . . . 64
CONTENTS
Wrap Up . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71
Functions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72
Default arguments . . . . . . . . . . . . . . . . . . . . . . 74
Keyword argument list . . . . . . . . . . . . . . . . . . . 75
Data lifecycle . . . . . . . . . . . . . . . . . . . . . . . . . 76
Changing data inside functions . . . . . . . . . . . . . . . 77
Lambda functions . . . . . . . . . . . . . . . . . . . . . . 79
Decorators . . . . . . . . . . . . . . . . . . . . . . . . . . . 84
Wrap up . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91
Importing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 131
Limiting parts that we want to import . . . . . . . . . . . 132
Importing everything . . . . . . . . . . . . . . . . . . . . 133
Exceptions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 134
Common Exceptions . . . . . . . . . . . . . . . . . . . . . 134
Handling exceptions . . . . . . . . . . . . . . . . . . . . . 135
Afterword . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 141
Introduction to Python
Python is one of the most popular programming languages that was
created in 1991 by Guido van Rossum.
According to Guido van Rossum, Python is a:
It represents one of the easiest languages that you can learn and also
work with. It can be used for software development, server side of
web development, machine learning, mathematics, and any type of
scripting that you can think of.
The good thing about it is that it is widely used and accepted
through many companies and academic institutions, making it a
really good choice especially if you are just starting your coding
journey. Moreover, it has a large community of developers that use
it and are willing to help. This community has already published
many open source libraries that you can start using. They also
actively keep improving them.
Its syntax is quite similar to that of the English language, making
it easier for you to understand and use it quite intuitively.
Python runs on an interpreter system, meaning that you are not
expected to wait for a compiler to compile the code and then
execute it. You can instead build prototypes fast.
It works on different platforms, such as Windows, Linux, Mac,
Rapsberry Pi, etc.
The Basics
In comparison to other languages, Python places a special impor-
tance on indentations.
In other programming languages, white spaces and indentations are
only used to make the code more readable and prettier, whereas in
Python they represent a sub-block of code.
The following code is not going to work, since the second line is
not properly intended:
This may look hard to understand, but you can have code editors
that highlight such syntax errors quite vividly. Moreover, the more
Python code you write, the easier it gets for you to consider such
indentations as second nature.
Comments
We use comments to specify parts that the program should be
simply ignored and not executed by the Python interpreter. This
means that everything written inside a comment is not taken into
The Basics 3
consideration at all and you can write in any language that you
want, including your own native language.
We can start comments with the symbol # in front of the line:
1 """
2 This is a comment in Python.
3 Here we are typing in another line and we are still insid\
4 e the same comment block.
5
6 In the previous line we have a space, because it is allow\
7 ed to have spaces inside comments.
8
9 Let us end this comment right here.
10 """
print() method
We are going to use the method print() since it helps us see results
in the console.
You do not need to know how it works behind the scenes or even
know what a method is right now. Just think of it as a way for us
to display results from our code in the console.
Operators
Arithmetic operators
Even though you may pull out your phone from your pocket and
do some calculations, you should also get used from now on to
implement some arithmetic operators inside Python.
When we want to add two numbers, we use the plus sign, just like
in Math:
1 print(50 + 4) # 54
1 print(50 - 4) # 46
1 print(50 * 4) # 200
1 print(50 / 4) # 12.5
2 print(8 / 4) # 2.0
1 print(50 // 4) # 12
2 print(8 / 4) # 2
1 print(50 % 4) # 2
1 print(50 % 2) # 0
2 # Since the remainder is 0, this number is even
3
4 print(51 % 2) # 1
5 #Since the remainder is 1, this number is odd
1 print(2 ** 3) # 8
2 # This is a short way of writing 2 * 2 * 2
3
4 print(5 ** 4) # 625
5 # This is a short way of writing 5 * 5 * 5 * 5
Assignment operators
These operators are used to assign values to variables.
When we declare a variable, we use the equal sign:
Operators 6
1 name = "Fatos"
2 age = 28
We can use this way to also swap values between variables, for
example, let us say that we have two variables a and b and want to
switch their values.
One logical way to do that would be to introduce a third variable
that serves as a temporary variable:
1 a, b = 1, 2
2
3 print(a) # 1
4 print(b) # 2
5
6 c = a
7 a = b
8 b = c
9
10 print(a) # 2
11 print(b) # 1
1 a, b = 1, 2
2
3 print(a) # 1
4 print(b) # 2
5
6 b, a = a, b
7
8 print(a) # 2
9 print(b) # 1
1 total_sum = 20
2
3 current_sum = 10
This may look as not accurate, since the right hand side is not equal
to the left hand side. However, in this case we are simply doing an
assignment and not a comparison of both sides of the equation.
To do this quickly, we can use the following form:
Operators 8
1 total_sum += current_sum
2
3 print(total_sum) # 30
• Subtraction:
1 result = 3
2 number = 4
3
4 result -= number # This is equal to result = result - nu\
5 mber
6
7 print(result) # -1
• Multiplication:
1 product = 3
2 number = 4
3
4 product *= number # This is equal to product = product *\
5 number
6
7 print(product) # 12
• Division:
Operators 9
1 result = 8
2 number = 4
3
4 result *= number # This is equal to result = result / nu\
5 mber
6
7 print(result) # 2.0
• Modulo operator:
1 result = 8
2 number = 4
3
4 result %= number # This is equal to result = result % nu\
5 mber
6
7 print(result) # 0
• Power operator:
1 result = 2
2 number = 4
3
4 result **= number # This is equal to result = result ** \
5 number
6
7 print(result) # 16
Comparison operators
We have learned in elementary school to do comparisons of num-
bers, such as checking whether a specific number is larger than
another number, or whether they are equal.
We can use almost the same operators in Python to do such
comparisons.
Operators 10
Equalities
Checking whether two numbers are equal can be done using the
==:
1 print(2 == 3) # False
1 print(2 != 3) # True
Inequalities
This is something that you should already know from your math
classes.
When trying to check whether a number is greater than or equal to
another number, we need to use this operator >=:
Operators 11
Logical operators
This is False since 0 is not greater than 2 and also 0 is not greater
than 1. Hence, the whole expression is False.
Data types
Variables
Variables can be considered as the building blocks of any computer
program that you can think of.
They can be used to store values and then be reused as many times
as you want. The moment you want to change their value, you can
just change it in one place and that new value that you just changed
is going to get reflected everywhere else where this variable is used.
Every variable in Python is an object.
A variable is created in the moment it is initialized with a value.
Here are the general rules for Python variables:
1 age = 28
1 age = 28
2 salary = 10000
We can use pretty much any name that we want, but it is a better
practice to use names that can be well understood both by you and
other work colleagues who work with you.
We have other variable types in Python, such as float numbers,
strings, and boolean values. We can even create our own custom
types.
Let us see an example of a variable that holds a float number:
1 height = 3.5
As you can see, this initialization is quite similar to the ones when
we had integer numbers. Here we are only changing the value on
the right and Python interpreter is smart enough to know that we
are dealing with another type of variable, namely a float type of
variable.
Let us see an example of a string:
1 reader = "Fatos"
1 text_visibile = False
1 is_greater = 5 > 6
This variable is going to get initialized with the value False since 5
is lower than 6.
Numbers
We have three numeric types in Python: integers, floats and com-
plex numbers.
Integers
Integers represent whole numbers that can be both positive and
negative and do not contain any decimal part.
Here are a few examples of integers: 1, 3000, -31234, etc.
When adding, subtracting, or multiplying two integers, we get an
integer as a result in the end.
1 print(3 + 5) # 8
2 print(3 - 5) # -2
3 print(3 * 5) # 15
Boolean
Boolean type represents truth values True and False. We include the
explanation of this type inside this Numbers section, since booleans
are indeed subtypes of the integer type.
More specifically, almost always a False value can be considered as
a 0, whereas a True value can be considered as a 1.
As such, we can also do arithmetic operations with them:
1 print(True * 5) # 5
2 print(False * 500) # 0, since False is equal to 0
Floats
Float numbers are numbers that contain the decimal part, such as
-3.14, 12.031, 9,3124, etc.
1 ten = float(10)
2
3 print(ten) # 10.0
1 print(3.4 * 2) # 6.8
2 print(3.4 + 2) # 5.4
3 print(3.4 - 2) # 1.4
4 print(2.1 * 3.4) # 7.14
Complex numbers
Complex numbers have both the real and the imaginary part that
we write in the following way:
1 complex_number = 1 + 5j
2
3 print(complex_number) # (1+5j)
Strings
Strings represent characters that are enclosed in either single quotes
or double quotes and both of them are treated the same:
1 string = "Word"
1 string = "Word"
2
3 print(string[0]) # W
4 print(string[1]) # o
5 print(string[2]) # r
6 print(string[3]) # d
1 print(string[-1]) # d
2 print(string[-2]) # r
3 print(string[-3]) # o
4 print(string[-4]) # W
We can also do slicing and include only a portion of the string and
not the entire string, for example, if we want to get characters that
start from a specific index until a specific index, we should write it
in the following way: string[start_index:end_index], excluding
the character at index end_index:
1 string = "Word"
2
3 print(string[0:3]) # Wor
1 string = "Word"
2
3 print(string[2:]) # rd
1 string = "Word"
2
3 print(string[:2]) # Wo
1 string = "Word"
2 string[0] = "A"
String operators
1 first = "First"
2 second = "Second"
3
4 concantenated_version = first + " " + second
5
6 print(concatenated_version) # First Second
1 string = "Abc"
2
3 repeated_version = string * 5
4
5 print(repeated_version) # AbcAbcAbcAbcAbc
len()
replace()
1 string = "Abc"
2
3 modified_version = string.replace("A", "Z")
4
5 print(modified_version) # Zbc
strip()
split()
join()
count()
find()
lower()
upper()
capitalize()
title()
isupper()
islower()
isalpha()
1 string = "A1"
2 another_string = "aA"
3
4 print(string.isalpha()) # False, since it contains 1
5 print(another_string.isalpha()) # True since both `a` an\
6 d `A` are letters of the alphabet
isdecimal()
1 string = "A1"
2 another_string = "3.31"
3 yet_another_string = "3431"
4
5 print(string.isdecimal()) # False, since it contains 'A'
6 print(another_string.isdecimal()) # False, since it cont\
7 ains '.'
8 print(yet_another_string.isdecimal()) # True, since it c\
9 ontains only numbers
Formatting
This now looks great, but I am not the only person who uses it,
right?
I am just one of the people who gets to use it.
Now if someone comes and signs in, I will have to use their own
names, such as:
This is now going to put the first parameter of the method format()
inside the first curly braces, which in our case is Fatos. Then, in the
second occurrence of the curly braces, it is going to put the second
parameter of the method format().
If we try to print the value of the string, we should get the following:
1 print(greeting)
2 # Good morning Fatos. Today is Friday.
We can specify parameters with indexes inside curly braces like the
following that can then be used:
We can also specify parameters inside the format() method and use
those specific words inside curly braces as a reference:
1 first_name = "Fatos"
2 day_of_the_week = "Friday"
3 continent = "Europe"
4
5 greeting = f'Good morning {first_name}. Today is {day_of_\
6 the_week}'
7
8 print(greeting) # Good morning Fatos. Today is Friday.
1 continent = "Europe"
2
3 i_am_here = F'''I am in {continent}'''
4
5 print(i_am_here) # I am in Europe
Lists
If you take a look at a bookshelf, you can see that they are
stacked and put closely together. You can see that there are many
examples of collecting, and structuring elements in some way.
This is also quite important in computer programming. We cannot
just continue declaring countless variables and manage them that
easily.
Let us say that we have a class of students and want to save their
names. We can start saving their names according to the way they
are positioned in the classroom:
1 first = "Albert"
2 second = "Besart"
3 third = "Fisnik"
4 fourth = "Festim"
5 fifth = "Gazmend"
The list can keep on going and it is also quite hard for us to keep
track of all of them.
There is fortunately an easier way for us to put these in a collection
in Python called list.
Let us create a list called students and store in them all the names
declared in the previous code block:
Data types 31
1 students[0]
1 students[1]
As you can probably get the gist, we simply need to write the name
of the list and also the corresponding index of the element that we
want to get in the square brackets.
Data types 32
This list is of course not static. We can add elements to it, like when
a new student joins the class.
Let us add a new element in the list students with the value Besfort:
1 students.append("Besfort")
1 students[0] = "Besim"
Slicing
1 my_list = [1, 2, 3, 4, 5]
2
3 print(my_list[0:3]) # [1, 2, 3]
Data types 33
1 my_list = [1, 2, 3, 4, 5]
2
3 print(my_list[3:]) # [4, 5]
1 my_list = [1, 2, 3, 4, 5]
2
3 print(my_list[:3]) # [1, 2, 3]
1 string = "String"
2 string[0] = "B"
Now if we have a list and want to modify its first element, then we
can successfully do so:
Data types 34
1 first_list = [1, 2, 3]
2
3 second_list = [4, 5]
4
5 first_list = first_list + second_list
6
7 print(first_list) # [1, 2, 3, 4, 5]
These lists do not need even have to have the same length.
To access elements of a list which is inside a list we need to use
double indexes.
Let us see how we can access the element math_points inside the
subjects list. Since math_points is an element in the subjects list
positioned at index 0, we simply need to do the following:
Data types 35
Now let us assume that we want to access Math inside subjects list.
Since Math is at index 1, we are going to need to use the following
double indexes:
1 print(subjects[0][1]) # 'Math'
List methods
Adding elements
We can delete elements from lists using the pop() method, which
removes the last element in the list:
1 my_list = [1, 2, 3, 4, 5]
2
3 my_list.pop() # removes 5 from the list
4
5 print(my_list) # [1, 2, 3, 4]
6
7 my_list.pop() # removes 4 from the list
8
9 print(my_list) # [1, 2, 3]
We can also specify the index of an element in the list that indicates
which element in the list we should delete:
Data types 37
1 my_list = [1, 2, 3, 4, 5]
2
3 my_list.pop(0) # Delete the element at index 0
4
5 print(my_list) # [2, 3, 4, 5]
We can also delete elements from lists using del statement and then
specifying the value of the element that we want to delete:
1 my_list = [1, 2, 3, 4, 1]
2
3 del my_list[0] # Delete element my_list[0]
4
5 print(my_list) # [2, 3, 4, 5]
1 my_list = [1, 2, 3, 4, 1]
2
3 del my_list[0:3] # Delete elements: my_list[0], my_list[\
4 1], my_list[2]
5
6 print(my_list) # [4, 1]
1 my_list = [1, 2, 3, 4]
2
3 my_list.remove(3)
4
5 print(my_list) # [1, 2, 4]
1 my_list = [1, 2, 3, 4]
2
3 my_list.reverse()
4
5 print(my_list) # [4, 3, 2, 1]
Index search
Membership
This is quite intuitive and related to real life: We get to ask ourselves
whether something is part of something or not.
- Is my phone in my pocket or bag?
Since y is not a vowel and not included in the declared array, the
expression in the second line of the previous code snippet is going
to result to False.
Similarly, we can also check whether something is not included
using not in:
1 odd_numbers = [1, 3, 5, 7]
2 print(2 not in odd_numbers) # True
Sorting
Sorting elements in a list is something that you may need to do from
time to time. sort() is a built-in method that makes it possible for
you to sort elements in a list in an ascending order alphabetically
or numerically:
1 my_list = [3, 1, 2, 4, 5, 0]
2
3 my_list.sort()
4
5 print(my_list) # [0, 1, 2, 3, 4, 5]
6
7 alphabetical_list = ['a', 'c', 'b', 'z', 'e', 'd']
8
9 alphabetical_list.sort()
10
11 print(alphabetical_list) # ['a', 'b', 'c', 'd', 'e', 'z']
There are other methods of lists that we have not included in here.
Data types 40
List comprehension
As you can see, this is much shorter and should take less time to
write.
We can also use list comprehensions with multiple lists as well.
Let us take an example where we want to add each element of a list
with each element in another list:
Data types 42
1 first_list = [1, 2, 3]
2 second_list = [50]
3
4 double_lists = [first_element +
5 second_element for first_element in first\
6 _list for second_element in second_list]
7
8 print(double_lists) # [51, 52, 53]
In the end, we are going to get a resulting list that has the same
number of elements as the list with the longest length.
Tuples
Tuples are collections that are ordered and immutable, meaning
that their content cannot be changed. They are ordered and their
elements can be accessed using indexes.
Let us start with our first tuple:
All the indexing and slicing operations that we have seen in the
chapter of lists apply for tuples as well:
Data types 43
1 print(len(vehicles)) # 4
2
3 print(vehicles[3]) # Tablet
4
5 print(vehicles[:3]) # ('Computer', 'Smartphone', 'Smart \
6 watch')
1 print(vehicles.index('tablet')) # 3
Membership check
We can check whether an element is part of a tuple using operators
in and not in just like with lists:
Data types 44
Nesting 2 tuples
Immutability
Since tuples are immutable, they cannot be changed after they are
created. This means that we cannot add, or delete elements in them,
or append a tuple to another tuple.
Data types 45
instead.
For dictionaries, we use curly braces and have each element that
has two parts: the key and the value. In our previous example, the
key was the German word, whereas the value was its translation in
English, as can be seen in the following example:
1 german_to_english_dictionary = {
2 "Wasser": "Water",
3 "Brot": "Bread",
4 "Milch": "Milk"
5 }
1 brot_translation = german_to_english_dictionary["Brot"]
2 print(translation) # Bread
When we print the value that we get, we are going to get the
translation in English.
Similarly, we can get the English translation of the word Milch by
getting the value of the element that has Milch as the key:
1 milch_translation = german_to_english_dictionary["Milch"]
2 print(milch_translation) # Milk
1 german_to_english_dictionary.get("Wasser")
1 words = dict([
2 ('abandon', 'to give up to someone or something on th\
3 e ground'),
4 ('abase', 'to lower in rank, office, or esteem'),
5 ('abash', 'to destroy the self-possession or self-con\
6 fidence of')
7 ])
8
9 print(words)
10 # {'abandon': 'to give up to someone or something on the \
11 ground', 'abase': 'to lower in rank, office, or esteem', \
12 'abash': 'to destroy the self-possession or self-confiden\
13 ce of'}
1 words = {
2 'a': 'alfa',
3 'b': 'beta',
4 'd': 'delta',
5 }
6
7 words['g'] = 'gama'
8
9 print(words)
10 # {'a': 'alfa', 'b': 'beta', 'd': 'delta', 'g': 'gama'}
Data types 48
1 words = {
2 'a': 'alfa',
3 'b': 'beta',
4 'd': 'delta',
5 }
6
7 words['b'] = 'bravo'
8
9
10 print(words)
11 # {'a': 'alfa', 'b': 'bravo', 'd': 'delta'}
Removing elements
1 words = {
2 'a': 'alfa',
3 'b': 'beta',
4 'd': 'delta',
5 }
6
7 words.pop('a')
8
9 print(words) # {'b': 'beta', 'd': 'delta'}
We can also delete values using popitem() which removes the last
inserted key-value pair starting from Python 3.7. In earlier versions,
it deletes a random pair:
Data types 49
1 words = {
2 'a': 'alfa',
3 'b': 'beta',
4 'd': 'delta',
5 }
6
7 words['g'] = 'gamma'
8
9 words.popitem()
10
11 print(words)
12 # {'a': 'alfa', 'b': 'beta', 'd': 'delta'}
1 words = {
2 'a': 'alfa',
3 'b': 'beta',
4 'd': 'delta',
5 }
6
7 del words['b']
8
9 print(words) # {'a': 'alfa', 'd': 'delta'}
Length
We can get the length of a dictionary using len() just like with lists
and tuples:
Data types 50
1 words = {
2 'a': 'alfa',
3 'b': 'beta',
4 'd': 'delta',
5 }
6
7 print(len(words)) # 3
Membership
1 words = {
2 'a': 'alfa',
3 'b': 'beta',
4 'd': 'delta',
5 }
6
7 print('a' in words) # True
8 print('z' not in words) # True
Comprehension
1 points = {
2 'Festim': 50,
3 'Zgjim': 89,
4 'Durim': 73
5 }
6
7 elements = points.items()
8
9 print(elements) # dict_items([('Festim', 50), ('Zgjim', 8\
10 9), ('Durim', 73)])
1 points = {
2 'Festim': 50,
3 'Zgjim': 89,
4 'Durim': 73
5 }
6
7 elements = points.items()
8
9 points_modified = {key: value + 10 for (key, value) in el\
10 ements}
11
12 print(points_modified) # {'Festim': 60, 'Zgjim': 99, 'Du\
13 rim': 83}
Data types 52
Sets
Sets are unordered and unindexed collections of data. Since ele-
ments in sets are not ordered, we cannot access elements using
indexes or using the method get().
We can add tuples, but we cannot add dictionaries or lists in a set.
We cannot add duplicate elements in sets. This means that when we
want to remove duplicate elements from another type of collection,
we can make use of this uniqueness in sets.
Let us start creating our first set using curly brackets as follows:
1 first_set = {1, 2, 3}
Like all data structures, we can find the length of a set using the
method len():
1 print(len(first_set)) # 3
Adding elements
Adding one element in a set can be done using the method add():
Data types 53
1 my_set = {1, 2, 3}
2
3 my_set.add(4)
4
5 print(my_set) # {1, 2, 3, 4}
1 my_set = {1, 2, 3}
2
3 my_set.update([4, 5, 6])
4
5 print(my_set) # {1, 2, 3, 4, 5, 6}
6
7 my_set.update("ABC")
8
9 print(my_set) # {1, 2, 3, 4, 5, 6, 'A', 'C', 'B'}
Deleting elements
1 my_set = {1, 2, 3}
2
3 my_set.remove(2)
4
5 print(my_set) # {1, 3}
1 my_set = {1, 2, 3}
2
3 my_set.remove(4)
4
5 print(my_set) # KeyError: 4
1 my_set = {1, 2, 3}
2
3 my_set.discard(4)
4
5 print(my_set) # {1, 2, 3}
Union
1 first_set = {1, 2}
2 second_set = {2, 3, 4}
3
4 union_set = first_set.union(second_set)
5
6 print(union_set) # {1, 2, 3, 4}
Data types 55
Intersection
1 first_set = {1, 2}
2 second_set = {2, 3, 4}
3
4 intersection_set = first_set.intersection(second_set)
5
6 print(union_set) # {2}
Difference
1 first_set = {1, 2}
2 second_set = {2, 3, 4}
3
4 difference_set = first_set.difference(second_set)
5
6 print(difference_set) # {1}
1 first_set = {1, 2}
2 second_set = {2, 3, 4}
3
4 first_difference_set = first_set.difference(second_set)
5
6 print(first_difference_set) # {1}
7
8 second_difference_set = second_set.difference(first_set)
9
10 print(second_difference_set) # {3, 4}
Type Conversions
Conversions between primitive
types
Python is an object oriented programming. That is why it uses
constructor functions of classes to do conversions from one type
into another.
int()
float()
1 int_literal = float(5)
2 print(int_literal) # 5.0
3
4 float_literal = float(1.618)
5 print(float_literal) # 1.618
6
7 string_int = float("40")
8 print(string_int) # 40.0
9
10 string_float = float("37.2")
11 print(string_float) # 37.2
str()
1 int_to_string = str(3)
2 print(int_to_string) # '3'
3
4 float_to_string = str(3.14)
5 print(float_to_string) # '3.14'
6
7 string_to_string = str('hello')
8 print(string_to_string) # 'hello'
Other Conversions
The way we can convert from one type of data structure into
another type is the following:
1 destination_type(input_type)
Conversions to lists
We can convert a set, tuple, or dictionary into a list using the list()
constructor.
When converting a dictionary into a list, only its keys are going to
make it into a list:
1 books_dict = {'1': 'Book 1', '2': 'Book 2', '3': 'Book 3'}
2 dict_to_list = list(books_dict) # Converting dict to list
3 print(dict_to_list) # ['1', '2', '3']
1 books_dict = {'1': 'Book 1', '2': 'Book 2', '3': 'Book 3'}
2
3 dict_to_list = list(books_dict.items()) # Converting dic\
4 t to list
5
6 print(dict_to_list)
7 # [('1', 'Book 1'), ('2', 'Book 2'), ('3', 'Book 3')]
Type Conversions 60
Conversions to tuples
Conversions to sets
Conversions to dictionaries
After we run the last code block, we are going to get an error:
Type Conversions 63
Wrap Up
In conclusion, Python has a variety of data types that you can use
to store data. These data types are important to know so that you
can choose the right one for your needs. Be sure to use the right
data type for the task that is in front of you to avoid errors and
optimize performance.
Control Flow
Conditional statements
When you think about ways we think and also communicate with
each other, you may get the impression that we are indeed always
using conditions.
1 if number > 0:
2 print("The given number is positive")
3 else:
4 print("The given number is not positive")
We can also add additional conditions and not just 2 like above
by using elif which is evaluated when the if expression is not
evaluated.
Let us see an example to make it easier for you to understand it:
1 if number > 0:
2 print("The given number is positive")
3 elif number == 0:
4 print("The given number is 0")
5 else:
6 print("The given number is negative")
Looping / Iterator
for loop
while loop
Let us now describe iterations with while. This is also another way
of doing iterations that is also quite straightforward and intuitive.
Here we need to specify a starting condition before the while block
and also update the condition accordingly. The while loop needs
a “loop condition.” If it stays True, it continues iterating. In this
example, when num is 11 the loop condition equals False.
Control Flow 67
1 number = 1
2
3 while number < 7:
4 print(number)
5 number += 1 # This part is necessary for us to add s\
6 o that the iteration does not last forever
This while block is going to print the same statements as the code
we used with the for block.
Now that we have covered both iteration and lists, we can jump
into ways of iterating through lists.
We do not just store things into data structures and leave them
there for ages. We are supposed to be able to use those elements
in different scenarios.
Let us take our list of students from before:
Yes, it’s that simple. We are iterating through each element in the
list and printing their values.
We can do this for dictionaries as well. However, since elements in
dictionaries have 2 parts (key and the value), we need to specify
both the key and the value as follows:
Control Flow 68
1 german_to_english_dictionary = {
2 "Wasser": "Water",
3 "Brot": "Bread",
4 "Milch": "Milk"
5 }
6
7 for key, value in german_to_english_dictionary:
8 print("The German word " + key + " means " + value + \
9 " in English")
We can also get only keys from the elements of the dictionary:
Note that key and value are simply variable names that we have
chosen to illustrate the iteration, but we can use any name that we
want for our variables, such as the following example:
This iteration is going to print the same thing in the console as the
code block before the last one.
We can also have nested for loops, for example, let us say that we
want to iterate through a list of numbers and finding a sum of each
element with each other element of a list. We can do that using
nested for loops:
Control Flow 69
1 numbers = [1, 2, 3]
2 sum_of_numbers = [] # Empty list
3
4 for first_number in numbers:
5 for second_number in numbers: # Loop through the lis\
6 t and add the numbers
7 current_sum = first_number + second_number
8 # add current first_number from the first_list to\
9 the second_number from the second_list
10 sum_of_numbers.append(current_sum)
11
12
13 print(sum_of_numbers)
14 # [2, 3, 4, 3, 4, 5, 4, 5, 6]
Stopping a for-loop
Sometimes we may need to exit a for loop before it reaches the end.
This may be the case when a condition has been met or we have
found what we were looking for and there is no need to continue
any further.
In those situations, we can use break to stop any other iteration of
the for loop.
Let us assume that we want to check whether there is a negative
number in a list. In case we find that positive number, we stop
searching for it.
Let us implement this using break:
Control Flow 70
As we can see, the moment we reach -3, we break from the for loop
and stop.
Skipping an iteration
1 my_sum = 0
2 my_list = [1, 2, -3, 4, 0]
3
4 for element in my_list:
5 if element < 0: # Do not include negative numbers in\
6 the sum
7 continue
8 my_sum += element
9
10 print(my_sum) # 7
Control Flow 71
1 my_list = [1, 2, 3]
2
3 for element in my_list:
4 pass # Do nothing
Wrap Up
In conclusion, Python offers conditional statements to help you
control the flow of your program. The if statement lets you run a
block of code only if a certain condition is met. The elif statement
lets you run a block of code only if another condition is met. And
the else statement lets you run a block of code only if no other
condition is met. These statements are very useful for controlling
the flow of your program.
Functions
There can be plenty of cases when we need to use the same code
block again and again. Our first guess would be to write it as many
times as we want.
Objectively, it does work, but the truth is, this is a really bad
practice. We are doing repetitive work that can be quite boring and
also prone to many mistakes that can be overlooked.
This is the reason why we need to start using code blocks that we
can define once and use that same code of instructions anywhere
else.
Just think about this in real life: You see a YouTube video that has
been recorded and uploaded to YouTube once. It is then going to
be watched by many other people, but the video still remains the
same one that was uploaded initially.
In other words, we use methods as a representative of a set of coding
instructions that are then supposed to be called anywhere else in
the code and that we do not have to write it repeatedly. In cases
when we want to modify this method, we simply change it at the
place where it was first declared and other places where it is called
do not have to do anything.
To define a method in Python, we start by using the def keyword,
then the name of the function and then a list of arguments that we
expect to be used. After that, we need to start writing the body of
the method in a new line after an indentation.
As you can see from the coloring, both defand return are keywords
in Python that you cannot use to name your variables.
Now, everywhere we want this add() to be called, we can just call
it there and not have to worry about implementing it entirely.
Since we have defined this method, we can call it in the following
way:
1 result = add(1, 5)
2
3 print(result) # 6
You can get the impression that this is such a simple method and
start asking, why are we even bothering to write a method for it?
You are right. This was a very simple method just to get you
introduced to the way we can implement functions.
Let us write a function that finds the sum of numbers that are
between two specified numbers:
This is now a set of instructions that you can call in other places
and do not have to write all of it again.
Functions 74
1 result = sum_in_range(1, 5)
2
3 print(result) # 10
Note that functions define a scope such that variables that are
defined in there are not accessible outside. For example, we cannot
access the variable named product outside the scope of the function:
Default arguments
When we call functions, we may make some of the arguments
optional by writing an initial value for them at the header of the
function.
Let us take an example of getting a user’s first name as a required
argument and let the second argument be an optional one.
We can now call that same function even though the the second
argument is not specified:
1 user = get_user("Durim")
2
3 print(user) # Hi Durim
Data lifecycle
Variables that are declared inside a function cannot be accessed
outside it. They are isolated.
Let us see an example to illustrate this:
1 def counting():
2 count = 0 # This is not accessible outside of the fu\
3 nction.
4
5
6 counting()
7
8 print(count) # This is going to throw an error when exec\
9 uting, since count is only declared inside the function a\
10 nd is not acessible outside that
1 count = 3331
2
3
4 def counting():
5 count = 0 # This is a new variable
6
7
8 counting()
9
10 print(count) # 3331
11 # This is declared outside the function and has not been \
12 changed
16
17 print("Outside the function:", names) # Outside the func\
18 tion: ['Betim', 'Durim', 'Gezim']
1 name = "Betim"
2
3
4 def say_hello(current_param):
5 current_param = current_param + " Gashi"
6 name = current_param # name is a local variable
7 print("Value inside the function:", name)
8 return current_param
9
10
11 say_hello(name) # Value inside the function: Betim Gashi
12
13 print("Value outside the function:", name) # Value outsi\
14 de the function: Betim
1 name = "Betim"
2
3
4 def say_hello(current_param):
5 current_param = current_param + " Gashi"
6 name = current_param # name is a local variable
7 print("Value inside the function", name)
8 return current_param
9
10
11 # Here we are assigning the value of name to the current_\
12 param that is returned from the function
13 name = say_hello(name) # Value inside the function Betim\
14 Gashi
15
16 # Value outside the function: Betim Gashi
17 print("Value outside the function:", name)
Lambda functions
Lambda functions are anonymous functions that can be used to re-
turn an output. We can write lambda functions using the following
syntax pattern:
Note that we cannot use the if clause without the else clause inside
a lambda function.
At this point, you may wonder, why do we need to use lambda
functions, since they seem to be almost the same as other functions.
We can see that illustrated in the following section.
1 def convert_to_meters(feet):
2 return feet * 0.3048
3
4
5 def convert_to_feet(meters):
6 return meters / 0.3048
7
8
9 def convert_to_miles(kilometers):
10 return kilometers / 1.609344
11
12
13 def convert_to_kilometers(miles):
14 return miles * 1.609344
We can now make use of lambdas and make this type of abstraction
much simpler.
Instead of writing all those four functions, we can simply write a
concise lambda function and use it as a parameter when calling the
conversion() function:
map()
1 map(function_name, my_list)
1 my_list = [1, 2, 3, 4]
2
3 triple_list = [x * 3 for x in my_list]
4
5 print(triple_list) # [3, 6, 9, 12]
1 my_list = [1, 2, 3, 4]
2
3 triple_list = map(lambda x: x * 3, my_list)
4
5 print(triple_list) # [3, 6, 9, 12]
filter()
This should now give you the intuition needed on how you can call
functions with other functions as arguments and why lambdas are
useful and important.
Decorators
A decorator represents a function that accepts another function as
an argument.
We can think of it as a dynamic way of changing the way a function,
method, or class behaves without having to use subclasses.
Once a function is being passed as an argument to a decorator, it
will be modified and then returned as a new function.
Let us start with a basic function that we want to decorate:
1 def reverse_list(input_list):
2 return input_list[::-1]
1 def reverse_list(input_list):
2 return input_list[::-1]
3
4
5 def reverse_input_list(another_function, input_list):
6 # we are delegating the execution to another_function\
7 ()
8 return another_function(input_list)
9
10
11 result = reverse_input_list(reverse_list, [1, 2, 3])
12
13 print(result) # [3, 2, 1]
1 def reverse_input_list(input_list):
2 # reverse_list() is now a local function that is not \
3 accessible from the outside
4 def reverse_list(another_list):
5 return another_list[::-1]
6
7 result = reverse_list(input_list)
8 return result # Return the result of th\
9 e local function
10
11
12 result = reverse_input_list([1, 2, 3])
13 print(result) # [3, 2, 1]
1 def reverse_list_decorator(input_function):
2 def function_wrapper():
3 returned_result = input_function()
4 reversed_list = returned_result[::-1]
5 return reversed_list
6
7 return function_wrapper
1 def reverse_list_decorator(input_function):
2 def function_wrapper():
3 returned_result = input_function()
4 reversed_list = returned_result[::-1]
5 return reversed_list
6
7 return function_wrapper
8
9 # Function that we want to decorate
10 def get_list():
11 return [1, 2, 3, 4, 5]
12
13
14 # This returns a reference to the function
15 decorator = reverse_list_decorator(get_list)
16
17 # Here we call the actual function using the parenthesis
18 result_from_decorator = decorator()
19
20 # We can now print the result in the console
21 print(result_from_decorator) # [5, 4, 3, 2, 1]
1 result_from_decorator = get_list()
2
3 print(result_from_decorator) # [5, 4, 3, 2, 1]
Stacking decorators
We can also use more than one decorator for a single function.
Their order of execution starts from top to bottom, meaning that
the decorator that has been defined first is applied first, then the
second one, and so on.
Let us do a simple experiment and apply the same decorator that
we defined in the previous section twice.
Let us first understand what does that mean.
So we first call the decorator to reverse a list:
[1, 2, 3, 4, 5] to [5, 4, 3, 2, 1]
Then we apply it again, but now with the returned result from the
previous calling of the decorator:
[5, 4, 3, 2, 1] ⇒ [1, 2, 3, 4, 5]
In other words, reversing a list and then reversing that reversed list
again is going to return the original ordering of the list.
Let us see this with decorators:
1 @reverse_list_decorator
2 @reverse_list_decorator
3 def get_list():
4 return [1, 2, 3, 4, 5]
5
6
7 result = get_list()
8
9 print(result) # [1, 2, 3, 4, 5]
Functions 89
1 def positive_numbers_decorator(input_list):
2 def function_wrapper():
3 # Get only numbers larger than 0
4 numbers = [number for number in input_list() if n\
5 umber > 0]
6 return numbers
7
8 return function_wrapper
Now we can call this decorator and the other decorator that we
have implemented:
1 @positive_numbers_decorator
2 @reverse_list_decorator
3 def get_list():
4 return [1, -2, 3, -4, 5, -6, 7, -8, 9]
5
6
7 result = get_list()
8 print(result) # [9, 7, 5, 3, 1]
1 def reverse_list_decorator(input_function):
2 def function_wrapper():
3 returned_result = input_function()
4 reversed_list = returned_result[::-1] # Reverse \
5 the list
6 return reversed_list
7
8 return function_wrapper
9
10
11 # First decoorator
12 def positive_numbers_decorator(input_list):
13 def function_wrapper():
14 # Get only numbers larger than 0
15 numbers = [number for number in input_list() if n\
16 umber > 0]
17 return numbers
18
19 return function_wrapper
20
21 # Function that we want to decorate
22
23
24 @positive_numbers_decorator
25 @reverse_list_decorator
26 def get_list():
27 return [1, -2, 3, -4, 5, -6, 7, -8, 9]
28
29
30 result = get_list()
31 print(result) # [9, 7, 5, 3, 1]
1 def add_numbers_decorator(input_function):
2 def function_wrapper(a, b):
3 result = 'The sum of {} and {} is {}'.format(
4 a, b, input_function(a, b)) # calling the in\
5 put function with arguments
6 return result
7 return function_wrapper
8
9
10 @add_numbers_decorator
11 def add_numbers(a, b):
12 return a + b
13
14
15 print(add_numbers(1, 2)) # The sum of 1 and 2 is 3
Built-in decorators
Wrap up
In conclusion, Python is an excellent language for writing functions
because they are easy to write.
Lambda functions are a great way to make small, concise functions
in Python. They’re perfect for when you don’t need a full-blown
function, or when you just want to test out a snippet of code.
Python decorators are a great way to improve code readability
and maintainability. They allow you to modularize your code and
make it more organized. Additionally, they can be used to perform
Functions 92
1 class Bicycle:
2 pass
1 favorite_bike = Bicycle()
1 class Bicycle:
2 def __init__(self, manufacturer, color, is_mountain_b\
3 ike):
4 self.manufacturer = manufacturer
5 self.color = color
6 self.is_mountain_bike = is_mountain_bike
Note the usage of underscores before and after the name init of
the method. They represent indicators to the Python interpreter to
treat that method as a special method.
This is a method that does not return anything. It is a good practice
to define it as the first method of the class, so that other developers
can also see it being at a specific line.
Now, if we want to create new objects using this blueprint of
bicycles, we can simply write:
We have provided our custom parameters for this bike and are
passing them to the constructor method so that we get a new bike
with those specific attributes in return. As you can probably tell,
we are creating a grey, mountain bike of the brand Connondale.
We can also create objects from classes by using optional arguments
as follows:
1 class Bicycle:
2 # All the following attributes are optional
3 def __init__(self, manufacturer=None, color='grey', i\
4 s_mountain_bike=False):
5 self.manufacturer = manufacturer
6 self.color = color
7 self.is_mountain_bike = is_mountain_bike
Object Oriented Programming 96
Now we have just created this object with these attributes, which
are not currently accessible outside the scope of the class. This
means that we have created this new object from the Bicycle class,
but its corresponding attributes are not accessible. To access them,
we can implement methods that help us access them.
To do that, we are going to define getters and setters, which
represent methods that are used to get and set values of attributes
of objects. We are going to use an annotation called @property to
hep us with that.
Let’s see it with code:
1 class Bicycle:
2 def __init__(self, manufacturer, color, is_mountain_b\
3 ike):
4 self._manufacturer = manufacturer
5 self._color = color
6 self._is_mountain_bike = is_mountain_bike
7
8 @property
9 def manufacturer(self):
10 return self._manufacturer
11
12 @manufacturer.setter
13 def manufacturer(self, manufacturer):
14 self._manufacturer = manufacturer
15
16
17 bike = Bicycle("Connondale", "Grey", True)
18
19 print(bike.manufacturer) # Connondale
We can write getters and setters for all the attributes of the class:
Object Oriented Programming 97
1 class Bicycle:
2 def __init__(self, manufacturer, color, is_mountain_b\
3 ike):
4 self._manufacturer = manufacturer
5 self._color = color
6 self._is_mountain_bike = is_mountain_bike
7
8 @property
9 def manufacturer(self):
10 return self._manufacturer
11
12 @manufacturer.setter
13 def manufacturer(self, manufacturer):
14 self._manufacturer = manufacturer
15
16 @property
17 def color(self):
18 return self._color
19
20 @color.setter
21 def color(self, color):
22 self._color = color
23
24 @property
25 def is_mountain_bike(self):
26 return self._is_mountain_bike
27
28 @is_mountain_bike.setter
29 def is_mountain_bike(self, is_mountain_bike):
30 self.is_mountain_bike = is_mountain_bike
31
32 bike = Bicycle("Connondale", "Grey", True)
Now that we have defined them, we can call these getter methods
as attributes:
Object Oriented Programming 98
1 print(bike.manufacturer) # Connondale
2 print(bike.color) # Grey
3 print(bike.is_mountain_bike) # True
We can also modify the value that we initially used for any attribute
by simply typing the name of object and the attribute that we want
to change the content:
1 bike.is_mountain_bike = False
2 bike.color = "Blue"
3 bike.manufacturer = "Trek"
Our classes can also have other methods as well and not just getters
and setters.
Let us define a method inside the class Bicycle that we can then call
from any object that we have created from that class:
1 class Bicycle:
2 def __init__(self, manufacturer, color, is_mountain_b\
3 ike):
4 self._manufacturer = manufacturer
5 self._color = color
6 self._is_mountain_bike = is_mountain_bike
7
8 def get_description(self):
9 desc = "This is a " + self._color + " bike of the\
10 brand " + self._manufacturer
11 return desc
Methods
Methods are similar to functions that we have already covered
before.
In a nutshell, we group a few statements in a code block called
method where we perform some operations that we expect to be
done more than once and do not want to write them again and
again. In the end, we may not return any result at all.
There are three types of methods in Python:
• instance methods
• class methods
• static methods
Let us briefly talk about the overall structure of methods and then
dive a little more into details for each method type.
Parameters
self argument
1 class Bicycle:
2 def __init__(self, manufacturer, color, is_mountain_b\
3 ike):
4 self._manufacturer = manufacturer
5 self._color = color
6 self._is_mountain_bike = is_mountain_bike
7
8 def get_description(self):
9 desc = "This is a " + self._color + " bike of the\
10 brand " + self._manufacturer
11 return desc
Class methods
We have covered instance methods so far that are methods that we
can call with objects.
Class methods are methods that can be called using class names
and can be accessed without needing to create any new object at
all.
Since it is a specific type of method, we need to tell Python
interpreter that it is actually different. We do that by making a
change in the syntax.
We use the annotation @classmethod above a class method and cls
similar to the usage of self for instance methods. cls is just a
conventional way of referring to the class that is calling the method,
and not that you have to use this name.
Let us declare our first class method:
1 class Article:
2 blog = 'https://www.python.org/'
3
4 # the init method is called when an instance of the c\
5 lass is created
6 def __init__(self, title, content):
7 self.title = title
8 self.content = content
9
Object Oriented Programming 102
10 @classmethod
11 def get_blog(cls):
12 return cls.blog
Now let us call this class method that we have just declared:
1 print(Article.get_blog()) # https://www.python.org/
Note that we did not have to write any argument when calling get_-
blog() method. On the other hand, when we declare methods and
instance methods, we should always include at least one argument.
Static methods
These are methods that do not have direct relations to class vari-
ables or instance variables. They can be seen as utility function
that are supposed to help us do something with arguments that are
passed when calling them.
We can call them by using both the class name and an object that is
created by that class where this method is declared. This means that
they do not need to have their first argument related to the object or
class calling them which was the case with using parameters self
for instance methods and cls for class methods.
There is no limit to the number of arguments that we can use to
call them.
To create it, we need to use the @staticmethod annotation.
Let us create a static method:
Object Oriented Programming 103
1 class Article:
2 blog = 'https://www.python.org/'
3
4 # the init method is called when an instance of the c\
5 lass is created
6 def __init__(self, title, content):
7 self.title = title
8 self.content = content
9
10 @classmethod
11 def get_blog(cls):
12 return cls.blog
13
14 @staticmethod
15 def print_creation_date(date):
16 print(f'The blog was created on {date}')
17
18
19 article = Article('First Article', 'This is the first art\
20 icle')
21
22 # Calling the static method using the object
23 article.print_creation_date('2022-07-18') # The blog was\
24 created on 2022-07-18
25
26 # Calling the static method using the class name
27 Article.print_creation_date('2022-07-21') # The blog was\
28 created on 2022-07-21
1 class Article:
2 blog = 'https://www.python.org/'
3
4 # the init method is called when an instance of the c\
5 lass is created
6 def __init__(self, title, content):
7 self.title = title
8 self.content = content
9
10 @classmethod
11 def get_blog(cls):
12 return cls.blog
13
14 @staticmethod
15 def set_title(self, date):
16 self.title = 'A random title'
If we try to call this static method now, we are going to get an error:
Access modifier
Public attributes
Public attributes are the ones that are accessible from both inside
and outside the class.
By default, all attributes and methods are public in Python. If we
want them to be private, we need to specify that.
Let us see an example of public attributes:
1 class Bicycle:
2 def __init__(self, manufacturer, color, is_mountain_b\
3 ike):
4 self.manufacturer = manufacturer
5 self.color = color
6 self.is_mountain_bike = is_mountain_bike
7
8 def get_manufacturer(self):
9 return self.manufacturer
Private attributes
1 class Bicycle:
2 def __init__(self, manufacturer, color, is_mountain_b\
3 ike, old):
4 self.manufacturer = manufacturer
5 self.color = color
6 self.is_mountain_bike = is_mountain_bike
7 self.__old = old # This is a private property
1 class Bicycle:
2 def __init__(self, manufacturer, color, is_mountain_b\
3 ike, old):
4 self.manufacturer = manufacturer
5 self.color = color
6 self.is_mountain_bike = is_mountain_bike
7 self.__old = old # This is a private property
8
9 def __get_old(self): # This is a private method
10 return self.__old
Now, if we want to call this private method from outside the class,
an error is going to be thrown:
Object Oriented Programming 107
Hiding information
When you go out there and use a coffee machine, it is not expected
from you to know all the engineering details that are behind that
machine. This is also with your car. When you sit in your driving
seat, you do not analyze and understand about all the details of
every part of the car. You have some basic idea about them, but
other than that, you are driving more freely.
This can be thought as a restriction of the access from people
outside, so that they do not have to worry about exact details that
are going on inside.
We can do that in Python as well.
We have seen so far the foundational blocks of object oriented
programming, such as classes and objects.
Classes are blueprints that are used to create instances called
objects. We can use objects of different classes to interact with each
other and build a robust program.
When we work on our own programs, we may need to not let
everyone know about all the details that our classes have. We can
hence limit access to them, so that certain attributes are less likely
to be accessed unintentionally and be modified wrongfully.
To help us with that, we hide parts of a class and simply provide an
interface that has less details about the inner workings of our class.
Object Oriented Programming 108
1. Encapsulation
2. Abstraction
Encapsulation
1 class Smartphone:
2 def __init__(self, type=None): # defining initialize\
3 r for case of no argument
4 self.__type = type # setting the type here in th\
5 e beginning when the object is created
6
7 def set_type(self, value):
8 self.__type = value
9
10 def get_type(self):
11 return (self.__type)
Now, let us use this class to set the type and also get the type:
What we have done so far is that we have set and also read the value
of a private attribute of an object created from the Smartphone class.
We can also define getters and setters, using the @property
annotation to hep us with that.
Let’s see it with code:
Object Oriented Programming 110
1 class Bicycle:
2 def __init__(self, manufacturer, color):
3 self._manufacturer = manufacturer
4 self._color = color
5
6 @property
7 def manufacturer(self):
8 return self._manufacturer
9
10 @manufacturer.setter
11 def manufacturer(self, manufacturer):
12 self._manufacturer = manufacturer
13
14 @property
15 def color(self):
16 return self._color
17
18 @color.setter
19 def color(self, color):
20 self._color = color
21
22
23 bike = Bicycle("Connondale", "Grey")
Now that we have defined them, we can call these getter methods
as attributes:
1 print(bike.manufacturer) # Connondale
2 print(bike.color) # Grey
We can also modify the value that we initially used for any attribute
by simply typing the name of the object and the attribute that we
want to modify:
Object Oriented Programming 111
1 bike.is_mountain_bike = False
2 bike.color = "Blue"
Our classes can also have other methods as well, and not just getters
and setters.
Let us define a method inside the class Bicycle that we can then call
from any object that we have created from that class:
1 class Bicycle:
2 def __init__(self, manufacturer, color, is_mountain_b\
3 ike):
4 self._manufacturer = manufacturer
5 self._color = color
6 self._is_mountain_bike = is_mountain_bike
7
8 def get_description(self):
9 desc = "This is a " + self._color + " bike of the\
10 brand " + self._manufacturer
11 return desc
This looks quite promising and fancy, but you may not get it quite
yet. You may get the impression that you need additional reasons
on why you need this type of hiding.
Object Oriented Programming 112
1 class Employee:
2 def __init__(self, name=None, email=None, salary=None\
3 ):
4 self.name = name
5 self.email = email
6 self.salary = salary
Now, let us create a new employee object and initialize its attributes
accordingly:
Since salary is not being protected in any way, we can set a new
salary for this new object without any problem:
1 betim.salary = 25000
2
3 print(betim.salary) # 25000
As we can see, this person got five times the salary of what he was
getting previously without going through any type of evaluation
or interviewing at all. In fact, it happened in a matter of seconds.
That’s probably going to hit the budget of the company heavily.
We obviously do not want to do that. We want to restrict access to
the salary attribute so that it is not called from other places. We can
do that by using the double underscore before the attribute name
as can be shown below:
Object Oriented Programming 113
1 class Employee:
2 def __init__(self, name=None, email=None, salary=None\
3 ):
4 self.__name = name
5 self.__email = email
6 self.__salary = salary
1 print(betim.salary) # 1000
1 class Employee:
2 def __init__(self, name=None, email=None, salary=None\
3 ):
4 self.__name = name
5 self.__email = email
6 self.__salary = salary
7
8 def get_info(self):
9 return self.__name, self.__email, self.__salary
Inheritance
In real life, we can share many characteristics with other human
beings.
We all need to eat food, drink water, work, sleep, move, and
so on. These and many other behaviors and characteristics are
shared among billions of people all around the world. They are not
something unique that only our generation has. These traits have
been for many generations that we have not had the chance to even
see at all.
This is also something that is going to last for future generations to
come.
Object Oriented Programming 115
1 class Book:
2 def __init__(self, title, author):
3 self.title = title
4 self.author = author
5
6 def get_short_book_paragraph(self):
7 short_paragraph = "This is a short paragraph of t\
8 he book."
9 return short_paragraph
Now, we can create an object from this class and access it:
Let us now create a subclass of the class Book that inherits attributes
and methods from the class Book, but also has another additional
method called get_book_description():
Object Oriented Programming 116
1 class Book:
2 def __init__(self, title, author):
3 self.title = title
4 self.author = author
5
6 def get_short_book_paragraph(self):
7 short_paragraph = "This is a short paragraph of t\
8 he book."
9 return short_paragraph
10
11
12 class BookDetails(Book):
13 def __init__(self, title, author):
14 Book.__init__(self, title, author)
15 # Here we are call the constructor of the parent \
16 class Book
17
18 def get_book_details(self):
19 description = "Title: " + self.title + ". "
20 description += "Author: " + self.author
21 return description
1 class BookDetails(Book):
If we try to access this new method from objects of the class Book,
we are going to get an error:
Object Oriented Programming 117
super() function
1 class Animal():
2 def __init__(self, name, age):
3 self.name = name
4 self.age = age
5
6
7 class Cat(Animal):
8 def __init__(self, name, age):
9 super().__init__(name, age) # calling the parent\
10 class constructor
11 self.health = 100 # initializing a new attribute\
12 that is not in the parent class
We can also replace super() with the name of the parent class,
which is going to work in the same way again:
1 class Animal():
2 def __init__(self, name, age):
3 self.name = name
4 self.age = age
5
6
7 class Cat(Animal):
8 def __init__(self, name, age):
9 Animal.__init__(name, age) # calling the parent \
10 class constructor
Object Oriented Programming 119
Even changing the order of the lines inside the child’s constructor
will not cause any error at all.
1 seller = Seller()
2
3 seller.get_product_details()
4
5 # Producer: Samsung
6 # Seller: Amazon
Types of inheritance
1. Single
2. Multi-level
3. Hierarchical
4. Multiple
5. Hybrid
1. Single inheritance
1 class Animal:
2 def __init__(self):
3 self.health = 100
4
5 def get_health(self):
6 return self.health
7
8
9 class Cat(Animal):
10 def __init__(self, name):
11 super().__init__()
12 self.health = 150
13 self.name = name
14
15 def move(self):
16 print("Cat is moving")
17
18 cat = Cat("Cat")
19
20 # Calling the method from the parent class
Object Oriented Programming 122
21 print(cat.get_health()) # 150
22
23 # Calling the method from the child class
24 cat.move() # Cat is moving
2. Multi-level inheritance
1 class Creature:
2 def __init__(self, alive):
3 self.alive = alive
4
5 def is_it_alive(self):
6 return self.alive
7
8
9 class Animal(Creature):
10 def __init__(self):
11 super().__init__(True)
12 self.health = 100
13
14 def get_health(self):
15 return self.health
16
17
18 class Cat(Animal):
19 def __init__(self, name):
20 super().__init__()
21 self.name = name
22
23 def move(self):
Object Oriented Programming 123
24 print("Cat is moving")
25
26
27 cat = Cat("Cat")
28
29 # Calling the method from the parent of the parent class
30 print(cat.is_it_alive())
31
32 # Calling the method from the parent class
33 print(cat.get_health()) # 150
34
35 # Calling the method from the child class
36 cat.move() # Cat is moving
3. Hierarchical inheritance
When we derive multiple child classes from the same parent class,
then we have hierarchical inheritance. These child classes inherit
from the parent class:
1 class Location:
2 def __init__(self, x, y):
3 self.x = x
4 self.y = y
5
6 def get_location(self):
7 return self.x, self.y
8
9
10 class Continent(Location):
11 pass
12
13
14 class Country(Location):
15 pass
16
Object Oriented Programming 124
17
18 continent = Continent(0, 0)
19 print(continent.get_location()) # (0, 0)
20
21 country = Country(10, 30)
22 print(country.get_location()) # (10, 30)
4. Multiple inheritance
1 class Date:
2 date = '2022-07-23' # Hardcoded date
3
4 def get_date(self):
5 return self.date
6
7
8 class Time:
9 time = '20:20:20' # Hardcoded time
10
11 def get_time(self):
12 return self.time
13
14
15 class DateTime(Date, Time): # Inheriting from both
16 def get_date_time(self):
17 return self.get_date() + ' ' + self.get_time() #\
18 getting methods from its parent classes
Object Oriented Programming 125
19
20
21 date_time = DateTime()
22 print(date_time.get_date_time()) # 2022-07-23 20:20:20
5. Hybrid inheritance
1 class Vehicle:
2 def print_vehicle(self):
3 print('Vehicle')
4
5
6 class Car(Vehicle):
7 def print_car(self):
8 print('Car')
9
10
11 class Ferrari(Car):
12 def print_ferrari(self):
13 print('Ferrari')
14
15
16 class Driver(Ferrari, Car):
17 def print_driver(self):
18 print('Driver')
Now, if we create an object from the class Driver, we can call all
methods in all classes:
Object Oriented Programming 126
1 driver = Driver()
2
3 # Calling all methods from the subclass
4 driver.print_vehicle() # Vehicle
5 driver.print_car() # Car
6 driver.print_ferrari() # Ferrari
7 driver.print_driver() # Driver
Polymorphism
This is another important concept from Object Oriented Program-
ming that refers to the possibility of an object behaving like
different forms and calling different behaviors.
An example of a built-in function that uses polymorphism is the
method len() which can be used for both strings and lists:
1 print(len('Python')) # 6
2
3 print(len([2, 3, -43])) # 3
• getAreaOfCondo()
• getAreaOfApartment()
Object Oriented Programming 127
• getAreaOfSingleFamilyHouse()
• getAreaOfMultiFamilyHouse()
1 class Condo:
2 def __init__(self, area):
3 self.area = area
4
5 def get_area(self):
6 return self.area
7
8
9 class Apartment:
10 def __init__(self, area):
11 self.area = area
12
13 def get_area(self):
14 return self.area
Object Oriented Programming 128
1 condo = Condo(100)
2
3 apartment = Apartment(200)
Now, we can put both of them in a list and call the same method
for both objects:
1 # 100
2 # 200
We can not just call a method from a superclass. We can also use
the same name but have a different implementation for it for each
subclass.
Let us first define a superclass:
Object Oriented Programming 129
1 class House:
2 def __init__(self, area):
3 self.area = area
4
5 def get_price(self):
6 pass
1 class House:
2 def __init__(self, area):
3 self.area = area
4
5 def get_price(self):
6 pass
7
8
9 class Condo(House):
10 def __init__(self, area):
11 self.area = area
12
13 def get_price(self):
14 return self.area * 100
15
16
17 class Apartment(House):
18 def __init__(self, area):
19 self.area = area
20
21 def get_price(self):
22 return self.area * 300
We can now create new objects from subclasses and call this
method which is going to polymorph based on the object that calls
it:
1 condo = Condo(100)
2
3 apartment = Apartment(200)
4
5 places_to_live = [condo, apartment]
6
7 for place in places_to_live:
8 print(place.get_price())
1 # 10000
2 # 60000
1 import os
1 import math
2
3 print(math.sqrt(81)) # 9.0
We can also use new names for our imported modules by specifying
an alias for them as alias where alias is any variable name that
you want:
Importing everything
We can also import everything from a module, which can turn out
to be a problem. Let us illustrate this with an example.
Let us assume that we want to import everything that is included
in the math module. We can do that by using the asterisk
1 sqrt = 25
When we try to call the function sqrt() from the math module, we
are going to get an error, since the interpreter is going to call the
latest sqrt variable that we have just declared in the previous code
block:
1 print(sqrt(100))
Common Exceptions
Here are some of the most common exceptions that happen in
Python with definitions taken from the Python documentation¹:
There are many other error types, but you may not need to see
about them now. It is also very unlikely that you are going to see
all types of errors all the time.
You can see more types of exception in the Python documentation².
Handling exceptions
Let us start with a very simple example and write a program that
throws an error on purpose so that we can then fix it.
We are going to do a division by zero, which is something that you
have probably seen at school:
1 print(5 / 0)
1 try:
2 5 / 0
3 except ZeroDivisionError:
4 print('You cannot divide by 0 mate!')
1 try:
2 5 / 0
3 except:
4 print('You cannot divide by 0 mate!')
1 name = 'User'
2
3 try:
4 person = name + surname # surname is not declared
5 except NameError:
6 print('A variable is not defined')
1 my_list = [1, 2, 3, 4]
2
3 try:
4 print(my_list[5])
5 # This list only has 4 elements, so its indexes range\
6 from 0 to 3
7 except IndexError:
8 print('You have used an index that is out of range')
We can also use a single try block with multiple except errors:
Exceptions 138
1 my_list = [1, 2, 3, 4]
2
3 try:
4 print(my_list[5])
5 # This list only has 4 elements, so its indexes range\
6 from 0 to 3
7 except NameError:
8 print('You have used an invalid value')
9 except ZeroDivisionError:
10 print('You cannot divide by zero')
11 except IndexError:
12 print('You have used an index that is out of range')
1 my_list = [1, 2, 3, 4]
2
3 try:
4 print(my_list[5])
5 # This list only has 4 elements, so its indexes range\
6 from 0 to 3
7 except (NameError, ZeroDivisionError, IndexError):
8 print('A NameError, ZeroDivisionError, or IndexError \
9 occurred')
finally
After the try and except are passed, there is another block that we
can declare and execute. This block starts with the finally keyword
and it is executed no matter whether we have an error is being
thrown or not:
1 a
2 The program is ending. This is going to be executed.
We can write statements inside try and except, but we can also use
an else block where we can write code that we want to be executed
if there are no errors being thrown:
1 a
2 No error occurred. Congratulations!
Wrap up