Study Material
Study Material
Fatos Morina
“high-level programming language, and its core design philosophy is all about code readability
and a syntax which allows programmers to express concepts in a few lines of code.”
Benefits of Learning Python
Python is one of the easiest languages that you can learn and work with. You can use it 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 Python is that it is widely used and accepted by many companies and
academic institutions. This makes it a really good choice, especially if you are just starting your
coding journey.
Python also has a large community of developers that use it and are willing to help you if you
get stuck or have questions. This community has already published many open source libraries
that you can start using. They also actively keep improving them.
Python's syntax is quite similar to that of the English language, which makes it easier for you to
understand and use quite intuitively.
Python runs on an interpreter system, meaning that you don't have to wait for a compiler to
compile the code and then execute it. You can instead build prototypes quickly.
It also works on different platforms, such as Windows, Linux, Mac, Rapsberry Pi, and so on.
So you see – it's a great programming language, whether you're just starting out or you're
looking to add another useful tool to your kit.
Table of Contents
Python Basics
Operators in Python
Data types in Python
Tuples in Python
Dictionaries in Python
Sets in Python
Type Conversions in Python
Control Flow in Python
Functions in Python
Object Oriented Programming in Python
Importing in Python
How to Handle Exceptions in Python
User Input in Python
Get the PDF version of the book
Python Basics
Indentation in Python
Compared to other languages, Python places special importance on indentation.
In other programming languages, white spaces and indentations are only used to make the
code more readable and prettier. But 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:
Comments in Python
We use comments to specify parts of the program that should be simply ignored and not
executed by the Python interpreter. This means that everything written inside a comment is not
taken into consideration at all and you can write in any language that you want, including your
own native language.
In Python, you start a comment with the symbol # in front of the line:
"""
This is a comment in Python.
Here we are typing in another line and we are still inside the same comment block.
In the previous line we have a space, because it's allowed to have spaces inside comments.
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 in Python
Arithmetic operators in Python
Even though you can just pull out your phone from your pocket and do some calculations, you
should also get used to implementing some arithmetic operators in Python.
When we want to add two numbers, we use the plus sign, just like in math:
print(50 + 4) # 54
Similarly, for subtraction, we use the minus sign:
print(50 - 4) # 46
For multiplication, we use the star or asterisk:
print(50 * 4) # 200
When doing divisions, we use the forward slash:
print(50 / 4) # 12.5
print(8 / 4) # 2.0
This often results in float numbers (numbers with a decimal point, that is, not a round number). If
we want to only get integer numbers when doing division, that's called floor division. In that
case, we should use the double forward slashes like this:
print(50 // 4) # 12
print(8 / 4) # 2
We can also find the remainder of a number divided by another number using the percent sign
%:
print(50 % 4) # 2
This operation can be helpful especially in cases when we want to check whether a number is
odd or even. A number is odd if, when divided by 2, the remainder is 1. Otherwise, it is even.
Here is an illustration:
print(50 % 4) # 2
# Since the remainder is 2, this number is even
print(51 % 4) # 1
#Since the remainder is 1, this number is odd
When we want to raise a number to a specific power, we use the double asterisk:
print(2 ** 3) # 8
# This is a short way of writing 2 * 2 * 2
print(5 ** 4) # 625
# This is a short way of writing 5 * 5 * 5 * 5
Assignment operators in Python
You use these operators to assign values to variables.
name = "Fatos"
age = 28
We can also declare multiple variables in the same line:
One logical way to do that would be to introduce a third variable that serves as a temporary
variable:
a, b = 1, 2
print(a) # 1
print(b) # 2
c=a
a=b
b=c
print(a) # 2
print(b) # 1
We can do that in a single line in the following way:
a, b = 1, 2
print(a) # 1
print(b) # 2
b, a = a, b
print(a) # 2
print(b) # 1
We can also merge the assignment operators with arithmetic operators.
total_sum = 20
current_sum = 10
Now we want to add the value of the current_sum to total_sum. To do that, we should write the
following:
print(total_sum) # 30
This may not look 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.
total_sum += current_sum
print(total_sum) # 30
This is equivalent to the previous statement.
Subtraction:
result = 3
number = 4
print(result) # -1
Multiplication:
product = 3
number = 4
print(product) # 12
Division:
result = 8
number = 4
print(result) # 2.0
Modulo operator:
result = 8
number = 4
print(result) # 0
Power operator:
result = 2
number = 4
print(result) # 16
Comparison operators in Python
You likely learned how to do basic comparisons of numbers in school, such as checking whether
a specific number is larger than another number, or whether they are equal.
Equality operators
You can check whether two numbers are equal using the == operator:
print(2 == 3) # False
The last expression evaluates to False since 2 is not equal to 3.
There is another operator that you can use to check whether 2 numbers are not equal. This is
an operator that you may have not have seen in your math classes written exactly in this way.
This is the operator !=.
print(2 != 3) # True
This expression evaluates to True since 2 is indeed not equal to 3.
Inequality operators
Now here we are going to see how to check whether a number is larger than another number:
When trying to check whether a number is greater than or equal to another number, we need to
use this operator >=:
Briefly speaking, for an expression to evaluate to True when using and, both statements should
be true. In Python, we implement it using and:
When you have statements where at least one of them should evaluate to True, then we should
use the or operator:
print(2 > 5 or 0 > -1) # True
This is going to evaluate to True since the statement in the right hand side evaluates to True –
so at least one of them is true.
You can use variables to store values and then reuse them as many times as you want. The
moment you want to change a 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 that variable is used.
A variable name must start with a letter or the underscore character. It cannot start with a
number.
A variable name can only contain alpha-numeric characters and underscores (A-z, 0-9, and _ ).
Variable names are case-sensitive, meaning that height, Height, and HEIGHT are all different
variables.
Let's define our first variable:
age = 28
In this example, we are initializing a variable with the name age and assigning the value 28 to it.
age = 28
salary = 10000
We can use pretty much any name that we want, but it is a better practice to use names that
both you and other any colleagues who work with you can understand.
We have other variable types in Python, such as float numbers, strings, and boolean values. We
can even create our own custom types.
Let's see an example of a variable that holds a float number:
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. The Python interpreter is smart enough to
know that we are dealing with another type of variable, namely a float type of variable.
reader = "Fatos"
We put string values either in double quotes or single quotes to specify the value that we want
to store in string variables.
When we want to store boolean values, we need to use reserved words, namely, True and
False.
text_visibile = False
We can also have a boolean value stored when we have expressions that result with a boolean
value, for example, when we compare a number with another number, such as:
is_greater = 5 > 6
This variable is going to get initialized with the value False since 5 is lower than 6.
Numbers in Python
We have three numeric types in Python: integers, floats, and complex numbers.
Integers
Integers represent whole numbers that can be both positive and negative and do not contain
any decimal part.
When adding, subtracting, or multiplying two integers, we get an integer as a result in the end.
print(3 + 5) # 8
print(3 - 5) # -2
print(3 * 5) # 15
These are all integers.
Booleans
The boolean type represents truth values, True and False. You learned the explanation of this
type in the 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.
print(True * 5) # 5
print(False * 500) # 0, since False is equal to 0
The exception for such integer representations of Boolean values is when these values are
strings such as "False" and "True".
Floats
Float numbers are numbers that contain the decimal part, such as -3.14, 12.031, 9.3124, and so
on.
ten = float(10)
print(ten) # 10.0
When adding, subtracting, or dividing two float numbers, or a float number and an integer, we
get a float number as a result in the end:
print(3.4 * 2) # 6.8
print(3.4 + 2) # 5.4
print(3.4 - 2) # 1.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:
complex_number = 1 + 5j
print(complex_number) # (1+5j)
Strings in Python
Strings represent characters that are enclosed in either single quotes or double quotes. Both of
them are treated the same:
double_quote_greeting = "Hello. I'm fine." # When using double quotes, we do not need to
escape the apostrophe
When we want to include a new line in a string, we can include the special character \n:
print(my_string)
# I want to continue
# in the next line
Since strings are arrays of characters, we can index specific characters using indexes. Indexes
start from 0 and go all the way until the length of the string minus 1.
We exclude the index that is equal to the length of the string since we start indexing from 0 and
not from 1.
Here is an example:
string = "Word"
In this example, if we were to select individual characters of the string, they would unfold as
follows:
string = "Word"
print(string[0]) # W
print(string[1]) # o
print(string[2]) # r
print(string[3]) # d
We can use negative indexes as well, which means that we start from the end of the string with
-1. We cannot use 0 to index from the end of a string, since -0 = 0:
print(string[-1]) # d
print(string[-2]) # r
print(string[-3]) # o
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:
string = "Word"
print(string[0:3]) # Wor
If we want to start from a specific index and continue getting all the remaining characters of the
string until the end, we can omit specifying the end index, as follows:
string = "Word"
print(string[2:]) # rd
If we want to start from 0 and go until a specific index, we can simply specify the ending index:
string = "Word"
print(string[:2]) # Wo
This means that the value of string is equal to string[:2] (excluding the character at position 2) +
string[2:].
Note that strings in Python are immutable. This means that once a string object has been
created, it cannot be modified, such as trying to change a character in a string.
As an illustration, let's say that we want to change the first character in the string, namely
switching W with A in the following way:
string = "Word"
string[0] = "A"
Now, if we try to print string, we would get an error like the following:
String operators
We can concatenate strings using the + operator:
first = "First"
second = "Second"
string = "Abc"
repeated_version = string * 5
print(repeated_version) # AbcAbcAbcAbcAbc
String built-in methods
There are a few built-in methods of strings that we can use that can make it easier for us to
manipulate with them.
len() method
len() is a method that we can use to get the length of a string:
print(len(sentence)) # 10
replace() method
We can use replace() to replace a character or a substring of a string with another character or
substring:
string = "Abc"
print(modified_version) # Zbc
strip() method
strip() removes white spaces that can be in the beginning or at the end of a string:
print(string.strip()) # Hi there
split() method
We can use split() to convert a string into an array of substrings based on a specific pattern that
is mentioned as the separator.
For example, let's assume that we want to save all words of a sentence in an array of words.
These words are separated by white spaces, so we will need to do the splitting based on that:
print(string.split(" "))
# ['This', 'is', 'a', 'sentence', 'that', 'is', 'being', 'declared', 'here']
join() method
join() is the opposite of split(): from an array of strings, it returns a string. The process of
concatenation is supposed to happen with a specified separator in between each element of the
array that in the end results in a single concatenated string:
print(string.count("e")) # 2
print(string.count("Hi")) # 1
print(string.count("Hi there")) # 1
find() method
find() lets us find a character or a substring in a string and returns the index of it. In case it does
not find it, it will simply return -1:
print(string.find("3")) # -1
print(string.find("e")) # 5
print(string.find("Hi")) # 0
print(string.find("Hi there")) # 0
lower()
lower() converts all characters of a string into lower case:
print(string.lower()) # hi there
upper()
upper() converts all characters of a string into lower case:
print(string.capitalize()) # Hi there
title() method
title() converts starting characters of each word (sequences that are separated by white spaces)
of a string into uppercase:
print(string.title()) # Hi There
isupper() method
isupper() is a method that we can use to check whether all characters in a string are upper
case:
print(string.isupper()) # False
print(another_string.isupper()) # True
islower() method
islower() similarly checks whether all characters are lower case:
print(string.islower()) # False
print(another_string.islower()) # True
isalpha() method
isalpha() returns True if all characters in a string are letters of the alphabet:
string = "A1"
another_string = "aA"
string = "A1"
another_string = "3.31"
yet_another_string = "3431"
Let's first illustrate why we need formatting and include string interpolation.
Imagine that I want to develop a software that greets people the moment they come in, such as:
Now if someone comes and signs in, I will have to use their own names, such as:
Now let's think that I get lucky and the second user also pops in on a Friday morning and our
application should display:
You should already remember that we are about to mention something that I introduced back at
the beginning.
Yes, we will need to use variables and include a variable next to the string, as follows:
If we try to print the value of the string, we should get the following:
print(greeting)
# Good morning Fatos. Today is Friday.
We can specify parameters with indexes inside curly braces like the following that can then be
used:
print(greeting)
# greeting = "Today is {1}. Have a nice day {0}.".format("Fatos", "Friday")
We can also specify parameters inside the format() method and use those specific words inside
curly braces as a reference:
short_bio = 'My name is {name}. My last name is {0}. I love {passion}. I like playing {1}.'.format(
'Morina',
'Basketball',
name='Fatos',
passion='Programming'
)
print(short_bio)
# My name is Fatos. My last name is Morina. I love Programming. I like playing Basketball.
As you can see, using named arguments as opposed to positional ones can be less error-prone,
since their ordering in the format() method does not matter.
We can also use another way of formatting strings which consists of beginning a string with f or
F before the opening quotation marks or triple quotation marks and including names of the
variables that we want to be included in the end:
first_name = "Fatos"
day_of_the_week = "Friday"
continent = "Europe"
continent = "Europe"
print(i_am_here) # I am in Europe
Lists in Python
If you take a look at a bookshelf, you can see that the books 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's 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:
first = "Albert"
second = "Besart"
third = "Fisnik"
fourth = "Festim"
fifth = "Gazmend"
The list can keep on going which will make it 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 a list.
Let's create a list called students and store in that list all the names declared in the previous
code block:
Moreover, this way, it is easier for us to manage and also manipulate the elements in the list.
You may think that, "Well it was easier for me to just call first and get the value stored in there.
Now it is impossible to get a value from this new list, called students".
If we couldn't read and use those elements that we just stored in a list, that would make it less
helpful.
Fortunately, lists have indexes, which start from 0. This means that if we want to get the first
element in a list, we need to use index 0 and not index 1 as you may think.
In the example above, the list items have these corresponding indexes:
students[0]
If we want to get the second element, we just write:
students[1]
As you can probably see, 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.
This list is, of course, not static. We can add elements to it, like when a new student joins the
class.
Let's add a new element in the list students with the value Besfort:
students.append("Besfort")
We can also change the value of an existing element. To do that, we need to simply reinitialize
that specific element of the list with a new value.
students[0] = "Besim"
Lists can contain contain different types of variables, for example, we can have a string that
contains integers, float numbers, and strings:
Let's see how we can get the first three elements of a list using slicing:
my_list = [1, 2, 3, 4, 5]
print(my_list[0:3]) # [1, 2, 3]
As you can see, we have specified 0 as the starting index and 3 as the index where the slicing
should stop, excluding the element at the ending index.
If we want to simply start from an index and get all the remaining elements in the list, meaning
that the end_index should be the last index, then we can omit and not have to write the last
index at all:
my_list = [1, 2, 3, 4, 5]
print(my_list[3:]) # [4, 5]
Similarly, if we want to start from the beginning of the list and do the slicing until a specific index,
then we can omit writing the 0 index entirely, since Python is smart enough to infer that:
my_list = [1, 2, 3, 4, 5]
print(my_list[:3]) # [1, 2, 3]
Strings in Python are immutable, whereas lists are mutable, meaning that we can modify lists'
content after we declare them.
As an illustration, let's say that we want to change the first character in the string, namely
switching S with B in the following way:
string = "String"
string[0] = "B"
Now, if we try to print string, we would get an error like the following:
my_list[0] = 50
first_list = [1, 2, 3]
second_list = [4, 5]
print(first_list) # [1, 2, 3, 4, 5]
How to nest a list inside another list
We can nest a list inside another list like this:
To access elements of a list which is inside a list we need to use double indexes.
Let's 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:
print(subjects[0][1]) # 'Math'
List methods
len() is a method that you can use to find the length of a list:
print(len(my_list)) # 4
How to add elements to a list
We can also expand lists by adding new elements, or we can also delete elements.
We can add new elements at the end of a list using the append() method:
my_list.append("New element")
print(my_list)
# ['a', 'b', 'c', 'New element', 'Yet another new element']
If we want to add elements at specific indexes in a list, we can use the insert() method. We
specify the index in the first argument and the element that we want to add in the list as a
second argument:
my_list.insert(1, "z")
my_list = [1, 2, 3, 4, 5]
print(my_list) # [1, 2, 3, 4]
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:
my_list = [1, 2, 3, 4, 5]
print(my_list) # [2, 3, 4, 5]
We can also delete elements from lists using the del statement and then specifying the value of
the element that we want to delete:
my_list = [1, 2, 3, 4, 1]
print(my_list) # [2, 3, 4, 5]
We can also delete slices of lists using del:
my_list = [1, 2, 3, 4, 1]
my_list = [1, 2, 3, 4]
my_list.remove(3)
print(my_list) # [1, 2, 4]
reverse() lets us reverse the elements in a list. This is quite easy and straightforward:
my_list = [1, 2, 3, 4]
my_list.reverse()
print(my_list) # [4, 3, 2, 1]
Index search
Getting elements of a list using indexes is simple. Finding indexes of elements of a list is also
easy. We simply need to use the method index() and mention the element that we want to find
inside a list:
print(my_list.index("Python")) # 2
Membership
This is quite intuitive and related to real life: We get to ask ourselves whether something is part
of something or not.
In Python, if we want to check whether a value is part of something, that we can use the
operator in:
We can also use it not only with arrays of numbers, but with arrays of characters as well:
Similarly, we can also check whether something is not included using not in:
odd_numbers = [1, 3, 5, 7]
print(2 not in odd_numbers) # True
Since 2 is not included in the array, the expression is going to evaluate to True.
my_list = [3, 1, 2, 4, 5, 0]
my_list.sort()
print(my_list) # [0, 1, 2, 3, 4, 5]
alphabetical_list.sort()
List comprehension
List comprehension represents a concise way in which we use a for loop to create a new list
from an existing one. The result is always a new list in the end.
Let's start with an example where we want to multiply each number of a list with 10 and save
that result in a new list. First, let's do this without using list comprehension:
Before we write the way we would implement this using list comprehension, let's write a way in
which we would create a list of only numbers that are greater than 0 in another list and increase
those positive numbers by 100:
positive_numbers = [number + 100 for number in numbers if number > 0] # List comprehension
Let's take an example where we want to add each element of a list with each element in another
list:
first_list = [1, 2, 3]
second_list = [50]
double_lists = [first_element +
second_element for first_element in first_list for second_element in second_list]
Tuples in Python
Tuples are collections that are ordered and immutable, meaning that their content cannot be
changed. They are ordered and we can access their elements using indexes.
print(vehicles)
print(len(vehicles)) # 4
print(vehicles[3]) # Tablet
print(vehicles.index('tablet')) # 3
We can also concatenate or merge two tuples using the + operator:
print(sciences)
# ('Chemistry', 'Astronomy', 'Earth science', 'Physics', 'Biology', 'Anthropology', 'Archaeology',
'Economics', 'Geography', 'History', 'Law', 'Linguistics', 'Politics', 'Psychology', 'Sociology')
Membership check
We can check whether an element is part of a tuple using the operators in and not in just like
with lists:
vehicles = ('Car', 'Bike', 'Airplane')
print(sciences)
# (('Chemistry', 'Astronomy', 'Earth science', 'Physics', 'Biology'), ('Anthropology', 'Archaeology',
'Economics', 'Geography', 'History', 'Law', 'Linguistics', 'Politics', 'Psychology', 'Sociology'))
Immutability
Since tuples are immutable, we can't change them after we create them. This means that we
cannot add or delete elements in them, or append a tuple to another tuple.
We cannot even modify existing elements in a tuple. If we try to modify an element in a tuple, we
are going to face a problem like the following:
vehicles[0] = 'Truck'
print(vehicles)
# TypeError: 'tuple' object does not support item assignment
Dictionaries in Python – Key-Value Data Structures
As we saw previously, elements in lists are associated with indexes that we can use to
reference those elements.
There is another data structure in Python that allows us to specify our own custom indexes and
not just numbers. These are called dictionaries, and they are similar to dictionaries that we use
to find the meaning of words we do not understand.
Let's assume that you are trying to learn German and there is a new word that you have not had
the chance to learn before that you just saw at a market: Wasser.
Now, you can pick up your phone and check its corresponding meaning in English using Google
Translate or any other application of your choice. But if you were to use a physical dictionary,
you would need to find this word by going to that specific page and check its meaning sitting
right beside it. The reference or the key for the meaning of this word would be the term Wasser.
Now, if we want to implement this in Python, we should not use lists that have indexes only as
numbers. We should use dictionaries 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 you can see in the following example:
german_to_english_dictionary = {
"Wasser": "Water",
"Brot": "Bread",
"Milch": "Milk"
}
Now, when we want to access specific elements in the dictionary, we simply use keys. For
example, let's assume that we want to get the meaning of the word Brot in English. To do that,
we can simply reference that element using that key:
brot_translation = german_to_english_dictionary["Brot"]
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:
milch_translation = german_to_english_dictionary["Milch"]
print(milch_translation) # Milk
We can also get the value of an element in a dictionary using get() and specifying the key of the
item that we want to get:
german_to_english_dictionary.get("Wasser")
Both keys and values can be of any data type.
Dictionaries can have duplicate values, but the all the keys should be unique. Take a look at this
example to see what I mean:
my_dictionary = dict([
('a', 1),
('b', 1),
('c', 2)
])
We can create dictionaries using dict():
words = dict([
('abandon', 'to give up to someone or something on the ground'),
('abase', 'to lower in rank, office, or esteem'),
('abash', 'to destroy the self-possession or self-confidence of')
])
print(words)
# {'abandon': 'to give up to someone or something on the ground', 'abase': 'to lower in rank,
office, or esteem', 'abash': 'to destroy the self-possession or self-confidence of'}
How to add new values to a dict
We can add new values inside dictionaries by specifying a new key and a corresponding value.
Then Python is going to create a new element inside that dictionary:
words = {
'a': 'alfa',
'b': 'beta',
'd': 'delta',
}
words['g'] = 'gama'
print(words)
# {'a': 'alfa', 'b': 'beta', 'd': 'delta', 'g': 'gama'}
If we specify the key of an element that is already part of the dictionary, that element is going to
be modified:
words = {
'a': 'alfa',
'b': 'beta',
'd': 'delta',
}
words['b'] = 'bravo'
print(words)
# {'a': 'alfa', 'b': 'bravo', 'd': 'delta'}
How to remove elements from a dict
If we want to remove elements from a dictionary, we can use the method pop() and also specify
the key of the element that we want to delete:
words = {
'a': 'alfa',
'b': 'beta',
'd': 'delta',
}
words.pop('a')
words = {
'a': 'alfa',
'b': 'beta',
'd': 'delta',
}
words['g'] = 'gamma'
words.popitem()
print(words)
# {'a': 'alfa', 'b': 'beta', 'd': 'delta'}
There is another way that we can delete elements, namely by using del statement:
words = {
'a': 'alfa',
'b': 'beta',
'd': 'delta',
}
del words['b']
words = {
'a': 'alfa',
'b': 'beta',
'd': 'delta',
}
print(len(words)) # 3
Membership
If we want to check whether a key is already part of a dictionary so that we avoid overriding it,
we can use the operator in and not in just like with lists and tuples:
words = {
'a': 'alfa',
'b': 'beta',
'd': 'delta',
}
To help us with that, we are going to need to use a method called items() that converts a
dictionary into a list of tuples. The element in index 0 is a key, whereas in position with index 1,
we have a value.
points = {
'Festim': 50,
'Zgjim': 89,
'Durim': 73
}
elements = points.items()
We can assume that a professor is in a good mood and generous enough to reward each
student with a bonus of 10 points. We want to add these new points to each student by saving
these new points in a new dictionary:
points = {
'Festim': 50,
'Zgjim': 89,
'Durim': 73
}
elements = points.items()
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's start creating our first set using curly brackets as follows:
first_set = {1, 2, 3}
We can also create sets using the set() constructor:
print(len(first_set)) # 3
How to add elements to a set
We can add one element in a set using the method add():
my_set = {1, 2, 3}
my_set.add(4)
print(my_set) # {1, 2, 3, 4}
If we want to add more than one element, then we need to use method update(). We use as an
input for this method a list, tuple, string or another set:
my_set = {1, 2, 3}
my_set.update([4, 5, 6])
print(my_set) # {1, 2, 3, 4, 5, 6}
my_set.update("ABC")
my_set = {1, 2, 3}
my_set.remove(2)
print(my_set) # {1, 3}
If we try to delete an element that is not part of the set using remove(), then we are going to get
an error:
my_set = {1, 2, 3}
my_set.remove(4)
print(my_set) # KeyError: 4
To avoid such errors when removing elements from sets, we can use the method discard():
my_set = {1, 2, 3}
my_set.discard(4)
print(my_set) # {1, 2, 3}
Set Theory Operations
If you remember high school math lessons, you should already know about union, intersection,
and the difference between two sets of elements. These operations are supported for sets in
Python as well.
Union
Union represents the collection of all unique elements from both sets. We can find the union of
two sets using the pipe operator | or the union() method:
first_set = {1, 2}
second_set = {2, 3, 4}
union_set = first_set.union(second_set)
print(union_set) # {1, 2, 3, 4}
Intersection
Intersection represents the collection that contains elements that are in both sets. We can find it
using operator & or the intersection() method:
first_set = {1, 2}
second_set = {2, 3, 4}
intersection_set = first_set.intersection(second_set)
print(union_set) # {2}
Difference
The difference between two sets represents the collection that contains only the elements that
are in the first set, but not in the second. We can find the difference between two sets using the
- operator or the method difference()
first_set = {1, 2}
second_set = {2, 3, 4}
difference_set = first_set.difference(second_set)
print(difference_set) # {1}
As you can probably remember from high school, ordering of sets when we find the difference of
two sets matters, which is not the case with the union and intersection.
first_set = {1, 2}
second_set = {2, 3, 4}
first_difference_set = first_set.difference(second_set)
print(first_difference_set) # {1}
second_difference_set = second_set.difference(first_set)
print(second_difference_set) # {3, 4}
Type Conversions in Python
Conversions Between Primitive Types
Python is an object oriented programming language. That is why it uses constructor functions of
classes to do conversions from one type into another.
int() method
int() is a method that you use do a conversion of an integer literal, float literal (rounding it to its
previous integer number, that is 3.1 to 3), or a string literal (with the condition that the string
represents an int or float literal):
three = int(3) # converting an integer literal into an integer
print(three) # 3
four = int(4.8) # converting a float number into its previous closest integer
print(four) # 4
int_literal = float(5)
print(int_literal) # 5.0
float_literal = float(1.618)
print(float_literal) # 1.618
string_int = float("40")
print(string_int) # 40.0
string_float = float("37.2")
print(string_float) # 37.2
str() method
We can use str() to create strings from strings, integer literals, float literals, and many other data
types:
int_to_string = str(3)
print(int_to_string) # '3'
float_to_string = str(3.14)
print(float_to_string) # '3.14'
string_to_string = str('hello')
print(string_to_string) # 'hello'
Other Conversions
To convert from one type of data structure into another type, we do the following:
destination_type(input_type)
Let us get started with specific types, so that it becomes much clearer.
Conversions to lists
We can convert a set, tuple, or dictionary into a list using the list() constructor.
books_tuple = ('Book 1', 'Book 2', 'Book 3')
tuple_to_list = list(books_tuple) # Converting tuple to list
print(tuple_to_list) # ['Book 1', 'Book 2', 'Book 3']
books_dict = {'1': 'Book 1', '2': 'Book 2', '3': 'Book 3'}
dict_to_list = list(books_dict) # Converting dict to list
print(dict_to_list) # ['1', '2', '3']
If we want to keep both keys and values of a dictionary, we need to use the method items() to
first convert it into a list of tuples where each tuple is a key and a value:
books_dict = {'1': 'Book 1', '2': 'Book 2', '3': 'Book 3'}
print(dict_to_list)
# [('1', 'Book 1'), ('2', 'Book 2'), ('3', 'Book 3')]
Conversions to tuples
All data structures can be converted to a tuple using the tuple() constructor method, including a
dictionary In that case we get a tuple with the keys of the dictionary:
books_dict = {'1': 'Book 1', '2': 'Book 2', '3': 'Book 3'}
dict_to_tuple = tuple(books_dict) # Converting dict to tuple
print(dict_to_tuple) # ('1', '2', '3')
Conversions to sets
Similarly, all data structures can be converted to a set using the set() constructor method,
including a dictionary. In that case we get a set with the keys of the dictionary:
books_list = ['Book 1', 'Book 2', 'Book 3']
list_to_set = set(books_list) # Converting list to set
print(list_to_set) # {'Book 2', 'Book 3', 'Book 1'}
books_dict = {'1': 'Book 1', '2': 'Book 2', '3': 'Book 3'}
dict_to_set = set(books_dict) # Converting dict to set
print(dict_to_set) # {'1', '3', '2'}
Conversions to dictionaries
Conversions into dictionaries cannot be done with any type of sets, lists, or tuples, since
dictionaries represent data structures where each element contains both a key and a value.
Converting a list, or a tuple into a dictionary can be done if each element in a list is also a list
with two elements, or a tuple with two elements.
books_tuple_list = [(1, 'Book 1'), (2, 'Book 2'), (3, 'Book 3')]
tuple_list_to_dictionary = dict(books_tuple_list) # Converting list to set
print(tuple_list_to_dictionary) # {1: 'Book 1', 2: 'Book 2', 3: 'Book 3'}
books_list_list = [[1, 'Book 1'], [2, 'Book 2'], [3, 'Book 3']]
tuple_list_to_dictionary = dict(books_list_list) # Converting list to set
print(tuple_list_to_dictionary) # {1: 'Book 1', 2: 'Book 2', 3: 'Book 3'}
books_tuple_list = ([1, 'Book 1'], [2, 'Book 2'], [3, 'Book 3'])
tuple_list_to_set = dict(books_tuple_list) # Converting tuple to set
print(tuple_list_to_set) # {'Book 2', 'Book 3', 'Book 1'}
books_list_list = ([1, 'Book 1'], [2, 'Book 2'], [3, 'Book 3'])
list_list_to_set = dict(books_list_list) # Converting tuple to set
print(list_list_to_set) # {'Book 2', 'Book 3', 'Book 1'}
In case when we want to convert a set into a dictionary, we need to have each element as a
tuple of length 2.
books_tuple_set = {('1', 'Book 1'), ('2', 'Book 2'), ('3', 'Book 3')}
tuple_set_to_dict = dict(books_tuple_set) # Converting dict to set
print(tuple_set_to_dict) # {'1', '3', '2'}
If we try to do a conversion of a set that has each element as a list of length 2 into a dictionary,
we are going to get an error:
books_list_set = {['1', 'Book 1'], ['2', 'Book 2'], ['3', 'Book 3']}
list_set_to_dict = dict(books_list_set) # Converting dict to set
print(list_set_to_dict) # {'1', '3', '2'}
After we run the last code block, we are going to get an error:
Be sure to use the right data type for the task that is in front of you to avoid errors and optimize
performance.
To do that, we use the reserved term if and an expression that evaluates to a True or False
value. We can then also use an else statement where we want the flow to continue in cases
when the if condition is not met.
To make it easier to understand, let's assume that we have an example where we want to check
whether a number is positive:
if number > 0:
print("The given number is positive")
else:
print("The given number is not positive")
If we were to have number = 2: we would enter into the if branch and execute the command that
is used to print the following text in the console:
if number > 0:
print("The given number is positive")
elif number == 0:
print("The given number is 0")
else:
print("The given number is negative")
Now if we were to have number = 0, the first condition is not going to be met, since the value is
not greater than 0. As you can guess, since the given number is equal to 0, we are going to see
the following message being printed in the console:
We can change both the starting and the ending numbers in the range as we want. This way, we
can be quite flexible depending on our specific scenarios.
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.
number = 1
We do not just store things in data structures and leave them there for ages. We are supposed
to be able to use those elements in different scenarios.
We can do this for dictionaries as well. But since elements in dictionaries have 2 parts (key and
the value), we need to specify both the key and the value as follows:
german_to_english_dictionary = {
"Wasser": "Water",
"Brot": "Bread",
"Milch": "Milk"
}
We can also have nested for loops. For example, let's say that we want to iterate through a list
of numbers and find a sum of each element with each other element of a list. We can do that
using nested for loops:
numbers = [1, 2, 3]
sum_of_numbers = [] # Empty list
print(sum_of_numbers)
# [2, 3, 4, 3, 4, 5, 4, 5, 6]
How to stop 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's assume that we want to check whether there is a negative number in a list. In case we find
that number, we stop searching for it.
# Current number: 1
# Current number: 2
# Current number: -3
# We just found a negative number
As we can see, the moment we reach -3, we break from the for loop and stop.
my_sum = 0
my_list = [1, 2, -3, 4, 0]
print(my_sum) # 7
pass is a statement we can use to help us when we are about to implement a method or
something but we haven't done it yet and do not want to get errors.
It helps us execute the program even if some parts of the code are missing:
my_list = [1, 2, 3]
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 in Python
There are 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 it's also prone to more mistakes that we might overlook.
This is the reason why we need to start using code blocks that we can define once and then use
that same code 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.
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:
result = add(1, 5)
print(result) # 6
You might think 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 introduce you to the way we can implement
functions.
Let's write a function that finds the sum of numbers that are between two specified numbers:
return result
This is now a set of instructions that you can call in other places and do not have to write all of it
again.
result = sum_in_range(1, 5)
print(result) # 10
Note that functions define a scope, which means that variables which are defined in that scope
are not accessible outside it.
For example, we cannot access the variable named product outside the scope of the function:
Let's take an example of getting a user's first name as a required argument and let the second
argument be an optional one.
user = get_user("Durim")
print(user) # Hi Durim
Keyword Argument List
We can define arguments of functions as keywords:
print(user) # Hi Gashi
As you can see, we can omit first_name since it is not required. We can also change the
ordering of the arguments when calling the function and it will still work the same:
def counting():
count = 0 # This is not accessible outside of the function.
counting()
print(count) # This is going to throw an error when executing, since count is only declared
inside the function and is not acessible outside that
Similarly, we cannot change variables inside functions that have been declared outside
functions and that are not passed as arguments:
count = 3331
def counting():
count = 0 # This is a new variable
counting()
print(count) # 3331
# This is declared outside the function and has not been changed
How to Change Data Inside Functions
We can change mutable data that is passed through a function as arguments. Mutable data
represents data that we can modify even after it has been declared, Lists, for example, are
mutable data.
def capitalize_names(current_list):
for i in range(len(current_list)):
current_list[i] = current_list[i].capitalize()
return current_list
print("Outside the function:", names) # Outside the function: ['Betim', 'Durim', 'Gezim']
In case of immutable data, we can only modify the variable inside a function, but the actual
value outside that function is going to remain unchanged. Immutable data are strings and
numbers:
name = "Betim"
def say_hello(current_param):
current_param = current_param + " Gashi"
name = current_param # name is a local variable
print("Value inside the function:", name)
return current_param
print("Value outside the function:", name) # Value outside the function: Betim
If we really want to update immutable variables through a function, we can assign a return value
of a function to the immutable variable:
name = "Betim"
def say_hello(current_param):
current_param = current_param + " Gashi"
name = current_param # name is a local variable
print("Value inside the function", name)
return current_param
# Here we are assigning the value of name to the current_param that is returned from the
function
name = say_hello(name) # Value inside the function Betim Gashi
We'll start with a function that multiples each input with 10:
print(tenfold(10)) # 100
Let's write another example in which we check whether the given argument is positive or not:
print(is_positive(3)) # 3 is positive
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 even provide an entire function as the argument of a function, which can provide a level
of abstraction that can be quite useful.
Let's see an example where we want to do a few conversions from one unit into another:
def convert_to_meters(feet):
return feet * 0.3048
def convert_to_feet(meters):
return meters / 0.3048
def convert_to_miles(kilometers):
return kilometers / 1.609344
def convert_to_kilometers(miles):
return miles * 1.609344
Now, we can make a general function and pass another function as an argument:
print(result) # 6.2137119223733395
As you can see, we have written convert_to_miles as a parameter of the function conversion().
We can use other already defined functions like that:
print(result) # 1017.0603674540682
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:
print(result) # 6.2137119223733395
This is of course simpler.
map() function
map() is a built-in function that creates a new object by getting results by calling a function on
each element of an existing list:
map(function_name, my_list)
Let's see an example of writing a lambda function as the function of a map.
my_list = [1, 2, 3, 4]
my_list = [1, 2, 3, 4]
filter() function
This is another built-in function that we can use to filter elements of a list that satisfy a condition.
Let's first filter out negative elements from a list using list comprehension:
non_negative_list = list(non_negative_filter_object)
Decorators in Python
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.
def reverse_list(input_list):
return input_list[::-1]
In this example, we are simply returning a reversed list.
def reverse_list(input_list):
return input_list[::-1]
print(result) # [3, 2, 1]
We can also nest a function inside another function:
def reverse_input_list(input_list):
# reverse_list() is now a local function that is not accessible from the outside
def reverse_list(another_list):
return another_list[::-1]
result = reverse_list(input_list)
return result # Return the result of the local function
def reverse_list_decorator(input_function):
def function_wrapper():
returned_result = input_function()
reversed_list = returned_result[::-1]
return reversed_list
return function_wrapper
reverse_list_decorator() is a decorator function that takes as input another function. To call it, we
need to write another function:
def reverse_list_decorator(input_function):
def function_wrapper():
returned_result = input_function()
reversed_list = returned_result[::-1]
return reversed_list
return function_wrapper
# Function that we want to decorate
def get_list():
return [1, 2, 3, 4, 5]
result_from_decorator = get_list()
print(result_from_decorator) # [5, 4, 3, 2, 1]
How to stack 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's do a simple experiment and apply the same decorator that we defined in the previous
section twice.
[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:
@reverse_list_decorator
@reverse_list_decorator
def get_list():
return [1, 2, 3, 4, 5]
result = get_list()
print(result) # [1, 2, 3, 4, 5]
I'll explain this with another example.
Let's implement another decorator that only returns numbers that are larger than 1. We then
want to reverse that returned list with our existing decorator.
def positive_numbers_decorator(input_list):
def function_wrapper():
# Get only numbers larger than 0
numbers = [number for number in input_list() if number > 0]
return numbers
return function_wrapper
Now we can call this decorator and the other decorator that we have implemented:
@positive_numbers_decorator
@reverse_list_decorator
def get_list():
return [1, -2, 3, -4, 5, -6, 7, -8, 9]
result = get_list()
print(result) # [9, 7, 5, 3, 1]
Here is the complete example:
def reverse_list_decorator(input_function):
def function_wrapper():
returned_result = input_function()
reversed_list = returned_result[::-1] # Reverse the list
return reversed_list
return function_wrapper
# First decoorator
def positive_numbers_decorator(input_list):
def function_wrapper():
# Get only numbers larger than 0
numbers = [number for number in input_list() if number > 0]
return numbers
return function_wrapper
@positive_numbers_decorator
@reverse_list_decorator
def get_list():
return [1, -2, 3, -4, 5, -6, 7, -8, 9]
result = get_list()
print(result) # [9, 7, 5, 3, 1]
How to pass arguments to decorator functions
We can also pass arguments to decorator functions:
def add_numbers_decorator(input_function):
def function_wrapper(a, b):
result = 'The sum of {} and {} is {}'.format(
a, b, input_function(a, b)) # calling the input function with arguments
return result
return function_wrapper
@add_numbers_decorator
def add_numbers(a, b):
return a + b
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. You can also use them to perform
various tasks such as logging, exception handling, and testing. So if you're looking for a way to
clean up your Python code, consider using decorators.
There's a cookie cutter at a factory that has been used to produce a large number of cookies
that are then distributed all throughout different stores where those cookies are then served to
the end customers.
We can think of that cookie cutter as a blueprint that has been designed once and is used many
times afterwards. We also use this sort of blueprint in computer programming.
A blueprint that is used to create countless other copies is called a class. We can think of a
class like a class called Cookie, Factory, Building, Book, Pencil, and so on. We can use the
class of cookie as a blueprint to create as many instances as we want of it that we call objects.
In other words, blueprints are classes that are used as cookie cutters, whereas the cookies that
are served at different stores are objects.
Object Oriented Programming represents a way of organizing a program using classes and
objects. We use classes to create objects. Objects interact with each other.
We do not use the exact same blueprint for every object that is out there. There is a blueprint for
producing books, another one for producing pencils, and so on. We need to categorize them
based on attributes and their functionalities.
An object that is created from the Pencil class can have a color type, a manufacturer, a specific
thickness, and so on. These are the attributes. A pencil object can also write which represents
its functionality, or its method.
class Bicycle:
pass
We have used the keyword class to indicate that we are about to start writing a class and then
we type the name of the class.
We have added the pass because we don't want the Python interpreter to yell at us by throwing
errors for not continuing to write the remaining part of the code that belongs to this class.
Now, if we want to create new objects from this class Bicycle, we can simply write the name of
the object (which can be any variable name that you want) and initiailize it with the constructor
method Bicycle() that is used to create new objects:
favorite_bike = Bicycle()
In this case, favorite_bike is an object that is created from the class Bicycle. It gets all the
functionalities and attributes of the class Bicycle.
We can enrich our Bicycle class and include additional attributes so that we can have custom
bikes, tailored to our needs.
class Bicycle:
def __init__(self, manufacturer, color, is_mountain_bike):
self.manufacturer = manufacturer
self.color = color
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 can also create objects from classes by using optional arguments as follows:
class Bicycle:
# All the following attributes are optional
def __init__(self, manufacturer=None, color='grey', is_mountain_bike=False):
self.manufacturer = manufacturer
self.color = color
self.is_mountain_bike = is_mountain_bike
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 we use to
get and set values of attributes of objects. We are going to use an annotation called @property
to hep us with that.
class Bicycle:
def __init__(self, manufacturer, color, is_mountain_bike):
self._manufacturer = manufacturer
self._color = color
self._is_mountain_bike = is_mountain_bike
@property
def manufacturer(self):
return self._manufacturer
@manufacturer.setter
def manufacturer(self, manufacturer):
self._manufacturer = manufacturer
print(bike.manufacturer) # Connondale
We can write getters and setters for all the attributes of the class:
class Bicycle:
def __init__(self, manufacturer, color, is_mountain_bike):
self._manufacturer = manufacturer
self._color = color
self._is_mountain_bike = is_mountain_bike
@property
def manufacturer(self):
return self._manufacturer
@manufacturer.setter
def manufacturer(self, manufacturer):
self._manufacturer = manufacturer
@property
def color(self):
return self._color
@color.setter
def color(self, color):
self._color = color
@property
def is_mountain_bike(self):
return self._is_mountain_bike
@is_mountain_bike.setter
def is_mountain_bike(self, is_mountain_bike):
self.is_mountain_bike = is_mountain_bike
print(bike.manufacturer) # Connondale
print(bike.color) # Grey
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 where we want to change the content:
bike.is_mountain_bike = False
bike.color = "Blue"
bike.manufacturer = "Trek"
Our classes can also have other methods as well and not just getters and setters.
Let's define a method inside the class Bicycle that we can then call from any object that we have
created from that class:
class Bicycle:
def __init__(self, manufacturer, color, is_mountain_bike):
self._manufacturer = manufacturer
self._color = color
self._is_mountain_bike = is_mountain_bike
def get_description(self):
desc = "This is a " + self._color + " bike of the brand " + self._manufacturer
return desc
We have created a very simple method in which we are preparing a string as a result from the
attributes of the object that we are creating. We can then call this method like any other method.
In a nutshell, we group a few statements in a code block called method. There 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.
instance methods
class methods
static methods
Let's briefly talk about the overall structure of methods and then dive a little more into detail for
each method type.
Parameters
Parameters of a method make it possible for us to pass on dynamic values that can then be
taken into consideration when executing the statements that are inside the method.
The return statement represents the statement that is going to be the last one to be executed in
that method. It is an indicator for the Python interpreter to stop the execution of any other line
and return a value.
It is not required that we name it self, but it is a convention that is widely practiced by
developers writing Python code all around the world.
Let's define an instance method inside the class Bicycle that we can then call from any object
that we have created from that class:
class Bicycle:
def __init__(self, manufacturer, color, is_mountain_bike):
self._manufacturer = manufacturer
self._color = color
self._is_mountain_bike = is_mountain_bike
def get_description(self):
desc = "This is a " + self._color + " bike of the brand " + self._manufacturer
return desc
We have created a very simple method in which we are preparing a string as a result from the
attributes of the object that we are creating. We can then call this method like any other method:
Class methods are methods that we can call using class names and that we can access without
needing to create any new object at all.
Since it is a specific type of method, we need to tell the 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 – you don't have to use this name.
class Article:
blog = 'https://www.python.org/'
# the init method is called when an instance of the class is created
def __init__(self, title, content):
self.title = title
self.content = content
@classmethod
def get_blog(cls):
return cls.blog
Now let's call this class method that we have just declared:
print(Article.get_blog()) # https://www.python.org/
Note that we did not have to write any argument when calling the 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 variables or instance variables. You
can think of them as utility functions 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 (as 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.
class Article:
blog = 'https://www.python.org/'
@classmethod
def get_blog(cls):
return cls.blog
@staticmethod
def print_creation_date(date):
print(f'The blog was created on {date}')
class Article:
blog = 'https://www.python.org/'
@classmethod
def get_blog(cls):
return cls.blog
@staticmethod
def set_title(self, date):
self.title = 'A random title'
If we try to call this static method now, we are going to get an error:
Access modifier
When creating classes, we can restrict access to certain attributes and methods so that they are
not accessible that easily.
We have publicand private access modifiers.
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.
class Bicycle:
def __init__(self, manufacturer, color, is_mountain_bike):
self.manufacturer = manufacturer
self.color = color
self.is_mountain_bike = is_mountain_bike
def get_manufacturer(self):
return self.manufacturer
In the previous code block, both color and get_manufacturer() are accessible outside the class
since they are public and can be accessed both inside and outside the class:
print(bike.color) # Grey
print(bike.get_manufacturer()) # Connondale
Private attributes
Private attributes can be accessed directly only from inside the class.
We can make properties attributes by using the double underscore, as you can see in the
following example:
class Bicycle:
def __init__(self, manufacturer, color, is_mountain_bike, old):
self.manufacturer = manufacturer
self.color = color
self.is_mountain_bike = is_mountain_bike
self.__old = old # This is a private property
Now if we try to access __old, we are going to get an error:
class Bicycle:
def __init__(self, manufacturer, color, is_mountain_bike, old):
self.manufacturer = manufacturer
self.color = color
self.is_mountain_bike = is_mountain_bike
self.__old = old # This is a private property
This is the same with your car. When you sit in your driver's seat, you do not analyze and
understand all the details of every part of the car. You have some basic idea about them, but
other than that, you just focus on driving.
This is a sort of restriction of access from people outside, so that they do not have to worry
about exact details that are going on inside.
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. So we can 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 fewer
details about the inner workings of our class.
Encapsulation
Abstraction
Let's begin with Encapsulation.
What is Encapsulation?
Encapsulation is not something special and unique just for Python. Other programming
languages use it as well.
In a nutshell, we can define it as binding data and methods in a class. We then use this class to
create objects.
We encapsulate classes by using private access modifiers that can then restrict direct access to
such attributes. This can restrict control.
We are then supposed to write public methods that can provide access to the outside world.
Let's define first define a getter and a setter method that we can use to get values:
class Smartphone:
def __init__(self, type=None): # defining initializer for case of no argument
self.__type = type # setting the type here in the beginning when the object is created
def get_type(self):
return (self.__type)
Now, let's use this class to set the type and also get the type:
smartphone = Smartphone('iPhone') # we are setting the type using the constructor method
We can also define getters and setters using the @property annotation.
class Bicycle:
def __init__(self, manufacturer, color):
self._manufacturer = manufacturer
self._color = color
@property
def manufacturer(self):
return self._manufacturer
@manufacturer.setter
def manufacturer(self, manufacturer):
self._manufacturer = manufacturer
@property
def color(self):
return self._color
@color.setter
def color(self, color):
self._color = color
print(bike.manufacturer) # Connondale
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:
bike.is_mountain_bike = False
bike.color = "Blue"
Our classes can also have other methods as well, and not just getters and setters.
Let's define a method inside the class Bicycle that we can then call from any object that we have
created from that class:
class Bicycle:
def __init__(self, manufacturer, color, is_mountain_bike):
self._manufacturer = manufacturer
self._color = color
self._is_mountain_bike = is_mountain_bike
def get_description(self):
desc = "This is a " + self._color + " bike of the brand " + self._manufacturer
return desc
We have created a very simple method in which we are preparing a string as a result from the
attributes of the object that we are creating. We can then call this method like any other method.
To drive this home, let's take another class, where we have a private attribute called salary. Let's
say that we don't care about encapsulation and we are only trying to build a class fast and use it
in our project for our accountant client.
class Employee:
def __init__(self, name=None, email=None, salary=None):
self.name = name
self.email = email
self.salary = salary
Now, let's create a new employee object and initialize its attributes accordingly:
print(betim.salary) # 5000
Since salary is not being protected in any way, we can set a new salary for this new object
without any problem:
betim.salary = 25000
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 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 you can see below:
class Employee:
def __init__(self, name=None, email=None, salary=None):
self.__name = name
self.__email = email
self.__salary = salary
Let's create a new object:
print(betim.salary) # 1000
Trying to access any of the attributes is going to be followed with an error:
class Employee:
def __init__(self, name=None, email=None, salary=None):
self.__name = name
self.__email = email
self.__salary = salary
def get_info(self):
return self.__name, self.__email, self.__salary
Now, we can access the information of objects created by this class:
Inheritance in Python
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 around as
long as humans have.
This is also something that is going to last for future generations to come.
We can also have certain shared characteristics between objects and classes that we
implement ourselves in computer programming using inheritance. This includes both attributes
and methods.
Let's imagine that we have a class called Book. It should contain a title, an author, a number of
pages, a category, an ISBN, and so on. We are going to keep our class simple and use only two
attributes:
class Book:
def __init__(self, title, author):
self.title = title
self.author = author
def get_short_book_paragraph(self):
short_paragraph = "This is a short paragraph of the book."
return short_paragraph
Now, we can create an object from this class and access it:
class Book:
def __init__(self, title, author):
self.title = title
self.author = author
def get_short_book_paragraph(self):
short_paragraph = "This is a short paragraph of the book."
return short_paragraph
class BookDetails(Book):
def __init__(self, title, author):
Book.__init__(self, title, author)
# Here we are call the constructor of the parent class Book
def get_book_details(self):
description = "Title: " + self.title + ". "
description += "Author: " + self.author
return description
Note the syntax in which we tell Python that BookDetails is a subclass of the class Book:
class BookDetails(Book):
If we try to access this new method from objects of the class Book, we are going to get an error:
print(first_book.get_book_details())
# AttributeError: 'Book' object has no attribute 'get_book_details'
This happens because this method get_book_details() can be accessed only from objects of
BookDetails:
print(first_book_details.get_book_details())
# Title: Atomic Habits. Author: James Clear
We can, however, access any method that is defined in the parent class, which in our case is
the Book class:
print(first_book_details.get_short_book_paragraph())
# This is a short paragraph of the book.
In the previous classes, Book is considered a parent class or a superclass, whereas
BookDetails is considered a child class, or a subclass.
super() function
There is a special function called super() that we can use from a child class to refer to its parent
class without writing the exact name of the parent class.
class Animal():
def __init__(self, name, age):
self.name = name
self.age = age
class Cat(Animal):
def __init__(self, name, age):
super().__init__(name, age) # calling the parent class constructor
self.health = 100 # initializing a new attribute 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:
class Animal():
def __init__(self, name, age):
self.name = name
self.age = age
class Cat(Animal):
def __init__(self, name, age):
Animal.__init__(name, age) # calling the parent class constructor
self.health = 100 # initializing a new attribute that is not in the parent class
Even changing the order of the lines inside the child's constructor will not cause any error at all.
To see that in action, let's assume that we have a class attribute called name which is present
both in the parent and the child class. We want to access this variable from both the parent
class and the child class.
To do that, we simply need to write super() and then the name of the variable:
def get_product_details(self):
# Calling the variable from the parent class
print("Producer:", super().name)
seller = Seller()
seller.get_product_details()
# Producer: Samsung
# Seller: Amazon
How to use super() with methods of the parent class
We can similarly call methods in the parent class using super().
def get_details(self):
return f'Producer name: {self.name}'
def get_details(self):
# Calling the method from the parent class
print(super().get_details())
Types of inheritance
We can have different types of inheritance based on the relationship of parent classes and child
classes:
Single
Multi-level
Hierarchical
Multiple
Hybrid
1. Single inheritance
We can have a class that inherits only from another class:
class Animal:
def __init__(self):
self.health = 100
def get_health(self):
return self.health
class Cat(Animal):
def __init__(self, name):
super().__init__()
self.health = 150
self.name = name
def move(self):
print("Cat is moving")
cat = Cat("Cat")
class Creature:
def __init__(self, alive):
self.alive = alive
def is_it_alive(self):
return self.alive
class Animal(Creature):
def __init__(self):
super().__init__(True)
self.health = 100
def get_health(self):
return self.health
class Cat(Animal):
def __init__(self, name):
super().__init__()
self.name = name
def move(self):
print("Cat is moving")
cat = Cat("Cat")
class Location:
def __init__(self, x, y):
self.x = x
self.y = y
def get_location(self):
return self.x, self.y
class Continent(Location):
pass
class Country(Location):
pass
continent = Continent(0, 0)
print(continent.get_location()) # (0, 0)
Let's assume that we have a class called Date and another one called Time.
We can then implement another class then inherits from both classes:
class Date:
date = '2022-07-23' # Hardcoded date
def get_date(self):
return self.date
class Time:
time = '20:20:20' # Hardcoded time
def get_time(self):
return self.time
date_time = DateTime()
print(date_time.get_date_time()) # 2022-07-23 20:20:20
5. Hybrid inheritance
Hybrid inheritance is a combination of multiple and multi-level inheritance:
class Vehicle:
def print_vehicle(self):
print('Vehicle')
class Car(Vehicle):
def print_car(self):
print('Car')
class Ferrari(Car):
def print_ferrari(self):
print('Ferrari')
driver = Driver()
print(len('Python')) # 6
print(len([2, 3, -43])) # 3
We can take another example with a class called House. We can have different subclasses that
inherit methods and attributes from that superclass, namely classes such as Condo, Apartment,
SingleFamilyHouse, MultiFamilyHouse, and so on.
Let's assume that we want to implement a method in the House class that is supposed to get
the area.
Each type of living residence has a different size, so each one of the subclasses should have
different implementations.
getAreaOfCondo()
getAreaOfApartment()
getAreaOfSingleFamilyHouse()
getAreaOfMultiFamilyHouse()
This would force us to remember the names of each subclass, which can be tedious and also
prone to errors when we call them.
Luckily, there is a simpler method that we can use that comes from polymorphism.
Now the method that we are going to call depends on the class type of the object:
class Condo:
def __init__(self, area):
self.area = area
def get_area(self):
return self.area
class Apartment:
def __init__(self, area):
self.area = area
def get_area(self):
return self.area
Let's create two objects from these classes:
condo = Condo(100)
apartment = Apartment(200)
Now, we can put both of them in a list and call the same method for both objects:
# 100
# 200
This is how you implement polymorphism with methods.
class House:
def __init__(self, area):
self.area = area
def get_price(self):
pass
Then we'll implement subclasses Condo and Apartment of the superclass House:
class House:
def __init__(self, area):
self.area = area
def get_price(self):
pass
class Condo(House):
def __init__(self, area):
self.area = area
def get_price(self):
return self.area * 100
class Apartment(House):
def __init__(self, area):
self.area = area
def get_price(self):
return self.area * 300
As we can see, both subclasses have the method get_price() but different implementations.
We can now create new objects from subclasses and call this method which is going to
polymorph based on the object that calls it:
condo = Condo(100)
apartment = Apartment(200)
# 10000
# 60000
This is another example of polymorphism where we have specific implementation of a method
that has the same name.
Importing in Python
One of the main benefits of using a popular language such as Python is its large number of
libraries that you can use and benefit from.
Many developers around the world are generous with their time and knowledge and publish a lot
of really useful libraries. These libraries can save us plenty of time both in our professional work,
but also on our side projects that we may do for fun.
Here are some of the modules with very useful methods that you can immediately start using in
your projects:
import os
Now, let's import multiple modules at once:
import math
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:
result = math_module_that_i_just_imported.sqrt(4)
print(result) # 2.0
How to Limit What We Want to Import
There are times when we do not want to import a whole package with all its methods. This is
because we want to avoid overriding methods or variables that are in the module with the ones
that we want to implement ourselves.
We can specify parts that we want to import by using the following form:
print(sqrt(100)) # 10.0
Issues with Importing Everything from a Module
We can also import everything from a module, which can turn out to be a problem. Let's
illustrate this with an example.
Let's assume that we want to import everything that is included in the math module. We can do
that by using the asterisk like this:
from math import * # The asterisk is an indicator to include everything when importing
Now, let's assume that we want to declare a variable called sqrt:
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:
print(sqrt(100))
TypeError: 'float' object is not callable
How to Handle Exceptions in Python
When we are implementing Python scripts or doing any type of implementation, we are going to
get many errors that are thrown even when the syntax is correct.
These types of errors that happen during execution are called exceptions.
We indeed do not have to surrender and not do anything regarding them. We can write handlers
that are there to do something so that the execution of the program does not stop.
Exception – This is a class that is as a superclass of most other exception types that happen.
NameError – Raised when a local or global name is not found.
AttributeError – Raised when an attribute reference or assignment fails.
SyntaxError – Raised when the parser encounters a syntax error.
TypeError – Raised when an operation or function is applied to an object of inappropriate type.
The associated value is a string giving details about the type mismatch.
ZeroDivisionError – Raised when the second argument of a division or modulo operation is
zero.
IOError – Raised when an I/O operation (such as a print statement, the built-in open() function
or a method of a file object) fails for an I/O-related reason, e.g., “file not found” or “disk full”.
ImportError – Raised when an import statement fails to find the module definition or when a
from … import fails to find a name that is to be imported.
IndexError – Raised when a sequence subscript is out of range.
KeyError – Raised when a mapping (dictionary) key is not found in the set of existing keys.
ValueError – Raised when a built-in operation or function receives an argument that has the
right type but an inappropriate value, and the situation is not described by a more precise
exception such as IndexError.
There are many other error types, but you don't really need to see about them now. It is also
very unlikely that you are going to see all types of errors all the time.
We are going to do a division by zero, which is something that you have probably seen at
school:
print(5 / 0)
If we try to execute that, we are going to be greeted with the following error in the console:
We need to write inside the try block the part of the code that we expect is going to throw errors.
We then catch those types of errors inside the except block by also specifying the type of error
that we except to happen.
Let's see how we can deal with that error so that we also get informed that such error
happened:
try:
5/0
except ZeroDivisionError:
print('You cannot divide by 0 mate!')
As you can see, we are printing a message in the console once we have reached the part
where a division by 0 is happening.
name = 'User'
try:
person = name + surname # surname is not declared
except NameError:
print('A variable is not defined')
In the previous example, we have used variable surname before declaring it, therefore a
NameErroris going to be thrown.
When we use lists, it can be a common mistake to use an index that is out of range. This means
that the index we've used is larger or smaller than the range of indexes of the elements in that
list.
my_list = [1, 2, 3, 4]
try:
print(my_list[5])
# This list only has 4 elements, so its indexes range from 0 to 3
except IndexError:
print('You have used an index that is out of range')
We can also use a single try block with multiple except errors:
my_list = [1, 2, 3, 4]
try:
print(my_list[5])
# This list only has 4 elements, so its indexes range from 0 to 3
except NameError:
print('You have used an invalid value')
except ZeroDivisionError:
print('You cannot divide by zero')
except IndexError:
print('You have used an index that is out of range')
In the previous example, we try to initially catch whether there is any variable that is used but
not declared. If this error happens, then this except block is going to be taking over the
execution flow. This execution flow is going to stop there.
Then, we try to check whether we are dividing by zero. If this error is thrown, then this except
block is going to take over the execution and everything that is inside it is going to be executed.
Similarly, we continue with the rest of the errors declared.
We can also put more than one error inside parenthesis to catch multiple exceptions. But this is
not going to be helpful for us, since we do not know what specific error has been thrown. In
other words, the following method does work, but it is not recommended:
my_list = [1, 2, 3, 4]
try:
print(my_list[5])
# This list only has 4 elements, so its indexes range from 0 to 3
except (NameError, ZeroDivisionError, IndexError):
print('A NameError, ZeroDivisionError, or IndexError occurred')
The finally keyword
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:
try:
print(my_list[0])
except IndexError:
print('An IndexError occurred')
finally:
print('The program is ending. This is going to be executed.')
If we execute the previous block of code, we are going to see the following in the console:
try:
print(my_list[0])
except IndexError:
print('An IndexError occurred')
else:
print('No error occurred. Congratulations!')
If we execute the code above, we are going to get the following printed in the console:
It is very simple and all you have to do is declare a variable where you want to save the value
that the user types:
Now that you have had the chance to learn how to write Python, go out there and make a
positive impact with your lines of code.
freeCodeCamp.org © 2022