0% found this document useful (0 votes)
19 views105 pages

2 Programming

Uploaded by

Daniel Yebra
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
19 views105 pages

2 Programming

Uploaded by

Daniel Yebra
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 105

PROGRAMMING FOR MACRO-FINANCE

Unit 2: Python Programming

Daniel Arrieta
[email protected]

IE University

Academic year: 24-25


Outline

Global Variables

Modules and Files

Testing and Debugging

Exceptions and Assertions

Object-Oriented Programming
Introduction
This unit is based on chapters 6 to 10 of “Introduction to
Computation and Programming Using Python” by Guttag (2021).
First sections are addressed to introducing global variables, modules
and how to handle files.
Next, testing and debugging will be introduced, these are two
important activities in software engineering.
Testing involves verifying that a software system meets its
requirements, while debugging focuses on identifying and fixing
defects or issues in the software.
Following testing and debugging, exceptions and assertions, which are
mechanisms that help handle and manage errors in code, will be
addressed.
Lastly, classes in the context of object-oriented programming will be
presented. They allow you to define blueprints for objects, encapsulate
data and behavior, and create reusable and modular code.
1/85
Next

Global Variables

Modules and Files

Testing and Debugging

Exceptions and Assertions

Object-Oriented Programming
Global Variables
A global variable in Python is a variable that is defined outside of any
function or class and can be accessed and modified from anywhere in
the code.
It has a global scope, meaning it can be used in different functions
and modules within the same program.
Global variables are useful when you have data that needs to be
shared and accessed by multiple functions or modules.
However, it’s generally recommended to use global variables sparingly,
as they can make code harder to understand and maintain.
It’s often better to pass variables as function arguments or use return
values to pass data between functions.
The most common use of global variables is probably to define a
global constant that will be used in many places.
Another possible use of global variables is in the implementation of
so-called recursive definitions.
2/85
Next

Global Variables
Recursive Definitions
Financial Example

Modules and Files

Testing and Debugging

Exceptions and Assertions

