Author: - Post Category: Python, Resources - Date:May 5, 2020

๐Ÿ Python Crash Course 2020

During the course we will cover the basics but also dig into more advenced
concepts like file manipulation, object oriented programming and testing.

Categories: Python and Resources. Tags: Programming and python.
Programming with Python Crash Course

๐Ÿ Python is a general-purpose programming language, easy to read and it’s definitely worth learning. It can be used to build different types of programs from web-based applications to software with Graphical user interfaces, command-line applications, Machine learning and IT Automation.

Course Introduction

Course Topics

  1. Course Introduction

    A quick introduction to the Programming with Python Crash Course

  2. Environment Setup

    Setup our development environment, Install Python (Windows/Linux/macOS), Install VSCode and the code runner extension.

  3. Variables

    Learn about variables in Python. How to define a valid variable and invalid variables errors

  4. Strings

    In this module, we will study strings, one of the Python basic data types.

  5. Numbers and Booleans

    In this module, we will study two more basic data types, Numbers and Booleans in Python.

  6. Lists

    In this module, we will cover the basic data type called List, and how to use them.

  7. Dictionaries

    In this module, we will cover the basic data type called Dictionary, and how to use them.

  8. Tuples

    In this module, we will cover the basic data type called Tuple, and how to use them.

  9. Conditionals

    In this module we will learn a powerful concept in python programming, and how to make our code behave differently based on a given condition.

  10. Loops

    In the loops module we will cover how to write programs to repeat certain tasks avoiding code duplication.

  11. List Comprehension

    A shot and practical alternative of writing lists with the resulting values of loops and conditionals

  12. Functions

    In this module we will learn how to write clean and reusable code by grouping it into reusable function.

  13. Object-Oriented Programming (Classes)

    This module covers the fundamentals of OOP in Python, from classes definition to modules and class attributes.

  14. Object-Oriented Programming (Inheritance)

    In this module we will keep working with classes and learn how to define child classes and use special methods

  15. Files and CSV Manipulation

    In this module we will cover techniques to manipulate files and have a quick look at the CSV module

  16. Testing

    The final Module of the course covers Testing in Python, including practical examples and a general overview around the different types of tests.

Playlist Completa Corso (Work in Progress)

Programming with Python
#2 Environment Setup

Let’s see how to install Python and a couple of other tools that we will use during the course.

Environment Setup

Windows Installation: 

To install python on windows, visit the website, click the download button to get the installer then open it and follow the instructions. Make sure to check the box during “Add python to your path” during the installation.

Once the installation is completed open the windows power shell and type:

python -V

Install on Linux/macOS: 

Linux distributions come with python 2 and 3 preinstalled. to verify that run the following in the terminal

python -v 
python3 -v

The same applies to Mac OS.

If the package is not present in your system you can use the package manager to install it in Linux.

sudo apt install python3

For macOS visit the Download page of to download it.

Open the python interpreter:

In Linux, open the terminal and type


In Windows power shell


Text editor and Python files:

To do this I assume that you already use some sort of text editor or IDE like sublime, Athom or VisualStudio code. If not I recommend you to use Visual Studio code because it’s what I will be using during the course.

You can download it at, it’s free.
Once downloaded and installed, start the program and click the extensions tab and install the extension “code runner”

Now that your environment is ready, let’s get started with Python programming.

# 3 Variables

The first building block of most programming languages. Variables.


The first concept that we will cover is Variables. If you already master other programming languages this is nothing new to you.
A Variable is a word that we define and use to store information in our program for later use. It can only start with letters and underscore, It can contain numbers and underscore. It cannot start with a number of other characters and cannot contain characters that are not numbers, letters or underscore. 

# Valid variables: 
_name = "Fabio"
name = "Fabio"
name_1 = "Fabio"
# Invalid: 
-name = "Fabio"
9_name = "Fabio"
name! = "Fabio"

#4 Strings

Strings are sequences of characters. We can write strings in single, double or triple quotes.

Python Crash Course – Strings
'This is a python crash course'
"This is a python crash course"
"""This is a python crash course"""

We can assign stings to a variable and concatenate them using the plus sign”+” or the .format() method.

name = "Fabio"
profession = "Developer"

sentence = name + " is a " + profession

sentence = "{} is a {}".format(name, profession)

We can assign a string to a variable and access single elements or part of the string using square brackets and the slice method. 

Access a single character:

name = "Fabio"

Using the Slice method:


String Indexing

we can get the position of an element of the string (character) using the index method


Strings are Immutable:

name[0] = "B" 
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'str' object does not support item assignment

new_name = "B" + name[1:]

#5 – Numbers and Booleans

Numbers are another basic data type in Python. There are two types of numbers. Integers and float.

15 # integer
15.5 # float

We can use numbers to perform calculations.

# Sum
# Subtraction
# Moltiplication
# Division

# Exponenet
# Modulo
# Floor division

Like in math operations inside parentheses take precedence. Then the exponent, multiplication and division, modulo and floor and finally additions and subtractions.


A boolean value is either True or False. We use it to make decisions in our code based on a condition.

is_risky = True
is_safe = False

#6 – Lists

A list is a sequence of elements. usually of the same type but it can contain different elements.

Define a list

To define a list we use square brackets and separate each element with a comma.

[1, 3, 5 , 6 , 7]
["Developer", "Writer", "Student", "Teacher"]

Access a list element:

We can access elements in a list by assigning it to a variable as we did with strings. Then we can use square brackets and the index method in the same way.

>>> jobs = ["Developer", "Writer", "teacher"]
>>> jobs[0]
>>> jobs[1:]
['Writer', 'teacher']
>>> jobs[:2]
['Developer', 'Writer']
>>> jobs[0:2]
['Developer', 'Writer']
>>> jobs.index("Writer")

Add elements

We can do a lot more with lists, like add an element to it using the append method. It adds it at the end of the list.

>>> jobs.append("Front-end engineer")
>>> jobs
['Developer', 'Writer', 'teacher', 'Front-end engineer']

Insert an element:

we can also insert an element in a specific position. The first argument is the index where we want to insert the element, it’s an integer. if the integer is a number greater than the length of items of the list the new element will be inserted at the end.

>>> jobs.insert(0, "SEO")
>>> jobs.insert(10, "Back-end developer")
>>> jobs
['SEO', 'Developer', 'Writer', 'teacher', 'Front-end engineer', 'Back-end developer']

Remove elements:

We can use pop() to remove elements from the end of the list or a specific element if we specify it’s index position.

>>> jobs.pop()
'Back-end developer'
>>> jobs.pop(3)
>>> jobs
['SEO', 'Developer', 'Writer', 'Front-end engineer']

Using the remove() method:

We can use remove to delete the first occurrence of an element

>>> jobs.append("Developer")
>>> jobs.remove("Developer")
>>> jobs
['SEO', 'Writer', 'Front-end engineer', 'Developer']

Sort elements:

‘Sort’ organizes elements in ascending order

>>> jobs
['SEO', 'Writer', 'Front-end engineer', 'Developer']
>>> jobs.sort()
>>> jobs
['Developer', 'Front-end engineer', 'SEO', 'Writer']

Clear the list:

We can remove all elements of a list in two ways

jobs = []

Attention: when you see the following symbol ” >>> ” in the code blocks I am executing them inside the Python interpreter. When there is no symbol before the line of code its in a Python file.

#7 – Dictionaries

In this video, we are going to cover another basic data type called Dictionary. A dictionary is a sequence of mutable elements. It’s a key-value pair of elements. Dictionaries are defined as withing curly brackets. Its elements are separated by a comma while the key is separated from the value with a semi-colon.

Define a dictionary:

user_stats = {"Name":"Fabio", "Age":40, "Job":"Developer","Skills":["Python","PHP","Laravel", "HTML5", "CSS3","Javascript"]}

A dictionary can contain all type of data types, however, its key is limited to certain data types like strings and numbers.

Access elements:

we can access elements using the keys inside the square brackets or with the get()method.



We can also get all values or all keys using the values() and keys() methods.


Add elements:

We can add a new element in this way

user_stats["Wife"] = "Serena"

Update elements:

We can assign a new value to an element using the equal sign.

user_stats["Job"] = "Full-stack Engineer"

Update method:

We can also use the update method. This method accepts a dictionary object as a parameter. It actually extends the original dictionary.

user_stats.update({"Interests": ["Coding", "Cycling"]})

Remove elements:

We can use the pop method to remove elements using their key.


There is a lot more you can do, for a deep dive check the documentation when you need it.

Clear the dictionary:

Using the clear method, we can remove all elements in a dictionary.


Or you could assign to the dictionary an empty dictionary, that will work too.

user_stats = {}

In the next video, I’ll show you another basic data type called a tuple. See you there. 

#8 – Tuples

In this video, I’ll give you a brief overview of another basic data type called a tuple. A tuple is an immutable sequence of elements.


It is defined within parentheses and each element is separated by a comma. Elements of a tuple can be of any datatype. Let’s see how to define a tuple.

user_data = ("Fabio","Pacifici","Developer","Norwich")

Assign tuples elements to variables

Knowing that a tuple is immutable, allows us to assign each element to a variable in this way

name,last_name,job,city = user_data

Access elements

We can then access the tuple’s elements either using square brackets like we do with other data types or using the newly created variables.


Slice method:

like with strings, lists and dictionaries we can use the slice method to get a portion of a tuple.


Add Elements

Tuples are an immutable sequence of elements. Meaning that once defined we cannot change its elements. 

However, we can extend the tuple with another tuple using the __add__  special method. This method accepts as parameter another tuple and returns one tuple containing both tuples.

new_data = user_data.__add__((40, ""))


Attempt to grab one of the elements and replace its content will result in a TypeError.

user_data[2] = "Full-Stack Engineer"
# TypeError: 'tuple' object does not support item assignment

So if we want to replace the content of the tuple we can either create a new tuple or replace its entire content

user_data = ("Fabio","Pacific","Full-Stack engineer")

new_user_data = user_data[0:2].__add__(("Developer",40)) 


We can use the index() method to find the position of an element in the tuple. This method accepts the element as a parameter

# returns 0

Count elements recurrence

The count() method instead returns how many time a given element is present inside the tuple. Returns zero if the element is not in the tuple

# returns 1
# returns 0


We can remove all elements of a tuple by assigning to the variable that stores the tuple an empty tuple. To remove one element from the tuple we have to create a new tuple and exclude the element we want to remove.

user_log = ("Fab", True, "Monday", "Fabio", "Pacifici" )
new_log = user_log[0:2].__add__(user_log[3:])

new_log = ()

That’s pretty much it. You can read more about tuples visiting the official python documentation and practice with what we studied in these videos.

#9 – Conditionals

In python like in most programming languages, we can use conditionals to make our program behave differently based on a specific condition. Conditionals are flow control tools that we will use over and over during our programming journey.

Imagine a typical situation where you need to take a decision in your daily life. It’s most of the time based on a condition. Like, if tomorrow is sunny, I’ll go to the beach, if it’s cloudy and cold I’ll go to a museum if it’s a rainy day I’ll stay at home and study Python otherwise I decide later what to do.

As you see there is a pattern that repeats over and over: 
IF something happens I DO that.
IF something else happens I DO that.
IF something else happens and something else happens I DO that.
OTHERWISE, I do that.

That is pretty much a control flow structure that lets us decide what to do based on the weather.    In Python, this is called ‘if/elif/else’ statement. Where the if and ‘elif’ expect a condition to be true to execute the code block inside them while the else block runs whenever none of the conditions is evaluated to true. Let’s see them in action with a practical example.

Define an if block:

name = "Fabio"
if name == "Fabio":
  print("Hello, {}".format(name))

To define an if block we use the “if” keyword followed by a condition and then a semicolon. The body of the if-block on the next line is indented to the right. To execute the code in the body the condition must evaluate to a boolean True value otherwise the code will not run.

The else clause:

Like in our real-world example we used an otherwise statement, here we replace the otherwise with an else clause that executes if none of the conditions set in the if blocks are True.

    name = "Louise"
    if name == "Fabio":
        print("Hello, {}".format(name))
        print("Hello, Stranger")

This is a basic example that can help you understand the basics of conditionals. We can use conditionals in many different ways as you will see later when we study loops and functions.

Elif block

Like in our real-world example we used many times the keyword “IF” to decide based on the weather what to do.    we can use an elif block to specify another action to execute based on a different condition.

    fabio_status = {"user_name":"Fabio", "logged_in": False}
    sere_status = {"user_name":"Sere", "logged_in": False}

    if fabio_status["logged_in"]:
        print("{} is currently logged in".format(fabio_status["user_name"]))
    elif sere_status["logged_in"]:
        print("{} is currently off line. But {} is online".format(fabio_status["user_name"], sere_status["user_name"]))
        print("Everyone is online.")

If we play with the logged_in values of both dictionaries we will get a different message every time we set one of them to True. But what if both users in our example are logged_in?
In this case, we only get the first user status message. The one in the first if block.

The “and” keyword

In our real-world example instead, we also said that if it was cloudy and cold we would go to a museum. Here we checked two conditions with and, and that’s what we are going to do next.

    fabio_status = {"user_name":"Fabio", "logged_in": True}
    sere_status = {"user_name":"Sere", "logged_in": True}

    if fabio_status["logged_in"] and sere_status["logged_in"]:
        print("{} and {} are both online now!".format(fabio_status["user_name"], sere_status["user_name"]))
    elif fabio_status["logged_in"]:
        print("{} is currently logged in".format(fabio_status["user_name"]))
    elif sere_status["logged_in"]:
        print("{} is currently off line. But {} is online".format(fabio_status["user_name"], sere_status["user_name"]))
        print("Everyone is online.")

The “not” keyword

The not keyword turns things upside down.    It returns true when a given value is False and False when is True.

    logged_in = True

    if not logged_in:
        print("user not online")
        print("user online")

“and” and “not” can be used to chain multiple conditions as we did in the previous example. I’ll leave it to you.

The ‘or’ keyword

The “or” keyword lets the code block run in one of the condition evaluates to True.

    x = 11
    if x > 5 and x < 10:
        print("The number is {}".format(x))
    elif x == 5 or x == 10:
        print("Your number is {}".format(x))
        print("Number {} is out of range".format(x))

if we run the code setting the x to a number between 6 and 9 we will get the first block, instead, if the x is either 5 or 10 we will get the second block inside the ‘elif’ block.  otherwise, we print the message in the else block.

Putting altogether

Let’s see how we can use altogether with a quick project.

    name = input("Type your name here: ").lower()
    age = int(input("Type your age: "))

    if name == "fabio" and age == 40 or name == "fabio pacifici" and age == 40:
    print("Welcome, {}. You are the Admin".format(name))
    elif name == "boris":
        print("Sorry {}, you have been banned.".format(name))
    elif age < 18:
        print("Sorry, you don't have the minimum age to access this content")
        print("Welcome, {}".format(name))

In the next video, I’ll show you another control flow tool called Loops.

#10 – Loops

In the previous video we studied conditionals and how to make our program behave differently based on a condition. In this episode, we are going to see how a loop works. Loops are a way to iterate sequences like strings, lists, dictionaries, tuples, sets etc and keep executing our code until a condition returns True. In this section, we are going to cover For and While loops.

