This lecture completes Part I of the course (Lectures 1-5), which covers the core Python language.
A class defines a new kind of object, such as a stock, a brokerage account, or a portfolio, that holds data together with the functions that work on that data.
Writing data to a file keeps it after the program finishes. Handling exceptions allows a program to continue when something goes wrong, such as a missing file or a price of zero.
Classes, files, and exceptions all appear again in Lectures 6-9. A pandas
DataFrameis an instance of a class, almost every analysis starts by reading a file, and the data loaders in Lectures 8-9 usetry-exceptto switch to another source when a download fails.
What We'll Cover
Ch 9 (Classes): defining a class,
__init__()and attributes, methods, default values, changing attributes, inheritance withsuper(), overriding methods, objects as attributes, and importing classes from modules.Ch 10 (Files and Exceptions): reading and writing text files with
pathlib, relative and absolute paths,try-except-else,ZeroDivisionErrorandFileNotFoundError, and saving data withjson.The running example is a set of classes for stocks, accounts, and portfolio holdings, followed in Ch 10 by price files, a trade log, and a saved watchlist. All prices, earnings, and balances are illustrative.
Ch 9: Classes
So far, data has been stored in plain values, lists, and dictionaries, and the code that works on it has been written as separate functions that take the data as arguments.
A class combines the two. It defines a new type of object with its own data, called attributes, and its own functions, called methods.
Writing programs around such objects is called object-oriented programming (OOP). Every string and list is an object, and so is every pandas
DataFramein Lecture 6.A class is a template, and each object created from it is an instance of the class. One
Stockclass can create an instance for AAPL, another for MSFT, and so on.
Defining and Using a Class
The Stock Class
The first class describes a stock. Each stock has a ticker, a share price, and earnings per share (EPS). It can compute its price-to-earnings (P/E) ratio, the share price divided by EPS, and print a one-line description of itself.
The
classline names the new class, and the indented lines below it belong to the class. The text in triple quotes is a docstring that describes the class, as with functions in Lecture 4.
class Stock:
"""A publicly traded stock."""
def __init__(self, ticker, price, eps):
"""Store the ticker, share price, and earnings per share."""
self.ticker = ticker
self.price = price
self.eps = eps
def pe_ratio(self):
"""Return the price-to-earnings ratio."""
return self.price / self.eps
def describe(self):
"""Print a one-line summary of the stock."""
print(f"{self.ticker}: ${self.price:,.2f} per share, "
f"P/E {self.pe_ratio():.1f}")How __init__() and self Work
A function defined inside a class is a method.
__init__()(two underscores on each side) is a special method that Python runs automatically each time a new instance is created.Its first parameter,
self, is the new instance. Python fills it in, so you never pass it yourself.A line such as
self.ticker = tickerstores the value passed in as an attribute of the instance. Every method in the class can then read it asself.ticker.pe_ratio()anddescribe()are ordinary methods. They also receiveself, which is how they read the instance's attributes and howdescribe()callsself.pe_ratio().
Building an Object from the Class
To create an instance, call the class like a function and pass the arguments that
__init__()expects (all exceptself).By convention, class names use CamelCase (
Stock) and instance names use lowercase (aapl), so the two are easy to tell apart.type()shows thataaplis aStock. (__main__is Python's name for the notebook or script in which the class was defined.)
aapl = Stock("AAPL", 230.00, 6.50)
type(aapl)__main__.Stock
Using Attributes and Methods
Read an attribute with dot notation:
instance.attribute.Call a method the same way, with parentheses:
instance.method(). Python passes the instance in asself, which is whyaapl.pe_ratio()takes no arguments.
print(aapl.ticker)
print(aapl.price)
print(round(aapl.pe_ratio(), 1))
aapl.describe()AAPL 230.0 35.4 AAPL: $230.00 per share, P/E 35.4
Several Stocks from One Class
A class can create any number of instances. Each instance has its own attributes, so changing one stock does not affect another.
Instances can be stored in a list like any other value. The loop below calls
describe()on each one.
msft = Stock("MSFT", 420.00, 12.00)
ko = Stock("KO", 62.00, 2.50)
jpm = Stock("JPM", 210.00, 18.00)
for stock in [aapl, msft, ko, jpm]:
stock.describe()AAPL: $230.00 per share, P/E 35.4 MSFT: $420.00 per share, P/E 35.0 KO: $62.00 per share, P/E 24.8 JPM: $210.00 per share, P/E 11.7
Note: Every argument that __init__() requires must be supplied. If any are missing, Python raises a TypeError that names them.
# This will fail: Stock() also needs a price and an EPS
xom = Stock("XOM")
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[5], line 2
1 # This will fail: Stock() also needs a price and an EPS
----> 2 xom = Stock("XOM")
TypeError: Stock.__init__() missing 2 required positional arguments: 'price' and 'eps'Changing Instances Over Time
An Account Class with a Default Value
An instance's attributes often change while a program runs. This section builds a class for a brokerage account, whose cash balance changes over time.
Not every attribute needs a parameter. The line
self.cash = 0.0gives every new account the same default value, so the caller does not pass one in.account_type="individual"is a parameter with a default value, as in Lecture 4. The caller can pass a different value.Unlike
describe()inStock,summary()returns its text instead of printing it, so the caller decides what to do with it.buying_power()is used later, in the section on inheritance.
class BrokerageAccount:
"""A cash account at a broker."""
def __init__(self, owner, account_type="individual"):
"""Set up a new account; every new account starts with no cash."""
self.owner = owner
self.account_type = account_type
self.cash = 0.0
def summary(self):
"""Return a one-line description of the account."""
return f"{self.owner} ({self.account_type}): ${self.cash:,.2f} in cash"
def set_cash(self, amount):
"""Set the cash balance, rejecting negative amounts."""
if amount < 0:
print("Cash balance cannot be negative.")
else:
self.cash = amount
def deposit(self, amount):
"""Add money to the account."""
self.cash += amount
def buying_power(self):
"""Return the dollar amount of stock the account can buy."""
return self.cashmy_account = BrokerageAccount("Avery Chen")
ira = BrokerageAccount("Avery Chen", account_type="Roth IRA")
print(my_account.summary())
print(ira.summary())Avery Chen (individual): $0.00 in cash Avery Chen (Roth IRA): $0.00 in cash
Updating an Attribute
There are three ways to change an attribute's value. Each example below changes
my_account.cash, the account's cash balance.The first is to assign a new value directly, with the attribute on the left of
=as with a variable.
my_account.cash = 1_000.0
print(my_account.summary())Avery Chen (individual): $1,000.00 in cash
The second is to set the value through a method.
set_cash()updates the balance, and because every update runs through code we wrote, the method can check a new value before accepting it.Python itself does not prevent
my_account.cash = -300.0. Changing attributes only through methods is a convention, and it keeps the account's rules in one place.
my_account.set_cash(2_500.0)
print(my_account.summary())
my_account.set_cash(-300.0)
print(my_account.summary())Avery Chen (individual): $2,500.00 in cash Cash balance cannot be negative. Avery Chen (individual): $2,500.00 in cash
- The third is to increment the value through a method.
deposit()uses+=to add each amount to the current balance instead of replacing it.
my_account.deposit(750.0)
my_account.deposit(1_200.0)
print(my_account.summary())Avery Chen (individual): $4,450.00 in cash
Inheritance
A class we need is often a special case of one we already have. A margin account, for example, is a brokerage account that can also borrow money from the broker.
Instead of starting from scratch, we write a child class (or subclass) that inherits from the existing parent class (or superclass). The child gets every attribute and method of the parent.
The child can then add its own attributes and methods, and override (replace) any parent method that should work differently.
A Child Class and super()
Put the parent's name in parentheses after the child's name:
class MarginAccount(BrokerageAccount):. The parent class must already be defined.Inside the child's
__init__(), the callsuper().__init__(...)runs the parent's__init__(). This setsowner,account_type, andcashwithout repeating the parent's code.This child always passes
account_type="margin", so the caller supplies only the owner.
class MarginAccount(BrokerageAccount):
"""A brokerage account that can borrow from the broker."""
def __init__(self, owner):
"""Set up everything a regular brokerage account has."""
super().__init__(owner, account_type="margin")MarginAccountdefines no other methods yet, but it can already usedeposit()andsummary()from its parent.isinstance()shows that aMarginAccountinstance is also aBrokerageAccount.
margin = MarginAccount("Jordan Park")
margin.deposit(5_000.0)
print(margin.summary())
print(isinstance(margin, BrokerageAccount))Jordan Park (margin): $5,000.00 in cash True
Attributes and Methods Only the Child Has
After the
super().__init__()line, the child sets attributes that only it needs: the annual margin rate (the interest rate charged on borrowed money) and a leverage limit with a default value of2.0.The new method
interest_cost()computes simple interest: amount borrowed × annual rate × years.margin_rate,leverage, andinterest_cost()belong only toMarginAccount. A plainBrokerageAccountdoes not have them, and callinginterest_cost()on one would raise anAttributeError.
class MarginAccount(BrokerageAccount):
"""A brokerage account that can borrow from the broker."""
def __init__(self, owner, margin_rate=0.08):
"""Set up the parent's attributes, then the borrowing terms."""
super().__init__(owner, account_type="margin")
self.margin_rate = margin_rate
self.leverage = 2.0
def interest_cost(self, borrowed, years=1):
"""Return the simple interest owed on a margin loan."""
return borrowed * self.margin_rate * yearsmargin = MarginAccount("Jordan Park", margin_rate=0.075)
interest = margin.interest_cost(4_000, years=0.5)
print(f"Margin rate: {margin.margin_rate:.1%}")
print(f"Interest on $4,000 for 6 months: ${interest:,.2f}")Margin rate: 7.5% Interest on $4,000 for 6 months: $150.00
Replacing a Parent Method (Overriding)
To override a parent method, define a method with the same name in the child. Instances of the child then use the child's version, and instances of the parent keep the original.
The parent's
buying_power()returns the cash balance. Under a simplified margin rule, the broker lends up toleverage - 1times the cash, so a margin account's buying power iscash * leverage.The class below repeats the previous definition and adds the override at the end. In the next cell, the same method call gives different results because each object uses the
buying_power()of its own class.
class MarginAccount(BrokerageAccount):
"""A brokerage account that can borrow from the broker."""
def __init__(self, owner, margin_rate=0.08):
"""Set up the parent's attributes, then the borrowing terms."""
super().__init__(owner, account_type="margin")
self.margin_rate = margin_rate
self.leverage = 2.0
def interest_cost(self, borrowed, years=1):
"""Return the simple interest owed on a margin loan."""
return borrowed * self.margin_rate * years
def buying_power(self):
"""Return cash plus the most the broker will lend."""
return self.cash * self.leveragecash_account = BrokerageAccount("Sam Rivera")
cash_account.deposit(10_000.0)
margin = MarginAccount("Sam Rivera")
margin.deposit(10_000.0)
print(f"Cash account buying power: ${cash_account.buying_power():,.2f}")
print(f"Margin account buying power: ${margin.buying_power():,.2f}")Cash account buying power: $10,000.00 Margin account buying power: $20,000.00
Objects as Attributes
An attribute can hold any value, including an instance of another class. A larger class can then be built from smaller ones.
A position is a holding in one stock: a ticker, a number of shares, and the average price paid per share (the cost basis per share). Its method
unrealized_gain()returns the profit or loss on paper, which is the current value of the shares minus what they cost.A portfolio is a collection of positions.
Portfoliostores itsPositioninstances in a dictionary attribute,positions, keyed by ticker.buy()creates a newPositionand stores it.total_value()andreport()take a dictionary of current prices and call the methods of each stored position to get its value and gain.
class Position:
"""A holding of shares in one stock."""
def __init__(self, ticker, shares, cost_basis):
"""Store the ticker, the share count, and the price paid per share."""
self.ticker = ticker
self.shares = shares
self.cost_basis = cost_basis
def market_value(self, price):
"""Return the value of the position at the given share price."""
return self.shares * price
def unrealized_gain(self, price):
"""Return the paper profit (or loss) at the given share price."""
return self.shares * (price - self.cost_basis)class Portfolio:
"""A named collection of Position objects."""
def __init__(self, name):
"""Start with no positions."""
self.name = name
self.positions = {}
def buy(self, ticker, shares, price):
"""Open a position in ticker at the given price."""
self.positions[ticker] = Position(ticker, shares, price)
def total_value(self, prices):
"""Return the market value of all positions."""
total = 0.0
for position in self.positions.values():
total += position.market_value(prices[position.ticker])
return total
def report(self, prices):
"""Print the value and gain of each position, then the total."""
print(f"Portfolio: {self.name}")
for ticker, position in self.positions.items():
value = position.market_value(prices[ticker])
gain = position.unrealized_gain(prices[ticker])
print(f" {ticker}: value ${value:,.2f}, gain ${gain:,.2f}")
print(f"Total value: ${self.total_value(prices):,.2f}")growth = Portfolio("Growth")
growth.buy("NVDA", 20, 110.00)
growth.buy("AMZN", 15, 180.00)
growth.buy("MSFT", 10, 400.00)
prices_today = {"NVDA": 125.00, "AMZN": 195.00, "MSFT": 410.00}
growth.report(prices_today)Portfolio: Growth NVDA: value $2,500.00, gain $300.00 AMZN: value $2,925.00, gain $225.00 MSFT: value $4,100.00, gain $100.00 Total value: $9,525.00
- To reach inside a nested object, chain the dots.
growth.positions["NVDA"]is aPosition, so we can read its attributes and call its methods directly.
nvda_position = growth.positions["NVDA"]
print(nvda_position.shares, nvda_position.cost_basis)
print(growth.positions["AMZN"].unrealized_gain(prices_today["AMZN"]))20 110.0 225.0
Importing Classes from Modules
A notebook full of class definitions is hard to read. As in Lecture 4, the code can be moved into a module (a
.pyfile) and imported where it is needed.%%writefile <filename>on the first line of a code cell saves the rest of the cell to a file instead of running it.In Colab, the file is saved in the working folder,
/content, which is where Python looks first when you import. Colab deletes these files when the runtime shuts down, so keep a copy of anything you need.
A Module with One Class
- The module
stock.pycontains only theStockclass, below a one-line docstring that describes the module.
%%writefile stock.py
"""A class for representing a single stock."""
class Stock:
"""A publicly traded stock."""
def __init__(self, ticker, price, eps):
"""Store the ticker, share price, and earnings per share."""
self.ticker = ticker
self.price = price
self.eps = eps
def pe_ratio(self):
"""Return the price-to-earnings ratio."""
return self.price / self.eps
def describe(self):
"""Print a one-line summary of the stock."""
print(f"{self.ticker}: ${self.price:,.2f} per share, "
f"P/E {self.pe_ratio():.1f}")Writing stock.py
from stock import Stockopens the modulestock(the filestock.py, without the extension) and brings the classStockinto the notebook.Once imported, the class works as if it were defined in the notebook.
from stock import Stock
nvda = Stock("NVDA", 125.00, 2.90)
nvda.describe()NVDA: $125.00 per share, P/E 43.1
A Module with Several Classes
A module can hold any number of classes. Related classes usually go together, so
accounts.pyholds shortened versions of both account classes.MarginAccountinherits fromBrokerageAccount, so the parent must come first in the file. Two blank lines separate the classes.
Note: Python loads a module only once per session. If you edit a module and rerun its %%writefile cell, a new import still uses the old version. Restart the session (in Colab, Runtime → Restart session) and import again.
%%writefile accounts.py
"""Classes for cash and margin brokerage accounts."""
class BrokerageAccount:
"""A cash account at a broker."""
def __init__(self, owner, account_type="individual"):
"""Set up a new account; every new account starts with no cash."""
self.owner = owner
self.account_type = account_type
self.cash = 0.0
def summary(self):
"""Return a one-line description of the account."""
return f"{self.owner} ({self.account_type}): ${self.cash:,.2f} in cash"
def deposit(self, amount):
"""Add money to the account."""
self.cash += amount
def buying_power(self):
"""Return the dollar amount of stock the account can buy."""
return self.cash
class MarginAccount(BrokerageAccount):
"""A brokerage account that can borrow from the broker."""
def __init__(self, owner, margin_rate=0.08):
"""Set up the parent's attributes, then the borrowing terms."""
super().__init__(owner, account_type="margin")
self.margin_rate = margin_rate
self.leverage = 2.0
def interest_cost(self, borrowed, years=1):
"""Return the simple interest owed on a margin loan."""
return borrowed * self.margin_rate * years
def buying_power(self):
"""Return cash plus the most the broker will lend."""
return self.cash * self.leverageWriting accounts.py
- You can import only the class you need.
MarginAccountworks even thoughBrokerageAccountwas not imported by name, because the parent class is defined in the module.
from accounts import MarginAccount
margin = MarginAccount("Lee Wong")
margin.deposit(8_000.0)
print(margin.summary())
print(f"Buying power: ${margin.buying_power():,.2f}")Lee Wong (margin): $8,000.00 in cash Buying power: $16,000.00
Picking Several Classes from a Module
- List the classes after
import, separated by commas.
from accounts import BrokerageAccount, MarginAccount
savings = BrokerageAccount("Lee Wong")
trading = MarginAccount("Lee Wong", margin_rate=0.065)
for account in [savings, trading]:
account.deposit(3_000.0)
print(f"{account.summary()} | buying power ${account.buying_power():,.2f}")Lee Wong (individual): $3,000.00 in cash | buying power $3,000.00 Lee Wong (margin): $3,000.00 in cash | buying power $6,000.00
Bringing In the Whole Module
import accountsloads the entire module. Each class is then accessed with dot notation:accounts.ClassName.This style shows where every class comes from. Only the name
accountsis added to the notebook, so the module's classes cannot overwrite names you already use. Ch 10 importsjsonthis way and callsjson.dumps()andjson.loads().
import accounts
ira = accounts.BrokerageAccount("Lee Wong", account_type="traditional IRA")
ira.deposit(6_500.0)
print(ira.summary())Lee Wong (traditional IRA): $6,500.00 in cash
Importing Everything with *
from accounts import *imports every class (and function) that the module defines.Avoid this in your own code. A reader cannot tell which names came from the module, and if the module defines a name you already use, your version is silently replaced. Import the classes you need by name, or import the whole module.
Named imports can also replace existing names. Earlier,
from accounts import BrokerageAccount, MarginAccountreplaced the notebook's ownBrokerageAccount, which had aset_cash()method, with the shorter version inaccounts.py. With*, such replacements are much harder to spot.
from accounts import *
print(MarginAccount("Lee Wong").summary())Lee Wong (margin): $0.00 in cash
Aliases with as
asgives an imported class or module a different name in your code. Use it when a name is long or clashes with one you already use.Lecture 4 applied
asto functions and modules. The standard short names in Lectures 6-9,import numpy as npandimport pandas as pd, use the same syntax.
from accounts import BrokerageAccount as Account
import accounts as acct
roth = Account("Lee Wong", account_type="Roth IRA")
joint = acct.MarginAccount("Lee and Sam Wong", margin_rate=0.07)
print(roth.summary())
print(f"{joint.summary()}, margin rate {joint.margin_rate:.1%}")Lee Wong (Roth IRA): $0.00 in cash Lee and Sam Wong (margin): $0.00 in cash, margin rate 7.0%
A Note on the Standard Library
Every Python installation includes the standard library, a large set of modules that you import the same way as your own, with nothing to install. Ch 10 uses two of them,
pathlibandjson; others includemath,random,statistics, andcsv.Many of these modules define classes.
date, from thedatetimemodule, is a class whose instances are calendar dates. Each instance has attributes such as.year, and subtracting one date from another gives the time between them.Before writing a general-purpose tool yourself, check the library documentation to see whether the standard library already has one.
from datetime import date
settlement = date(2026, 3, 20)
maturity = date(2031, 3, 20)
print(type(maturity))
print(maturity.year)
print((maturity - settlement).days, "days to maturity")<class 'datetime.date'> 2031 1826 days to maturity
Class Style Conventions
These conventions follow PEP 8, Python's official style guide, as did the function conventions in Lecture 4.
Class names use CamelCase: capitalize each word and drop underscores (
MarginAccount, notmargin_account). Instance and module names use lowercase with underscores (my_account,accounts.py).Give every class a docstring, right below the
classline, that says what the class represents. Give every method a docstring that says what it does, and start each module with a docstring too.Separate methods with one blank line. In a module, separate classes with two blank lines.
Put all imports at the top of a file or notebook: standard-library modules first, then a blank line, then your own modules.
Keep each class focused on one idea. If a class starts doing two unrelated jobs, split it, as we did with
PositionandPortfolio.
In-Class Exercise
9-1. A Bond Class:
- Write a class
Bondwith attributesissuer,face_value,coupon_rate, andyears_to_maturity. - Add a method
annual_coupon()that returnsface_value * coupon_rate, and a methoddescribe()that prints a one-line summary. - Create two bonds with different coupon rates and call both methods on each.
9-2. Bond Prices and Yields:
- Give your
Bondan attributepricewhose default value equalsface_value. - Write a method
update_price(new_price)that rejects negative prices, and a methodcurrent_yield()that returnsannual_coupon() / price. - Change the price three times (directly, through the method, and through the method with a negative value) and print the current yield after each change.
9-3. An ETF Child Class:
- Write a class
ETFthat inherits fromStockand adds an attributeexpense_ratio(for example,0.0009for 0.09% per year). - Add a method
annual_fee(amount_invested)that returns the yearly fee in dollars. - Override
describe()so that it also prints the expense ratio.
9-4. A Bond Ladder Module:
- A bond ladder is a set of bonds that mature in different years. Use
%%writefile bonds.pyto save yourBondclass and a new classBondLadderwhose attributebondsis a list ofBondinstances. - Give
BondLaddera methodadd(bond)and a methodtotal_annual_income()that adds up the coupons. - In a new cell, import both classes from
bonds, build a ladder of three bonds, and print its total annual income.
Ch 10: Files and Exceptions
Everything created so far (variables, lists, instances) is lost when the session ends. To keep results, a program writes them to a file; to use data from elsewhere, it reads one.
Programs that work with outside data also run into errors, such as a missing file or a price recorded as zero. Python signals such errors with exceptions, and a program that handles them can respond to the problem instead of stopping.
We use two standard-library modules,
pathlibandjson. In Lecture 6, pandas reads an entire CSV file into a table in one line.
Reading Text Files
Creating a Small Price File
To keep the notebook self-contained, we first create a small file ourselves. In practice, files come from a data vendor, a download, or another program.
Path, from the standard-library modulepathlib, represents the location of a file. Its methodwrite_text()saves a string to that file.The file is in CSV (comma-separated values) format: a header line, then one line per trading day with the date and NVDA's closing price.
write_text()returns the number of characters it wrote, which Jupyter displays below the cell.
from pathlib import Path
price_data = """date,close
2026-03-02,121.40
2026-03-03,123.10
2026-03-04,120.75
2026-03-05,124.60
2026-03-06,126.30
"""
price_path = Path("nvda_prices.csv")
price_path.write_text(price_data)101
Reading the Whole File
read_text()returns the entire contents of a file as one string, including line breaks.
contents = price_path.read_text()
print(contents)date,close 2026-03-02,121.40 2026-03-03,123.10 2026-03-04,120.75 2026-03-05,124.60 2026-03-06,126.30
Relative vs. Absolute Paths
A relative path, such as
Path("nvda_prices.csv"), is looked up starting from the current working directory, the folder the notebook runs in. In Colab that is/content; in Jupyter on your own computer it is usually the folder that holds the notebook.Path.cwd()returns this folder.To keep a project organized, put data files in a subfolder. The
/operator joins parts of a path, as inPath("data") / "nvda_prices.csv", and uses the correct separator on every operating system.mkdir(exist_ok=True)creates the folder, or does nothing if it already exists.An absolute path gives the full location from the top of the file system:
/content/data/nvda_prices.csvin Colab, or something likeC:\Users\you\Documents\data\nvda_prices.csvon Windows.Absolute paths work from any working directory, but they differ from one computer to the next. Relative paths keep a project portable, so this course uses them. The
is_absolute()method of aPathreports which kind it holds.
data_dir = Path("data")
data_dir.mkdir(exist_ok=True)
nvda_copy = data_dir / "nvda_prices.csv"
nvda_copy.write_text(price_data)
print(nvda_copy, nvda_copy.is_absolute())
colab_path = Path("/content/data/nvda_prices.csv")
print(colab_path, colab_path.is_absolute())data/nvda_prices.csv False /content/data/nvda_prices.csv True
Working Through the File Line by Line
splitlines()splits a string into a list with one item per line, so we can loop over the lines of the file.The first line is the header;
lines[1:]skips it.Each remaining line, such as
"2026-03-02,121.40", splits at the comma into a date and a price. The price is read as a string, sofloat()converts it to a number.
lines = price_path.read_text().splitlines()
print(lines[0])
dates = []
closes = []
for line in lines[1:]:
day, close = line.split(",")
dates.append(day)
closes.append(float(close))
print(dates)
print(closes)date,close ['2026-03-02', '2026-03-03', '2026-03-04', '2026-03-05', '2026-03-06'] [121.4, 123.1, 120.75, 124.6, 126.3]
With the prices stored as numbers, we can compute each day's simple return: today's close divided by the previous close, minus 1.
The loop starts at index 1 because the first day has no previous close.
for i in range(1, len(closes)):
daily_return = closes[i] / closes[i - 1] - 1
print(f"{dates[i]}: {daily_return:+.2%}")
week_return = closes[-1] / closes[0] - 1
print(f"Return for the week: {week_return:+.2%}")2026-03-03: +1.40% 2026-03-04: -1.91% 2026-03-05: +3.19% 2026-03-06: +1.36% Return for the week: +4.04%
Writing Text Files
Saving a Single Record
write_text()creates the file if needed and replaces anything already in it.It accepts only strings. Use an f-string (or
str()) to put numbers into the text, and end the line with the newline character\n.The example starts a trade log, a text file with one line per trade.
log_path = Path("trade_log.txt")
shares, price = 20, 121.40
log_path.write_text(f"BUY {shares} NVDA at {price:.2f}\n")
print(log_path.read_text())BUY 20 NVDA at 121.40
Saving Many Records at Once
write_text()writes exactly the string it is given, so several lines must be combined into one string with\nbetween them.One way is to collect the lines in a list and join them:
"\n".join(log_lines)combines the strings with a newline between each pair.Writing the file again replaces the one-line log from the previous cell.
trades = [
("BUY", "NVDA", 20, 121.40),
("BUY", "KO", 50, 61.20),
("SELL", "NVDA", 5, 126.30),
]
log_lines = []
for side, ticker, shares, price in trades:
log_lines.append(f"{side} {shares} {ticker} at {price:.2f}")
log_path.write_text("\n".join(log_lines) + "\n")
print(log_path.read_text())BUY 20 NVDA at 121.40 BUY 50 KO at 61.20 SELL 5 NVDA at 126.30
Note: A trade log usually grows one trade at a time, and rewriting the whole file for each trade is wasteful. log_path.open("a") opens the file in append mode, which adds text to the end instead. The with statement closes the file automatically when its indented block ends. write() adds the text exactly as given, so include the \n.
with log_path.open("a") as log_file:
log_file.write("BUY 10 JPM at 210.00\n")
print(log_path.read_text())BUY 20 NVDA at 121.40 BUY 50 KO at 61.20 SELL 5 NVDA at 126.30 BUY 10 JPM at 210.00
Exceptions
When an error occurs while a program runs, Python raises an exception, an object that describes the problem. If nothing handles it, the program stops and prints a traceback that ends with the exception's type and a message.
A
try-exceptblock handles an exception. It says in advance what to do if a particular error occurs, and the program then continues instead of stopping.Handling errors matters most in code that runs unattended, such as a script that updates prices every night. One bad record should not stop the whole job.
ZeroDivisionError
- A return divides by the starting price. Data feeds sometimes record a missing price as
0, and division by zero is undefined.
Note: Python cannot divide by zero, so it raises a ZeroDivisionError and stops.
# This will fail: the starting price is zero
start_price, end_price = 0.0, 48.20
end_price / start_price - 1
---------------------------------------------------------------------------
ZeroDivisionError Traceback (most recent call last)
Cell In[38], line 3
1 # This will fail: the starting price is zero
2 start_price, end_price = 0.0, 48.20
----> 3 end_price / start_price - 1
ZeroDivisionError: float division by zeroCatching an Exception with try and except
Put the line that might fail inside a
tryblock. If it raises the exception named afterexcept, Python skips the rest of thetryblock and runs theexceptblock instead.If no exception occurs, the
exceptblock is skipped. Either way, the program then continues with the code after thetry-exceptblock.
try:
print(end_price / start_price - 1)
except ZeroDivisionError:
print("Cannot compute a return: the starting price is zero.")
print("The program keeps running.")Cannot compute a return: the starting price is zero. The program keeps running.
Adding an else Block
Code that should run only if the
tryblock succeeded goes in anelseblock afterexcept.This keeps the
tryblock to the one line that can fail, so an error from another line is not caught by mistake.Below, one record has a starting price of zero. The loop reports the problem and moves on to the next stock.
records = [
("AAPL", 225.00, 231.75),
("XOM", 0.0, 112.40),
("KO", 61.20, 62.10),
]
for ticker, start, end in records:
try:
period_return = end / start - 1
except ZeroDivisionError:
print(f"{ticker}: starting price missing, return skipped")
else:
print(f"{ticker}: {period_return:+.2%}")AAPL: +3.00% XOM: starting price missing, return skipped KO: +1.47%
FileNotFoundError
- Reading a file that does not exist is a common error in data work. The name may be misspelled, the file may be in another folder, or it may not have been created yet.
Note: We never created a file for AMZN, so reading it raises a FileNotFoundError.
# This will fail: there is no file named amzn_prices.csv
amzn_text = Path("amzn_prices.csv").read_text()
---------------------------------------------------------------------------
FileNotFoundError Traceback (most recent call last)
Cell In[41], line 2
1 # This will fail: there is no file named amzn_prices.csv
----> 2 amzn_text = Path("amzn_prices.csv").read_text()
File /usr/lib/python3.13/pathlib/_local.py:546, in Path.read_text(self, encoding, errors, newline)
543 # Call io.text_encoding() here to ensure any warning is raised at an
544 # appropriate stack level.
545 encoding = io.text_encoding(encoding)
--> 546 return PathBase.read_text(self, encoding, errors, newline)
File /usr/lib/python3.13/pathlib/_abc.py:632, in PathBase.read_text(self, encoding, errors, newline)
628 def read_text(self, encoding=None, errors=None, newline=None):
629 """
630 Open the file in text mode, read it, and close the file.
631 """
--> 632 with self.open(mode='r', encoding=encoding, errors=errors, newline=newline) as f:
633 return f.read()
File /usr/lib/python3.13/pathlib/_local.py:537, in Path.open(self, mode, buffering, encoding, errors, newline)
535 if "b" not in mode:
536 encoding = io.text_encoding(encoding)
--> 537 return io.open(self, mode, buffering, encoding, errors, newline)
FileNotFoundError: [Errno 2] No such file or directory: 'amzn_prices.csv'The last line of a traceback names the exception type, which is what goes after
except.Here, the
elseblock counts the trading days only if the file was read successfully.
amzn_path = Path("amzn_prices.csv")
try:
amzn_text = amzn_path.read_text()
except FileNotFoundError:
print(f"No price file at {amzn_path}. Check the name or create it first.")
else:
print(f"{amzn_path} has {len(amzn_text.splitlines()) - 1} trading days.")No price file at amzn_prices.csv. Check the name or create it first.
Skipping Errors Quietly with pass
Sometimes a missing file is expected and needs no message. For example, a nightly job may read the latest close for each ticker from its price file and skip tickers that have no file yet.
The
passstatement does nothing. In anexceptblock, it makes Python ignore the error and continue without a message.The cell below first creates a short price file for KO. AMZN still has no file, so it is left out of the result.
Use this with care, because a silent failure can hide a real problem. Always name the exception type, as in
except FileNotFoundError:. A bareexcept:would also catch errors you did not expect, including typos in your own code.
ko_data = "date,close\n2026-03-05,61.20\n2026-03-06,61.85\n"
Path("ko_prices.csv").write_text(ko_data)
latest_close = {}
for ticker in ["NVDA", "AMZN", "KO"]:
file_path = Path(f"{ticker.lower()}_prices.csv")
try:
price_lines = file_path.read_text().splitlines()
except FileNotFoundError:
pass
else:
latest_close[ticker] = float(price_lines[-1].split(",")[1])
print(latest_close){'NVDA': 126.3, 'KO': 61.85}
Saving Structured Data with json
Text files work well for simple records. Many programs also need to save structured data, such as a list of tickers or a dictionary of holdings, and load it back later in the same form.
JSON (JavaScript Object Notation) is a text format for this. The standard-library module
jsonconverts between Python objects and JSON text.json.dumps()("dump to string") converts a Python object into a JSON-formatted string, which we then save withwrite_text().
import json
watchlist = ["AAPL", "MSFT", "NVDA", "JPM", "XOM"]
watchlist_path = Path("watchlist.json")
watchlist_path.write_text(json.dumps(watchlist))
print(watchlist_path.read_text())["AAPL", "MSFT", "NVDA", "JPM", "XOM"]
json.loads()("load from string") does the reverse: it converts JSON text back into a Python object.The result is a Python list again, so indexing and loops work as usual.
loaded_watchlist = json.loads(watchlist_path.read_text())
print(loaded_watchlist)
print(type(loaded_watchlist), loaded_watchlist[2])['AAPL', 'MSFT', 'NVDA', 'JPM', 'XOM'] <class 'list'> NVDA
Saving and Restoring a Portfolio
jsonalso handles dictionaries and nested structures. The argumentindent=4spreads the output over several indented lines, which makes the file easier to read.jsoncannot save a class instance directly, so we first copy the attributes of eachPositionin thegrowthportfolio from Ch 9 into a plain dictionary.
holdings = {}
for ticker, position in growth.positions.items():
holdings[ticker] = {
"shares": position.shares,
"cost_basis": position.cost_basis,
}
portfolio_path = Path("portfolio.json")
portfolio_path.write_text(json.dumps(holdings, indent=4))
print(portfolio_path.read_text()){
"NVDA": {
"shares": 20,
"cost_basis": 110.0
},
"AMZN": {
"shares": 15,
"cost_basis": 180.0
},
"MSFT": {
"shares": 10,
"cost_basis": 400.0
}
}
- Loading the file returns the nested dictionary, and passing its contents to
buy()rebuilds an equivalentPortfolio. This also works in a later session, as long as thePortfolioclass is defined or imported first.
saved_holdings = json.loads(portfolio_path.read_text())
restored = Portfolio("Growth (restored)")
for ticker, info in saved_holdings.items():
restored.buy(ticker, info["shares"], info["cost_basis"])
restored.report(prices_today)Portfolio: Growth (restored) NVDA: value $2,500.00, gain $300.00 AMZN: value $2,925.00, gain $225.00 MSFT: value $4,100.00, gain $100.00 Total value: $9,525.00
Strengths and Limits of JSON
A JSON file is plain text, so you can open it and check it by eye.
Almost every programming language can read and write JSON, and most web data services, including financial data APIs, send their results in it.
JSON objects become Python dictionaries and JSON arrays become lists, so the data need little conversion.
JSON can store only basic types: strings, numbers,
True/False,None, lists, and dictionaries. A tuple comes back as a list, and dictionary keys always come back as strings, as the cell below shows.
coupon_dates = {2026: ("Mar 20", "Sep 20")}
coupon_text = json.dumps(coupon_dates)
print(coupon_text)
print(json.loads(coupon_text)){"2026": ["Mar 20", "Sep 20"]}
{'2026': ['Mar 20', 'Sep 20']}
Loading or Creating a Saved Watchlist
A common pattern combines
jsonwith exceptions: try to load saved data, and if the file does not exist yet (for example, on the first run), start from a default value and save it.The first time the cell below runs,
my_watchlist.jsondoes not exist, so theexceptblock creates it. If you run the cell again, theelseblock loads the saved list instead.
my_path = Path("my_watchlist.json")
try:
my_watchlist = json.loads(my_path.read_text())
except FileNotFoundError:
my_watchlist = ["AAPL", "MSFT"]
my_path.write_text(json.dumps(my_watchlist))
print(f"No saved watchlist found; created one with {my_watchlist}")
else:
print(f"Loaded saved watchlist: {my_watchlist}")No saved watchlist found; created one with ['AAPL', 'MSFT']
Refactoring into Functions
Refactoring means restructuring working code without changing what it does, usually by splitting it into smaller functions with one job each.
Below, the logic from the previous cell is split into three functions:
load_watchlist()reads the file (or returns an empty list),save_watchlist()writes it, andadd_ticker()uses the other two.Each function is short and can be tested on its own. Because the list is saved to a file after each change, it is still there after you restart the Python session.
def load_watchlist(path):
"""Return the saved watchlist, or an empty list if there is none yet."""
try:
contents = path.read_text()
except FileNotFoundError:
return []
else:
return json.loads(contents)
def save_watchlist(path, watchlist):
"""Save the watchlist to path as JSON."""
path.write_text(json.dumps(watchlist))
def add_ticker(path, ticker):
"""Add ticker to the saved watchlist unless it is already there."""
watchlist = load_watchlist(path)
if ticker not in watchlist:
watchlist.append(ticker)
save_watchlist(path, watchlist)
return watchlist- On a first run of this notebook,
my_watchlist.jsonholds["AAPL", "MSFT"]at this point, saved by the load-or-create example above. Adding NVDA and then KO extends the saved list; adding NVDA a second time leaves it unchanged.
print(add_ticker(my_path, "NVDA"))
print(add_ticker(my_path, "KO"))
print(add_ticker(my_path, "NVDA"))['AAPL', 'MSFT', 'NVDA'] ['AAPL', 'MSFT', 'NVDA', 'KO'] ['AAPL', 'MSFT', 'NVDA', 'KO']
In-Class Exercise
10-1. Reading a Price File:
- Use
Path.write_text()to createmsft_prices.csvwith a header line and six days of made-up closing prices. - Read it back, convert the prices to a list of floats, and print the highest close, the lowest close, and the return over the six days.
10-2. A Dividend Log:
- Starting from a list of tuples such as
("KO", "2026-04-01", 0.51)(ticker, payment date, dividend per share), write one line per payment todividends.txt. - Append one more payment with
path.open("a"), then read the file back and compute the total dividends received on 100 shares of each stock.
10-3. Safe Returns:
- Write a function
simple_return(start, end)that usestry-except-elseto returnend / start - 1, or prints a message and returnsNonewhenstartis zero. - Test it on the pairs
(50.0, 52.5),(0.0, 30.0), and(80.0, 76.0).
10-4. A Robust Price Loader:
- Write a function
load_closes(path)that returns the closing prices in a price file as a list of floats, or an empty list if the file doesn't exist. - Use it to print the number of prices and the last close for
nvda_prices.csv,ko_prices.csv, andtsla_prices.csv.
10-5. Saving Portfolio Weights:
- Save the dictionary
{"AAPL": 0.40, "JPM": 0.35, "XOM": 0.25}toweights.jsonwithjson.dumps(). - Write a function
load_weights(path)that returns the saved dictionary, or an empty dictionary if the file doesn't exist. - Load the weights and check that they sum to 1.