Object-Oriented Programming
Recursive definitions
In general, a recursive definition is made up of two parts.
Base case: there is at least one a special case, usually the first one,
that directly specifies the result of the definition.
Recursive or inductive case: there is at least one recursive case that
defines the output of the definition on some other input, typically a
simpler version.
It is the presence of a base case that keeps a recursive definition from
being a circular definition or and endless loop.
A simple recursive definition is the factorial function, denoted in
mathematics using the symbol “!”, on natural numbers N.
For each n ∈ N, an inductive definition of the factorial is given by
(
1 if n = 1,
n! :=
n × (n − 1)! if n > 1.

3/85
Implementing the factorial of a natural number
The next code snippet
def fact_iter ( n ) :
''' Assumes n an int > 0. Returns n ! '''
result = 1
for i in range (1 , n +1) :
result *= i
return result

shows an iterative implementation of the factorial of a natural number.


On the other hand, the code
def fact_rec ( n ) :
''' Assumes n an int > 0. Returns n ! '''
if n == 1:
return 1
else :
return n * fact_rec (n -1)

is a recursive implementation of the same factorial function.


4/85
Fibonacci sequence
The Fibonacci sequence is another common mathematical function
that is usually defined recursively.
In the year 1202, the Italian mathematician Leonardo of Pisa, also
known as Fibonacci, developed a formula to quantify a rapidly
growing population.
For each n ∈ N, the nth Fibonacci number is given by
(
1 if n ∈ {0, 1},
f (n) :=
f (n − 1) + f (n − 2) if n > 1.

This definition has some differences from the above recursive


definition of the factorial function:
i. It has two base cases, not just one. In general, we can have as
many base cases as we want.
ii. In the recursive case, there are two recursive calls, not just one.
Again, there can be as many as we want.
5/85
Computing Fibonacci numbers by recursion
Below code displays a recursive implementation of the calculation of
the nth Fibonacci number.
def fib ( n ) :
''' Assumes n int >= 0. Returns Fibonacci of n . '''
if n == 0 or n == 1:
return 1
else :
return fib (n -1) + fib (n -2)

If fib is called with a large number it will took a very long time to run.
A sensible guess is that is caused by the huge number of recursive
calls that are made.
We could do a careful analysis of the code and figure out why this is
happening.
Another approach is to add some code that counts the number of
calls, one way to do that is by using global variables.

6/85
Fibonacci numbers with a global variable
To define a global variable in Python, simply declare the variable
outside of any function or class.
Consider the code,
def fib ( n ) :
''' Assumes n int >= 0. Returns Fibonacci of n . '''
global num_fib_calls
num_fib_calls += 1
if n == 0 or n == 1:
return 1
else :
return fib (n -1) + fib (n -2)

def test_fib ( n ) :
for i in range ( n +1) :
global num_fib_calls
num_fib_calls = 0
print ( ' fib of ' , i , '= ' , fib ( i ) )
print ( ' fib called ' , num_fib_calls , ' times . ')

7/85
Fibonacci numbers with a global variable (ii)
By declaring a variable as global within a function, you can access and
modify the global variable from within that function.
Within the fib definition, the line of code global num_fib_calls
implies that the name num_fib_calls is a global variable and it should
be defined outside of the function.
If the global keyword is omitted, Python will create a new local
variable with the same name instead of modifying the global variable.
That is why if we had not included the code global num_fib_calls,
the name num_fib_calls would have been local to each of the
functions fib and test_fib.
The functions fib and test_fib both have unfettered access to the
object referenced by the variable num_fib_calls.
The function test_fib binds num_fib_calls to 0 each time it calls
fib, and fib increments the value of num_fib_calls each time fib is
entered.
8/85
Next

Global Variables
Recursive Definitions
Financial Example

Modules and Files

Testing and Debugging

Exceptions and Assertions

Object-Oriented Programming
Financial example
As we have just seen, recursion is a programming technique where a
function calls itself to solve a problem.
Furthermore, in some cases, it is useful to use global variables within
a recursive function to keep track of certain information, e.g., for
computing the total number of recursive calls that are made when
executing the function.
Let’s consider a financial example to illustrate recursion with a global
variable in Python.
Suppose you have a portfolio of investments, and you want to
calculate the total value of the portfolio.
Each investment has a quantity and a price, and, consequently, a
portfolio can be represented as a list of tuples, where each tuple
contains the quantity and price of an investment.
Next slides will detail the code to calculate the total value of a
portfolio using recursion and a global variable.
9/85
Portfolio value example
Let’s consider the next code snippet
total_value = 0 # Global variable to store the total
value

def c a l c u l a t e _ p o r t f o l i o _ va l u e ( index ) :
global total_value # Declare the global variable
inside the function

if index >= len ( portfolio ) :


return # Base case : reached the end of the
portfolio

quantity , price = portfolio [ index ]


investment_value = quantity * price
total_value += investment_value # Add the
investment value to the total

c a l c u l a t e _ p o r t f o l i o _ v a l u e ( index + 1) # Recursive
call to process the next investment

10/85
Portfolio value example (ii)
In this example, the calculate_portfolio_value function takes an
index parameter that represents the current position in the portfolio
list.
The function uses the global keyword to indicate that the
total_value variable is a global variable.
The base case of the recursion occurs when the index is greater than
or equal to the length of the portfolio, indicating that all investments
have been processed.
At each step, the function calculates the value of the current
investment and adds it to the total_value variable.
Now running the below code lines
portfolio = [(10 , 100) , (5 , 50) , (8 , 75) ]
c a l c u l a t e _ p o r t f o l i o _ v a l u e (0)
print ( " Total portfolio value : " , total_value )

will print a “Total portfolio value” of 1850.


11/85
Next

Global Variables

Modules and Files

Testing and Debugging

Exceptions and Assertions

Object-Oriented Programming
Modules and files
So far, we have operated under the assumptions that:
i. the entire program is stored in one file;
ii. programs do not depend upon previously written code (other
than the code implementing Python); and
iii. programs do not access previously gathered data nor do they
store their results in a way that allows them to be accessed after
the program is finished running.
The first assumption is perfectly reasonable as long as programs are
small.
The second and third assumptions are reasonable for exercises
designed to help people learn to program, but rarely reasonable when
writing programs designed to accomplish something useful.
As programs get larger, however, it is typically more convenient to
store different parts of them in different files.
12/85
Modules and files (ii)
Imagine, for example, that multiple people are working on the same
program.
It would be a nightmare if they were all trying to update the same file.
We will discuss a mechanism, i.e., Python modules, that allow us to
easily construct a program from code in multiple files.
A Python module is a .py file containing Python definitions and
statements.
In addition, we will show how to take advantage of library modules
that are part of the standard Python distribution.
We will use a couple of these modules in this subsection of the unit,
and many others later in other parts of the course.
Lastly, it will be provided a brief introduction to reading from and
writing data to files.

13/85
Next

Global Variables

Modules and Files


Importing Workflow
Predefined Packages
Handle Access

Testing and Debugging

Exceptions and Assertions

Object-Oriented Programming
Creating a module
A module is a .py file containing Python definitions and statements.
We could create, for example, a file named as U2_1_circle.py
containing the following code
pi = 3.14159

def area ( radius ) :


return pi *( radius **2)

def circumference ( radius ) :


return 2* pi * radius

def sphere_surface ( radius ) :


return 4.* area ( radius )

def sphere_volume ( radius ) :


return (4./3.) * pi *( radius **3)

A program gets access to a module through an import statement.

14/85
import statement
As aforementioned, a module is accesed through an import statement.
For instance, the code
import U2_1_circle
pi = 3
print ( pi )
print ( U2_1_circle . pi )
print ( U2_1_circle . area (3) )
print ( U2_1_circle . circumference (3) )
print ( U2_1_circle . sphere_surface (3) )

will print
3
3.14159
28.27431
18.84953 99 99 99 99 98
113.09724

15/85
Importing context
Modules are typically stored in individual files. Each module has its
own private symbol table.
Executing import M creates a binding for module M in the scope in
which the import appears.
Therefore, in the importing context we use dot notation to indicate
that we are referring to a name defined in the imported module.
Consequently, within U2_1_circle.py we access objects (e.g., pi and
area) in the usual way.
Outside of U2_1_circle.py, the references pi and U2_1_circle.pi
can (and in this case do) refer to different objects.
That is why, once the module has been imported, the statements
print ( pi )
print ( U2_1_circle . pi )

print different values.


16/85
Importing context and dot notation
When importing a module one often has no idea what local names
might have been used in the implementation of that module.
The use of dot notation to fully qualify names avoids the possibility of
getting burned by an accidental name clash.
For example, executing the assignment pi = 3 outside of the
U2_1_circle.py module does not change the value of pi used within
the U2_1_circle.py module.
A module can contain executable statements as well as function
definitions. Typically, statements are used to initialize the module and
are executed only the first time a module is imported into a program.
A module is imported only once per interpreter session.
If a console is started and a module is imported, then if the contents
of that module are changed, the interpreter will still be using the
original version of the module.
This can lead to puzzling behavior, when in doubt, start a new shell.
17/85
.pyc files
When importing a module in Python, the interpreter first searches for
the module in the directories listed in the sys.path variable. If the
module is found, the interpreter loads and executes the module’s code.
However, if the module has already been compiled into bytecode, the
interpreter can load the compiled bytecode instead of recompiling the
source code.
Python compiles the source code of a module into bytecode1 and
saves it as a .pyc file in a subdirectory called pycache . The
bytecode is platform-independent and can be executed faster than the
source code.
The .pyc files are created to improve the startup time of Python
programs, as the interpreter can skip the compilation step if the
bytecode is already available.
This helps to improve the performance of subsequent imports of the
same module.
1
Caching mechanism can vary across Python versions and implementations. 18/85
.pyc files (ii)
The naming convention for .pyc files is module_name.version.pyc,
where module_name is the name of the module and version represents
the Python implementation and version that created the bytecode.
For example, script.cpython-39.pyc indicates that the bytecode was
created by CPython version 3.9.
When you import a module, Python checks if there is a corresponding
.pyc file in the pycache directory. If the .pyc file exists and is
up-to-date, Python loads the bytecode from the file instead of
recompiling the source code.
To summarize, Python automatically compiles source code into
bytecode and saves it as .pyc files to improve the startup time of
Python programs.
When importing a module, Python checks if the corresponding .pyc
file exists and loads the bytecode if it’s available and up-to-date.
Otherwise, it compiles the source code and creates a new .pyc file.
19/85
from statement
Using the keyword from is a variant of the import statement that
allows the importing program to omit the module name when
accessing names defined inside the imported module.
Executing the statement from M import * creates bindings in the
current scope to all objects defined within M, but not to M itself.
For example, the code
from U2_1_circle import *
print ( pi )
print ( U2_1_circle . pi )

will first print


3.14159

and then produce the error message


NameError : name ' U2_1_circle ' is not defined

20/85
Renaming an imported module
Using the character “*” when importing is said to be a “wild card”
import.
Many Python programmers believe that this kind of import makes
code more difficult to read because it is no longer obvious where a
name is defined.
A commonly used variant of the import statement is to rename the
imported module, i.e., using the keyword as. For example,
import module_name as new_name

instructs the interpreter to import the module named module_name,


but rename it to new_name.
This is useful if module_name is already being used for something else
in the importing program.
The most common reason programmers use this form is to provide an
abbreviation for a long name.
21/85
Next

Global Variables

Modules and Files


Importing Workflow
Predefined Packages
Handle Access

Testing and Debugging

Exceptions and Assertions

Object-Oriented Programming
Python Standard Library
The Python Standard Library is a collection of modules that provide a
wide range of functionalities for various tasks.
It includes modules for file I/O, networking, string manipulation, data
structures, mathematical operations, and more.
These modules are built-in and readily available when you install
Python.
Below are summarized some key modules in the Python Standard
Library.
os: Provides functions for interacting with the operating system, such
as file and directory operations.
calendar: functionalities related to calendars, such as determining
leap years, calculating week numbers, and generating calendars.
datetime: Allows you to work with dates, times, and timezones.
math: Provides mathematical functions and constants.
22/85
Python Standard Library (ii)
csv: Provides functionality for reading and writing CSV files.
json: Allows you to work with JSON data.
urllib: Provides tools for working with URLs and making HTTP
requests.
re: Provides regular expression matching operations.
collections: Offers additional data structures like named tuples,
deque, Counter, and defaultdict.
sys: Provides access to system-specific parameters and functions.
logging: Enables logging functionality for debugging and error
handling.
These are just a few examples of the modules available in the Python
Standard Library.
Each module has its own set of functions, classes, and methods that
can be used to accomplish specific tasks.
23/85
math module examples
For example, to print the log of x base 2, using the math module, can
be done with the code
import math
print ( math . log (x , 2) )

A simple financial application is given by the next code snippet.


import math

# Calculate compound interest


principal = 1000 # Initial investment
rate = 0.05 # Annual interest rate
time = 5 # Number of years

final_amount = principal * math . pow (1 + rate , time )


interest = final_amount - principal

print ( f " Final amount : { final_amount :.2 f } " )


print ( f " Interest earned : { interest :.2 f } " )

24/85
math module financial example
In this example, we calculate the final amount and interest earned on
an investment using compound interest formula.
We use the math.pow() function to raise the base (1 + rate) to the
power of time.
The result is then multiplied by the principal to get the final amount.
The interest earned is the difference between the final amount and the
principal.
This is just a simple example to demonstrate the usage of the math
module for a very simple financial calculation.
In real-world financial modeling, more advanced libraries, like pandas
or numpy, are tipically used. We will see the most useful in further
units.
In addition to containing approximately 50 useful mathematical
functions, the math module contains several useful floating-point
constants, e.g., math.pi or math.inf for positive infinity.
25/85
calendar module example
Let’s consider the following code
import calendar

def c a l c u l a t e _ a v e r a g e _ i n f l a t i o n ( inflation_data , year ) :


# Get the abbreviated month names
months = list ( calendar . month_abbr ) [1:]

total_inflation = sum ( inflation_data )


average_i nflation = total_inflation / len (
inflation_data )

print ( f " Average monthly inflation for { year }: " )


for month , inflation in zip ( months , inflation_data ) :
print ( f " { month }: { inflation }% " )

print ( f " \ nTotal inflation for the year : {


total_inflation }% " )
print ( f " Average inflation for the year : {
aver age_i nflation :.2 f }% " )

26/85
Inflation example
Last code provides a macroeconomic example using the calendar
module. This code computes the average monthly inflation rate for a
given year.
In this example, we have a def calculate_average_inflation using
two inputs: one inflation_data which is a list that represents the
monthly inflation rates for a given year; and two year to which said
inflation_data corresponds.
We use the calendar.month_abbr attribute to get the abbreviated
month names.
We calculate the total inflation by summing up all the inflation rates
and then calculate the average inflation by dividing the total by the
number of months.
The example prints the average monthly inflation for each month and
the total inflation for the year, along with the average inflation for the
year.
27/85
Next

Global Variables

Modules and Files


Importing Workflow
Predefined Packages
Handle Access

Testing and Debugging

Exceptions and Assertions

Object-Oriented Programming
Opening a file
Every computer system uses files to save things from one computation
to the next.
Python provides many facilities for creating and accessing files.
Each operating system, e.g., Windows and macOS, comes with its
own file system for creating and accessing files.
Python achieves operating-system independence by accessing files
through something called a file handle.
The code
name_handle = open ( ' kids ' , 'w ')

instructs the operating system to create a file with the name kids and
return a file handle for that file.
The argument 'w' to open indicates that the file is to be opened for
writing.

28/85
Writing to a file
In a Python string, the escape character “\” is used to indicate that
the next character should be treated in a special way.
For instance, the string '\n' denotes a newline character.
The following code
name_handle = open ( ' students . txt ' , 'w ')
for i in range (2) :
name = input ( ' Enter name : ')
name_handle . write ( name + '\ n ')
name_handle . close ()

opens a .txt file, and uses the write method to write two lines ending
each line with a newline character insertion. Finally, the code closes
the file.
Remember to close a file when the program is finished using it.
Otherwise there is a risk that some or all of the writes may not be
saved.
29/85
with statement
File closing can be ensured if it is open using a with statement. The
syntax is of the form
with open ( file_name ) as name_handle :
code_block

opens a file, binds a local name to it that can be used in the


code_block, and then closes the file when code_block is exited.
The following code
with open ( ' students . txt ' , 'r ') as name_handle :
for line in name_handle :
print ( line )

opens a file for reading, using the argument 'r', and prints its
contents.
Since Python treats a file as a sequence of lines, we can use a for
statement to iterate over the file’s contents.
30/85
Appending
If a file is opened for writing, then the previous contents of the file
overwritten.
A file can be opened for appending, instead of writing, by using the
argument 'a'.
For example, if the next code is executed
name_handle = open ( ' students . txt ' , 'a ')
name_handle . write ( ' Maria ' + '\ n ')
name_handle . write ( ' Catalina ')
name_handle . close ()
name_handle = open ( ' students . txt ' , 'r ')

then the characters contained within the strings 'Maria' and


'Catalina' will be added to the previous file contents.
This can be verified by running
for line in name_handle :
print ( line )

31/85
Common operations on files
Given a string representing a file name fn and a file handle fh, then
open(fn,'w'): creates a file for writing and returns a file handle.
open(fn,'r'): opens a file for reading and returns a file handle.
open(fn,'a'): opens a file for appending and returns a file handle.
fh.read(): returns a string containing the contents of the file
associated with the file handle fh.
fh.readline(): returns the next line in the file associated with fh.
fh.readlines(): returns a list, each element of which is one line of
the file associated with he file handle fh.
fh.write(s): writes the string s to the end of the file associated with
the file handle fh.
fh.writelines(S): writes each element of the sequence of strings S
as a separate line to the file associated with the file handle fh.
fh.close(): closes the file associated with the file handle fh.
32/85
Next

Global Variables

Modules and Files

Testing and Debugging

Exceptions and Assertions

Object-Oriented Programming
Testing and Debugging
The first step in getting a program to work is eliminating syntax and
static semantic errors that can be detected without running the code.
Nevertheless programs don’t always function properly the first time
they are run.
This section of the unit provides a highly summarized discussion of
about how to deal with this problem.
Obviously, all the programming examples are in Python, however,
general principles apply to running any complex system.
Testing is the process of running a program to try to determine if it
works as intended.
Debugging is the process of trying to fix a program that is not
working as expected.
The key to doing this is breaking the program into separate
components that can be implemented, tested, and debugged
independently of other components.
33/85
Next

Global Variables

Modules and Files

Testing and Debugging


Conducting Tests
Fixing Errors in Code

Exceptions and Assertions

Object-Oriented Programming
Partitions and test suites
The purpose of testing is to show that bugs exist, not to show that a
program is bug-free.
Nevertheless, even the simplest of the programs has billions of
possible inputs.
The key to testing is finding a collection of inputs, called a test suite,
that has a high likelihood of revealing bugs, yet does not take too
long to run.
A partition of a set divides that set into a collection of subsets such
that each element of the original set belongs to exactly one of the
subsets.
The key to finding a test suite is partitioning the space of all possible
inputs into subsets that provide equivalent information about the
correctness of the program, and then constructing a test suite that
contains at least one input from each partition.
Usually, constructing such a test suite is not actually possible.
34/85
Black-box testing
Black-box tests are constructed without looking at the code to be
tested.
This kind of testing is based on exploring paths through the code
specification.
Black-box testing allows testers and implementers to be drawn from
separate populations.
A good way to generate black-box test data is to explore paths
through a specification.
Boundary conditions should also be tested.
Looking at an argument of type list often means looking at the empty
list, a list with exactly one element, a list with immutable elements, a
list with mutable elements, and a list containing lists.
When dealing with numbers, it typically means looking at very small
and very large values as well as “typical” values.

35/85
Black-box test example
An important boundary condition to think about is aliasing.
Consider the code
def copy ( L1 , L2 ) :
""" Assumes L1 , L2 are lists .
Mutates L2 to be a copy of L1 . """

while len ( L2 ) > 0: # remove all elements from L2


L2 . pop () # remove last element of L2

for e in L1 : # append L1 's elements to initially


empty L2
L2 . append ( e )

It will work most of the time, but not when L1 and L2 refer to the
same list.
Any test suite that did not include a call of the form copy(L,L),
would not reveal the bug.

36/85
Glass-box testing
Glass-box tests are based on exploring paths through the internal
structure of the code.
Consider the following code snippet
def is_prime ( x ) :
""" Assumes x is a nonnegative int .
Returns True if x is prime ; False otherwise . """
if x <= 2:
return False
for i in range (2 , x ) :
if x % i == 0:
return False
return True

Because of the test if x <= 2, the values 0, 1, and 2 are treated as


special cases, and therefore need to be tested.
Without looking at the code, one might not test is_prime(2), and
would not discover that is_prime(2) returns False erroneously.
37/85
Path completeness
Glass-box test suites are usually easier to construct because of the
notion of a path through code is well defined.
In addition, it is relatively easy to evaluate how thoroughly the space
of cases is being explored.
A glass-box test suite is said to be path-complete if it evaluates every
potential path through the program.
This is typically impossible to achieve, because it depends upon the
number of times each loop is executed and the depth of each
recursion.
For example, a recursive implementation of factorial follows a different
path for each possible input, because the number of levels of recursion
will differ.
Furthermore, even a path-complete test suite does not guarantee that
all bugs will be exposed.

38/85
Glass-box testing best practices
Below are summarized a few rules that are usually worth following
when conducting glass-box tests.
i. Exercise both branches of all if statements.
ii. For each for loop, have test cases in which:
a) The loop is not entered, e.g., if the loop is iterating over the
elements of a list, make sure that it is tested on the empty list.
b) The body of the loop is executed exactly once.
c) The body of the loop is executed more than once.
iii. For each while loop:
a) Look at the same kinds of cases as when dealing with for loops.
b) Include test cases for all possible ways of exiting the loop.
iv. Make sure that each except clause, as we will see in the next
section of the current unit, is executed.
v. For recursive functions, include test cases that cause the function
to return with no recursive calls, exactly one recursive call, and
more than one recursive call.
39/85
Testing phases
Testing starts with unit testing, during this phase, testers construct
and run tests designed to ascertain whether individual units of code
(e.g., functions) work properly.
This is followed by integration testing, which is designed to check
whether groups of units function properly when combined.
Finally, functional testing is used to check if the program as a whole
behaves as intended.
In industry, the testing process is often highly automated. Automating
the testing process facilitates the regression testing.
As programmers attempt to debug a program, it is all too common to
install a “fix” that breaks something, or maybe many things, that
used to work.
Whenever any change is made, no matter how small, you should check
that the program still passes all of the tests that it used to pass.

