0% found this document useful (0 votes)
4 views87 pages

Python Crash Course 2nd Edition Part I

The document outlines the content of 'Python Crash Course 2nd Edition Part I', covering fundamental programming concepts in Python, including variables, data types, lists, and functions. It provides a structured index of chapters and topics, along with explanations and examples to facilitate understanding. The document serves as a guide for beginners to learn Python programming effectively.

Uploaded by

Leonardo Perez
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
Download as pdf or txt
0% found this document useful (0 votes)
4 views87 pages

Python Crash Course 2nd Edition Part I

The document outlines the content of 'Python Crash Course 2nd Edition Part I', covering fundamental programming concepts in Python, including variables, data types, lists, and functions. It provides a structured index of chapters and topics, along with explanations and examples to facilitate understanding. The document serves as a guide for beginners to learn Python programming effectively.

Uploaded by

Leonardo Perez
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
Download as pdf or txt
Download as pdf or txt
You are on page 1/ 87

Python Crash Course 2nd Edition Part I

Index

1. Chapter 1: Getting Started


1. Before Reading this Document
2. Setting Up Python
2. Chapter 2: Variables and Simple Data Types
1. How to Declare and Initialize a Variable in Python
2. Variable Naming Rules
3. How to Read a Simple Error
4. Strings
5. Changing Case in a String with Methods
6. Using Variables in Strings
7. Adding Whitespace to Strings with Tabs or Newlines
8. Stripping Whitespaces
9. Underscores in numbers
10. Multiple Assignations
11. Constants
12. Comments
13. What kind of comments should you write?
14. The Zen Of Python
3. Chapter 3: Introducing Lists
1. What is it?
2. Accessing Elements in a List
3. Modifying Elements in a List
4. Adding elements to a list
5. Removing elements from a List
6. Organizing a List
4. Chapter 4: Working with Lists
1. For Loops in Python
2. Making Numerical Lists
3. Simple Statistics with a List of Numbers
4. List Comprehensions
5. List Comprehension with a Conditional
6. Slicing a List
7. Copying a List
8. Tuples
9. Other Ways for Declaring Tuples
10. Styling our Code
5. Chapter 5: If statements
1. If Statements in Python
2. Ignoring Case When Checking for Equality
3. Checking for Multiple Conditions
4. Checking Whether a Value is in or not in a List
5. Boolean Expressions
6. Checking that a List is not Empty
7. Using Multiple Lists
6. Chapter 6: Dictionaries
1. Working with Dictionaries
2. Accessing Values in a Dictionary
3. Adding New Key-Value Pairs
4. Modifying Values in a Dictionary
5. Removing Key-Value Pairs
6. A Dictionary of Similar Objects
7. Using get() to Access Values
8. Looping Through a Dictionary
9. Looping Through All the Keys in a Dictionary
10. Looping Through a Dictionary’s Keys in Order
11. Looping Through a Dictionary’s Values
12. Using the set() function
13. Sets
14. The Unique Property of a Set
15. Nesting
16. A List of Dictionaries
17. A List in a Dictionary
18. A Dictionary in a Dictionary
19. Dictionary Comprehensions
7. Chapter 7: User Input and While Loops
1. How the input() Function Works
2. Using int() to Accept Numerical Input
3. The Modulo Operator `%`
4. The While Loop
5. Letting the User Choose When to Quit
6. Using the Continue statement
7. Moving Items from One List to Another
8. Removing All Instances of Specific Values from a List
9. Filling a Dictionary with User Input
8. Chapter 8: Functions
1. Positional Arguments
2. Keyword Arguments
3. Default Arguments
4. Making an Argument Optional
5. The "return" statement
6. Returning a Dictionary
7. Returning Several Values
8. Using a Function within a While Loop
9. Passing a List
10. Modifying a List in a Function
11. Preventing a Function form Modifying a List
12. Passing an Arbitrary Number of arguments
13. Using Arbitrary Keyword Arguments
14. Avoiding errors when specifying Arguments
15. Storing Our Functions in Modules
16. Styling Functions
9. Chapter 9 Classes
1. The __init__( ) Method
2. Making an Instance from a Class
3. Accessing Attributes
4. Calling Methods
5. Creating Multiple Instances
6. Setting a Default Value for an Attribute
7. Modifying Attributes Values
8. Inheritance
9. The __init__() Method for a Child Class
10. Defining Attributes and Methods for the Child Class
11. Overriding Methods from the Parent Class
12. Instances as Attributes
13. Modeling Real-World Objects
14. Importing Classes
15. Finding Our Own Workflow
16. The Python Standard Library
1. The random module
17. More information about modules
18. Styling Classes
10. Chapter 10 Files and Exceptions
1. Reading an Entire File
2. File Paths
3. Reading Line by Line
4. Working with a File’s Contents
5. Writing to a File
6. Exceptions
7. The Try-Except Block
8. Failing Silently
9. Deciding Which Errors to Report
10. Bonus Methods .split() and .count()
11. Storing Data
12. The json.load() and json.dump() functions
13. Refactoring
11. Chapter 11: Testing Your Code
1. Testing a function
2. Unit Test and Test Cases
3. Passing a Test
4. A Failing Test
5. Responding to a Failed Test
6. Adding New Tests
7. A Variety of "assert" Methods
8. A Class to Test
9. The setUp() Method
12. Python Crash Course Part I (Ended)

Chapter 1: Getting Started

Before Reading this Document


The content of this document includes basic programming concepts in order to get started (or to re-
learn forgotten concepts) on the Python programming language. I finished writing this "Before
Reading" section in order to let me know (in case I need to re-read my notes) that the content of this
summary is primarily based on the book "Python Crash Course: A Hands-On, Project-Based
Introduction to Programming 1st Edition" by Eric Matthews. The knowledge of these notes was later
updated with the "2nd Edition" of the same book. And I'm planning on updating them when the "3rd
Edition" comes out later this year (2023).

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

Chapter 2: Variables and Simple Data Types

How to Declare and Initialize a Variable in Python


What's wonderful about Python is that its syntax allows us to #declare and #initialize a variable
without specifying its #data_type and won't trigger an unexpected behavior even if they are not
initialized at the moment.

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.

Variable Naming Rules