The for loop

A for loop is a control flow tool used to iterate through elements of a sequence and return the code in its body.

How to define a for loop

To define a for loop we use the keyword “for” followed by a variable placeholder than the sequence that we want to iterate and a semi-colon to close the line. The body of the loop in the following lines is indented to the right. The for loop can be used to iterate over sequences of numbers using the range() function that is our first example below. But also very useful with strings, lists, dictionaries tuples and sets.

Simple for loop using the range function

for i in range(10):

Executing this code will result in the following output in the terminal:


The range function returns a sequence of elements in this case numbers up to the parameter we specify. Alternatively, It can accept other parameters to set the starting and ending point as well as the steps.

for n in range(10, 100, 10):

In the example above, the first parameter is the starting point for our sequence, then the ending point and finally the steps to increment the count.

The output of the code above will be the following:


Looping over a string

In one of the previous videos we studied strings and we said that strings are sequences of characters. As a sequence, we can iterate through it and get every single element using a for a loop.

name = "Fabio Pacifici"
for w in name:

The example above returns each character of the string one per line.


In the same way, we can use the for loop with lists.

Looping over a list

When we studied lists we said that a list is a sequence of elements so we can use the for loop to loop over its elements and perform any kind of thing with each element. In the following example, we will create a basic program that asks the user if he likes a type of fruit and outputs a message based on his input.

fruits = ["Apple","Banana", "Apricot","Pear", "Grape"]
for fruit in fruits:
    response = input("Do you like {}? [y/n]".format(fruit)).lower()
    if response == "y":
        print("Mee too")
    elif response == "n":
        print("I love {}".format(fruit))
        print("You exit the loop")

After lists, we studied Dictionaries and Tuples. Both are sequences, therefore, we can iterate through them using the for a loop. Let’s see an example.

Looping a Dictionary

user_stats = {"Name": "Fabio", "Age": 40, "Job": "Developer", "Skills": [
    "Python", "PHP", "Laravel", "HTML5", "CSS3", "JavaScript"]}

for key, value in user_stats.items():
    print("This is the key: {} and this the value: {}".format(key,value))
for key in user_stats.keys():
    print("Only the key: {}".format(key))
for value in user_stats.values():
    print("Only the value: {}".format(value))

The code here outputs the following:

This is the key: Name and this the value: Fabio
This is the key: Age and this the value: 40
This is the key: Job and this the value: Developer
This is the key: Skills and this the value: ['Python', 'PHP', 'Laravel', 'HTML5', 'CSS3', 'JavaScript']
Only the key: Name
Only the key: Age
Only the key: Job
Only the key: Skills
Only the value: Fabio
Only the value: 40
Only the value: Developer
Only the value: ['Python', 'PHP', 'Laravel', 'HTML5', 'CSS3', 'JavaScript']

In the example above with dictionaries, we used three types of methods. In the first, we used the items() method of the dictionary object and assigned it’s key and value to the variable placeholders key and value that we defined after the ‘for’ keyword. Then we printed a message with both results.

In the second example, the keys() method was used and each key stored in the “key” variable placeholder then we output a message with only the keys.

Finally, we used the values() method to grab only the values and storing the content in a variable placeholder “value” before printing a message.

we can call these variable placeholders as we like as long as they make sense.

Looping Tuples

Finally, a tuple as a sequence can also be used with the for loop in the same way we did before with the other data types like strings and lists.

user_data = ("Fabio", "Pacifici", 40,"Developer", "Norwich", "Italian", True)

for el in user_data:



Make sure to practice with loops as they are one of the most powerful tools we have. Next, another loop called While.

The ‘while’ loop

The while loop keeps executing its code as long as the condition that we pass to it returns true. Attention! Make sure that inside the loop body you change the value of the condition to make sure it ends at some point otherwise you will fall into an infinite loop and you will need to close your terminal window because it will be stuck forever.

To define a for loop we first initialise a variable with the condition that we want to use in our loop. Then we use the “while” keywords followed by our condition and a semi-colon to close the line. the body of the loop is indented to the right. then we need to change the value of your variable to make sure our code stops execution when we expect it to do so.

Lets first see a basic example then we will put all together with a simple project.

A basic while loop

number = 0
while number < 10:
    print("We are in loop mode, Iteration number: {}".format(number))
    number = number + 1


We are in loop mode, Iteration number: 0
We are in loop mode, Iteration number: 1
We are in loop mode, Iteration number: 2
We are in loop mode, Iteration number: 3
We are in loop mode, Iteration number: 4
We are in loop mode, Iteration number: 5
We are in loop mode, Iteration number: 6
We are in loop mode, Iteration number: 7
We are in loop mode, Iteration number: 8
We are in loop mode, Iteration number: 9

inside the loop, we printed a message and after it we incremented the variable number of 1. each time the loop runs the value of number will be number +1. so the first time number is 0 then we add 1 and in the next iteration number is 1 that is still less than 10 so we print the message then we increment number that is 1 to 1+1 that is 2 and we keep going until our condition becomes false because when the loop runs 10 times the value of the number is 9 that is the last time its less than 10 so at the next iteration our condition is false and the loop stops.

If we forget to increment the value of number then we will get an infinite loop. the value can be incremented as we like, it can be 1, 2, 3 or any number you want.

number = 0
while number < 10:
    print("We are in loop mode, Iteration number: {}".format(number))
    number = number + 3

Simple Project – The lucky number game

Let’s use our knowledge to create a simple program that asks the user a number and returns its lucky number

play = True
while play == True:
    number = input("Type  number ")
    lucky_numb = int(number) * 3
    print("Your lucky number is {}".format(lucky_numb))
    wants_to_play = input("Do you want to continue? [y/n]")
    if wants_to_play.lower() == "y":
        play = True
    elif wants_to_play.lower() == "n":
        play = False
        print("c'mon, just type y or n")
        wants_to_play = input("Do you want to continue? [y/n]")
        if wants_to_play.lower() == "y":
            play = True
        elif wants_to_play.lower() == "n":
            play = False

The game will output something like that depending on the numbers you pass:

Type  number between 0 and 9 3
Your lucky number is 9
Do you want to continue? [y/n] d
c'mon, just type y or n
Do you want to continue? [y/n] y
Type  number between 0 and 9 45
Your lucky number is 135

That’s it for loops! while you study these concepts if you enjoyed the video make sure to subscribe, leave a comment and a thumb up. it takes a second.

# 11 – List Comprehensions

Now that we covered conditionals and loops in the previous videos we can talk about list comprehensions. A list comprehension is a shortcut to create lists where each element is the result of an operation. A list comprehension uses for loops and if-clauses to create a new list, therefore, we had to learn about loops and conditional first.

How to define a list comprehension

A list comprehension is defined within square brackets – inside them:

  • First, there is an expression
  • followed by a for clause,
  • then zero or more for or if clauses. The result will be a new list resulting from evaluating the expression in the context of the for and if clauses which follow it.

Example with a single for loop

In the following example, we will use only one ‘for’ loop.

skillsList = ["PHP", "Python", "HTML5", "CSS3", "Javascript"]

skillsMsg = [ "{}: {}".format(index +1, skill) for index, skill in enumerate(skillsList) ]


['1: PHP', '2: Python', '3: HTML5', '4: CSS3', '5: Javascript']

Example with conditionals

Now let’s add a conditional to check if our result matches our decided conditions:

print([n*2 for n in range(10) if n%2 != 0])


[2, 6, 10, 14, 18]