40/85
Next

Global Variables

Modules and Files

Testing and Debugging


Conducting Tests
Fixing Errors in Code

Exceptions and Assertions

Object-Oriented Programming
Runtime errors
The process of fixing flaws in software is known as debugging.
Runtime bugs can be categorized along two dimensions:
i. Overt/covert: An overt bug has an obvious manifestation, e.g.,
the program crashes or takes far longer (maybe forever) to run
than it should. A covert bug has no obvious manifestation. The
program may run to conclusion with no problem other than
providing an incorrect answer. Many bugs fall between the two
extremes, and whether the bug is overt can depend upon how
carefully you examine the behavior of the program.
ii. Persistent/intermittent: A persistent bug occurs every time the
program is run with the same inputs. An intermittent bug occurs
only some of the time, even when the program is run on the
same inputs and seemingly under the same conditions.
A lot of financial applications, e.g., Monte Carlo simulation, involve
programs that model situations in which randomness plays a role. In
programs of that kind, intermittent bugs are common.
41/85
Learning to debug
Debugging starts when it has become clear that the program behaves
in undesirable ways. Debugging is the process of searching for an
explanation of that behavior and fixing it.
The key to being consistently good at debugging is being systematic
in conducting that search.
For at least four decades people have been building tools called
debuggers, and debugging tools are built into all of the popular
Python IDEs.
These tools can help, but what is much more important is how you
approach the problem.
Start by studying all of the test results, and the program text, then
form a hypothesis that you believe to be consistent with all the data.
Next, design and run a repeatable experiment with the potential to
refute the hypothesis.
Finally, keep a record of what experiments you have tried.
42/85
How to debug
Next slides gather a few pragmatic hints about how to debug.
i. Look for the usual suspects.
a) Passed arguments to a function in the wrong order.
b) Misspelled a name, e.g., typed a lowercase letter when it should
be an uppercase one.
c) Failed to reinitialize a variable.
d) Tested that two-floating point values are equal (==) instead of
nearly equal (remember that floating-point arithmetic is not the
same as the arithmetic you learned in school).
e) Tested for value equality (e.g., compared two lists by writing the
expression L1 == L2) when you meant to test for object equality
(e.g., id(L1) == id(L2)).
f) Forgotten that some built-in function has a side effect.
g) Forgotten the () that turns a reference to an object of type
function into a function invocation.
h) Created an unintentional alias.
ii. Understand why the code is doing what it does, it is a good first
step in figuring out how to fix the program.
43/85
How to debug (ii)
iii. Keep in mind that the bug is probably not where you think it is.
If it were, you would have found it long ago. One practical way
to decide where to look is asking where the bug cannot be. As
Sherlock Holmes said, “Eliminate all other factors, and the one
which remains must be the truth.”
iv. Try to explain the problem to somebody else. Attempting to
explain the problem to someone will often lead you to see things
you have missed. Don’t believe everything you read. In
particular, don’t believe the documentation. The code may not
be doing what the comments suggest.
v. Stop debugging and start writing documentation. This will help
you approach the problem from a different perspective.
vi. Walk away and try again tomorrow. This may mean that bug is
fixed later than if you had stuck with it, but you will probably
spend less of your time looking for it. That is, it is possible to
trade latency for efficiency.
44/85
Debugging in VS Code
The main features of VS Code Python Debugging are:
i. Setting Breakpoints: Breakpoints are specific lines in your code
where you want the debugger to pause execution. To set a
breakpoint in VS Code, you can simply click on the gutter next
to the line number or use the keyboard shortcut (F9).
ii. Stepping Through Code: Once you have set breakpoints, you
can start debugging your Python code. When the execution
reaches a breakpoint, the debugger will pause, allowing you to
step through the code line by line. You can use the step over
(F10) or step into (F11) commands to navigate through your
code and see how variables change.
iii. Inspecting Variables: While debugging, it’s important to
inspect the values of variables at different points in your code.
VS Code provides a Variables panel that allows you to view and
track the values of variables. You can hover over variables in your
code or use the Watch panel to add specific variables to monitor.
45/85
Debugging in VS Code (ii)
iv. Debug Console: The Debug Console in VS Code allows you to
execute Python code and interact with your program while
debugging. This can be useful for testing specific code snippets
or evaluating expressions. The Debug Console supports both
single-line and multiline code execution.
v. Conditional Breakpoints: In addition to regular breakpoints,
VS Code also supports conditional breakpoints. With conditional
breakpoints, you can specify a condition that must be met for the
debugger to pause. This can be helpful when you want to break
only when a certain condition is true, such as when a variable
reaches a specific value.
vi. Exception Breakpoints: VS Code allows you to set breakpoints
on exceptions, so the debugger will pause whenever an exception
is raised. This can be useful for identifying and fixing errors in
your Python code. You can set exception breakpoints by opening
the Breakpoints view and clicking on the “+” button.
46/85
Next