Can contain only letters, numbers, and underscores. They can start with a letter or an under
score, but not with a number. Example: message_1 but no 1_message.
Spaces are not allowed in variable names, but underscores can be used to separate words in
variable names.
Avoid using Python keywords and functions names as variable names such as the word print
(See ["Python Keywords and Built-in Functions"] on page 471 of the book.).
Variable names should be short but descriptive (more on [[]]).
All variables should be written in lowercase unless they are constants.

Return to Index

How to Read a Simple Error


Python handles errors in a beautiful manner, being as clear as posible over where an error was
committed. Here's a simple example:

Traceback (most recent call last):


File "hello_world.py", line 2, in <module>
print(mesage)
NameError: name 'mesage' is not defined

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.

Changing Case in a String with Methods


A #method is an action that python can perform on a piece of data. Each method begins with the
variable or function, then a dot ( . ), the name of the method, and is followed by a set of parentheses
because many of them require additional information to do their work.

Some useful methods are the following:

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

Using Variables in Strings


In some situations, you’ll want to use a variable’s value inside a string. To do this, place the letter f
immediately before the opening quotation mark. Put braces around the name(s) of any variable you
want to use inside the string. Python will replace each variable with its value when the string is
displayed. These strings are called #f-strings . Here's a quick example that illustrates how to use
them:

first_name = "ada"
last_name = "lovelace"
full_name = f"{first_name} {last_name}"
print(full_name)

Adding Whitespace to Strings with Tabs or Newlines


In programming, whitespaces refer to any nonprinting character, such as spaces, tabs, and end-of-
line symbol.

To add a tab to the string, use \t​


To add a newline to the string, use \n​
Both \t​ and \n​ can be combined at the same string.
Stripping Whitespaces
It´s important to think about white spaces, because often you´ll want to compare two strings to
determine whether they are the same, for example, when performing a user's poll. Two users may
enter the same answer, but if they differ in a whitespace then the python interpreter will consider
them as two completely different strings. Some methods to get rid of extra whitespaces are:

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 + }".

What kind of comments should you write?


In short, meaningful comments in order to explain the code. It's important to make this a habit
because good programmers always comment their code. When you're determining whether to write a
comment, ask yourself if you had to consider several scenarios before coming up with a reasonable
way to make a certain part of the code work.

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.

The Zen Of Python:


Python philosophy… Beautiful is better than ugly Simple is better than complex Complex is better
than complicated Readability counts There should be one-- and preferably one –obvious way to do it
Etc…

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!.

Accessing Elements in a List


To access the values within a list, we type the name of the lists followed by the index operator, which
is composed of a pair of square brackets ( [] ), and an integer called an #index to specify which
value we want to access. Example:

animals = ['tiger', 'lion', 'kitty', 'dog', 'fish']

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.

Modifying Elements in a List


You can change the value of an element of a list by indicating the index of it, and then assigning the
new value. Example:

motorcycles = ["honda", "yamaha", "susuki"]


print(motorcycles)
motorcycles[0] = "ducati"
print(motorcycles)
["honda", "yamaha", "susuki"]
["ducati", "yamaha", "susuki"]

Return to Index

Adding Elements to a List


To add an element at the end of the list, use the .append() method.

.append(value/string/boolean)

To insert an element in a specific position, use the .insert() method.

.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.

Removing Elements from a List


To remove an element from a specific position, we use the del statement.

del list[index_number]

To remove an element with a known value, we use the remove method.

.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:

motorcycles = ['honda', 'yamaha', 'suzuki']


print(motorcycles)
popped_motorcycle = motorcycles.pop(2)
print(motorcycles)
print(popped_motorcycle)

['honda', 'yamaha', 'suzuki']


['honda', 'yamaha']
'suzuki'

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.

.sort() #This method can accept the reverse=True parameter.


#like this .sort(reverse=True)

To organize a list temporary alphabetically, we use the sorted() function.

sorted(name of the list, reverse = True) # reverse = True is optional

To print a list in reverse order permanently, we use the .reverse() method.


.reverse()

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

Chapter 4: Working with Lists

For Loops in Python


In Python, a #for_loop works completely different that in other programming languages. The
standard syntax of a for loop is basically the following:

for (i = 0; i < value; i++){


// Code to be reapeted according to "value"
}

However, in Python, the thing changes completely, reducing the previous syntax to the following
syntax:

for element in iterable:


# Code to be repeated according to the number of elements at "iterable"

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!

Making Numerical Lists


We make numerical lists by creating one, or by using the range() function. By itself, the range()
function creates a temporary tuple containing the beginning of a specified number to the end of
another specified number, it never touches the ending number. We can also pass it an optional
third argument which will skip an amount of numbers to reach the next element of the tuple:

range(starting number, ending number, amount to be skipped)

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]

Simple Statistics with a List of Numbers

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:

squares = [value ** 2 for value in range(1, 11)]


#element #loop
print(squares)

[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

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.

List Comprehension with a Conditional


As a matter of a fact, list comprehensions are actually a way of simplifying loops that involve
appending a series of elements into a list. Since loops can have conditionals, so they do list
comprehensions, we can add them to a normal list comprehension call in order to filter the values
which will be stored in the list. Example:

numbers = [number for number in range(0, 11) if number % 2 == 0]


print(numbers)

[0, 2, 4, 6, 8, 10]

Here's a diagram that explains the previously discussed:


Slicing a List
#slices are fragments of a list. In order to make a slice we use the : symbol when indicating the
index. Example:

players = ["charles", "martina", "michael", "florence", "eli"]


print(players[0:3])

["charles", "martina", "michael"]

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:

dimensions = (200, 50, 10)


print(dimensions[0])
print(dimensions[2])

200
50

You can loop around tuples as well. Also, even though tuples can't be modified, they can be
overwritten like variables. Example:

dimensions = (200, 50, 10)


print(dimensions)

dimensions = (100, 20, 5)


print(dimensions)

(200, 50, 10)


(100, 20, 5)
An important aspect about tuples is that they're inmutable data-types (meaning that we can not
change it's elements) and they serve a specific purpose, which is iterating.

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.

Other Ways for Declaring Tuples


Since tuples are great for building iterables, Python has many forms to declare them, one is with the
Python Constructor Function tuple() . It accepts one argument which is the object/iterable that we
want to transform into a tuple. Example:

new_tuple = tuple(iterable_object)

The other famous way for declaring a fresh tuple is using a multiple assignation into one variable like
this:

new_tuple = 1, 2.5, 'hi', 3, 'cheese'


print(new_tuple)

(1, 2.5, 'hi', 3, 'cheese')

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.

Styling our Code


When someone wants to make a change to the Python language, they write a Python Enhancement
Proposal (PEP). One of the oldest PEPs is PEP 8 – Style Guide for Python Code, which instructs
Python programmers on how to style their code. PEP 8 is fairly lengthy, but much of it relates to more
complex coding structures than what we have seen so far.

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 - elif - else statement:

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)"

