Python Crash Course 2nd Edition Part I
Python Crash Course 2nd Edition Part I
Index
Many of the examples used here follow the structure of the first edition (when Python haven't
implemented yet the #f-strings and other modern perks); however, many other examples where
updated according to the second edition modern knowledge about the language. Also, many key
concept's explanations where complemented by me, Leonardo, in order to make the content of this
book more explanatory. Needless to say, such extra content is NOT covered by the content of the
book (if it is, it is explained in other way).
In summary, what you are about to read (or re-read) is a bunch of important concepts about the
language and well explained examples that will guide you so you can learn such concepts. Many
sections of these notes were expanded to add detailed explanations on how some Python perks
(functions, methods, built-in data types, etc.) works and where you can learn more about them.
Without further ido, let me introduce you to the most beautiful programming syntax in existence,
the Python programming language.
Setting Up Python
In Chapter 1 there's a brief description on how to install Python on your computer and run your first
program, which prints the message Hello world ! to the screen, check it out if you feel lost.
Note: on second thought, you may want to check how to configure your own Python setup
because the book's method is outdated.
Return to Index
To create a variable we just need to name it and that's it! There's no need of setting it to an specific
value (unless we are required to) or to add a special character to specify a new line (like using the ;
symbol in C or Java).
By the way, if you struggle on understanding what #declaration and #inicialization stand for, you can
always check Programación en C - Creando Variables for a more detailed explanation about these
two key programming concepts.
Return to Index
Line 1 states that a traceback has occurred. A #traceback is a record of where the interpreter ran
into trouble when trying to execute your code. Line 2 to 3 reports that an error occurred in line 2 of
the file hello_world.py and line 4 states the name of the error, which makes it easier to classify
them.
Strings
A #string is a series of characters. Anything inside quotes is considered a string in Python, and you
can use single or double quotes around your strings. Using "" and '' can help us to write
apostrophes and quotes inside of strings without breaking the code.
Note: as a matter of fact, strings in Python are arrays (lists) of bytes representing Unicode
characters. However, Python does not have a character data type, a single character is simply an
array with length 1. Square brackets can be used to access elements of the string.
Method Description
.upper() Used to write the string completely in uppercase.
.lower() Used to write the string completely in lowercase.
.tittle() It capitalizes all the firsts letters of each word.
Return to Index
first_name = "ada"
last_name = "lovelace"
full_name = f"{first_name} {last_name}"
print(full_name)
Method Description
.rstrip() Used to get rid of all the right whitespaces of the string.
.lstrip() Used to get rid of all the left whitespaces of the string.
.strip() Used to get rid all of the whitespaces of the string from both sides.
It's important to point out that strings in python are #case_sensitve meaning that even if they have the
same characters, if they differ in at least one capitalized letter, both string will be considered different
by the #python_interpreter .
Return to Index
Underscores in numbers
When you’re writing long numbers, you can group digits using underscores to make large numbers
more readable. Example:
large_number = 14_000_000_000
Multiple Assignations
You can assign values to more than one variable at a time using just a single line to make it easier to
read. Example:
x, y, z = 0, 1, 2
Note 1: the name of this technique is #unpackaging . There's a more advanced technique called
#packaging using this specific fundamental.
Note 2: actually in Python, whenever we use commas for separating values inside a variable
assignment, what we are actually doing is creating a tuple. In the previous example we are
assigning to each variable, an element of that tuple. You'll learn more about this technique at
More about For Loops in Chapter 6.
Constants
A #constant is like a variable whose value stays the same throughout the life of a program. Since
python doesn’t have built-in constant types, programmers write constants as variables but in capital
letter. Example:
MAX_CONNECTIONS = 5_000
Return to Index
Comments
Comments are useful to add notes or explanation along the code. We can write comments using the
# symbol, or we can select the code lines you want to comment and press "CTRL + }".
By my personal experience, I can say that writing comments is one of the most useful things you can
do, especially when we are writing long programs. And why you may ask? When running a long
program, most of the times an error will pop up. Errors like typing incorrectly the name of a variable or
declaring an non-existent function are called #syntaxis_errors . However, there'll be cases where a
part of our code will produce an unexpected output creating another type of error known as a
#semantic_error . Those errors are harder to track down and fix since most of the times we will not
know what part of our code is causing that error. Here's where comments become handy. By writing
meaningful comments at each section of our code it'll be easier to spot which section isn't working as
it should and correctly handle it.
Return to Index
Chapter 3: Introducing Lists
What is it?
A #list is a collection of items in a particular order. Python works with lists differently as other
programming languages. Pythonic lists can be heterogenous, meaning that they can contain different
elements of different data types. They also do not requiere a previous specification of its final size and
can be expanded as long as we want!.
print(animals[0])
print(animals[1])
print(animals[2])
tiger
lion
kitty
Note: remember that index position starts at 0, NOT 1, because Python (as most languages do)
uses zero-based indexing.
Return to Index
.append(value/string/boolean)
.insert(index_number, value)
Note: it's important to let you know that using this method to insert the value at index_number
will also change the index number of the followed elements to one unit to the right. If an element
already exists at the whished position, such existing element will also be moved to the right.
del list[index_number]
.remove(value)
Note: the .remove() method deletes only the first occurrence of the value you specify. If there’s
a possibility the value appears more than once in the list, you’ll need to use a loop to make sure
all occurrences of the value are removed. You’ll learn how to do this in Chapter 7 User Input and
While Loops.
To remove an element but still conserve that removed item, we use the .pop() method.
.pop(index_number)
To use the pop method correctly is important to store the value of the popped element in a new
variable, here's an illustrative example:
Note: when removing items from a list by using any of the previous methods is important to
consider that we are decreasing the number of elements in that list so the index number of certain
elements could change. For example, if we remove the element at 2 index number, the elements
at 0 and 1 would not present any changes, but elements with a greater index number than 2
will have a decrease of 1 unit.
Return to Index
Organizing a List
To organize a list permanently alphabetically, we use the .sort() method.
Note: sorting a list alphabetically is more complicated when there are capital letters included. To
keep it simple, use .lower() to change every element into lowercase.
To count the number of elements included in a list, we use the len() function.
len(list_name)
Return to Index
However, in Python, the thing changes completely, reducing the previous syntax to the following
syntax:
What this code block actually does is repeat the code that's inside its structure a specific amount of
times which concords with the number of elements in the selected iterable. The element value will
change of value according to each iteration and according to the values stored in the iterable,
moving one index to the right. Such element can be used inside its own for loop code block to
perform more complex commands.
Note 1: consider that if we remove elements from the iterable, the for loop could drastically
change, even more if such changes are performed inside the loop (which could cause a crash, or
an infinite loop).
Note 2: in Python, it's super important to never miss the 4 spaces indentation when working with
code blocks such as a #for_loop .
Important: you may already know it, but for loops are used to iterate a code section a specific
amount of times. The number of times the loop will iterate depends purely on the iterable
number of elements, rather than the elements of the iterable themselves. Always remember
this when working with this loop!
The list() function is used to create a list. Within its parameters we can define the content of that
list. Example:
numbers = list(range(1,6))
print(numbers)
[1, 2, 3, 4, 5]
Function Description
min(list_name) Used to identify the lowest value of a number list.
max(list_name) Identifies the greatest value of a number list.
sum(list_name) Adds each value of a list, as long all values are numbers.
Return to Index
List Comprehensions
List comprehensions are a manner of writing a loop that involves the creation of a list in order to
simplify it. To use this syntax, begin with a descriptive name for the list. Next, open a set of square
brackets and define the expressions for the values you want to store in the new list. Then, write a
#for_loop to generate the numbers you want to feed into the expression, and close the square
brackets. Example:
Note: when using a #list_comprehension , the variable that represents the values which will be
stored inside the list should always be a returnable value, NOT an action.
[0, 2, 4, 6, 8, 10]
The first number indicates at which position the slice should start, the second number indicates
when it must end, without including the value (both numbers begin counting from 0). Explanation:
list_name[:3] #This will include all the numbers until the third index.
list_name[3:] #This will begin the list from the third index and end
it when there are no more elements to include.
When using negative indices, it's important to know which negative number is assigned to each list
element, for example:
# positve 0 1 2 3 4
felines = ["lion", "tiger", "cat", "puma", "cheeta"]
# negative -5 -4 -3 -2 -1
Important note: a #string behaves very much like a list, being each character an element of the
string. However, strings are inmutable, meaning that when modifying a string object, Python
doesn't alter the string, in some cases it may rewrite the string with the desired modifications.
Another important note: when we want to access the last element of the list we use the -1
index number; the first element of a list will always have the index number of 0 .
Return to Index
Copying a List
You can make a copy of a list by using [:] parameters and storing the copy in a variable with a
different name of the original list, or you can use such copy to make a temporary copy the actual list.
Tuples
#tuples are similar to lists, but with the difference that they are inalterable. They will keep the same
values and will not allow any modifications nor additions. Tuples are defined using parenthesis
instead of squared brackets. Example:
200
50
You can loop around tuples as well. Also, even though tuples can't be modified, they can be
overwritten like variables. Example:
Lists may be useful if we are planning to change the elements stored inside the [iterable] (append new
elements, inset them, delete them, etc.) but tuples are great if we are planning only in traveling along
the iterable; that's because tuples consume less memory when created compared to lists so if
you are writing a loop that only iterates through an iterable consider using tuples instead of a list.
Note: if you are wondering which methods can be applied to the built-in tuples data type, you can
check it here.
new_tuple = tuple(iterable_object)
The other famous way for declaring a fresh tuple is using a multiple assignation into one variable like
this:
As we can perceive, each element separated by a comma was stored inside a tuple's element. Such
technique is not limited to declarations, in fact, each time we separate different values and variable
names with a comma, what we are actually doing is declaring a temporary tuple; in the previous
example we declared a temporary tuple and stored it at new_tuple to later print it. This technique
receives the name of #packaging and it's the counterpart is named #unpackaging saw at Multiple
Assignations in Chapter 2.
Note: please, keep in mind this part because it'll become useful when we reach Chapter 6 More
about For Loops section.
The Python style guide was written with the understanding that code is read more often than it is
written. Some of it's guidelines are:
Use spaces rather than tabs when indenting, using only four spaces per indentation.
Each of our program's line should be at most, 80 characters each.
To group parts of your program visually, use blank lines using Enter . For example, if you have
five lines of code that build a list, and then another three lines that do something with that list, it’s
appropriate to place a blank line between the two sections. However, you should not place three
or four blank lines between the two sections.
Return to Index
Chapter 5: If statements
If Statements in Python
Python structure for the if statement is very simple:
Simple if statement:
if condition:
# Code to be executed if "condition == True"
if - else statement:
if condition:
# Code to be executed if "condition == True"
else:
# Code to be executed if "condition == False"
if condition_1:
# Code to be executed if "condition_1 == True"
elif condition_2:
# Code to be executed if "condition_2 == True"
elif condition_3:
# Code to be executed if "condition_3 == True"
# .
# .
# .
elif conditon_n:
# Code to be executed if "condition_n == True"
else:
# Code to be executed if "(condition_1 == False) and (condition_2 == False)
and (condition_3 = False) and ... and (condition_n == False)"
Where:
Option_1 represents the value that the ternary operator would have returned if the condition
was evaluated to True .
Option_2 represents the value that the ternary operator would have returned if the condition
was evaluated to False .
Example:
condition = True
message = valueIfTrue if condition else valueIfFalse
print(message)
condition = False
message = valueIfTrue if condition else valueIfFalse
print(message)
Return to Index
Boolean Expressions
Are True or False , also called #conditional_tests .
requested_toppings = []
if requested_toppings:
for requested_topping in requested_toppings:
print("Adding" + requested_topping + ".")
print("\nFinished making your pizza!")
else:
print("Are you sure you want a plain pizza?")
In the previous example, when the name of a list is used in an if statement, Python returns True if
the given list contains at least one item; an empty list is evaluated to False .
Using Multiple Lists
Example:
Return to Index
Chapter 6: Dictionaries
Observation: we can say that a #key-value_pair is a set of values associated with each other.
Important: a dictionary's key should always be a hashable object. To know more about hashes
and hashable objects refer to Hash Function, Hashes, and Hashable Objects at "Beyond the
Basic Stuff with Python."
Return to Index
Accessing Values in a Dictionary
To get the value associated with a key, give the name of the dictionary and then place the key inside
a set of square brackets. Example:
green
Return to Index
alien_0["x_position"] = 0
alien_0["y_position"] = 25
print(alien_0)
del alien_0["points"]
print(alien_0)
Return to Index
favorite_languages = {
"jen": "python",
"sarah": "c",
"edward": "ruby",
"phil": "python",
}
dictionary_name.get(key_name, message)
Note: the second argument is completely optional and is set to None by default.
The key_name is the name of the key we are looking for. If such key isn't included in the dictionary,
the message will be returned. Here's an example:
Here, instead of getting an error, we get now as an output a clean message indicating what went
wrong.
Up to now, we have seen how the for loop works. Here's the so-far-seen procedure:
We use a variable (called index) that will iterate throughout a list or tuple's elements.
For each iteration, the index will take the value of the current element which the iterable is placed
at, and travel to the right once one iteration is completed.
Once the for loop has detected that there are no left elements to iterate left, the loop will end
and the program will continue it's normal flow.
But what if the iterables structure is way more complex, for example:
However, this method is considered not Pythonic given that we are using index numbers to specify
the element we want to grab from each tuple. A Pythonic way of approaching this problem will be
unpackaging each tuples' value inside a individual temporary variable inside the for loop syntax:
Doing it this way, our code remains the does the same but becomes more readable. You can run the
previous code and see that the output is the same as the last example. This behavior is possible
because of #unpackaging . For each element of the list (each element is a tuple) the for loop
creates a temporary tuple (name, number) where it will unpack the first index item inside the
variable name and the second index item inside the variable number.
user_0 = {
"username": "efermi",
"first": "enrico",
"last": "fermi",
}
for key, value in user_0.items():
print("\nKey: " + key)
print("Value: " + value)
Key: last
Value: fermi
Key: first
Value: enrico
Key: username
Value: efermi
The .items() method returns a dictionary object that contains a list, where each list's item is a tuple
containing two values: the key name and the value. By unpackaging each tuple's elements inside
the temporary variables key and value, we are able to use them as we please.
It´s important to notice that the key-value pairs are not returned in the order in which they were
stored, even when looping through a dictionary. Python only tracks the connections between
individual keys and their values.
Return to Index
favorite_languages = {
"jen": "python",
"sarah": "c",
"edward": "ruby",
"phil": "python
}
Note 2: actually, by default, Python returns only the keys if we place only one variable to store the
information, if we place two variables it will pop up an error because we need the method
.items() to print both keys and values.
favorite_languages = {
"jen": "python",
"sarah": "c",
"edward": "ruby",
"phil": "python",
}
Return to Index
favorite_languages = {
"jen": "python",
"sarah": "c",
"edward": "ruby",
"phil": "python",
}
print("The answers of the poll where: ")
for value in favorite_languages.values():
print(f"- {value.title()}")
Sets
Sets are used to store multiple items in a single variable. It's one of 4 built-in data types in Python
used to store collections of data, the other 3 are lists, tuples and dictionaries, all with different qualities
and usage.
Unordered: unordered means that the items in a set do not have a defined order. Set items can
appear in a different order every time you use them, and cannot be referred to by index or key.
Unchangeable: meaning that once a set is created, you cannot change its items, but you can
remove items and add new items.
Unindexed: since set's items can appear in a different order every time you use them, they
cannot be referred to by index or key.
Note: the values True and 1 are considered the same value in sets, and are treated as
duplicates.
Example:
To perform the intersection between two sets we use the & operator:
{'a', 'd'} # All elements that are in set_a that ARE NOT in set_b
To perform the symmetric difference between two sets we use the ^ operator:
{'e', 'd', 'a', 'f'} # All elements that are NOT the intersection of both sets
Nesting
#nesting , in simple words, is placing a data structure (like a #list or a #dictionary ) inside another
data structure, for example, placing inside a list different dictionaries or place a list inside the key's
value of a dictionary. The book presents three types of nesting:
A list of dictionaries
A list IN a dictionary
A dictionary inside another dictionary
A List of Dictionaries
Dictionaries are placed inside lists when we want to form a data structure where it’s included a series
of values assigned to a single element. However, the difference between nesting dictionaries inside
dictionaries and nesting dictionaries in lists is that we won’t be able to access the value/name of that
single element. Nesting dictionaries will be covered later. Example:
emiliano = {
"last name": "perez",
"second last name": "lazaro",
"favorite color": "blue",
"age": "13",
"gender": "masculine",
}
daniel = {
"last name": "acevedo",
"second last name": "ramos",
"favorite color": "green",
"age": "17",
"gender": "masculine",
}
gabriel = {
"last name": "padron",
"second last name": "flores",
"middle name": "gerardo",
"favorite color": "yellow",
"age": "17",
"gender": "masculine",
}
Note: notice that at any time we obtain the name of each dictionary, we only use the values and
keys of each one of them. The names of each dictionary: emiliano, danile and gabriel won't
be available for usage.
Return to Index
A List in a Dictionary
Using this technique is useful when a key can have multiple values. Example:
favorite_places = {
"gabriel": ["home", "room", "cinema"],
"emiliano": ["plaza", "fast food restaurants", "home", "room"],
"daniel": ["school", "campus", "Tech"],
}
Name: Gabriel
Favorite places:
-Home
-Room
-Cinema
Name: Emiliano
Favorite places:
-Plaza
-Fast food restaurants
-Home
-Room
Name: Daniel
Favorite places:
-School
-Campus
-Tech
Note: each key can have a list or just a single value in it.
A Dictionary in a Dictionary
This technique is useful when we want to store the same type of information of several units inside a
unit. In other words, it can be used when we want to store inside our main "box" other small boxes
that each one contains as well small boxes. Is similar as nesting dictionaries inside lists, but with two
main differences, the first one is that in this case you can use the name of the data that is storing
other data; the second main difference is that doing this technique only works properly if each
dictionary's value holds the same data-type because it’ll get more difficult to loop inside each
dictionary as a value. Example:
cities = {
"nuevo león": {
"country": "mexico",
"population": "5,784 m",
"fact": "best food of the world.",
},
"paris": {
"country": "france",
"population": "2.161 m",
"fact": "fashion country.",
},
"new york": {
"country": "united states",
"population": "8.419 m",
"fact": "very expensive to live in.",
},
}
Nuevo León:
-Country: Mexico
-Population: 5,784 m
-Fact: Best food of the world.
Paris:
-Country: France
-Population: 2.161 m
-Fact: Fashion country.
New York:
-Country: United States
-Population: 8.419 m
-Fact: Very expensive to live in.
Note: notice that in this case we are printing the names of each dictionary because they are
classified as keys of each smaller dictionary containing the data of each country
Return to Index
Dictionary Comprehensions
One advanced technique to declare a dictionary is using a #dictionary_comprehension . Such
technique is rather similar to list comprehensions. To declare one we must follow the next structure:
{key:value for index in iterable} # In case we are working with one value
{key:value for key, value in iterble} # 'iterable' must be a map object
# Example 1
dictionary_1 = {i: i ** 2 for i in range(11)}
print(dictionary_1)
# Example 2
names = ['leonard', 'daniel', 'emiliano']
ages = [18, 19, 14]
Note: we could also use an if statement in dictionary comprehensions to filter the key-value
pairs we want to store in the dictionary.
Return to Index
Also remember to write clear prompts in order to let the user know what type of information we are
expecting of him. A #prompt is the message that pops up at the same time it does the input.
Example of a prompt:
Note: if we don't if the number will have a decimal point or not, we need to use the float()
function.
Return to Index
Return to Index
counter = 0
while counter <=10:
print(counter)
counter = counter + 1 #this line of code can be represented as
#"counter += 1"
Return to Index
Letting the User Choose When to Quit
We can let the user choose when to quit our program by using the value of an input the user typed.
There are several ways to archive this condition:
1. By != a message of the user: this means to set an initial string to enter the while loop. Next is
to create the while loop to tell that, as long as the message IS NOT our "quit" message, keep
running. Example:
Note: notice that if we don’t set an initial message, it will never enter the while loop. Since Python
interprets whitespaces´ string as True , it will enter the loop.
2. By using a flag: flags are variables assigned to a True value. We can use them to enter a while
loop. If we want to stop the loop, we need to set the flag to False by creating a conditional that will
analyze if certain expression is True , which will end our loop. Example:
3. Using the break statement: the #break_statemet is used inside a loop to end the loop even if
there is code remaining to be run. We can use it in a similar way we use flags. Example:
while True:
message = input("Tell me something and I’ll repeat it back to you: ")
if message == "quit":
print("---Ending Program---")
break
else:
print(message)
Note: notice that we placed the break statement after the "---Ending Program---" message.
That’s because the break will stop running the loop immediately even if there are some
commands left to run.
Return to Index
One way to transfer elements of one list to another is using the .pop() method together with a
while loop. Example:
while unconfirmed_users:
current_user = unconfirmed_users.pop()
print(f"Verifying user: {current_user}")
confirmed_users.append(current_user)
Note: when a while loop receives a list as a condition, it will return True if the list is filled with at
least one value.
Return to Index
responses = {}
while polling_active:
# Prompt for the person's name and response.
name = input(f"\nWhat is your name?")
response = input("Which mountain would you like to climb someday? ")
if repeat == "no":
polling_active = False
Return to Index
Chapter 8: Functions
#function (s) are blocks of code that can be called at any given moment we want to perform the code
that is written inside of them. We can pass them #parameters in order to use the function with
different values and that it adapts to the case we want. It’s important to point out that parameters and
arguments are not the same. #parameters are the variables requested, and #arguments are the
information that is taken by the parameter to make the function work. Example:
Note: many times, people confuse arguments and parameters, so don’t worry if you see
arguments defined as parameters and vice versa in books or in other resources.
There are several types of arguments, each one of it has its own function:
Positional Arguments
Keyword Arguments
Default Values
Optional Arguments
Positional Arguments
Python must match each argument in the function call with a parameter in the function definition. In
other words, we need to place our arguments in the same order we have the parameters. You can get
unexpected results if you mix up the order of the arguments in a function call. Example:
I have a harry.
My harry’s name is Hamster
In the previous example, we got a funny message instead of the one we were expecting. Remember
to always place positional arguments in the right order.
Return to Index
Keyword Arguments
A #keyword_argument is a name-value pair that you pass to a function. You directly associate the
name and the value within the arguments, so when you pass the arguments to the function, there’s no
confusion. Example:
I have a hamster.
My hamster’s name is Harry.
Note: notice that even if the animal_type and pet_name arguments are placed in different order
according to the parameters, Python assigns each argument to each of its corresponding
parameters. Using keyword arguments are useful when we know the name of ALL the function´s
parameters. We cannot use this technique with only one argument and expect that Python
interprets the other as the missing one(s); doing this will pop up an error saying that we are
assigning more than one argument to one parameter.
Default Arguments
We can assign a value to a parameter, so in case that an argument is assigned to that parameter, the
function will run using the argument provided as normal; however, if an argument is not provided, it
will run the function using the value previously assigned to the parameter. Example:
describe_pet(pet_name = "willie")
I have a dog.
My dog’s name is Willie.
Note 1: notice that the order of the parameters in the function definition had to be changed.
Remember that default values need to be placed after positional arguments. This allows
Python to continue interpreting positional arguments correctly.
Note 2: if all arguments have default values we'll need to use keyword arguments to specify
which arguments correspond to its specific parameter. The parameters not mentioned as keyword
arguments will have its corresponding default value assigned.
Return to Index
Optional values allow functions to handle a wider range of cases while letting function calls as elegant
as possible.
The return statement
The #return_statement is frequently used when working with functions. Normally, when coding the
code block of the function, the variables inside of them will not be enabled globally (meaning that the
variables declared inside a function will only be usable inside the current function). Sometimes we
want to make a function that, when given certain information (arguments), it processes them and
returns back a value that later we can use in other part of our program, say the main function
(discussion on that later). We use the return statement for that purpose.
As an important reminder, functions will end once a return statement is reached. For example, let's
see the following code:
def stop_at_first_return(value):
if value == 10:
return 100
elif value == 9:
return 90
print('Pog!')
return 'Other value besides 100 and 90'
values = [10, 9, 8]
for i in values:
eggs = stop_at_first_return(i)
print(eggs)
100
90
Pog!
Other value besides 100 and 90
Now as we can see, if functions behaved as "outside-function" code, all calls to the function
stop_at_first_return() would have ended in returning 'Other value beside 100 and 90' and
printing the string 'Pog!' . But that is not the case, meaning that once a return statement is
reached, the function will exit it's own code block and continue with the normal flow of our program.
Returning a Dictionary
We can return any kind of value we need through a function. I’m not going to explain how this works
because is really easy to understand, just remember that to add a new key-value pair we need to type
the name of the dictionary, followed by the key’s name inside squared brackets, next is to place a =
symbol and the value of the key. Example:
Instead of declaring a parameter with an empty string as a parameter, we can use the None
statement. We can think of None as a placeholder value. In conditional tests, None evaluates to
False , however if such value is replaced with another data type (string, integer, float, etc.) it will be
evaluated to True . That's why we can use this statement instead of an empty string. Example:
Return to Index
def party_orginizer(guests):
guest_names = []
for i in range(guests):
new_guest = input(f'Enter the name of the guest #{i + 1}: ')
guest_names.append(new_guest)
pizzas = guests * 2
seats = guests - 5 # Main guest family (total of 5) do not require a seat
return pizzas, seats, guest_names
Done! Now, in order to use the returned values we can do two things:
The first option will allow us to handle a tuple, that we later can use in a for loop in order to access
each individual element; nonetheless, unpackaging the values results quicker and easier to do (see
more about unpackaging here) and to show how to do it, observe the following example:
def party_orginizer(guests):
--snip--
This will allow us to manipulate each returned value individually, no need for loop required. Of
course, remember that the order of such declaration matters: total_pizzas will point to pizzas,
total_seats will point to seats and so on. Messing this order could result in unexpected semantic
errors.
while True:
print("\nPlease tell me your name:")
print("(enter 'q' at any time to quit)")
Passing a List
There will be cases where we’ll be forced to pass a list to a function as an argument. Here is a simple
example where we greet each user included in a list. Example:
def greet_users(names):
for name in names:
msg = f"Hello, {name.title(-)}!"
print(msg)
Return to Index
print(f"{animals} {empty_list}\n")
list_modifier(animals, empty_list)
print(f"\n{animals} {empty_list}")
print(f"{animals} {empty_list}\n")
list_modifier(animals[:], empty_list) # Notice that we added a [:] index
# to animals
print(f"\n{animals} {empty_list}")
Note: we must avoid creating a copy of a list if we really don’t need it. Creating meaningless
copies of a list only consumes memory. If we know beforehand we’ll need to keep the same
values in the original list, then it's fine to use the previous approach.
Return to Index
def pizza_toppings(*toppings):
print(toppings)
Note: notice how the three calling statements returned a tuple of the elements we specified at the
arguments. We can know for sure that they are tuples because all of them are inside parenthesis
and not inside square brackets.
def dictionary_creator(**keyword_arguments):
simple_dictionary = {}
for key, value in keyword_arguments.items():
simple_dictionary[key] = value
return simple_dictionary
Note: in this example we used the keys and values of the **keyword_arguments dictionary and
copy them into the empty dictionary named simple_dictionary .
print(positional_argument_1, positional_argument_2)
if optional_argument:
print(default_argument_2)
print(tuple_1)
print(dictionary)
Return to Index
def printing_bacon():
bacon = 'bacon'
print(bacon)
printing_bacon()
print(bacon)
Here we can see at the print() statement that, even though we want to print the variable bacon ,
we won't, because Python will raise a NameError exception, meaning that the variable bacon was
not defined. This error happens because the variable bacon is not at the global scope, but at a local
scope. #scope , in simple words, means the range in which a variable life-time lasts. Here, the
variable bacon life-time will only last once the print_bacon() function ends executing, then it'll be
destroyed; since it is destroyed, we can no longer use it in the rest of the program.
Note: the name of the parameters we use at function's declaration are also considered local
variables.
Another thing about variables is that local ones will be consider different to global ones. What I mean
by that is, even if we have a local variable named equally to an external variable, both will be
considered different. To illustrate this, run the following program:
def printing_bacon():
bacon = 'bacon' # Line 2
print(bacon)
bacon
salami
As we can see, the variable bacon at line 2 has a local scope, so it prints 'bacon' but at line 5 it is
referenced with a global scope value, so it prints 'salami' . In case we needed to use the salami
global value, we could use the global statement:
bacon = 'salami'
def printing_bacon():
global bacon
print(bacon)
printing_bacon()
print(bacon)
salami
salami
Now, the global scope value is also available for the function printing_bacon() to use. Here, the
global statement tells Python, "In this function, bacon refers to the global variable, so don't create a
local variable with this name".
Note: even though there's no specific rule that states it, is considered a bad practice to use global
variables such as in the previous case. There'll be cases where such technique will be needed,
but if it is avoidable, then avoid it.
Storing Our Functions in Modules
#modules are separated files that contains blocks of code such as functions and classes. They are
helpful because they make our code more efficient and easier to read. They also become handy when
we want to hide our code in order to focus more on its high logic level. Since modules store code in
separated files, we can import that code to more than one file. We can also share that bunch of code
with other programmers so they can use it. Furthermore, we can use code written by other
programmers by importing it in our main program.
But how do we bring that code to the program we want? We use the import statement which makes
available external code in the current file we are working on. There’re several approaches to import
modules:
1. Importing an Entire Module: this first approach is simple; we just need to write import
module_name at the beginning of our program. If we want to call a function, we must write the name of
the module and the name of the function separated by a dot. Structure:
module_name.function_name()
2. Importing Specific Functions: we can import specific functions that are included inside a module.
This approach is more efficient than importing the entire module for two reasons: it doesn’t waste
memory in functions we’ll not be using and we don’t need to use the dot notation when we call a
function. To do this we need to use the from statement when importing our functions (always at the
beginning of our program):
If we want to import more than one function, we need to separate them by commas:
This approach doesn’t have a calling structure since if we want to call a function, we just call it with its
original in-module name.
3. Using as to Give a Function an Alias: if the name of a function we’re importing might conflict
with an existing name in our program or, if the function name is long or not descriptive, we can use a
short, unique alias to a specific function:
4. Using as to Give a Module an Alias: it’s almost the same as importing a function with an alias. In
this case we assign a module a short alias and use the dot notation when calling a function:
import module_name as mn
If we want to call a function, we must use the mn alias and the function name separated by a dot.
Structure:
mn.function_name()
5. Importing All the Functions in a Module: we can import all the functions a module has in it by
using the * symbol.
By doing this, we won’t need to use the dot notation since calling a function of this module only
requires the function module name. However, it’s not recommended to do this approach in modules
WE DIDN’T WRITE since some functions of the module may conflict with the names of variables or
other functions written in our program.
Return to Index
Styling Functions
We need to keep in mind few details when styling functions. They should have descriptive names and
these names should always be written in lowercase using underscore to separate words. Each
function should have a #docstring (docstrings are comments that explain a piece of code, they are
written inside triple quotation marks """ """ ) after the function definition. Each function should be
separated from one another by two empty lines.
Return to Index
Chapter 9: Classes
Object Oriented Programming is one of the most effective approaches to writing software. In OOP we
write classes that represent real-world things and situations, and we create objects based on these
classes. When we write a #class , you define the general behavior that a whole category of objects
can have. When we create individual objects of a class, each object is equipped with a general
behavior; in case of need, we can equip to those objects’ unique traits.
Making an object from a class is called #instantiation , and you work with instances of a class.
Essentially, the first function declaration that a class should have should be the following: __init__()
. This method (class' function) is a special method that Python runs automatically whenever we
create a new instance based on the class. As we can see, it has two leading underscores and two
trailing underscores, a convention that helps prevent Python’s default method names from conflicting
with our method names.
The __init__() method should always have the parameter self and it must come first before
other parameters. When we create a new instance of a class (saying, running the __init__()
method), the method call (which happens at the moment of creating an instance) will automatically
pass the self parameter. Every method call associated with an instance automatically passes self ,
which is a reference to the instance itself; this parameter will be used to give the individual instances
access to the attributes and methods of the class. Any variable prefixed with self is available to
every method. Such as:
self.name = name
self.age = age
self.number = number
Since the self parameter will pass automatically by the __init__() method, there’s no need to
specify it at the calling statement. Inside the __init__() function, all of the attributes of the class
should be placed. Now, with all that in mind, let’s get started building our first class named Dog .
Example:
class Dog:
def __init__(self, name, age): # The parameter "self" goes always first
self.name = name # Attribute
self.age = age # Attribute
Note 1: each class should be capitalized on the first letter of its name.
Note 2: as we can appreciate, the parameters' names name and age , match the names of their
assigned attributes. To differentiate the parameters between the , we could name them
differently, however is a normal convention that such names match.
Return to Index
class Dog:
def __init__(self, name, age):
self.name = name
self.age = age
def sit(self):
print(f"{self.name.title()} is now sitting.")
def roll_over(self):
print(f"{self.name.title()} rolled over")
Note: notice that we need to store our class inside a variable. That’s because the __init__()
method returns automatically a value containing the instance.
Accessing Attributes
It’s simple to access to a specific attribute of a class once the instance has been created. First, we
need to type the name of the instance followed by the name of the attribute we want to access
separated by a dot. According to the previous example, we can type:
print(my_dog.name)
print(my_dog.age)
And the console should return us the requested data we specified to the instance my_dog (in this
case, Rick and 12 ).
Calling Methods
If we want to call the methods of a class, it’s similar as accessing attributes, with the small difference
of adding parenthesis and, if requested, arguments. Using the previous class of Dog we can come
up with the following example:
my_dog.sit()
my_dog.roll_over()
By doing this, Python looks for the functions called sit and roll_over inside the Dog class to
execute them.
Return to Index
class Car():
def __init__(self, make, model, year):
self.make = make
self.model = model
self.year = year
def get_descriptive_name(self):
long_name = f"{self.year} {self.make} {self.model}"
return long_name.title()
my_new_car = Car("audi", "a4", "2016")
print(my_new_car.get_descriptive_name())
2016 Audi A4
So, in this example, how do we add a default value for an attribute? This is different from adding a
default value to a function. To perform this, we just add the value we want beside it. Say we want to
always have a odometer that always begins at 0, we can do this by doing the next procedure:
class Car():
def __init__(self, make, model, year):
self.make = make
self.model = model
self.year = year
self.odometer = 0 #Default value for an attribute
def get_descriptive_name(self):
--snip--
def read_odometer(self):
print(f"This car has {self.odometer} miles on it.")
2016 Audi A4
This car has 0 miles on it.
Note: in this example, we set the odometer value to 0 in order to keep it the same for all the
instances we create from the Car() class. We can also access to that default attribute as the
same way we did with the others.
Return to Index
my_new_car.odometer = 23
Now, each time we retrieve this attribute, it will have a value of "23" instead of "0".
2. Modifying an Attribute’s Value Through a Method: sometimes it’s useful to have a method that
sets an attribute’s value for us. Doing this is simple as writing a function. Example:
class Car():
--snip---
my_new_car.update_odometer(21)
my_new_car.read_odometer()
3. Incrementing Attribute’s Value Through a Method: doing this is similar as modifying an attribute
through a method, but with the slight difference that instead of "setting" a value, we add to our current
value another one of our preference. Example:
class Car():
--snip—
Inheritance
If we don’t want to start from scratch when writing a class, we can "copy" the attributes and methods
from another class. This is called #inheritance ; the class from where we inherit those
attributes/methods is called parent class and the one who receives those attributes/methods is called
child class. When one class inherits from another, it takes on the attributes and methods of the first
class.
Return to Index
When you’re writing a new class based on an existing class, you’ll often want to call the __init__()
method from the parent class, that is coping exactly the same __init__() method that the parent
class has. This will initialize any attributes that were defined in the parent __init__() method and
make them available in the child class. Also, the name of the parent class must be included in
parentheses in the definition of its child class.
Finally, in order to make a connection between the child and the parent class, we use the function
super() . After this function, we must specify which attributes we want to connect. Example:
class Car():
def __init__(self, make, model, year):
self.make = make
self.model = model
self.year = year
self.odometer = 0
def get_descriptive_name(self):
long_name = f"{self.year} {self.make} {self.model}"
return long_name.title()
def read_odometer(self):
print(f"This car has {self.odometer} miles on it.")
class ElectricCar(Car):
def __init__(self, make, model, year): # Specifies the attributes of
# the class
super().__init__(make, model, year) # Connects attributes with
# the parent class
Note: it’s important to remember that the first __init__() method from the child class, only
states the attributes we’ll be using. If we want to connect those attributes with the parent class,
we must use the super(). function and tell Python which attributes we want to connect. Doing
this also connects the methods from the parent class with the child class. We must copy the
same requested information in the __init__() and in the super(). function, or it won’t work.
class Car():
--snip--
class ElectricCar(Car):
def __init__(self, make, model, year, battery):
super().__init__(make, model, year)
self.battery = battery
def describe_battery(self):
print(f"This car has {self.battery} of battery remaining.")
Return to Index
class Car():
def __init__(self, make, model, year):
self.make = make
self.model = model
self.year = year
self.odometer = 0
def get_descriptive_name(self):
long_name = f"{self.year} {self.make} {self.model}"
return long_name.title()
--snip--
class ElectricCar(Car):
def __init__(self, make, model, year, baterry):
super().__init__(make, model, year)
self.battery = baterry
def describe_battery(self):
print(f"This car has {self.battery} of battery remaining.")
def get_descriptive_name(self):
long_name = f"{self.year}, {self.make}, {self.model}, Battery:
{self.battery}"
print(long_name)
Return to Index
Instances as Attributes
When modeling something from the real world, we may find that we’re adding more and more details
to a class. We’ll find that we have a growing list of attributes and methods that can be separated
from that class to create another one. However, we want to access those attributes from one single
main class. For example, in the class ElectricCar , we may find that we are adding many attributes
and methods only to specify the battery . In that case we can move those attributes and methods
to another class named Battery . However, since ElectricCar instances require a battery, we won’t
be able to access them if the battery attributes are in another separated class. In this case, we can
relate them by defining that the Battery class is an attribute of the ElectricCar class. Example:
class Car():
--snip---
class Battery():
def __init__(self, capacity, price):
self.capacity = capacity
self.price = price
def battery_duration(self):
duration = self.capacity * 3
duration = f"My tesla's battery lasts around {duration} hours."
return duration
class ElectricCar(Car):
def __init__(self, make, model, year, battery_cap, battery_price):
super().__init__(make, model, year)
(1) self.battery = Battery(battery_cap, battery_price)
def describe_battery(self):
print(f"This car has {self.battery} of battery remaining.")
def get_descriptive_name(self):
long_name = f"{self.year}, {self.make}, {self.model}, Battery:
{self.battery}"
print(long_name)
my_tesla = ElectricCar("tesla", "model s", "2016", 10, "1000")
print(my_tesla.battery.battery_duration())
Return to Index
Importing Classes
As we add more functionalities to our main program, we might notice that we are adding too much
code to one single file. In those cases, we can move our classes to some modules. This will also help
us to keep our code simple and efficient.
There are several ways in which we can import classes: 1. Importing a single class: with this
approach we just move a class into a separated file from our main program and follow the next
structure:
This will allow us to use the Class_Name class without any dot notation. We can also the as
statement to assign an alias to a class.
2. Storing Multiple Classes in a Module: we can place more than one class per module. Just
remember that child classes should be in the same file as it’s parent class (or at least have some
connection to it).
3. Importing Multiple Classes from a Module: if we want to import more than one class in one
single statement, we can do it by separating each class by a comma:
4. Importing an Entire Module: this approach is harsher because it imports everything from a
module in one go. In this case we need to use a dot notation when we call a class.
#Calling a class:
module_name.classname()
5. Importing All Classes from a Module: this is the harshest method to use due to its inefficiency. By
using:
It’ll import every class that is included in the module. With this import statement there is no need to
use the dot notation, we can call a class just by it’s name. However, it’s not recommended to use
since if there is a class or function with the same name in our program as the one in the module, it
could create unexpected errors.
6. Importing a Module into a Module: in the specific case we want to separate a parent class and
it’s child class in different modules, we can do it; however, we need to place an import statement at
the child class’s file so when we call that child class from another file, our code doesn’t broke.
Return to Index
favorite_languages = OrderedDict()
favorite_languages["jen"] = "python"
favorite_languages["sarah"] = "c"
favorite_languages["edward"] = "ruby"
favorite_languages["edward"] = "python"
In the previous example, we make a dictionary object using the OrderedDict class. There’ll be
some exact scenarios where we’ll need to use an ordered dictionary, in those cases, an OrderedDict
instances can become handy.
We also have the random module. In this case, we need to import random ; it is used to randomize a
value between the specified parameters. Some of its methods are:
Method Description
.randrange(starting number, Returns a random number between the given range
ending number) without including the ending number.
.randint(starting number, ending Returns a random number between the given range
number) INCLUDING the ending number.
.random() Returns a random float number between 0 and 1.
Method Description
.shuffle(list_name) Takes a sequence and returns the sequence in a
random order.
.choice(list_name) Returns a random element from the given sequence.
Structure:
random.method(parameters)
Return to Index
Styling Classes
There are some rules we must follow when writing classes.
1. Class names should be written in CamelCaps . To do this, capitalize the first letter of each word
and don’t use underscores to separate them.
2. Instances and module names should be written in serpent case.
3. Every class should have a #docstring immediately following the class definition. This
docstring should be a brief description of what the class does using the same docstring format
we use in functions.
4. Module files should also have a docstring describing what the classes in a module can be used
for.
5. There should be one blank line separating each method of a class and two white lines separating
classes from each other’s.
6. When importing modules, made by us and external ones, import the external modules first, then
leave a white space and then import the modules written by us.
Return to Index
3.1415926535
8979323846
2643383279
Just as a quick note, procure that this file is in the same directory as your program file, we’ll explain
the importance of this later. Now, in order to read this file through a Python program, we need to learn
new statements and functions used to handle plain text files (such as .txt). Reading the
pi_digits.txt file using Python language would look something like this:
print(content)
Let’s analyze the block code line by line to understand what really happened. The open() function is
used to return an object that contains the content of our file. We store that object in the file_object
variable using the as statement. On the second line, we can see a new method named .read() .
This method is compatible with a #file_object so when we pass this method through the variable that
contains our pi_numbers.txt file, the file_object will return a long string containing the same
characters of the whole text file.
The with statement, which appears at the mere beginning, is used to close the file once Python
interprets you are no longer going to need it. We could perfectly use the open() and close()
statements to open and close a file, however this could lead to more technical problems such as
closing a file ahead of time, or keeping a file open, which could corrupt the file data. It’s way better to
let Python decide when to close the file.
Note: sometimes, when writing code inside our with block, we might notice that the file closes
ahead of the time we were expecting to do it. This happens because Python interprets wrongly
that you are not going to use the file, so it closes it; however if this happens, try writing your code
outside the with block, that may fix any problems.
Return to Index
File Paths
I´m not going to dig deeper on this topic since I already know enough to handle file paths. Just as a
quick reminder, file paths work exactly the same as in the terminal, both relative and absolute paths. If
we want to specify a path to Python, we do it through a string. Example:
file_path = "./sample_texts/pi_digits.py"
1. Looping through each element of the file object: since the file object is divided by elements,
each element representing an individual text line, we can loop through the elements of a file object, as
long as it is inside the with block. Example:
file_name = "./sample_texts/pi_numbers.txt"
with open (file_name) as f_obj:
for i in f_obj:
print(i)
3.1415926535
8979323846
2643383279
Note: as you can see in the output, there is added an extra white space. This happens because
each line has an invisible \n character at the end of it. When we pass those lines through the
print() function, it adds another \n character as well.
This is how the text file would like if the \n characters weren’t invisible:
And this is how it’ll look if we passed each line through the print() function. Remember that we can
get rid of those spaces using .strip() on each line or we can pass the key-word argument end=''
so the print() function doesn't include the scape sequence \n :
file_name = "./sample_texts/pi_numbers.txt"
with open(file_name) as f_obj:
content = f_obj.readlines()
print(content)
Return to Index
file_name = "./sample_texts/1000_digits_of_py.txt"
False
True
True
False
This example illustrates how we can check if certain characters appear in our text file. However, this is
not the only way we can use to check this, there are many other ways you can do by applying what
you have learned until now.
Writing to a File
So far we have been working with the open() function, using only one of it’s parameters. However,
this function can receive two arguments, the first one indicating the file route and name, and the
second one, indicating in which mode we want to set the file. There are 3 main modes, read mode
"r" , write mode "w", and append mode "a" , or a mode that allow us to both read and write in
the file "r+" . So far we haven’t passed any secondary argument to the open() function, that’s
because open() uses the "r" option by default. There are two ways of writing in a file:
1. Writing by using the "w" argument: When we set our file into writing mode, if the file doesn’t
exist, it’ll create the file with the indicated file name and add the lines we write. However, if the file
does exist and has content in it, it’ll overwrite the previous file content. Be careful when using this file
mode since it may overwrite information that you needed. Example:
File: guest.txt
Note 1: when we write on files, the changes do not appear at the terminal screen, rather at the
file. So, when we open the guest.txt file, we’ll see that it has the lines we indicated. The
.write() method is used to write a string we pass as argument.
Note 2: in the previous example, the guest.txt file didn’t existed until we run the program. This
happens because, as explained before, the "w" mode creates files if they don’t exist.
2. Writing by using the "a" argument: The append mode is useful if we want to add content to a
file, instead of just overwriting on it. It works pretty much the same as the "w" argument, the only
thing that changes is the final output since, to say an example, if we write "hello" using "w"
several times, the file will be kept the same as if we used "a" which will result in a file having a
repetition of the world "hello". Example:
file_name = "text_buffer.txt"
print("Welcome to the Text Buffer program, type something and I'll store it in a
file. \n(Press 'q' anytime to quit)")
with open(file_name, "a") as f_obj:
while True:
user_input = input("Type the text you want to store: ")
if user_input != "q":
f_obj.write(user_input + "\n")
else:
break
Note: it seems that "a" can also create files if they don’t exist, I didn’t know that until I run some
experiments.
Return to Index
Exceptions
Whenever an error occurs that makes Python unsure what to do next, it creates an
#exception_object . If we write code that handles that exception, the program will continue running;
nonetheless, if we don’t handle the exception, the program will halt and show a traceback, which
includes a report of the exception that was raised.
Exceptions are handled with #try-except blocks that tell Python exactly what to do in case they
found an exception. Using this blocks prevents our program from crashing, so it continues running
even after the exception was detected.
print(5/0)
Using the name of the exception we can tell Python what to do if they found it at any given moment of
our program. For example, let’s create a program that divides two given numbers and shows the
result as an output:
Tell me a number: 4
Tell me another number: 2
4/2 = 2.0
This short example receives two numbers as inputs and then divides them. However, what will happen
if by mistake the user typed a letter instead of a number. This would rise the ValueError exception
since the int() function can’t convert characters into divisible numbers:
Tell me a number: 4
Tell me another number: a
Traceback (most recent call last):
File "pruebas.py", line 5, in <module>
result = int(number_1) / int(number_2)
ValueError: invalid literal for int() with base 10: 'a'
In order to keep this short program more user friendly, we can use the try_except block to handle
the exception:
Tell me a number: da
Tell me another number: 5
Can't use letters, please type only numbers
Now, as you can see, we don’t get a traceback but instead we get a friendly message saying us what
we did wrong. Letting unexperienced users read tracebacks is a bad idea since they’ll be confused,
however it can also become dangerous if the person who is using our program knows what he’s
reading since, he might use the traceback information to attack our program. In conclusion, never
leave any possible tracebacks unchecked.
The try-except block functions this way: Python attempts to run the code in the try statement. The
only code that should go in a try statement is code that might cause an exception to be raised.
Sometimes you’ll have additional code that should run only if the try block was successful; THIS
bunch of code goes in the else block. The except block tells Python what to do in case a certain
exception arises when it tries to run the code in the try statement. We can have more than one
except block if our program requires it.
By anticipating likely sources of errors, we can write robust programs that continue to run even when
they encounter invalid data and missing resources.
Return to Index
Failing Silently
But what happens if we don’t want to let user know why our program failed. This can be archived
using the pass statement. For example, let’s say that we don’t want to let the user know that they
can’t use letters as input in our previous program:
try:
result = int(number_1) / int(number_2)
except ValueError:
pass
else:
print(f"{number_1}/{number_2} = {result}")
Tell me a number: 4
Tell me another number: a
Now, each time the program fails, it doesn’t report it to the user.
It’s important to consider that well written programs do not tempt to crash or rise exceptions, do not
abuse the usage of this blocks since it may make your program much harder to understand. A little
of experience will help you know where to include exception handling blocks in your program and
how much to report to users about errors that arise.
Return to Index
Storing Data
When working with user’s input, most of the times we'll want to store that input in a file to later work
with it, either for make a table, display a previous score, etc. Even though we could do this by the
methods we previously learn as using .write() , there is a more efficient way to do it, and that is
using the json module.
file_name = "numbers.json"
numbers = [1, 2, 3, 4, 5, 6]
with open(file_name, "w") as f_obj: #Notice that we used the "w" option
dump(numbers, f_obj)
This program doesn’t have an output to show, however if we go to our File Explorer we’ll see that the
file number.json has been created and that it contains the list: [1, 2, 3, 4, 5, 6] .
When working with the json module, it’s important to always end file names that will contain data as
.json because it will make json functions and methods work more efficiently. Also, ending file
names with this termination m akes that the data is read as true data structures. Reading, for
example, a Python list written inside a common .txt file won’t behave like a list since what Python is
reading is this “[list]” string and not this [list] data-type. Here is where our second main function
jumps in.
Using the load() function makes that the data structures (lists, dictionaries, strings, tuples, etc.)
located on external files can be read as normal data structures and not like strings. This is useful
since we can store the data structures we read in a variable and then use them as we please.
Example:
[1, 2, 3, 4, 5, 6, 7]
file_name = "numbers.json"
for i in numbers:
print(i**2)
1
4
9
16
25
36
49
It’s not a specific rule to create two different programs if we want to load and dump data. We can do
both load (read) and dump (write) data in the same program, to the same file.
Return to Index
Refactoring
#refactoring means to divide our code into smaller functions, each one fulfilling a certain task. In
theory, this should make your code more understandable and easier to improve. Try figuring out what
does this code do:
from json import dump, load
def get_stored_username():
"""Get stored username if available"""
file_name = "./text_files/username.json"
try:
with open(file_name) as f_obj:
username = load(f_obj)
except FileNotFoundError:
return None
else:
return username
def get_new_username():
"""Prompt for a new username"""
username = input("What is your name? ")
file_name = "./text_files/username.json"
def greet_user():
"""Greet the user by name."""
username = get_stored_username()
if username:
user_confirmation = input(f"Are you {username}? (type y/n) ")
if user_confirmation == "y":
print(f"Welcome back {username}!")
elif user_confirmation == "n":
username = get_new_username()
else:
username = get_new_username()
greet_user()
# If you couldn’t figure it out, see "page 214" and "page_214.py, 10-13".
Return to Index
Chapter 11: Testing Your Code
Testing a function
So far, we have been running our programs in the [terminal] to test if our program execution works
fine. However, some of those programs required to be tested more than once, with different types of
output. Running the same program over and over again can become tedious, but Python's standard
library has a module that can automate the testing of our code, which name is unittest . It’s
important to clarify that, when testing our code, it’s more efficient to test each function/class
individually so we know that everything is working as expected; the unittest module allows such
experimentation. In case of further doubts about the module, you can visit it's documentation right
here.
Imagine we have a function that turns a formatted name in the style of “Name Last name”. If we
wanted to test that function, we must first create a new file since all the testing using unittest
should be done in a separated file from the code we are about to test.
Leonardo Perez
As we can see, this function works completely as we expected it, however in order to test it we
needed to manually register “leonardo” and “perez” to see if the output was what we wanted. We
could automate the testing by making a unit test of it:
# File name: tester.py
import unittest
class FormattedName(unittest.TestCase):
def test_name_lastname(self):
formatted_name = get_formatted_name("leonardo", "perez")
self.assertEqual(formatted_name, "Leonardo Perez")
if __name__ == '__main__':
unittest.main()
.
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK
Let’s analyze the code section trough section. As we can see, in the first lines we import both the
function we want to test and the unittest module. Once we imported the tools we’re going to need,
we create a class that inherits all of the properties from the unittest.TestCase parent class. Since
we are only going to make one simple unit test , there’s no point in assigning attributes to this child
class, that’s why we don’t see the def __init__(): method nor the super().__init__()
statement.
Now, we just create one single method that will check if the returned value of the function
get_formatted_name is equal to “Leonardo Perez” only if we pass the arguments “leonardo” and
“perez”. All methods created should begging with the word test because this way, unittest will run
them automatically when running the program.
if __name__ == '__main__':
unittest.main()
Such lines are very important, and will be used now on in our programs. In simple terms, these lines
are used for the python community as an entry point, where all the main code (excluding functions
and classes) will be placed. Such convention is incredibly useful for organizing our code. [Here's a
detailed explanation](python - What does if name == "main": do? - Stack Overflow) on what such
lines actually do, but I recommend reading such explanation once you have mastered more advanced
Python topics.
Return to Index
Passing a Test
Reading the output of a successful test is pretty simple. Taking the previous output as an example: the
single dot at the beginning of our output points out that a single test passed; as we add more tests,
the quantity of dots will increase. The next line tells us that Python ran one test, and it took less than
0.001 seconds to run. The final “OK” tells us that all unit tests in the test case passed.
This output indicates that the function get_formatted_name() will always work for names that have a
first and last name unless we modify the function. When we modify get_formatted_name() , we can
run this test again. If the test case passes, we know that the function will still work for names like
Leonardo Perez. Know we can say that a unit test is a method from the class, and that the class is
the test case .
A Failing Test
Let’s modify the get_formatted_name() function so it now handle middle names as well:
Now, if we run again tester.py we’ll see that our test failed:
E #1
================================================================
ERROR: test_name_lastname (__main__.FormattedName) #2
----------------------------------------------------------------------
Traceback (most recent call last): #3
File "tester.py", line 6, in test_name_lastname
formatted_name = get_formatted_name("leonardo", "perez")
TypeError: get_formatted_name() missing 1 required positional argument: 'last_name'
----------------------------------------------------------------------
Ran 1 test in 0.000s #4
FAILED (errors=1) #5
The first item in the output is a single E at (1), which tells us that one unit test in the test case
resulted in an error. At (2), we'll see that test_name_lastname in FormattedName caused an error.
Knowing which test failed is critical when your test case contains many unit tests. At (3) we see a
standard traceback, which reports to us that the function call get_formatted_name(“leonardo”,
“perez”) no longer works because it’s missing a required positional argument.
We also see that one unit-test was run at (4). Finally, we see an additional message that the overall
test case failed, and that one error occurred when running the test case at (5). This information
appears at the end of the output so you see it right away; you don’t want to scroll up through a long
output listing to find out how many tests failed.
Return to Index
Getting back to the get_formatted_name() function, what we want to archive is that the function
receives a middle name only if the person has a middle name, if not, then just return the name and
last name. For that we need to make the parameter middle_name optional:
return stringer.title()
Now, if we attempt to run tester.py we’ll see that everything works fine:
.
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK
In conclusion, our main goal is to keep test cases as they are and try fixing our code, so no errors are
reported when running our test cases.
Return to Index
class FormattedName(unittest.TestCase):
def test_name_lastname(self):
formatted_name = get_formatted_name("leonardo", "perez")
self.assertEqual(formatted_name, "Leonardo Perez")
def test_name_middle_last(self):
formatted_name = get_formatted_name("gabriel", "padron", "gerardo")
self.assertEqual(formatted_name, "Gabriel Gerardo Padron")
if __name__ == '__main__':
unittest.main()
..
----------------------------------------------------------------------
Ran 2 tests in 0.000s
OK
Great! We now know that the function still works for names like “Leonardo” “Perez”, and we can be
confident that it will work form names like “Gabriel Gerardo Padron” as well.
Method Use
.assertEqual(a, b) Verify that a == b
.assertNotEqual(a, b) Verify that a != b
.assertTrue(x) Verify that x is True
.assertFalse(x) Verify that x is False
.assertIn(item, list) Verify that item is in list
.assertNotIn(item, list) Verify that item is not in list
Return to Index
A Class to Test
Now lets test a class using the unittest module. Consider a class that helps administer anonymous
surveys:
def show_question(self):
print(self.question)
def show_all_responses(self):
print("Responses:")
for i in self.responses:
print(f"- {i.lower()}.\n")
my_survey = AnonymousSurvey(question)
while True:
my_survey.show_question()
print("(Press 'q' to quit)")
user_response = input("Type your answer: ")
if user_response == "q":
break
my_survey.append_response(user_response)
repeat_survey = input("Want to add one more response? y/n \n")
if repeat_survey == "n":
break
continue
my_survey.show_all_responses()
Testing a class is not very different from testing a function, it just requires some extra steps since in
order to test one of them we need to create an instance of that class and use it, so it can undergo our
unit tests:
import unittest
class AnonymousSurveyTest(unittest.TestCase):
"""Testing that a single response is stored correctly."""
def test_store_single_response(self):
question = "Which is your favorite color?"
my_survey = AnonymousSurvey(question) #1
my_survey.store_response("Red") #2
self.assertIn("Red", my_survey.responses) #3
if __name__ == '__main__':
unittest.main()
What this test case does is that first, it creates a survey called my_survey at (1), passing it as an
argument a simple example question. Then, we store a single value, red , using the
.store_response() method at (2). To check if the method .store_response() actually stored the
value red in the class attribute .responses , we can use the assertion method .assertIn which
checks if an element is in a list (3). If we run this test we should get this:
.
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK
Now let’s add a new unit test that checks if the method .store_response() can store more than
one response:
import unittest
def test_store_three_responses(self):
question = "Which is your favorite color?"
my_survey = AnonymousSurvey(question)
responses = ["red", "blue", "yellow"]
for i in responses: #1
my_survey.store_response(i)
for i in my_survey.responses: #2
self.assertIn(i, my_survey.responses)
if __name__ == '__main__'
unittest.main()
This will append each of the elements in responses to the class attribute .responses at (1). Then we
check if .store_responses() actually stored each of our responses at (2).
Return to Index
import unittest
class AnonymousSurveyTest(unittest.TestCase):
"""Testing that a single response is stored correctly."""
def setUp(self): # Here
question = "Which is your favorite color?"
self.my_survey = AnonymousSurvey(question)
self.examples = ["red", "blue", "yellow"]
def test_store_single_response(self):
self.my_survey.store_response(self.examples[0])
self.assertIn(self.examples[0], self.my_survey.responses)
def test_store_three_responses(self):
for i in self.examples:
self.my_survey.store_response(i)
for i in self.examples:
self.assertIn(i, self.my_survey.responses)
unittest.main()
Return to Index
CONGRATULATIONS LEONARDO!!! You now know enough about Python to start building
interactive and meaningful projects. Remember that “It doesn't matter how slow you go as long as you
don't stop”. Maybe you are reading this because you haven’t practice Python in a while, and that’s
okey. If some of my previous notes left some questions unanswered, remember to always check the
book (check the second edition, since the used book version is now outdated).
This is goodbye for now, but we’ll see you in the Python Crash Course 2nd Edition Part II to start
building amazing projects such as a small videogame, data visualizations, and a web application. Or,
you can explore other notes I have taken for other Pythonic books that will enhance your skills such
as:
1. Python - Formulary: A brief summary of useful functions that you may need in your Python
programming career, it can be called a Cheat sheet.
2. [[]]
3. [[]]
4. [[]]
Remember: "never stop learning, be better than yesterday and always keep in mind your goals".
Return to Index