In the example, we first defined our expression saying that we wanted our result multiplied by 2 then we started a for loop and used the range function to generate a list of numbers, then we used the if clause to check if the resulting number of the list was a multiple of 2 using the modulo operator.

Using a classic approach we would have written the code above like follows:

resultList = []
for n in range(10):
    if n % 2 !=0:
        result = n*2

as you see the code to write is only one line instead of six.

Example using functions

roundedNumbers = [round(number / 2) for number in range(10)]


[0, 0, 1, 2, 2, 2, 3, 4, 4, 4]

Example getting the output as a tuple with multiple elements

roundedNumbers = [(number/2, round(number / 2)) for number in range(10)]


[(0.0, 0), (0.5, 0), (1.0, 1), (1.5, 2), (2.0, 2), (2.5, 2), (3.0, 3), (3.5, 4), (4.0, 4), (4.5, 4)]

Example with operations

print([(n, n*3, n**2) for n in range(10) if n%2 != 0])


[(1, 3, 1), (3, 9, 9), (5, 15, 25), (7, 21, 49), (9, 27, 81)]

Example with methods

names = ["Fabio", "Serena", "Luke", "Carlo", "Darius", "Luis", "Danny"]
usersName = [ "username: {}".format(name.lower()) for name in names if name != "Fabio" if name != "Serena"]
print("Users:", usersName)


Users: ['username: luke', 'username: carlo', 'username: darius', 'username: luis', 'username: danny']

In this example we used two methods on our expression, the format method and the lower() method on the name variable.

Example with multiple loops

using tuples to wrap the expression elements we can return the result of two or more for loops like so

print([(x,y) for x in range(2,10) for y in range(3,20) if x%y == 0])


[(3, 3), (4, 4), (5, 5), (6, 3), (6, 6), (7, 7), (8, 4), (8, 8), (9, 3), (9, 9)]

Example with nested lists

we can use multiple for loops as well as multiple if clause inside our list comprehensions. We can also nest multiple list comprehensions. In fact, the first expression can be the result of another list comprehension.