Ternary if - else operator


This operator is a shortcut in writing an if - else conditional. Such operator is used, only to return
a value, not to perform actions or execute further code. Here's its syntax:

ternary_operator = Option_1 if condition else Option_2

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:

valueIfTrue = 'Access granted'


valueIfFalse = 'Access denied'

condition = True
message = valueIfTrue if condition else valueIfFalse
print(message)

condition = False
message = valueIfTrue if condition else valueIfFalse
print(message)

Checking Whether a Value is in or not in a List


To find out whether a particular value is already or not in a list, use the key-word in or not in .
Examples:

#Checking if a value is in a list


>>> requested_toppings = ["mushrooms", "onions", "pineapple"]
>>> "mushrooms" in requested_toppings
True
>>>"peperoni" in requested_toppings
False

#Checking if a value is not in a list


banned_users = ["andrew", "carolina", "david"]
user = "marie"

if user not in banned_users:


print(user.title() + ", you can post a response if you wish.")

Return to Index

Boolean Expressions
Are True or False , also called #conditional_tests .

Checking that a List is not Empty


Example:

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:

available_toppings = ["mushrooms", "olives", "green peppers", "pepperoni",


"pineapple",
"extra cheese"]

requested_toppings = ["mushrooms", "french fries", "extra cheese"]

for requested_topping in requested_toppings:


if requested_topping in available_toppings:
print(f"Adding {requested_topping}.")
else:
print(f"Sorry, we don’t have {requested_topping}.")

print("\nFinished making your pizza!")

Return to Index

Chapter 6: Dictionaries

Working with Dictionaries


A #dictionary in Python is a collection of key-value pairs. Each #key is connected to a #value ,
and you can use a key to access the value associated with that key. A key’s value can be a number, a
string, a list, or even another dictionary. Use {} to assign a dictionary to a variable. Example:

alien_0 = {"color": "green", "points": 5, "health": 100}

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:

alien_0 = {"color": "green", "points": 5, "health": 100}


print(alien_0["color"])

green

Return to Index

Adding New Key-Value Pairs


Since dictionaries are dynamic structures, we can add new key-value pairs at any time, without
limitations. We do this by providing the dictionary name followed by the name of the new key inside
brackets along with the new value. Example:

alien_0 = {"color": "green", "points": 5, "health": 100}


print(alien_0)

alien_0["x_position"] = 0
alien_0["y_position"] = 25
print(alien_0)

{"color": "green", "points": 5, "health": 100}


{"color": "green", "points": 5, "health": 100, "x_position": 0, "y_position": 25}

Modifying Values in a Dictionary


To modify a value, give the name of the dictionary with the key's name in square brackets and then
the new value you want associated with that key. Example:

alien_0 = {"color": "green"}


print("The alien is " + alien_0["color"] + ".")
alien_0["color"] = "yellow"
print("The alien is now " + alien_0["color"] + ".")

The alien is green.


The alien is now yellow.

Removing Key-Value Pairs


We can use the del operator to completely remove a key-value pair. Everything the del statement
needs is just the name of the dictionary and the key that you want to remove. Example:

alien_0 = {"color": "green", "points": 5, "health": 100}


print(alien_0)

del alien_0["points"]
print(alien_0)

{"color": "green", "points": 5, "health": 100}


{"color": "green", "health": 100}

Return to Index

A Dictionary of Similar Objects


The previous examples involved storing different kinds of information about one object (the alien). You
can also use a dictionary to store one kind of information about many objects. Example:

favorite_languages = {
"jen": "python",
"sarah": "c",
"edward": "ruby",
"phil": "python",
}

Using .get() to Access Values


Using keys in square brackets to retrieve the value you’re interested in from a dictionary might cause
one potential problem: if the key you ask for doesn’t exist, you’ll get an error. For dictionaries,
specifically, you can use the .get() method to set a default value that will be returned if the
requested key doesn’t exist and by doing this, avoid getting an error.

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:

alien_0 = {'color': 'green', 'speed': 'slow'}


point_value = alien_0.get('points', 'No point value assigned.')
print(point_value)

No point value assigned.

Here, instead of getting an error, we get now as an output a clean message indicating what went
wrong.

More about For Loops


If we remember correctly, in Chapter 2 Multiple Assignations and in Chapter 5 Other Ways For
Declaring Tuples, we discussed briefly about the techniques called #packaging and #unpackaging ;
such techniques will need to be applied if we want to iterate through dictionaries.

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:

my_list = [('leonardo', 18), ('daniel', 21), ('gabriel', 7), ('carlos', 18)]


One way on which we can iterate through this list is by using the index numbers of each tuple inside
the list:

my_list = [('leonardo', 18), ('daniel', 21), ('gabriel', 7), ('carlos', 18)]

for tupl in my_list:


print(f"Hi {tupl[0].title()}, your age is {tupl[1]}!")

Hi Leonardo, your age is 18!


Hi Daniel, your age is 21!
Hi Gabriel, your age is 7!
Hi Carlos, your age is 18!

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:

my_list = [('leonardo', 18), ('daniel', 21), ('gabriel', 7), ('carlos', 18)]

for name, number in my_list:


print(f"Hi {name.title()}, your age is {number}!")

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​.

Looping Through a Dictionary


When using dictionaries, we can play with both the key and the value. What I mean by that is, if we
plan on looping through a dictionary keys and values, we'll need to use the .items() method.
Example:

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

Looping Through All the Keys in a Dictionary