Global Variables

Modules and Files

Testing and Debugging

Exceptions and Assertions

Object-Oriented Programming
Python exceptions
During the execution of a program, exceptions are events that can
take place. If an exception occurs, an error message is shown, and the
program might terminate unless the exception is appropriately
handled. Common Python built-in exceptions are:
ZeroDivisionError: Raised when a division or modulo operation is
performed with zero as the divisor.
TypeError: Raised when an operation or function is applied to an
object of an inappropriate type.
ValueError: Raised when a function receives an argument of the
correct type but an inappropriate value.
IndexError: Raised when an index is out of range for a sequence.
NameError: Occurs when you try to use a variable, function, or
module that hasn’t been defined or imported correctly
FileNotFoundError: Raised when an attempt is made to open a file
that does not exist.
47/85
Assertions in Python
In Python, an assertion is a statement that allows to test a condition
and raise an AssertionError if the condition is not met.
The syntax for using an assertion in Python is:
assert condition , message

If the condition evaluates to False, Python raises an condition


AssertionError exception.
The message is an optional string displayed when the assertion fails, it
helps in debugging by providing context about the failed condition.
Assertions are commonly used during development and testing to
check for specific conditions that should be true at a certain point in
the code.
They can be used to validate input parameters, check the state of
variables, or ensure that a certain calculation or operation produces
the expected result.
48/85
Usage example
Below code provide an example to illustrate the usage of assertions.
def divide_numbers (a , b ) :
assert b != 0 , " Cannot divide by zero "
return a / b