users_statuses = [
["Fabio", "Serena", "Luke", "Carlo", "Darius", "Luis", "Danny"],
["Online", "Online", "Offline","Offline","Online", "Offline","Offline"]
user_stats = [ [element[i] for element in users_statuses] for i in range(7)]


[['Fabio', 'Online'], ['Serena', 'Online'], ['Luke', 'Offline'], ['Carlo', 'Offline'], ['Darius', 'Online'], ['Luis', 'Offline'], ['Danny', 'Offline']]

In this example, we used two for loops and nested one list comprehension into the other. The expression of our list is the result of the list comprehension taking takes a specific element of each list thanks to the external for loop that returns a number at each iteration that we can pass to the first list to get the element at the specific location on every iteration.

List comprehensions can be very handy once you understand how they work and can help write less code while keeping it readable. Find out more:

#12 – Functions

Functions are reusable code blocks, a way of creating a code once and make it reusable in different parts of our project. Helping us to respect the DRY principles (Don’t Repeat yourself).

If you create a bit of code and find yourself copying and pasting it over and over, it’s a good use case for a function. In this tutorial I am not going to show you how to create a function that writes a Fibonacci series or other mathematical oriented functions, instead, I’ll teach you how to write functions and use them, it’s up to you then to define your use cases based on what you need to accomplish.

Let’s start from the beginning: how do you define a function in Python?

How to define a function:

A function definition starts with the keyword “def” follows the name of the function, a pair of parentheses and a semicolon to close the line. The body of the function starts on the following line and it’s indented to the right.

Inside the parentheses, we can define arguments that our function can use inside its body.

Arguments are like variables names that we define only within the scope of the function and they are not available outside.

Functions can define variables inside their body and these are accessible only inside the function.


Let’s imagine code like the following:

nameOne = input("What's your name? ")
greetOne = f"Hello {nameOne}!"
nameTwo=input("What's your name? ")
greetTwo = f"Hello {nameTwo}!"
nameThree=input("What's your name? ")
greetThree = f"Hello {nameThree}!"

In this example here we had to copy the same three lines of code over and, and define each time an input function a greeting message and print it. We could have printed directly the greeting wrapping the message in a print function but still, we copied two lines of code and what if we wanted to reuse the greeting message somewhere else? that’s not much practical.

Let’s convert it in a function and see how they work.

def greetings():
    name = input("What's your name")
    msg = f"hello {name}"

one = greetings()
two = greetings()
three = greetings()

In the example, we printed a message to the console using the name variable that we defined inside our Function. The name variable is only accessible within the scope of the function.

Functions’ Scope

In fact, if we try to print the name variable from outside the function body we will get an error because the name variable is not defined in the global scope.

NameError                                 Traceback (most recent call last)
----> 1 print(name)

NameError: name 'name' is not defined

But of course, if we instead try to print the nave variable inside the function block we will get its value

def greetings():
    name = input("What's your name")
    msg = f"hello {name}"


If we define the name variable in the global scope instead we can use it like we usually do. And it will be treated as a completely different variable with nothing to do with the variable inside the function body.

name = "Random name here"
def greetings():
    name = input("What's your name")
    msg = f"hello {name}"



Random name here


Using the global keyword

To use and modify a variable defined in the global scope we can use the global keyword.

namesList = []

def askName():
    return input("Add a name? ")

def addName():
    play = True
    while play == True:
        name = askName()
        global namesList
        addMore = input(
            "Do you want to add another name to the list? [yes/no]")
        if addMore == "yes":
            play = True
            play = False


Using the Return keyword

Instead of printing a message inside the function, we can use the “return” keyword. This keyword lets us store the result of a function for later use. let’s see the same function but using the return keyword instead of the print function.

def greetings():
    name = input("What's your name")
    msg = f"hello {name}"
    return msg
one = greetings()
two = greetings()
three = greetings()

In this example here we have stored the resulting message so we won’t see it in the terminal when we type our name in the input field. By assigning the result of the function to a variable however, we will be able to access it’s message whenever we print the variable.



hello Fabio

Example with external modules

Inside a function, we can also import a specific module if we know that it will be used only here. In the next example, we will create a function that calculates the factorial of a sequence of numbers. Basically it multiplies all the numbers in the sequence and retunds the result.

def calc_fact(n):
    import math
    return math.factorial(n)



That is equivalent to the following operation.


Inside the function definition we imported the math module then we used the factorial() method to run the calculation and we returned it. The module is only available inside the function using it this way we have a function that contains all it needs to run correctly. However, if you have other functions that require a specific module to work, it’s better to import the modules you need only once at the top of the file.

Functions with Arguments:

Functions can accept arguments between parentheses. Arguments are like variables only available to the function and that we need to use when calling the function. An argument can be a positional or keyword argument. Positional arguments need to be defined first.

def collectUserStatus(username, email, system="Ubuntu", status=False):
    user_stats = {"Username":username, "email":email, "system":system, "Status":status}
    return user_stats



TypeError                                 Traceback (most recent call last)
      3     return user_stats
----> 5 print(collectUserStatus())

TypeError: collectUserStatus() missing 2 required positional arguments: 'username' and 'email'

In a function declaration, a Positional argument is always required, while a keyword argument has a default value so we can safely omit it when we call the function.

Let’s call the function passing to it the username and email and see what we get.

print(collectUserStatus("fabio","[email protected]"))


{'Username': 'fabio', 'email': '[email protected]', 'system': 'Ubuntu', 'Status': False}

Inside the function, we defined a variable user_stats and passed to it a dictionary containing our arguments then we returned it. We can assign the result of the function to a variable so we can then iterate over the dictionary using a for loop or use the for loop inside the function declaration.

Inside a function definition, we can use all sort of data types and control flow tools that we studied so far. let’s use a for loop inside it to iterate over the dictionary and output a message instead.

Functions with loops

Inside a function definition, we can use all the data types, control flow tools, modules or even call another function. In the following example, we will see how to use loops inside a function to iterate sequences.

def collectUserStatus(username, email, system="Ubuntu", status=False):
    user_stats = {"Username":username, "email":email, "system":system, "Status":status}
    return user_stats

stats = collectUserStatus("fabio","[email protected]")
for key, val in stats.items():

Execute the code above outputs:

email:[email protected]

In the previous example we returned from the function a dictionary then outside the function definition we saved the function’s returned value in a variable and then we used a for loop to iterate over its items. The same can be done inside the function with a difference.

def collectUserStatus(username, email, system="Ubuntu", status=False):
    user_stats = {"Username":username, "email":email, "system":system, "Status":status}
    for key, val in user_stats.items():
collectUserStatus("fabio","[email protected]")

In this example inside the for loop we used the print function to print the all the elements otherwise using return stops the execution of the loop after the first iteration.

Call a function from another function

As we mentioned earlier we can call a function from inside another function. let’s define two functions first.

def askName():
    name = input("What's your name? ")
    return name

def calcAge():
    import datetime
    name = askName()
    yob = input(f"What's your year of birth, {name}? ")
    current_year =
    age = current_year - int(yob)
    return "{} is {}".format(name, age)


There is a lot to learn about functions, head over the documentation to learn more.


#13 – Object-oriented Programming | Classes

In this video, we are going to cover a powerful concept of many programming languages. Object-Oriented Programming or OOP.

Let’s first start by quoting the definition provided by Wikipedia:

Object-oriented programming (OOP) is a programming paradigm 
based on the concept of "objects", which can contain data, 
in the form of fields (often known as attributes or properties),
and code, in the form of procedures (often known as methods).

To understand OOP we first need to understand its terminology. The key terms here are:

  • class
  • object
  • attributes
  • methods

A class is a sort way of bundling data and functions together.

When we create a class we can use it to create multiple instances of it and each instance is an object.

Attributes are data that we use inside our classes and they can be of any type from numbers to dictionaries.

Methods are functions used in a class to manipulate its data. Methods can contain any data type as well as any control flow tool like conditionals and loops.

How to define a Class:

Let’s start by looking at the syntax to define a new class.

class ClassName:

First, we have the “class” keyword, followed by the name of the class, and a semi-colon.

The Class name starts with a capital letter. If the class name contains more than one word, each word should start with a capital letter.

The body of the class is indented to the right. Like with the control flow tools we can use the pass keyword to define a class that we will implement later.

Instantiate a class object

Once a class is defined we can instantiate it.

class Person:

user = Person()


<__main__.Person object at 0x061E9DC0>

In the example above we have created a Person class and used the pass keyword so we can implement the rest of the class later.

On the next line, we have instantiated the Person class and stored its instance inside the variable user. That’s how we work with classes most of the time.

Printing the user variable in the next line returns the Person object instance definition. “<main.Person object at 0x06A3C1C0>”

Define Class Attributes:

We can replace the pass keyword now and define one attribute called name to which we assigned the value of “Fabio”.

We can access the name attribute using the dot notation. will return the content of the name attribute of the Person class object.

class Person:
    name = "Fabio"

user = Person()

We can also change the value of an attribute by assigning to it a new value. Here we used again the dot notation again calling the user object instance and referencing to the name attribute. Then we assigned to it a new value.

Before we move to see how methods work we must introduce a special method called the constructor. As it is now, every time we instantiate the class Person we will get the same result and that’s not what we use classes for.

Define class constructor

The syntax of a constructor method is the same as of a function. We start by indenting the “def” keyword to the right inside the class then we follow with the name of the special constructor method that is “init()”. Inside the parentheses, we first specify the self special attribute that refers to the class instance then we define the building blocks of our class by defining arguments as we do with functions. Inside the body of the constructor, we must initialize these attributes by using the self attribute followed by a dot then the attribute name and assign it back to the attribute. So every time we instantiate a new object out of this class the constructor defines the class attributes that we need to pass making each object unique and dynamic. The key here is to make sure to pass to the constructor body every argument you want to use to initialize the object using the self.attribute_name_here = attribute_name_here otherwise you will get an error message when you try to access the attribute as it is not reconsigned.

class Person:
    def __init__(self,name): = name

user_1 = Person("Serena")
user_2 = Person("Fabio")


In the example above we defined a special method init() that is our constructor. It runs every time we instantiate a new object using the parameters that we pass to it.

The first argument self is a special attribute that refers to the object instance. We must use it every time we define a new method or reference to an attribute inside a class.

Define Multiple attributes and Assign default values

Arguments of the constructor are like those of a function, positional and keyword arguments. To instantiate a new object we must pass to it every positional argument as these are mandatory, while we can skip keyword arguments as they already have a default value assigned.

class Person:
    def __init__(self, name, age, nationality="Italian", skills=[]): = name
        self.age = age
        self.nationality = nationality
        self.skills = skills
user_a = Person()


TypeError                                 Traceback (most recent call last)
----> 1 user_a = Person()

TypeError: __init__() missing 2 required positional arguments: 'name' and 'age'

In the example above we have instantiated the class without any argument, therefore, we get a type error saying that: missing 2 required positional arguments: ‘name’ and ‘age’.

Let’s instantiate the object properly this time.

user_a = Person("Fabio",40, skills=["Python","PHP", "JavaScript"])
print(, user_a.age, user_a.skills)


<__main__.Person object at 0x014D2C28>
Fabio 40 ['Python', 'PHP', 'JavaScript']

As you see here we have instantiated the Person object and passed to it all the required argument plus we defined the last keyword argument “skills” and passed to it a list of skills so when we access the skills attribute of the object we will get its content.

Define a Method

So we said that methods are like functions but are defined inside the class. to define a method we use the same syntax but inside the parentheses, we need to use the “self” special attribute so it can access the class instance attributes. Let’s see it in action using again our person class.

import datetime
class Person:
    def __init__(self, name, age, skills=[]): = name
        self.age = age
        self.skills = skills
    def calc_year_of_birth(self):
        now =
        yob = now - self.age
        return "{} was born in {}".format(, yob)

user_a = Person("Fabio",50)


'Fabio was born in 1970'

In the previous example, we defined a method inside the Person class that calculates the year of birth of the user. Inside the method, we defined two variables, variables defined inside a method does not need to be prepended with the self attribute as they are local variables inside the method scope and not class attributes. But when we refer to one of the class attributes we must use the self.attribute_name syntax.

What if we don’t?

In case we forget to use the self attribute we will get a NameError as the variable we are referring to doesn’t actually exist anywhere. Using the same example above…

import datetime
class Person:
    def __init__(self, name, age, skills=[]): = name
        self.age = age
        self.skills = skills
    def calc_year_of_birth(self):
        now =
        yob = now - self.age
        return "{} was born in {}".format(name, yob)

user_a = Person("Fabio",50)


NameError                                 Traceback (most recent call last)
     12 user_a = Person("Fabio",50)
---> 13 user_a.calc_year_of_birth()

 in calc_year_of_birth(self)
      8         now =
      9         yob = now - self.age
---> 10         return "{} was born in {}".format(name, yob)
     12 user_a = Person("Fabio",50)

NameError: name 'name' is not defined

That’s it for this episode, in the next video, we will cover Hineritance and subclasses. I’ll see you there.

#14 – inheritance

Before moving to the next topic it’s a good time to see how to document our code. What I am going to show you is not only useful to document our classes and methods but can be used also to describe functions in the same way.

To describe what a certain piece of code does in Python we use doc blocks by writing the text within tripe quotes right under the class, method or function definition.

How to document your code:

class Person_2:
    """ ## Defines a Person class 
        :str name = A string containing the name of the person
        :str profession = A string containing the person's professsion  
    name = "Fabio"
    profession = "Full-stack developer"

    def introduction(self):
        """ returns an introduction message for the given person """
        return "Hi, my name is {} I am a {}".format(, self.profession)

user = Person_2()

user_2 = Person_2() = "Serena"
user_2.profession = "Writer"



Hi, my name is Fabio I am a Full-stack developer
Hi, my name is Serena I am a Writer
Help on class Person_2 in module __main__:

class Person_2(builtins.object)
 |  ## Defines a Person class 
 |  :str name = A string containing the name of the person
 |  :str profession = A string containing the person's professsion
 |  Methods defined here:
 |  introduction(self)
 |      returns an introduction message for the given person
 |  ----------------------------------------------------------------------
 |  Data descriptors defined here:
 |  __dict__
 |      dictionary for instance variables (if defined)
 |  __weakref__
 |      list of weak references to the object (if defined)
 |  ----------------------------------------------------------------------
 |  Data and other attributes defined here:
 |  name = 'Fabio'
 |  profession = 'Full-stack developer'


Another thing that you can do to provide details about your code is to define the type of attributes in use.

How to declare attribute’s types:

class Athlete:
    """ Define athlete class """
    discipline: str
    medals: int
    def __init__(self, discipline, medals):
        self.discipline = discipline
        self.medals = medals



Help on class Athlete in module __main__:

class Athlete(builtins.object)
 |  Athlete(discipline, medals)
 |  Define athlete class
 |  Methods defined here:
 |  __init__(self, discipline, medals)
 |      Initialize self.  See help(type(self)) for accurate signature.
 |  ----------------------------------------------------------------------
 |  Data descriptors defined here:
 |  __dict__
 |      dictionary for instance variables (if defined)
 |  __weakref__
 |      list of weak references to the object (if defined)
 |  ----------------------------------------------------------------------
 |  Data and other attributes defined here:
 |  __annotations__ = {'discipline': <class 'str'>, 'medals': <class 'int'...

As you can see from the output of the help command we get the documentation and at the end, there is another section that shows how data and other attributes of the class are defined. In this case, we know that the discipline must be of a class string and that medals must be of a class integer. This prevents that others using our code, pass the wrong data type when instantiating the class object.

The help command in case you wonder is used to print the documentation of classes but also of methods or functions.

That’s it for the documentation.

How to Define a subclass

Now that we know how to document our code let’s move forward to class inheritance. Classes are essentially a blueprint to build something over and over. like plans to build a bicycle, a house or any factory product.

Using classes we can define the building blocks while with subclasses we can extend them. We will refer to the main class as parent class while the sub-classes are often called child-classes.

A child class basically inherits attributes and methods from his parent and therefore has access to them.

The child class can use these methods and attributes but can also define new methods and attributes or override the parent’s attributes and methods.

Let’s see a practical example.

I am going to define a Vehicle class then I’ll create child-classes for bicycles, cars, ferries and planes.

class Vehicle:
    goes_on = ""
    """ ## Defines the main Vehicle class """    
    def __init__(self, color, power):
        """ ## Class constructor """
        self.color = color
        self.power = power

    def overview(self):
        msg = "{}: color: {}, power: {}, goes on : {} ".format(self.__class__.__name__.upper(),self.color, self.power, self.goes_on)
        return msg

class Bicycle(Vehicle):
    """ ## Defines the Bicycle sub-class hinerits from the Vehicle class  
class Car(Vehicle):
    """ ## Defines the Car sub-class hinerits from the Vehicle class  
class Ferry(Vehicle):
    """ ## Defines the Ferry sub-class hinerits from the Vehicle class  
class Plane(Vehicle):
    """ ## Defines the Plane sub-class hinerits from the Vehicle class  

bicycle = Bicycle("black", "electric")
bicycle.goes_on = "cycle paths"

car = Car("red", "electric")
car.goes_on = "road"

ferry = Ferry("white", "electric")
ferry.goes_on = "water"

airplane = Plane("white", "electric")
airplane.goes_on = "air"


BICYCLE: color: black, power: electric, goes on : cycle paths 
CAR: color: red, power: electric, goes on : road 
FERRY: color: white, power: electric, goes on : water 
PLANE: color: white, power: electric, goes on : air 

In the example up here, we have defined a class Vehicle using the syntax that we already know. Then we defined 4 sub-classes.

To define a subclass we use the class keyword followed by the name of our subclass and a pair of parentheses. Between parentheses, we specify the name of the class we want to inherits from.

a class can inherit from multiple classes.

Each sub-class that we defined has access to the parent attributes and methods. In our example, the attributes defined in the constructor are unique for each class object instance while the goes_on attribute is shared Between classes unless we override it.

How to override parent’s methods and attributes

to override a parent method we simply redefine the method inside the child-class, like so:

class Plane(Vehicle):
    goes_on = "Sky"
    def overview(self):
        msg = "Airplanes Fly in the {}".format(self.goes_on)
        return msg

plane = Plane("white","electric")


Airplanes Fly in the Sky

If your child class needs to override one of the parent methods to behave uniquely it can do so by redefining the parent method. the same is true for shared attributes like goes_on this example.

inherits from multiple classes:

As we said earlier a sub-class can inherit attributes and methods from multiple classes.

class Boy:
    """ Define a Boy class """
    def play(self):
        """ Returns a string message """
        msg = "We play hard"
        return msg

class Adult:
    """ Define a Adult class """
    def work(self):
        """ Returns a string message """
        msg = "We work harder"
        return msg

class OldMan:
    """ Define a Adult class """
    def rest(self):
        """ Returns a string message """
        msg = "We rest and enjoy our garden"
        return msg

class Human(Boy, Adult, OldMan):

human = Human()
print("When we are youngh {}, then we grow adults and {} finally we get older so {}!".format(,,

output :

When we are youngh WE PLAY HARD, then we grow adults and WE WORK HARDER finally we get older so WE REST AND ENJOY OUR GARDEN!

In this example here, we created 3 classes and one subclass. The subclass inherits from all the classes that we defined as we passed each parent class inside the subclass parentheses separating each of them with a comma.

Now when we instantiate the class object and we call the methods that were defined inside each patent class as you can see we get access to them and their messages. If we need we can override as always the parent methods inside a sub-class.

How to use the super function

This function is useful to access methods inherited that have been overridden in a class. In the next example, we override inside the Human class the rest method. Then we define another method using the super() function and calling the original rest method. This will allow us to access either the original message and the new one that we have overridden inside the class.

class Human(Boy, Adult, OldMan):
    """ Define a OldMan class """
    def rest(self):
        """ Returns a string message """
        msg = "Time to rest"
        return msg
    def restOld(self):
        return super().rest()

human = Human()
Time to rest
We rest and enjoy our garden

There is a lot to learn here so I suggest you visit the documentation for further details and future reference.

#15 – Files and CSV Manipulation

In this video, we are going to work with files. we will also see how to use one module from the Python standard library called CSV that as you might have guessed it’s a module to manipulate CSV files.

Think about how you create, edit and close a file. This process is the same that we will follow with python using the open function.

Lets first start by manually creating a text file inside our project folder. I am going to call it sample_file.txt and add some text to it.

How to read a file

To read a file we first need to open it, read its content and when we are done close the file. That’s what we do using any text editor.

Work with files can be done in two ways.

Option 1: using the open/close functions.

file = open("sample_file.txt","r")
for line in file.readlines():



In this example, we used the open function and assigned the instance of the file to the file variable. Then we used a for loop to loop over the lines of the file using the readlines() method on the file instance. Finally, we printed each line stripping out new lines using the strip method and closed the file.

Option 2: Using the ‘with’ keyword

The “with” keywords lets up open a file and close it automatically when we are done with it. First, we start the line using the ‘with’ keyword followed by the open function, after that we use the “as” keyword to assign the file instance to a variable and we follow it with a semi-colon. Like with functions the body is indented to the right. Then we use a ‘for’ loop as we did in the previous example and print the content of the file.

with open("sample_file.txt", "r") as file:
    for line in file.readlines():



As you see, in this example we omitted the close() method on the file instance as the with keyword lets us close the file automatically after we finish with it.[-]

How to Write on a file

You might have noticed that we passed a second argument to the open() function. It’s the mode argument that lets us define if the file we are opening is opened to read or write. If we specify “w” instead of reading we can write in the file and its content will be overridden. Alternatively, to append to a file we can pass the “a” to the open function mode’s argument. If the file that we pass does not exist it will be created.

I prefer using the “with” syntax when working with files, so there is one less thing to remember but you can use the first option if you like.

  • ‘r’ open for reading (default)
  • ‘w’ open for writing, truncating the file first
  • ‘x’ open for exclusive creation, failing if the file already exists
  • ‘a’ open for writing, appending to the end of the file if it exists
  • ‘b’ binary mode
  • ‘t’ text mode (default)
  • ‘+’ open for updating (reading and writing)

Write list elements into a file

we can write to a file the contents of a list in the following way:

contents  = ["one", "two","three","four", "six", "seven", "eith"]

with open("new_sample_file.txt", "w") as file:
    for line in contents:
        text = line + "\n"

We use the \n newline special character to place each element on its own line. otherwise, they will be written into the file on the same line altogether.

Write multi-line strings into a file

We can write to a file a multi-line string. And since it’s a multi-line string each line will be placed on a separate line inside our file without having to use the new-line special character.

paragraph = """ Lorem ipsum
dolor sit
amet """
with open("new_sample_file_2.txt", "w") as file:

How to append to an existing file

This example adds to the end of the file the content of the new_p variable. without overriding the original content of the file.

new_p = """ Sit amet dolor
lorem ipsum
sit amet"""

with open("new_sample_file_2.txt", "a") as file:

How to open a file in binary mode

The “rb” character helps us opening a file in binary mode.

with open("new_sample_file_2.txt", "rb") as file:
    for line in file.readlines():


b' Lorem ipsum\r\n'
b'dolor sit\r\n'
b'amet  Sit amet dolor\r\n'
b'lorem ipsum\r\n'
b'sit amet'

The “b” in front of our strings stands for “binary”.

How to manipulate CSV files

The module CSV will help us opening and writing on a CSV file. First, we need to import it. Then we can use it like so to create a CSV file.

import csv

lines = [
    ["Fabio", "Developer", 40,10],
    ["Serena", "Writer", 30,10]

with open("employees.csv", "w", newline='') as f:
    writer = csv.writer(f)

Read the CSV file

# option 1 (without using the module)
with open("employees.csv", "r", newline='') as file:
    for l in file.readlines():

#option 2:
with open("employees.csv", "r", newline='') as file:
    csv_reader = csv.reader(file)
    for row in csv_reader:
        print(", ".join(row))


Fabio, Developer, 40, 10
Serena, Writer, 30, 10

How to use the DictWriter and Reader

The CSV module has two functions that help us to work with CSV files and dictionaries. The DictWriter writes elements of a dictionary into the CSV file and places the keys as the table headers. While the DictReader reads the content of a CSV back into a dictionary

Create a CSV file using DictWriter

dict_list = [
    {"name":"Fabio", "Job title":"Developer", "Age": 40, "Yoe": 10},
    {"name":"Fabio", "Job title":"Developer", "Age": 40, "Yoe": 10},
    {"name":"Fabio", "Job title":"Developer", "Age": 40, "Yoe": 10},

with open("employees.csv", "w", newline='') as f:
    fields = ["name", "Job title", "Age", "Yoe"]
    writer = csv.DictWriter(f, fieldnames=fields)

Read a CSV file using DictReader

with open("employees.csv") as file:
    for row in csv.DictReader(file):


open function:

CSV module:ย

# Testing in Python

This is the final episode of the Python 3 Crash course 2020. In this episode I am going to give you a brief overview of Testing in Python. I’ll mainly focus on unittesting and maybe do some basic example of load tests. These are advanced concepts. If you don’t understand something, don’t worry, it might take a while before everything clicks.

Testing in Python | Python 3 Crash Course 2020 – Final Episode

This is a brief overview, I suggest you visit the documentation to read more and practice writing your own code and tests.

In this video we are going to use the unittest module from the Python standard library to do some tests.

Unit Tests

Unit tests are used to test a small bits of code, like functions and methods in our code. They are isolated and do not interact with other sevices our software maybe use. So, with unit tests we make sure our code behaves correctly and does not fail because of other serivices issues, like an interruption to the network or a failed database connection.

Unit tests focus on testing our functions and methods for multiple test cases so that we can make sure our code works even with edge cases. More cases we test the better our code will be.

The goal is verify that small isolated parts of our code are working correctly without introducing unexpected bugs.

How it is done

What unit test does is check that given an specific input the output matches our expectations.

Lets first write a script that we can test, inside a file called we will write the following function:

def print_names_lower(names: list):
    ''' Prints all names in lower case  '''
    names_lower = []
    for name in names:
    return names_lower

This function coverts a list of strings into lower case and returns a new list.

Manual Unit Testing

We can use the python interpreter to perform a manual unittest using a function we have written on a file.

Open the interpreter typing the command below in the terminal then use the ‘from’ keyword to import a specific function from an external file.

$ python3

Now inside the interpreter

from lower_names import print_names_lower

now we can use the print_names_lower function and run a unit test in it. Since we are focusing on a specific function our test is isolated and does not take into account any of the other functions that we might have written in the file. More test cases we add the better will be our code.

> print_names_lower(["Fabio","Serena","Carlo","Mary"])
['fabio', 'serena', 'carlo', 'mary']

In this exaple we have successfully run our unit test and obtained the output we expected given the input we passed. However, our goal with unit tests is to run tests automatically and test our function with multiple test cases. That will help us spot bugs and fix them before releasing our code.In this exaple we have successfully run our unit test and obtained the output we expected given the input we passed. However, our goal with unit tests is to run tests automatically and test our function with multiple test cases. That will help us spot bugs and fix them before releasing our code.

Automated unittests in python

To get started with automated tests we need to write our tests in a separate file. This file should follow a naming convention:

  • use the same name of the module we are testing
  • append to the name _test

So to write tests for a module called we will call the tests file

We will also use a python module called unittests.

Using the unittest module

to use the unittest module we need to import it at the top of our test file just under the import statement to import our module like we did earlier in the video when testing manually inside the interpreter.

Our test file will be “” and contain the following code:

#!/usr/bin/env python3
from lower_names import print_names_lower
import unittest

The unittest module provides classes and methods that we can use to run our tests. In particular we will use the TestCase class as parent class so we can use all its methods to test our code.

class TestLowerNames(unittest.TestCase):
    # write our test method here

To run our test function code automatically we need to instruct python to run our tests. So, at the end of our file we will call the main() method of the unittest module.

Our file will now look like the following:

#!/usr/bin/env python3
from lower_names import print_names_lower
import unittest

class TestLowerNames(unittest.TestCase):
    # write our test method here


Our test file is now ready and, we can start writing our first test case method.

def test_is_lower(self):
        test_case = print_names_lower(["Fabio", "Serena", "Luis"])
        expected = ["fabio", "serena", "luis"]
        self.assertEqual(test_case, expected)

Inside our test class we start writing our firt test case. We use the assertEqual method of the unittest module to verify that given a specific input we get our desired result, which in this case is the list we passes as a lower case characters.

To run out test file we first need to make sure it’s executable and we can do so in the terminal usingchmod u+x filenameย then we can run our test file as we usually run python scriptsย ./

$ ./ 
Ran 1 test in 0.000s


We have written our first test case, we can now move forward and write more edge cases.

def test_returns_a_list(self):
        test_case = print_names_lower(["Fabio", "Serena", "Luis"])
        expected = list
        self.assertIsInstance(test_case, expected)

def test_is_in_the_list(self):
       test_case = print_names_lower(["Fabio", "Serena", "Luis"])
       self.assertIn("fabio", test_case)

So far all tests we wrote passed because we passed an input that we know it will produce the output we wanted.

$ ./ 
Ran 3 tests in 0.001s


But what if we pass a number instead of a list to our function?

That’s an example of edge case. Edge cases are inputs that passed to our script produce an unexpected behaviour. lets try it now.

def test_number(self):
    test_case = print_names_lower(20)
    expected = 20
    self.assertTrue(test_case, expected)

In this example we passed a number to our test case instead of a list, a number is not iterable therefore it will produce an error, but let’s say that we expect to get the number instead as result and see what happens when we run our tests.

$ ./

ERROR: test_number (main.TestLowerNames)
Traceback (most recent call last): File "./", line 23, in test_number test_case = print_names_lower(20) File "/mnt/c/Users/FabioHood/projects/YouTube/PYTHON/Python_crash_course_en/testing/", line 7, in print_names_lower for name in names: TypeError: 'int' object is not iterable

Ran 4 tests in 0.003s

FAILED (errors=1)

The test outputs that we run 4 tests but one failed with a TypeError: ‘int’ object not iterable.

Let’s fix our code and make the test pass. We will add a check to see if the type of object passed to our function is an integer and if so we will return it as it is. Our function in will becore the following:

def print_names_lower(names: list):
    ''' Prints all names in lower case  '''
    if type(names) == int:
        return names

    names_lower = []
    for name in names:
    return names_lower

Alternatively, depending on how we want our program to behave we might show an error message. So, our function could look like the following:

def print_names_lower(names: list):
    ''' Prints all names in lower case  '''
    if type(names) == int:
        print("You must pass a valid list of strings to this function, not a number!")
        raise TypeError

    names_lower = []
    for name in names:
    return names_lower

To make this work we will need to change our test case to look like this:

def test_number(self):
        with self.assertRaises(TypeError):

So far we wrote our tests knowing exactly how the code works. This way of testing is called white box testing. It means that the person who writes the tests has a deep knowledge of how the code works before writing any test. On the opposite, if we write tests before writing the actual code, or if we write tests on a code base that we dont know, this is called black-box testing. With black-box testing the person who writes the tests doesn’t know how the code works but he knows what the output should be.

We said that unit tests focust on testing an isolated part of the code, like functions and methods. There are also other types of tests like:

  • Integration tests (verify that the integration between different pieces of code works as expected)
  • Regression tests (written as part of the debugging process to verify that an issue has been fixed once idenfitied)
  • Smoke test (comes from hardware verification, they verify that the program doesn’t have major bugs and runs correctly)
  • Load test (verify that the system behaves well when it’s under significant load)

The goal of integration tests is to verify that each part of the code works, so if the code makes api requests, db calls or call other external services the integration tests verify they work correcly. Ususally we need to create a test environment of the software we are trying to verify like a test db. We don’t run integration tests against actual production environments.

Software smoke tests do like a sanity check to find major bugs in a program, answering questions like, does the program run? Smoke tests run before more refined tests since if the software fails the smoke test, sure enough none of the other tests will pass.

Google says thatย For a web service the smoke test would be to check if there's a service running on the corresponding port. For an automation script, the smoke test would be to run it manually with some basic input and check that the script finishes successfully.


Load tests: These tests verify that the system behaves well when it’s under significant load. To actually perform these tests will need to generate traffic to our application simulating typical usage of the service.

TDD: Create tests before writing the code

TDD is a development process where we write our tests before writing the code. This sounds like wird but writing tests before actually helps up thinking about how our program should behave before writing it. It helps us thinking about how our program could fail and break providing valuable information.

In a TDD cycle, we first write the test, making it fail (because we didn’t write the actual code), then we debug and write the code to make the test pass. The we continue to the next part of our program and repeat this process for all its features until it is complete.

Lets try. So, first we write a test file called and place a method in it. Say our code should return a luky number to the user when he runs our code. We ask a number to the user and we multiply it by 9, and that’s his lucky number.

#!/usr/bin/env python3
from lucky_number import lucky_number
import unittest

class TestLuckyNumber(unittest.TestCase):

    def test_number(self):
        test_case = lucky_number(3)
        expected = 27
        self.assertEqual(test_case, expected)


When we run this test file, we get an error as the terminal tell us that the module lucky_number was not found.

$ ./
Traceback (most recent call last):
  File "./", line 2, in <module>
    from lucky_numbers import lucky_number
ModuleNotFoundError: No module named 'lucky_numbers'

So that’s the fist thing we need to do. Let’s create a file called and place only a shebang inside it.

#!/usr/bin/env python3

We know our code won’t work again as we have not defined any function in it but we used a lucky_number function in our test suit.

Traceback (most recent call last):
  File "./", line 2, in <module>
    from lucky_numbers import lucky_number
ImportError: cannot import name 'lucky_number'

So our next step is to write the function inside the file. Let’s do it!

def lucky_number(number):

Let’s run our tests and see what happens now.

$ ./
FAIL: test_number (__main__.TestLuckyNumber)
Traceback (most recent call last):
  File "./", line 10, in test_number
    self.assertEqual(test_case, expected)
AssertionError: None != 27

Ran 1 test in 0.001s

FAILED (failures=1)

It’s time to fix another error message as you see now we get an assertion errorย AssertionError: None != 27ย We did not implement any logic inside our function. So, that’s the next step.

def lucky_number(number:int):
    return number * 9

We finally made it pass. Let’s now make a couple more test cases.

$ ./
Ran 1 test in 0.000s


What if we don’t pass a number to the function? Let’s write another test case inside the file

def test_not_a_number(self):
        with self.assertRaises(TypeError):

Here we say that if a string is passed we want to raise a type error. One of our test fails because of the assertion, the Type Error is not raised.

$ ./
FAIL: test_not_a_number (__main__.TestLuckyNumber)
Traceback (most recent call last):
  File "./", line 14, in test_not_a_number
AssertionError: TypeError not raised

Ran 2 tests in 0.001s

FAILED (failures=1)

Lets make it pass. Inside our we will change our function like so.

def lucky_number(number: int):
    if type(number) == int:
        return number * 9
    raise TypeError

During this video we used two assert methods, assertTrue and assertRaises. As you might have guessed thare are lots more we can use, but that’s it for this video. We have just scratched the surface of testing. Let me know if you liked this video and want to learn more about testing, I might make a separate course.

In the mean time, the documentation link is in the description.

The Python 3 Crash Course 2020 is officially complete. I am working on a bonus lesson where we will apply all our knowledge and work on a final project. So stay tuned.

If you liked the video leave a like and if something wasn’t clear review the lesson or leave a comment. I try to reply to everyone as soon as I see the comment.

Thanks you all for watching the course. I hope you learned something useful for your carrear.

Take care and I’ll see you in the next video.