The .keys() method is useful when you don’t need to work with all of the values in a dictionary. This
method isn´t just for looping: it actually returns a list of all the dictionary's keys. Example:

favorite_languages = {
"jen": "python",
"sarah": "c",
"edward": "ruby",
"phil": "python
}

friends = ["phil", "sarah"]

for name in favorite_languages.keys():


print(name)
if name in friends:
print(f"Hi {name.title()}, I see that your favorite language is
{favorite_languages[name]}")
Note 1: if you don’t remember what these bunch of lines of code actually do, go to VsCode and
code it yourself.

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.

Looping Through a Dictionary’s Keys in Order


Since Python doesn’t return dictionary values and keys in any specific order, we can use the
sorted() function to get a copy of the keys (or values) in alphabetic order. Example:

favorite_languages = {
"jen": "python",
"sarah": "c",
"edward": "ruby",
"phil": "python",
}

for name in sorted(favorite_languages.keys()):


print(f"{name.title()}, thank you for taking the poll!")

Edward, thank you for taking the poll!


Jen, thank you for taking the poll!
Phil, thank you for taking the poll!
Sarah, thank you for taking the poll!

Return to Index

Looping Through a Dictionary’s Values


This is similar on how we loop using the .items() method, but instead, if we want to loop only
through the values, we use the .values() method (such method also returns a list containing all the
key's values).

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()}")

The answers of the poll where:


- Python
- C
- Ruby
- Python

Using the set() function


This function is used to grab an iterable and create a list where no values are repeated. A #set is a
collection in which each item must be unique. When you wrap the set() function around a list (or an
iterable) that contains duplicate items, Python identifies the unique items inside the list and builds a
set from those items.

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.

In a nutshell, a set is a collection which is unordered, unchangeable, and unindexed.

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.

The Unique Property of a Set


Duplicates Not Allowed: Sets cannot have two items with the same value. Example:

# Duplicate values will be ignored:


this_set = {"apple", "banana", "cherry", "apple"}
print(this_set)
{'banana', 'cherry', 'apple'}

Note: the values True and 1 are considered the same value in sets, and are treated as
duplicates.

Example:

this_set = {"apple", "banana", "cherry", True, 1, 2}


print(this_set)

{True, 2, 'banana', 'cherry', 'apple'}

Operations with Sets


The sets data-type is often considered as a representation of a mathematical set. If you remember, in
elementary you saw how to unite, intersect and differ the elements of a set with the elements of
another set. Here's how you can do such operations using Python operators: | , & , - , and ^ .

To perform the union between two sets we use the | operator:

set_a = {'a', 'b', 'c', 'd'}


set_1 = {1, 2, 3, 4}

this_set = set_a | set_1


print(this_set)

{1, 2, 3, 4, 'd', 'a', 'b', 'c'}

To perform the intersection between two sets we use the & operator:

set_a = {'a', 'b', 'c', 'd'}


set_b = {'b', 'c', 'e', 'f'}

this_set = set_a & set_b


print(this_set)
{'b', 'c'}

To perform the difference between two sets we use the - operator:

set_a = {'a', 'b', 'c', 'd'}


set_b = {'b', 'c', 'e', 'f'}

this_set = set_a - set_b


print(this_set)

{'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:

set_a = {'a', 'b', 'c', 'd'}


set_b = {'b', 'c', 'e', 'f'}

this_set = set_a ^ set_b


print(this_set)

{'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",
}

people = [emiliano, daniel, gabriel]

for person in people:


for requested_data, data in person.items():
print(f'{requested_data.capitalize()}: {data.title()}')
print(f"\n")

Last name: Perez


Second last name: Lazaro
Favorite color: Blue
Age: 13
Gender: Masculine

Last name: Acevedo


Second last name: Ramos
Favorite color: Green
Age: 17
Gender: Masculine

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"],
}

for name, favorite_place in favorite_places.items():


print(f'Name: {name.title()}\nFavorite places:')

for place in favorite_place:


print(f'\t-{place.capitalize()}')
print("\n")

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.",
},
}

for city, city_info in cities.items():


print(f'{city.title()}:')
for info, data in city_info.items():
if info == "country":
print(f'\t-{info.capitalize()}: {data.title()}')
else:
print(f'\t-{info.capitalize()}: {data.capitalize()}')
print("\n")

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

Here are some examples:

# Example 1
dictionary_1 = {i: i ** 2 for i in range(11)}
print(dictionary_1)

{0: 0, 1: 1, 2: 4, 3: 9, 4: 16, 5: 25, 6: 36, 7: 49, 8: 64, 9: 81, 10: 100}

# Example 2
names = ['leonard', 'daniel', 'emiliano']
ages = [18, 19, 14]

dictionary_1 = {name: age for (name, age) in zip(names, ages)}


print(dictionary_1)

{'leonardo': 18, 'daniel': 19, 'emiliano': 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

Chapter 7: User Input and While Loops

How the input() Function Works


It’s important to remember that when using this function, we store it inside a variable for a later
usage. If we don’t do it, the program will just request the user to enter an input, but without storing it.

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:

user_name = input("Tell me your name: ")

Using int() to Accept Numerical Input


It's important to say that, when storing a user input, it will always be of the string/character type, no
matter if the user typed: 3.1415 , our program will store it as the string "3.1416" . This is a problem
because most of the times we'll want to perform arithmetic/relational operations (+/<), so we need to
transform this input into an int type. In order to do such thing, we use the int() function. By
wrapping up the variable that contains the user's input, we are able to convert a numerical string into
an integer.

age = input("Tell me your age: ")


age = int(age)

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

The Modulo Operator %


Like using other modules such as +, -, /, // (used to dive numbers without getting reminders or
decimals), the % operator is used to dive numbers to get just the reminder, in case of no reminder
such as dividing even numbers (2/2) the result will be 0.

Return to Index

The While Loop


A #while_loop will run as long as certain condition result True . Since this loop doesn’t iterate a
specific number of times, we have the risk to create an infinite loop that will never end our program. To
avoid this, it’s important to place a condition that will fall in a False value. One of the most basic
ways to end a while loop is to place a counter variable, which will be assigned with an initial value of
0 and as each loop repeats, it will add 1 to that variable. We can just tell the while loop to stop when
that variable has surpassed a certain limit. Example:

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:

message = " "


while message != "quit":
message = input("Tell me something and I’ll repeat it back to you: ")
print(message)

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:

active = True # flag


while active:
message = input("Tell me something and I’ll repeat it back to you: ")
if message == "quit":
active = False # changing flag value to False
print("---Ending Program---")
else:
print(message)

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

Using the Continue statement


The #continue_statement will skip the current iteration and jump to the next one, even if there's some
remaining code be be run in the loop.

Moving Items from One List to Another


A #for_loop is effective for looping through a list, but we shouldn’t modify a list inside a for loop
because Python will have trouble keeping track of the items in the list. Instead use the while loop; it
can be used to collect, store, and organize lots of input of dictionaries and lists.

One way to transfer elements of one list to another is using the .pop() method together with a
while loop. Example:

unconfirmed_users = ["leonardo", "daniel", "gabriel", "carlos"]


confirmed_users = []

while unconfirmed_users:
current_user = unconfirmed_users.pop()
print(f"Verifying user: {current_user}")
confirmed_users.append(current_user)

print(f"The following users have been confirmed:\n{confirmed_users}")

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.

Removing All Instances of Specific Values from a List


We can use a while loop to iterate when we don’t know exactly the number of items a
list/dictionary/tuple contains. If we want to remove all instances of an element from a list we can
use a while loop. Example:

pets = ["pets", "cat", "dog", "goldfish", "cat", "rabbit", "cat"]


print(pets)

while "cat" in pets:


pets.remove("cat")
print(pets)

["pets", "dog", "goldfish", "rabbit"]

Return to Index

Filling a Dictionary with User Input


We can do this by implementing the right logic. The following example creates an empty dictionary, to
later fill it with user input. It’s important to remember that if we want to add a new key-value pair, we
need to type the dictionary name, followed by the name of the key inside square brackets and finally,
placing the value of the key after the = symbol. Example:

responses = {}

# Set a flag to indicate that polling is active.


polling_active = True

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? ")

# Store the response in the dictionary:


responses[name] = response

# Find out if anyone else is going to take the poll.


repeat = input("Would you like to let another person respond? (yes/no) ")

if repeat == "no":
polling_active = False

# Polling is complete. Show the results.


print("\n--- Poll Results ----")
for name, response in responses.items():
print(f"{name} would like to climb {response}.")

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:

def greet_user(username): # "username" is the parameter


print(f"Hello {username.title( )}!")

greet_user("jesse") # "jesse" is the argument

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:

def describe_pet(animal_type, pet_name): # Observe the parameters' order


"""Display information about a pet."""
print(f"\n I have a {animal_type}.")
print(f"My {animal_type}´s name is {pet_name.title( )}.")

describe_pet("harry", "hamster") # The arguments do not match the order


# of the parameters

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:

def describe_pet(animal_type, pet_name): # Observe the parameters' order


"""Display information about a pet."""
print(f"\n have a {animal_type}.")
print(f"My {animal_type}´s name is {pet_name.title( )}.")

describe_pet(pet_name = "harry", animal_type = "hamster")

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:

def describe_pet(pet_name, animal_type = "dog"): # We NEED to change the

# order of the parameters


"""Display information about a pet."""
print(f"\n I have a {animal_type}.")
print(f"My {animal_type}´s name is {pet_name.title()}.")

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

Making an Argument Optional


If we want to let the user choose to provide extra information only if they want to, we can use default
values. Python interprets empty strings as False so we can use this factor to make arguments
optional. Example:

def get_formatted_name(name, last_name, middle_name=""):


if middle_name:
print(f"{name.title()} {middle_name.title()} {last_name.title()}")
else:
print(f"{name.title()} {last_name.title()}")

get_formatted_name(name="gabriel", last_name="padron", middle_name="gerardo")


get_formatted_name(name="leonardo", last_name="pérez")

Gabriel Gerardo Padron


Leonardo Pérez

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:

def build_person(first_name, last_name, age=""):


person = {
"first name": first_name,
"last name": last_name,
}
if age:
person["age"] = age
return person

leonardo = build_person("leonardo", "perez", 17)


print(leonardo)

{'first name': 'leonardo', 'last name': 'perez', 'age': 17}

Using the None Statement to Create an Optional Argument

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:

def build_person(first_name, last_name, age=None):


"""Return a dictionary of information about a person."""
person = {'first': first_name, 'last': last_name}
if age:
person['age'] = age
return person

musician = build_person('jimi', 'hendrix', age=27)


print(musician)

Return to Index

Returning Several Values


If needed, a function can return more than one value, not necessarily of the same data-type (but is a
good practice that it does). Imagine a function that receives the number of guests and returns the
amount of pizzas, the number of seats, and a list containing all of the guests' names for a party. We'll
call such function party_orginizer() :

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:

Store it in a single value as a tuple


Or unpackage the values returned by the function in other three new variables.

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--

total_pizzas, total_seats, guest_names = party_orginizer(3)

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.

Using a Function within a While Loop


Remember that placing the while loop inside the function will behave differently than placing it outside
the function. Example:

def get_formatted_name(first_name, last_name):


full_name = f"{first_name} {last_name}"
return full_name

while True:
print("\nPlease tell me your name:")
print("(enter 'q' at any time to quit)")

f_name = input("First name: ")


if f_name == "q":
break

l_name = input("Last name: ")


if l_name == "q":
break

formatted_name = get_formatted_name(f_name, l_name)


print(f"Hello, {formatted_name.title()}!")

Please tell me your name:


(enter 'q' at any time to quit)
First name: leo
Last name: perez
Hello, Leo Perez!

Please tell me your name:


(enter 'q' at any time to quit)
First name: q

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)

usernames = ["hannah", "ty", "margot"]


greet_users(usernames)

Return to Index

Modifying a List in a Function


When we pass a list to a function, we can add some code, so it modifies each of its elements.
Modifying means altering, deleting or creating new elements to an #iterable . We don’t need to return
any value because the changes happen as if it was written code and not a mere function. To modify
the values of a list, we need at least two parameters, the list we want to modify and a new empty list
where we will move the elements. Example:

def list_modifier(original_list, new_list):


while original_list:
element = original_list.pop(0)
print(f"Moving '{element}' to the empty list.")
new_list.append(element)

animals = ["cat", "dog", "fish", "horse", "dolphin"]


empty_list = []

print(f"{animals} {empty_list}\n")
list_modifier(animals, empty_list)
print(f"\n{animals} {empty_list}")

['cat', 'dog', 'fish', 'horse', 'dolphin'] []

Moving 'cat' to the empty list.


Moving 'dog' to the empty list.
Moving 'fish' to the empty list.
Moving 'horse' to the empty list.
Moving 'dolphin' to the empty list.

[] ['cat', 'dog', 'fish', 'horse', 'dolphin']

Preventing a Function from Modifying a List


In the previous example we changed the elements from animals to empty_list . In the specific case
we want to keep those elements in the animals list but also move them to the empty_list we need
to create a copy of the original list. If we recall correctly, we can create a copy by slicing a list without
specifying the end and beginning of it using the [:] expression. At the calling statement of the
function, we can specify the following:

def list_modifier(original_list, new_list):


while original_list:
element = original_list.pop(0)
print(f"Moving '{element}' to the empty list.")
new_list.append(element)
animals = ["cat", "dog", "fish", "horse", "dolphin"]
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}")

['cat', 'dog', 'fish', 'horse', 'dolphin'] []

Moving 'cat' to the empty list.


Moving 'dog' to the empty list.
Moving 'fish' to the empty list.
Moving 'horse' to the empty list.
Moving 'dolphin' to the empty list.

['cat', 'dog', 'fish', 'horse', 'dolphin'] ['cat', 'dog', 'fish', 'horse',


'dolphin']

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

Passing an Arbitrary Number of Arguments


Sometimes we won’t know ahead of time how many arguments a function needs to accept.
Fortunately, Python allows a function to collect an arbitrary number of arguments from the calling
statement. If we add a * at the beginning of a parameter, the same function will create a tuple
including all the arguments it receives. Example:

def pizza_toppings(*toppings):
print(toppings)

pizza_toppings("cheese", "pepperoni", "pineaple", "onion")


pizza_toppings("peppers", "motzarella")
pizza_toppings("sausage", "cheedar", "sardines")

('cheese', 'pepperoni', 'pineaple', 'onion')


('peppers', 'motzarella')
('sausage', 'cheedar', 'sardines')

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.

Using Arbitrary Keyword Arguments


In the previous topic, we used the * symbol to indicate Python to create a tuple. If we add ** at the
beginning of a parameter, instead of creating a tuple, we’ll create a dictionary. In order to do this
technique correctly, we must pass a keyword arguments at the calling statement. If we recall
correctly, keyword arguments follow a simple structure: parameter = argument . The dictionary is
created using the parameter as the key, and the argument as the value. Example:

def dictionary_creator(**keyword_arguments):
simple_dictionary = {}
for key, value in keyword_arguments.items():
simple_dictionary[key] = value
return simple_dictionary

leonardo = dictionary_creator(name="Leonardo", last_name = "Pérez Lázaro")


print(leonardo)

gabriel = dictionary_creator(name="Gabriel", middle_name="Gerardo",


last_name="Padrón Flores")
print(gabriel)

{'name': 'Leonardo', 'last_name': 'Pérez Lázaro'}


{'name': 'Gabriel', 'middle_name': 'Gerardo', 'last_name': 'Padrón Flores'}

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 .

Avoiding errors when specifying arguments


Along this chapter, we have seen several types of arguments. In order to avoid unexpected errors, we
must follow a certain order. For example, positional arguments should always be first than keyword
arguments. Also, when using more than one default-value, it’s important to remember that we’ll need
to specify which value correspond to which default-value. The following example illustrates all the
rules previously discussed:
def arguments_in_order(positional_argument_1, positional_argument_2, *tuple_1,
optional_argument = "", default_argument_2 = "Hi!", **dictionary):

print(positional_argument_1, positional_argument_2)
if optional_argument:
print(default_argument_2)
print(tuple_1)
print(dictionary)

arguments_in_order("Leonardo", "Pérez", "cheese", "pepperoni", "onion", "hot-souce",


optional_argument = "active", default_argument_2 = "Hola!", name = "Leonardo",
last_name = "Pérez Lázaro", age = 17)

Return to Index

Scope and the global Statement


As we have already seen, functions can execute code chunks in order to not repeat code. Inside
functions we can write code including all the coding statements we have seen so far: for and while
loops, if - else conditionals, etcetera, and we can also declare new variables inside our functions.
Such variables receive the name of #local_variables , because they will have their referenced value
only in the function's body; if we try to use a local function outside their proper function, an error will
be raised. To illustrate this, try the next simple program:

def printing_bacon():
bacon = 'bacon'
print(bacon)

printing_bacon()
print(bacon)

# This code will raise a 'NameError' exception...

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' # Line 5


printing_bacon()
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):

from module_name import function_name

If we want to import more than one function, we need to separate them by commas:

from module_name import function_1, function_2, function_3

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:

from module_name import function_name as fn


When doing this, if we want to call the function named function_name , we must address it now on as
fn .

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.

from module_name import *

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.

The __init__() Method


It’s important to clarify that a function of a class is called a #method and that values that can be
accesible for each instance and method of a class are called #attributes .

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

def sit(self): # Class's funcitons that will use any of the


# attributes should pass "self" as a parameter.
print(f"{self.name.title()} is now sitting.")

def roll_over(self): # The same as in the previous method.


print(f"{self.name.title()} rolled over")

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

Making an Instance from a Class


Think of a #class as a blueprint to know how to make an instance. In order to make an instance,
we need to call the class. Example:

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")

my_dog = Dog("Rick", 12) # Instance of the class

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

Creating Multiple Instances


In this case we created the instance named my_dog , but Python doesn’t have a limit on how many
instances we can create. Just remember to always store the instance inside a different variable so it
doesn't overwrite the current one.

Setting a Default Value for an Attribute


Let’s make a different example. Let’s create a class named Car that requests a specific make ,
model , and year .

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.")

my_new_car = Car("audi", "a4", "2016")