result = divide_numbers (10 , 0)


print ( result )

In this example, the assertion b != 0 checks if the divisor b is not


zero. If b is zero, the assertion fails and raises an AssertionError
with the specified message "Cannot divide by zero".
This helps catch potential errors early and provides a clear indication
of what went wrong.
Assertions are a useful tool for writing reliable and robust code by
validating assumptions and detecting potential issues.
They help ensure that the code behaves as expected and can aid in
identifying and fixing bugs.
49/85
Next

Global Variables

Modules and Files

Testing and Debugging

Exceptions and Assertions


Handling Exceptions
Flow Control

Object-Oriented Programming
Handling exceptions
Python provides a convenient mechanism, try-except, for catching
and handling exceptions.
The general form is
try
code block
except ( tuple with exception names ) :
code block
else :
code block

If it is known that a line of code might raise an exception when


executed, the programmer should handle the exception.
When an exception is raised that causes the program to terminate, we
say that an unhandled exception has been raised.
In a well-written program, unhandled exceptions should be the
exception.

50/85
try-except example
Consider the code
s uc cess _ f a i l u r e _ r a ti o = num_successes / num_failures
print ( ' The success / failure ratio is ' ,
suc c e s s _ f a i l u r e _ rati o )

Most of the time, this code will work just fine, but it will fail if
num_failures happens to be zero.
The attempt to divide by zero will cause the Python runtime system
to raise a ZeroDivisionError exception, and the print statement will
never be reached. A possible solution is
try :
suc c e s s _ f a i l u r e_ rati o = num_successes / num_failures
print ( ' The success / failure ratio is ' ,
s u c c e s s _fai lure_ rati o )
except Ze roDivi sionError :
print ( ' No failures , so the success / failure ratio is
undefined . ')

51/85
Several except statements
If it is possible for a block of program code to raise more than one
kind of exception, the reserved word except can be followed by a
tuple of exceptions.
For example, in the following line of code
except ( ValueError , TypeError ) :

the except block will be entered if any of the listed exceptions is


raised within the try block.
Alternatively, we can write a separate except block for each kind of
exception, which allows the program to choose an action based upon
which exception was raised.
If the programmer writes
except :

the except block will be entered if any kind of exception is raised


within the try block.
52/85
Several except statements example
Consider the next function definition.
def get_ratios ( vect1 , vect2 ) :
''' Assumes : vect1 and vect2 are equal length lists
of numbers .
Returns : a list containing the meaningful values
of vect1 [ i ]/ vect2 [ i ]. '''
ratios =[]
for index in range ( len ( vect1 ) ) :
try :
ratios . append ( vect1 [ index ]/ vect2 [ index ])
except Ze roDivisionError :
ratios . append ( float ( ' nan ') ) # nan := Not a
Number
except :
raise ValueError ( ' get_ratios called with bad
arguments ')
return ratios

There are two except blocks associated with the try block.
53/85
Several except statements example (contd.)
If an exception is raised within the try block, Python first checks to
see if it is a ZeroDivisionError. If so, it appends a special value,
'nan', of type float to ratios.
The value nan stands for “not a number”, and there is no literal for it,
but it can be denoted by converting the string 'nan' or the string
'NaN' to type float. When nan is used as an operand in an
expression of type float, the value of that expression is also nan.
If the exception is anything other than a ZeroDivisionError, the
code executes the second except block, which raises a ValueError
exception with an associated string.
The second except block should never be entered, because the code
invoking get_ratios should respect the assumptions in the
specification of get_ratios.
However, it is probably worth practicing defensive programming and
checking anyway.
54/85
Next

Global Variables

Modules and Files

Testing and Debugging

Exceptions and Assertions


Handling Exceptions
Flow Control

Object-Oriented Programming
Exceptions as a control flow mechanism
Exceptions are a convenient flow-of-control mechanism that can be
used to simplify programs.
In many programming languages, the standard approach to dealing
with errors is to have functions return a value (often something
analogous to Python’s None) indicating that something is amiss.
In Python, it is more usual to have a function raise an exception when
it cannot produce a result that is consistent with the function’s
specification.
The Python raise statement forces a specified exception to occur, its
form is
raise exceptionName ( arguments )

the exceptionName is usually one of the built-in exceptions, e.g.,


ValueError.
However, new exceptions can be defined by creating a subclass (see
next section) of the built-in class Exception.
55/85
raise example
Let’s consider the following function
def get_grades ( fname ) :
grades = []
try :
with open ( fname , 'r ') as grades_file :
for line in grades_file :
try :
grades . append ( float ( line ) )
except :
raise ValueError ( ' Cannot convert
line to float ')
except IOError :
raise ValueError ( ' get_grades could not open '
+ fname )
return grades

The function get_grades either returns a value or raises an exception


with which it has associated a value.
It raises a ValueError exception if the call to open raises an IOError.
56/85
raise example (ii)
The function get_grades could have ignored the IOError and let the
part of the program calling get_grades deal with it.
Nevertheless, that would have provided less information to the calling
code about what went wrong.
Let’s consider the code
try :
grades = get_grades ( ' quiz1grades . txt ')
grades . sort ()
median = grades [ len ( grades ) //2]
print ( ' Median grade is ' , median )
except ValueError as error_msg :
print ( ' Whoops . ' , error_msg )

Above code calling get_grades either uses the returned value to


compute another value or handles the exception and prints an
informative error message.

57/85
Next

Global Variables

Modules and Files

Testing and Debugging

Exceptions and Assertions

Object-Oriented Programming
Classes and Object-Oriented Programming
We now turn our attention to our last major topic related to
programming in Python: using classes to organize programs around
data abstractions.
Classes can be utilized in a multitude of ways, in the following their
usage, in the context of object-oriented programming (OOP), will be
explained.
The key to object-oriented programming is thinking about objects as
collections of both data and the methods that operate on that data.
Recall that objects are the core things that Python programs
manipulate. Every object has a type that defines the kinds of things
that programs can do with that object.
So far, we have relied upon built-in types such as float and str and
the methods associated with those types.
In this section, we will explore the mechanism that enables
programmers to define new types in Python.
58/85
Next

Global Variables

Modules and Files

Testing and Debugging

Exceptions and Assertions

Object-Oriented Programming
Abstract Data Types and Classes
Inheritance and Data Encapsulation
Financial Examples
Abstract data types
An abstract data type is a set of objects and the operations on those
objects.
These are bound together so that programmers can pass an object
from one part of a program to another, providing both access to the
data attributes and to the operations to manipulate such data.
These components are interconnected in a way that allows
programmers to seamlessly transfer an object from one part of a
program to another.
This facilitates not only access to the data attributes held within the
object, but also the ability to perform operations and manipulate the
data effectively.
The interface of the data type defines the behavior of the operations.
It is an abstraction barrier that isolates the rest of the program from
the data structures, algorithms, and code involved in a realization of
the type abstraction.
59/85
Decomposition and abstraction
Programming is about managing complexity, two powerful
mechanisms are available for accomplishing this: decomposition and
abstraction.
Decomposition creates structure in a program, and abstraction
suppresses detail.
The key is to suppress the appropriate details. This is where data
abstraction hits the mark.
We can create domain specific types that provide a convenient
abstraction.
Ideally, these types capture concepts that will be relevant over the
lifetime of a program.
We have been using abstract data types so far, e.g., integers, lists,
floats, strings, and dictionaries.
This built-in abstract data types were utilized without any
consideration for their underlying implementation.
60/85
Python class definition example
In Python, a class definition begins with the reserved word class
followed by the name of the class and some information about how it
relates to other classes.
A class definition creates an object of type type and associates with
that class object a set of objects called attributes.
Consider the following example
class Toy ( object ) :
def __init__ ( self ) :
self . _elems = []
def add ( self , new_elems ) :
''' new_elems is a list . '''
self . _elems += new_elems
def size ( self ) :
return len ( self . _elems )

The first line indicates that Toy is a subclass of object, and the three
attributes associated with the class are __init__, add, and size.
These three attributes are of type function.
61/85
Python class definition example (ii)
Consequently, the code,
print ( type ( Toy ) )
print ( type ( Toy . __init__ ) , type ( Toy . add ) , type ( Toy . size ) )

yields
< class ' type ' >
< class ' function ' > < class ' function ' > < class ' function ' >

Python has a number of special function names, often referred to as


magic methods, that start and end with two underscores.
The __init__ method is called whenever a class is instantiated, e.g.,
s = Toy ()

assignment will create a new instance of type Toy, and then call
Toy.__init__ with the newly created object as the input argument
that is bound to the formal parameter, i.e., the variable name, self.
62/85
Python class definition example (iii)
When invoked, Toy.__init__ creates2 the list object _elems, which
becomes part of the newly created instance of type Toy.
The list _elems is called a data attribute of the instance of Toy.
The code
t1 = Toy ()
print ( type ( t1 ) )
print ( type ( t1 . add ) )
t2 = Toy ()
print ( t1 is t2 ) # test for object identity

results in
< class ' __main__ . Toy ' >
< class ' method ' >
False

i.e., t1.add is of type method, whereas Toy.add is of type function.


Recall that methods can be invoked using dot notation.
2
using “[]” which is simply an abbreviation for list(). 63/85
Class attributes
Attributes can be associated either with a class itself or with instances
of a class.
Class attributes are defined in a class definition, for example Toy.size
is an attribute of the class Toy.
When the class is instantiated, e.g., by the statement t = Toy(),
instance attributes, e.g., t.size, are created.
While t.size is initially bound to the size function defined in the class
Toy, that binding can be changed during the course of a computation.
Data attributes associated with a class are said to be class variables,
whereas data attributes associated with an instance are called
instance variables.
For example, _elems is an instance variable because for each instance
of class Toy, _elems is bound to a different list.
A class should not be confused with instances of that class, just as an
object of type list should not be confused with the list type.
64/85
Class attributes example
Let’s consider the code
t1 = Toy ()
t2 = Toy ()
t1 . add ([3 , 4])
t2 . add ([4])
print ( t1 . size () + t2 . size () )

Since each instance of Toy is a different object, they will have a


different _elems attribute. As a consequence, the code prints 3.
Noctice that add has two formal parameters, but it is called with only
one actual parameter.
This is a feature of dot notation, the object associated with the
expression preceding the dot is implicitly passed as the first parameter
to the method.
Python common convention is using self as the name of the formal
parameter to which this actual parameter is bound.

65/85
Magic methods
One of the design goals for Python was to allow programmers to use
classes to define new types that are as easy to use as the built-in
types of Python.
Using magic methods to provide class-specific definitions of built-in
functions such as str and len plays an important role in achieving
this goal.
Magic methods can also be used to provide class-specific definitions
for infix operators such as “==” and “+”.
The names of the methods available for infix operators are
“+”:__add__ “*”:__mul__ “/”:__truediv__
“-”:__sub__ “**”:__pow__ “%”:__mod__
“//”:__floordiv__ “|”:__or__ “<”:__lt__
“«”:__lshift__ “∧”:__xor__ “>”:__gt__
“»”:__rshsift__ “==”:__eq__ “<=”:__le__
“&”:__and__ “!=”:__ne__ “>=”:__ge__
66/85
Next

Global Variables

Modules and Files

Testing and Debugging

Exceptions and Assertions

Object-Oriented Programming
Abstract Data Types and Classes
Inheritance and Data Encapsulation
Financial Examples
Inheritance
Many types have properties in common with other types. For
example, types list and str each have len functions that mean the
same thing.
Inheritance provides a convenient mechanism for building groups of
related abstractions.
It allows programmers to create a type hierarchy in which each type
inherits attributes from the types above it in the hierarchy.
The class object is at the top of the hierarchy. This makes sense,
since in Python everything that exists at runtime is an object.
As an example use of classes, imagine that you are designing a
program to help keep track of all the students professors, and staff at
IE University.
Each student would have a family name, a given name, a home
address, a year, some grades, etc.
This data could all be kept in a combination of lists and dictionaries.
67/85
Person class
Is there an abstraction that covers the common attributes of students,
professors, and staff?
As a starting point consider a class named Person as given in the file
“U2_7_class_person.py”, which first code lines are
''' Class Person example '''

import datetime

class Person ( object ) :


def __init__ ( self , name ) :
''' Assumes name a string . Create a person . '''
self . _name = name
try :
last_blank = name . rindex ( ' ')
self . _last_name = name [ last_blank +1:]
except :
self . _last_name = name
self . birthday = None

68/85
Person class (ii)
def get_name ( self ) :
''' Returns self 's full name . '''
return self . _name

def get_last_name ( self ) :


''' Returns self 's last name . '''
return self . _last_name

def set_birthday ( self , birthdate ) :


''' Assumes birthdate is of type datetime . date .
Sets self 's birthday to birthdate . '''
self . _birthday = birthdate

def get_age ( self ) :


''' Returns self 's current age in days . '''
if self . birthday == None :
raise ValueError
return ( datetime . date . today () - self . _birthday ) .
days

69/85
Person class (iii)
def __lt__ ( self , other ) :
''' Assume other a Person .
Returns True if self precedes other in
alphabetical order , and False otherwise .
Comparison is based on last names , but if
these are the same full names are compared .
'''
if self . _last_name == other . _last_name :
return self . name < other . _name
return self . _last_name < other . _last_name

def __str__ ( self ) :


''' Returns self 's name . '''
return self . name

Person uses the standard library module datetime, which provides


many convenient methods for creating and manipulating dates.
Notice that whenever Person is instantiated, an argument is supplied
to the __init__ function.
70/85
Person class (iv)
Class Person provides a specific definition for yet another specially
named method, i.e., __lt__, which overloads the “<” operator.
The method Person.__lt__ gets called whenever the first argument
to the “<” operator is of type Person.
It is implemented using the binary “<” operator of type str.
The expression self._name < other._name is shorthand for
self._name.__lt__(other._name).
Since self._name is of type str, this __lt__ method is the one
associated with type str.
In addition, this overloading provides automatic access to any
polymorphic method defined using __lt__.
For example, if p_list is a list composed of elements of type Person,
the call p_list.sort() will sort that list using the __lt__ method
defined in class Person.

71/85
Subclasses and inheritance
class IE_person ( Person ) :
_next_id_num = 0 # identification number

def __init__ ( self , name ) :


super () . __init__ ( name )
self . _id_num = IE_person . _next_id_num
IE_person . _next_id_num +=1

def get_id_num ( self ) :


return self . _id_num

def __lt__ ( self , other ) :


return self . _id_num < other . _id_num

The class IE_person inherits attributes from its parent class, Person,
including all of the attributes that Person inherited from its parent
class, object.
In the jargon of object oriented programming, IE_person is a subclass
of Person, and therefore inherits the attributes of its superclass.
72/85
Multiple levels of inheritance
The followng code adds another couple of levels of inheritance to the
class hierarchy
class Student ( IE_person ) :
pass

class UG ( Student ) :
def init_ ( self , name , class_year ) :
super () . __init__ ( name )
self . _year = class_year
def get_class ( self ) :
return self . _year

class Grad ( Student ) :


pass

The reserved word pass indicates that the class has no attributes
other than those inherited from its superclass.
Introducing the class Grad allows to create two kinds of students and
use their types to distinguish one kind of object from another.
73/85
Substitution principle
In addition to what it inherits, a subclass can: (i) add new attributes;
and (ii) override, i.e., replace, attributes of the superclass.
When subclassing is used to define a type hierarchy, the subclasses
should be thought of as extending the behavior of their superclasses.
We do this by adding new attributes or overriding attributes inherited
from a superclass.
Sometimes, the subclass overrides methods from the superclass, but
this must be done with care.
In particular, important behaviors of the supertype must be supported
by each of its subtypes.
If client code works correctly using an instance of the supertype, it
should also work correctly when an instance of the subtype is
substituted.
Hence the phrase substitution principle for the instance of the
supertype.
74/85
Encapsulation and information hiding
The idea of encapsulation means the bundling together of data
attributes and the methods for operating on them.
Another important concept is information hiding, it is one of the keys
to modularity.
If those parts of the program that use a class (i.e., the clients of the
class) rely only on the specifications of the methods in the class, a
programmer implementing the class is free to change the
implementation of the class without worrying that the change will
break code that uses the class.
Programmers can make the attributes of a class private, so that clients
of the class can access the data only through the object’s methods.
Python 3 uses a naming convention to make attributes invisible
outside the class.
If the name of an attribute starts with “__” (double underscore) but
does not end with “__”, that attribute is not visible outside the class.
75/85
Generators
Any function definition containing a yield statement is treated in a
special way.
The presence of yield tells the Python system that the function is a
generator.
Generators are typically used with for statements.
At the start of the first iteration of a for loop that uses a generator,
the generator is invoked and runs until the first time a yield
statement is executed, at which point it returns the value of the
expression in the yield statement.
On the next iteration, the generator resumes execution immediately
following the yield, with all local variables bound to the objects to
which they were bound when the yield statement was executed, and
again runs until a yield statement is executed.
It continues to do this until it runs out of code to execute or executes
a return statement, at which point the loop is exited.
76/85
Next

Global Variables

Modules and Files

Testing and Debugging

Exceptions and Assertions

Object-Oriented Programming
Abstract Data Types and Classes
Inheritance and Data Encapsulation
Financial Examples
Financial account
The code within “U2_8_financial_account.py” defines a class
called “FinancialAccount” that represents a financial account.
It has attributes for the account holder’s name and the current
account balance.
class FinancialAccount :
''' FinancialAccount class with attributes for the
account holder 's name ( account_holder ) and the
current account balance ( balance ) .
The class has methods for depositing ( deposit )
and withdrawing ( withdraw ) funds , as well as
getting the current balance ( get_balance ) .
The __str__ method is defined to provide a
string representation of the account . '''

def __init__ ( self , account_holder , balance = 0.0) :


self . account_holder = account_holder
self . balance = balance

77/85
Financial account (ii)
def deposit ( self , amount ) :
if amount > 0:
self . balance += amount
print ( f " Deposited $ { amount :.2 f }. New balance
: $ { self . balance :.2 f } " )
else :
print ( " Invalid deposit amount . Amount must
be greater than 0. " )

def withdraw ( self , amount ) :


if amount > 0:
if self . balance >= amount :
self . balance -= amount
print ( f " Withdrew $ { amount :.2 f }. New
balance : $ { self . balance :.2 f } " )
else :
print ( " Insufficient funds . " )
else :
print ( " Invalid withdrawal amount . Amount
must be greater than 0. " )
78/85
Financial account (iii)
def get_balance ( self ) :
return self . balance

def __str__ ( self ) :


return f " Account holder : { self . account_holder }\
nBalance : $ { self . balance :.2 f } "

The class provides methods for depositing and withdrawing funds, as


well as getting the current balance.
The __str__ method is defined to provide a string representation of
the account.
The class allows managing a financial account by performing
operations such as depositing, withdrawing, and retrieving the
balance.
Use example
account1 = FinancialAccount ( " John Doe " , 1000.0)
print ( account1 )

79/85
Mortgages
A mortgage consists in borrowing money from a bank and made a
fixed payment each month for the life of the mortgage, typically from
15 to 30 years.
At the end of that period, the bank had been paid back the initial loan
(the principal) plus interest, and the homeowner owned the house.
For example, “interest-only” mortgages, i.e., for some number of
months at the start of the loan the borrower paid only the accrued
interest and none of the principal.
Other loans involved multiple rates. Typically the initial rate (called a
“teaser rate”) was low, and then it went up over time.
Many of these were variable-rate, i.e., the rate to be paid would vary
depending upon some index intended to reflect the cost to the lender
of borrowing on the wholesale credit market.
To provide some experience in the incremental development of a set
of related classes the code within “U2_9_mortgage.py” is provided.
80/85
Mortgage base class
def find_payment ( loan , r , m ) :
''' Assumes : loan and r are floats , m an int .
Returns the monthly payment for a mortgage of size
loan at a monthly rate of r for m months . '''
return loan *(( r *(1+ r ) ** m ) /((1+ r ) ** m - 1) )

class Mortgage ( object ) :


''' Abstract class for mortgages . '''
def __init__ ( self , loan , ann_rate , months ) :
''' Assumes : loan and ann_rate are floats , months
an int . Creates a new mortgage of size loan ,
duration months , and annual rate ann_rate . '''
self . _loan = loan
self . _rate = ann_rate /12
self . months = months
self . _paid = [0.0]
self . _outstanding = [ loan ]
self . _payment = find_payment ( loan , self . _rate ,
months )
self . _legend = None # description of mortgage
81/85
Mortgage base class (ii)
def make_payment ( self ) :
''' Make a payment . '''
self . _paid . append ( self . _payment )
reduction = self . _payment - self . _outstanding
[ -1]* self . _rate
self . _outstanding . append ( self . _outstanding [ -1] -
reduction )

def get_total_paid ( self ) :


''' Return the total amount paid so far . '''
return sum ( self . _paid )

def __str__ ( self ) :


return self . _legend

All Mortgage objetcs will have variables corresponding to the above


properties.
Notice that the amount of money to be paid each month is initialized
using the value returned by the function find_payment.
82/85
Mortgage subclasses
The code within “U2_9_mortgage.py” also contains classes
implementing three types of mortgages.
class Fixed ( Mortgage ) :
def __init__ ( self , loan , r , months ) :
Mortgage . __init__ ( self , loan , r , months )
self . _legend = f ' Fixed , { r *100:.1 f }% '

class Fixed_with_pts ( Mortgage ) :


def __init__ ( self , loan , r , months , pts ) :
Mortgage . __init__ ( self , loan , r , months )
self . _pts = pts
self . _paid = [ loan *( pts /100) ]
self . _legend = f ' Fixed , { r *100:.1 f }% , { pts }
points '

The classes Fixed and Fixed_with_pts override __init__ and inherit


the other three methods from Mortgage.
Next, the class Two_rate treats a mortgage as the concatenation of
two loans, each at a different interest rate.
83/85
Mortgage subclasses (ii)
class Two_rate ( Mortgage ) :
def __init__ ( self , loan , r , months , teaser_rate ,
teaser_months ) :
Mortgage . __init__ ( self , loan , teaser_rate ,
months )
self . _teaser_months = teaser_months
self . _teaser_rate = teaser_rate
self . _nextRate = r /12
self . _legend = ( f ' {100* teaser_rate :.1 f }% for ' +
f '{ self . _teaser_months } months ,
then {100* r :.1 f }% ')

def make_payment ( self ) :


if len ( self . _paid ) == self . _teaser_months + 1:
self . _rate = self . _nextRate
self . _payment = find_payment (
self . _outstanding [ -1] , self . _rate ,
self . _months - self . _teaser_months )
Mortgage . make_payment ( self )

84/85
Mortgages comparison
def compare _mortg ages ( amt , years , fixed_rate , pts ,
pts_rate , var_rate1 , var_rate2 , var_months ) :
tot_months = years *12
fixed1 = Fixed ( amt , fixed_rate , tot_months )
fixed2 = Fixed_with_pts ( amt , pts_rate , tot_months ,
pts )
two_rate = Two_rate ( amt , var_rate2 , tot_months ,
var_rate1 , var_months )
morts = [ fixed1 , fixed2 , two_rate ]
for m in range ( tot_months ) :
for mort in morts :
mort . make_payment ()
for m in morts :
print ( m )
print ( f ' Total payments = ' +
'$ { m . get_total_paid () : ,.0 f } ')

compare_m ortgag es ( amt =200000 , years =30 ,


fixed_rate =0.035 , pts =2 , pts_rate =0.03 ,
var_rate1 =0.03 , var_rate2 =0.05 , var_months =60)

85/85
References
Guttag, John V. (2021). Introduction to Computation and
Programming Using Python. with Application to Computational
Modeling and Understanding Data. Third Edition. Cambridge,
MA: The MIT Press.

You might also like