print(my_new_car.get_descriptive_name())
my_new_car.read_odometer() #Function that uses the default value

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

Modifying Attributes Values


Attributes are like variables, so as variables, we can overwrite them. We can overwrite that current
value in three different ways:
1. Modifying an Attribute’s Value Directly: this is doing it through the manual way. We write the
instance’s attribute we want to change, and we set the value of our preference. Example:

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---

def update_odometer(self, mielage):


self.odometer = mielage

my_new_car = Car("audi", "a4", "2016")

my_new_car.update_odometer(21)
my_new_car.read_odometer()

This car has 21 miles on it.

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—

def increment_odometer(self, miles):


self.odometer = self.odometer + miles

my_new_car = Car("audi", "a4", "2016")

my_new_car.increment_odometer (10) #Here we add 10 to 0


my_new_car.read_odometer()
my_new_car.increment_odometer (13) #Here we add 13 to 10
my_new_car.read_odometer()

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

The __init__() Method for a Child Class


When writing a #child_class , we must check that the #parent_class always appears before the child
class; in other words, that the parent class is written on top of the child class.

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.")

def update_odometer(self, mielage):


self.odometer = mielage
def increment_odometer(self, miles):
self.odometer = self.odometer + miles

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

my_tesla = ElectricCar("tesla", "model s", "2016")

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.

Defining Attributes and Methods for the Child Class


Once we have inherited all the attributes and methods from the parent class, we can begin to specify
which different attributes we want the child class to have. Example:

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.")

my_tesla = ElectricCar("tesla", "model s", "2016", "100%")


my_tesla.describe_battery()

This car has 100% of battery remaining.


In this example, we added a new attribute called battery . This new attribute will only be part of the
ElectricCar class so in case we want to create an instance from the Car class, it’ll not request us
to pass a battery argument.

Return to Index

Overriding Methods from the Parent Class


Suppose that there is a specific method we want to change because it doesn’t fit our child class
description. If that’s the case, we can overwrite them in the child class and that will tell Python to
ignore the method we assigned to the parent class and focus on the child class. In order to do this we
need to write the exact name of the method we want to replace. 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()

--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)

my_tesla = ElectricCar("tesla", "model s", "2016", "100%")


my_tesla.get_descriptive_name()
2016, tesla, model s, Battery: 100%

In this example, we overwrote the method get_descriptive_name so it includes the battery


percentage as well.

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(f"My tesla's battery is {my_tesla.battery.capacity}-kW of capacity.")

print(my_tesla.battery.battery_duration())

My tesla's battery is 10-kW of capacity.


My tesla's battery lasts around 30 hours.

Modeling Real-World Objects


As we begin modeling more complex items like electric cars, we’ll wrestle with interesting questions. Is
the range of an electric car a property of the battery or of the car? Does the model of an electric can
be the same as the model of another type of car? Where do I include the attribute of the motor if
electric cars don’t have one? When we face such questions, it means that we have grown as a former
programmer and now we are focusing on the program’s high-level logic 😉.

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:

from module_name import Class_Name

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.

form module_name import Class_Name as CN

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:

from module_name import class_1, class_2, class3

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.

#Importing the module


import module_name

#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:

from module_name import *

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

Finding Our Own Workflow


Until now, we have seen several ways to make our code cleaner to read. We should follow our own
criteria on how we choose to organize our programs and how do we separate our classes and
functions in modules. Try doing everything in one file and moving your classes and functions to
separated modules once everything IS WORKING. Find an approach that lets you write code that
works and go from there.
The Python Standard Library
The Python Standard Library is a set of modules included with every Python installation. Now that we
have a basic understanding on how classes work, we can start to use modules like these that other
programmers have written. One of them is OrderedDict() . This subclass from the dictionary returns
an ordered dictionary (since standard dictionaries return the keys in no specific order) that, when
used, it will return our key-value pairs in the same order we registered them. Example:

from collections import OrderedDict

favorite_languages = OrderedDict()

favorite_languages["jen"] = "python"
favorite_languages["sarah"] = "c"
favorite_languages["edward"] = "ruby"
favorite_languages["edward"] = "python"

for name, language in favorite_languages.items():


print(f"{name.title()}'s favorite language is {language.title()}.")

Jen's favorite language is Python.


Sarah's favorite language is C.
Edward's favorite language is Ruby.
Phil's favorite language is 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.

The random module

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

More information about modules


Visit http://pymotw.com/ and look at the table of contents. Fin a module that looks interesting and read
about it. Or explore the documentation of the collections and random modules. You can also
check the document Python Modules for more information about important modules you can use to
nourish your knowledge.

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

Chapter 10: Files and Exceptions


Reading an Entire File
To begin, we need a file that has a few lines in it. Let’s create a .txt file that includes the first 30
decimals of pi named pi_digits.txt :

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:

with open("pi_numbers.txt") as file_object:


content = file_object.read()

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"

Reading Line by Line


In the previous example, we used the .read() method, which returns a single-long string containing
the content of the text file. We could print that string and the program could be over, but what if we
wanted to work with each line of the file individually. There’re two main approaches to perform this:

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:

3.1415926535 \n 8979323846 \n 2643383279

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 :

3.1415926535 \n\n 8979323846 \n\n 2643383279 \n


2. Using the .readlines() method: this one is more reliable than the previous approach. By using
the .readlines() method, we can create a list including the text file’s lines. This must be done inside
the with block, or it won’t work since once we write code outside of this block the file will be closed
and we won´t be available to interact with it (unless we open it again). Example:

file_name = "./sample_texts/pi_numbers.txt"
with open(file_name) as f_obj:
content = f_obj.readlines()

print(content)

['3.1415926535\n', ' 8979323846\n', ' 2643383279']

Return to Index

Working with a File’s Contents


Once we have read the file into memory (in other words, store it in a variable), we can do whatever we
want with that data. Just as a quick reminder, strings are immutable data structures, but iterable. They
behave very much like tuples; we can loop through them and check if certain characters match.
Example:

def analyzing_pi_digits(string, content):


if string in content:
print("True")
else:
print("False")

file_name = "./sample_texts/1000_digits_of_py.txt"

with open(file_name) as f_obj:


content = f_obj.read()

analyzing_pi_digits("124", content) # False


analyzing_pi_digits("846", content) # True
analyzing_pi_digits("582", content) # True
analyzing_pi_digits("123", content) # False

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:

# The "guest.txt" file doesn’t exist


with open("text_files/guest.txt", "w") as f_obj:
f_obj.write("File to store guest's names\n")
f_obj.write("All usernames should be in lowercase")

File: guest.txt

File to store guest's names


All usernames should be in lowercase

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.

The Try-Except Block


One common error is to divide any number by cero. If we tell Python to do it, it’ll generate a traceback
including the name of the exception that was raised:

print(5/0)

Traceback (most recent call last):


File "pruebas.py", line 2, in <module>
print(5/0)
ZeroDivisionError: division by zero

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:

number_1 = input("Tell me a number: ")


number_2 = input("Tell me another number: ")

result = int(number_1) / int(number_2)


print(f"{number_1}/{number_2} = {result}")

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:

number_1 = input("Tell me a number: ")


number_2 = input("Tell me another number: ")

result = int(number_1) / int(number_2)


print(f"{number_1}/{number_2} = {result}")

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:

number_1 = input("Tell me a number: ")


number_2 = input("Tell me another number: ")
try:
result = int(number_1) / int(number_2)
except ValueError:
print("Can't use letters, please type only numbers")
else:
print(f"{number_1}/{number_2} = {result}")

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:

number_1 = input("Tell me a number: ")


number_2 = input("Tell me another number: ")

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.

Deciding Which Errors to Report


How do you know when to report an error to your users and when to fail silently? If users would like to
know why our program failed or what they did wrong to make our program fail, they might appreciate a
message informing them why it did occur. However, if the users only care about the final output, even
if they know that it might cause an exception to our program, then we could fail silently. Giving users
information they aren’t looking for can decrease the usability of our program.

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.

Bonus Methods .split() and .count()

Method Description Example


string.split() It takes a string as
object and returns
a list containing >>> my_string = "Hi, my name is
each word in a Leonardo Pérez and I'm mexican."
different element. >>> my_string.split()
It stores each of ['Hi,', 'my', 'name', 'is',
the characters 'Leonardo', 'Pérez', 'and',
that are separated "I'm", 'mexican.']
by a white space,
although it may
also include
punctuation marks.
iterable.count(element) It takes an iterable
(can be a tuple,
list, string, etc) >>> my_string = "Row, row, row a
and returns a boat"
number >>> my_string.count("row")
representing the 2
Method Description Example
amount of times >>>
an element my_string.lower().count("row")
appears in that 3
iterable. # Remember that Python interprets
that “Row” is NOT the same as
“row”, that’s why at the first
test the expression returns 2
instead of 3.

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.

The json.load() and json.dump() functions


These functions behave very much like the .read() and .write() methods we have seen so far.
For example, if we want to write a list inside a document, we'll first need to open the file and set it in
writing mode, like this:

from json import dump

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:

# Content of the "numbers.json" file

[1, 2, 3, 4, 5, 6, 7]

from json import load

file_name = "numbers.json"

with open(file_name) as f_obj:


numbers = load(f_obj)

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"

with open(file_name, "w") as fo_obj:


dump(username, fo_obj)

print(f"We'll remeber you when you come back {username}!")


return username

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.

Unit Test and Test Cases


The module unittest from the [Python standard library] provides tools for testing our code. A unit
test verifies that one specific aspect of a function’s behavior is correct. A test case is a collection
of unit tests that together prove that a function behaves as it’s supposed to, within the full range of
situations you expect it to handle. Don’t worry if these two terminologies are confusing, we’ll see them
as we use some examples.

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.

# File name: formatted_name.py

def get_formatted_name(name, last_name):


stringer = f"{name} {last_name}"
return stringer.title()

name = get_formatted_name("leonardo", "perez")


print(name)

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

from formatted_name import get_formatted_name

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.

At the end of our program we can see the following lines:

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:

# File name: formatted_name.py


def get_formatted_name(name, middle_name, last_name):
stringer = f"{name} {middle_name} {last_name}"
return stringer.title()

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

Responding to a Failed Test


Assuming that your test case is correctly comparing the output you expect to receive from your
functions, when a test fails it means that your function isn’t behaving as you expect it to do. Instead of
fixing our test class, we need to fix our function.

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:

# File name: formatted_name.py


def get_formatted_name(name, last_name, middle_name=""):
if middle_name:
stringer = f"{name} {middle_name} {last_name}"
else:
stringer = f"{name} {last_name}"

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

Adding New Tests


But what if we also wanted to check that get_formatted_name() behaves correctly when receiving a
middle name. For that, we add one more unit test to the test case FormattedName :

# File name: tester.py


import unittest

from formatted_name import get_formatted_name

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.

A Variety of assert Methods


So far, we have been using the self.assertEqual() method to compare if the returned value of a
function is equal to the value we want; however, there are more assert methods we can use:

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:

# File name: anonymous_survey.py


class AnonymousSurvey():
def __init__(self, question):
self.question = question
self.responses = []

def show_question(self):
print(self.question)

def append_response(self, response):


self.responses.append(response)

def show_all_responses(self):
print("Responses:")
for i in self.responses:
print(f"- {i.lower()}.\n")

Now let’s write a program that uses this class:

# File name: survey_1.py


from anonymous_survey import AnonymousSurvey

question = "Which is your favorite color?"

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

from anonymous_survey import AnonymousSurvey

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

from anonymous_survey import AnonymousSurvey


class AnonymousSurveyTest(unittest.TestCase):
def test_store_single_response(self):
question = "Which is your favorite color?"
my_survey = AnonymousSurvey(question)
my_survey.store_response("Red")
self.assertIn("Red", my_survey.responses)

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

The setUp() Method


If we observe very carefully the previous examples, we could notice that in each unit test we
needed to create a question variable and an instance to perform our tests. This could work if we only
need to test one aspect of our class using a single unit test. However, if our test case involved
more unit tests , then creating an instance for each unit would become very tedious. That’s why we
can declare a sample instance as an attribute of our test case by defining the setUp() method.

import unittest

from pruebas import AnonymousSurvey

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

Python Crash Course Part I (Ended)

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

You might also like