DS 208 · Week 4

Modular Programming

Keeping a program readable once it outgrows a single notebook cell — with classes, error handling, and files that import each other.

Programming for Data Science · University of the Philippines Cebu

Session Map

Three tools for bigger programs

Where We Left Off

You can load data — now shape the code

Last week: lists, dictionaries, and reading files. That fills a notebook fast. Structure is what stops it turning into a wall of tangled cells.

The problem

Copy-pasted logic and mile-long cells become impossible to fix or trust.

The fix

Group related data and behaviour, guard against bad input, and split into files.

Words for Today · 1 of 2

Seven words for Part A: classes

class

A blueprint (think: cookie cutter) that describes what one kind of thing holds and what it can do.

e.g. class Student: starts the blueprint for a student

object (instance)

One actual thing built from a class — one cookie from the cutter. “Instance” means the same thing.

e.g. ana = Student("2024-001", "Ana") makes one student object

attribute

A value stored on an object. You read it with a dot and no brackets.

e.g. ana.name gives 'Ana'

method

A function that lives inside a class and works on that object’s own data. Called with a dot and ( ).

e.g. ana.average() works out Ana’s mean grade

self

Inside a class, the name for “this particular object”. Python fills it in for you.

e.g. self.name = name stores the name on this object

__init__

The set-up method Python runs by itself every time you create a new object. Said “dunder init” (double underscore).

e.g. Student("2024-001", "Ana") quietly runs __init__

inheritance

Making a new class from an existing one: it gets everything the parent class has, then adds or changes a little.

e.g. class DiscountedSale(Sale): is a Sale, with a discount

Words for Today · 2 of 2

Six words for Parts B and C: errors and files

exception

Python’s report that something went wrong while running. It stops the program unless your code handles it.

e.g. ValueError, KeyError, IndexError

traceback

The error printout. It shows the route Python took; the last line names the error type and message.

e.g. last line ValueError: invalid literal for int()

try / except

Try a risky line; if a named exception happens, run the except block instead of crashing.

e.g. try: int(raw) … except ValueError: score = 0

raise

Stop on purpose and report an exception yourself, with a message you write.

e.g. raise ValueError("out of range: 130")

module

A .py file of code that other files can import (bring in) and reuse.

e.g. grades.py, then from grades import average

package

A folder of modules that Python can import from, marked by a file called __init__.py.

e.g. a src/ folder holding load.py and clean.py

Part A

Classes & Objects

Data and behaviour, bundled into one thing.

You already know that a function groups steps (week 2) and a dictionary groups values (week 3). A class does both at once: it keeps the values and the functions that use them together.

The Motivation

Related values want to travel together

A student has a number, a name, and grades. Passing three separate variables (named boxes) everywhere is fragile. A class is a blueprint for one kind of thing. Each thing built from it is an object (also called an instance) that keeps those values together — with its own methods (functions that belong to it).

A class is a cookie cutter; each object is one cookie. You make the cutter once, and every cookie has the same shape but its own toppings — Ana’s grades are not Ben’s grades.

A function

Groups behaviour: give it inputs, get an output.

A class

Groups data + behaviour: the values and the actions that belong with them.

The Blueprint

__init__ sets up each new object

__init__ (“dunder init”) is the set-up method: it runs by itself when you create an object. self is this particular object, and an attribute is a value stored on it with a dot (self.name). The attributes you set are that object’s own.

class Student: def __init__(self, sid, name): self.sid = sid self.name = name self.grades = [] s = Student("2024-001", "Ana") s.name # "Ana"
  • class Student:start a blueprint called Student (class names start with a capital letter by habit)
  • def __init__(self, sid, name):the set-up method; sid and name are the two inputs you will give
  • self.sid = sidstore the input on this object as an attribute called sid (same for name)
  • self.grades = []every new student starts with their own empty list of grades
  • s = Student("2024-001", "Ana")make one object; Python runs __init__ with self = the new student
  • s.nameread an attribute with a dot: "Ana" is the name stored on s
Behaviour That Belongs Inside

A method is a function that uses self

A method is a function written inside a class. Its first parameter is self, so it can reach the object’s own attributes. Because the grades live on the object, the object can compute its own average. No need to pass the data back in — it is already there.

class Student: # ...__init__ as before... def average(self): return sum(self.grades) / len(self.grades) s.grades = [88, 92, 79] s.average() # 86.33
  • # ...__init__ as before...the set-up code from the previous slide is still there; it is just not repeated
  • def average(self):a method: no extra inputs, only self — the student it is asked about
  • sum(self.grades) / len(self.grades)add this student’s grades and divide by how many there are
  • s.average()call the method with a dot and ( ); 86.33 is Ana’s mean, 259 ÷ 3 = 86.333… shown rounded
The Mental Model

One blueprint, many objects

class Student the blueprint Ana · [88, 92, 79] avg 86.33 Ben · [70, 75] avg 72.5 Cy · [95, 91, 100] avg 95.33 Each object carries its own data.
Building One

__init__ runs the moment you make an object

It is not magic and it is not a constructor in the C++ sense (if you have never met C++, ignore that: it simply means __init__ is ordinary Python). It is a plain method Python calls for you, with the new object as self.

Making an object is like filling in a new form. Sale("kape", 15, 3) hands Python three answers, and __init__ copies each one onto the new form, which it calls self.

class Sale: def __init__(self, item, price, qty): self.item = item # attach to THIS object self.price = price self.qty = qty s = Sale("kape", 15, 3) print(s.item, s.price) # kape 15
  • self.item = itemcopy the input item onto this new object (“attach to THIS object”)
  • s = Sale("kape", 15, 3)create one Sale: kape (coffee), price 15, quantity 3
  • print(s.item, s.price)print two attributes; kape 15 is the two values with a space between them

self is just the first argument

You never pass it. Sale("kape", 15, 3) becomes __init__(s, "kape", 15, 3) behind the scenes.

Two Kinds Of Thing

Attributes hold; methods do

An attribute is data the object carries. A method is a function that can reach that data through self.

An attribute is a noun — what the sale has (a price). A method is a verb — what the sale can do (work out its total).

class Sale: def __init__(self, item, price, qty): self.item, self.price, self.qty = item, price, qty def total(self): # a method return self.price * self.qty s = Sale("kape", 15, 3) print(s.price) # attribute — no () # 15 print(s.total()) # method — needs () # 45
  • self.item, self.price, self.qty = item, price, qtyset three attributes in one line: each name on the left gets the value in the same place on the right
  • def total(self):a method that works out price × quantity for this sale
  • s.pricean attribute is stored data, so no brackets: 15
  • s.total()a method is an action, so it needs ( ) to run: 15 × 3 = 45
The Ugly Default

Why your object prints as gibberish

Print an object you wrote and Python falls back to its type and memory address. Useless in a loop, useless in a debugger.

New words

memory address
where the object happens to sit in the computer’s memory (0x103400e00); tells you nothing about the data
debugger
a tool that pauses a program so you can look at its values (you meet it in a later week)
__repr__
a special method that returns the text Python shows for your object
__str__
its sibling: print() prefers it when a class has both (the lab uses it)
f-string
text with f before the quote; each {name} inside is replaced by its value: f"{2+3} cups" → '5 cups'
print(s) <__main__.Sale object at 0x103400e00> # add one method: def __repr__(self): return f"Sale({self.item!r}, {self.price}, {self.qty})" print(s) Sale('kape', 15, 3)
  • <__main__.Sale object at 0x…>the default: a Sale object from your main program, at some memory address
  • def __repr__(self):indented, so it goes inside class Sale; Python calls it whenever it has to show the object
  • {self.item!r}fill in the item; !r shows it the way you would type it, in quotes: 'kape'
  • Sale('kape', 15, 3)now printing shows what is inside the object

It fixes lists too

print([s1, s2]) now shows both sales instead of two addresses — which is when you actually need it.

Less Typing

@dataclass writes the boilerplate

If a class is mostly "hold these three fields", Python will generate __init__, __repr__ and == for you.

New words

boilerplate
set-up code you write the same way every time
decorator
a line starting with @ just above a class or function; it adds features to what is below
dataclass
a class whose __init__, __repr__ and == Python writes for you
field
one attribute listed in a dataclass, with the type it should hold
from dataclasses import dataclass @dataclass class Sale: item: str price: int qty: int def total(self): return self.price * self.qty print(Sale("kape", 15, 3)) Sale(item='kape', price=15, qty=3) print(Sale("kape",15,3) == Sale("kape",15,3)) True
  • from dataclasses import dataclassbring the dataclass tool in from Python’s built-in dataclasses module
  • @dataclassthe decorator: it rewrites the class below, adding set-up, printing and comparing
  • item: stra field called item that should hold a string (a hint: Python does not check it)
  • Sale(item='kape', price=15, qty=3)the automatic __repr__: every field with its value
  • True== now compares the fields, so two sales with equal fields count as equal

Equality for free

Two plain objects with identical fields are not equal by default — Python compares identity. A dataclass compares the fields.

Your Turn · 8 min

Write the class

In a notebook, build it from scratch. Three fields, one method, one repr.

Words in these steps

field
one of the attributes listed in a dataclass (item, price, qty)
repr
the printed form of an object, from __repr__ (two slides back)
comprehension
a one-line loop that builds a result, e.g. [x * 2 for x in [1, 2, 3]] gives [2, 4, 6]

Try it now

  1. Make a Sale dataclass with item, price, qty.
  2. Add total() returning price × qty.
  3. Build three sales in a list; print the list.
  4. Sum every total with a comprehension.

Check yourself

A dataclass prints its fields, so the list is readable without writing __repr__ yourself.

Reuse

Inheritance: a specialised kind of thing

Inheritance builds a new class on an existing one. A discounted sale is a sale. Subclass it, override only what differs, and call super() for the rest.

New words

parent / subclass
the existing class (Sale) and the new one built on it (DiscountedSale)
override
write a method with the same name in the subclass, so it replaces the parent’s version
super()
“the parent class”: use it to run the parent’s version of a method
composition
the alternative: store one object as an attribute of another, instead of inheriting

Inheritance is a recipe variation: “chocolate cake” starts from the plain cake recipe and only changes the flavour. It passes the is-a test: a chocolate cake is a cake.

class DiscountedSale(Sale): def __init__(self, item, price, qty, pct): super().__init__(item, price, qty) self.pct = pct def total(self): return round(super().total() * (1 - self.pct/100)) print(Sale("kape", 15, 4)) Sale('kape', total=60) print(DiscountedSale("kape", 15, 4, 25)) DiscountedSale('kape', total=45)
  • class DiscountedSale(Sale):a new class built on Sale (the parent goes in the brackets); it gets all of Sale’s methods
  • super().__init__(item, price, qty)let the parent’s set-up store the three usual attributes…
  • self.pct = pct…then add the one extra attribute only a discounted sale has
  • def total(self):override: this total replaces the parent’s for discounted sales
  • super().total() * (1 - self.pct/100)the parent’s total (15 × 4 = 60) with 25% off = 45
  • Sale('kape', total=60)printed by a __repr__ on Sale (not shown) that writes the class name and total()

Use it sparingly

Deep hierarchies are hard to follow. If the child does not genuinely pass an is-a test, prefer composition — hold the object instead.

Trap One

A class attribute is shared by every object

Put a mutable default on the class and all instances point at the same list. One cart fills another.

New words

mutable
can be changed in place after it is made: lists, dicts and sets are; numbers and strings are not
class attribute
a value written directly under class: one copy, shared by every object
instance attribute
a value set on self in __init__: each object gets its own
class Cart: items = [] # ONE list for everyone def add(self, x): self.items.append(x) a, b = Cart(), Cart() a.add("kape") print(a.items, b.items) ['kape'] ['kape'] # b was never touched # fix: give each object its own def __init__(self): self.items = [] ['kape'] []
  • items = []directly under class: made once, shared by every Cart
  • self.items.append(x)the object has no list of its own, so this adds to the shared one
  • ['kape'] ['kape']a and b print the same list: b “has” kape though you only added it to a
  • def __init__(self): self.items = []the fix: each new Cart gets its own empty list, so b stays []

How to spot it

Assignments directly under class run once, when the class is defined. Assignments in __init__ run per object.

Trap Two

A default argument is evaluated once, ever

The empty list in the signature is created when the def line runs — not on each call. It then persists between calls.

New words

signature
the def line: the function’s name and its parameters
default argument
a value a parameter uses when the caller gives none (week 2): basket=[]
None
Python’s value for “nothing here”; test for it with is None
def add_sale(sale, basket=[]): # evaluated ONCE basket.append(sale) return basket print(add_sale("kape")) ['kape'] print(add_sale("load")) ['kape', 'load'] # the old one is still there # the fix, every time def add_sale(sale, basket=None): if basket is None: basket = [] ['kape'] then ['load']
  • basket=[]the [] is made once, when def runs, and reused by every call
  • add_sale("load")the second call gets that same list, still holding kape: ['kape', 'load']
  • if basket is None: basket = []the fix: no basket given → build a fresh list inside, on every call

The rule

Never use a list, dict or set as a default. Use None and build it inside — that runs on every call.

Break — 10 minutes

Next: when things go wrong, and how Python tells you.

Part B

Errors & Exceptions

Real data breaks assumptions — plan for it.

You met your first traceback in week 1 and wrote if checks in week 2. This part adds how to read an error calmly, catch the ones you expect, and raise your own.

Read The Traceback

An error is a message, not a scolding

When Python can't continue, it raises an exception — a report of what went wrong — and prints a traceback, the error printout. The last line names the problem; read it bottom-up.

An exception is like a smoke alarm: annoying, but it tells you something real, and the label on it (IndexError, ValueError) says which room to check.

nums = [10, 20] nums[5] IndexError: list index out of range int("ninety") ValueError: invalid literal for int()
  • IndexError: list index out of rangeread it as type: message. IndexError = asked for a position that does not exist; nums only has positions 0 and 1
  • ValueError: invalid literal for int()ValueError = the right type (text) but a value int() cannot turn into a whole number
Read It

A traceback is read bottom-up

Traceback (most recent call last): File "sales.py", line 8, in <module> print(total(rows)) File "sales.py", line 5, in total return sum(load_price(r) for r ...) File "sales.py", line 2, in load_price return int(row["price"]) ValueError: invalid literal for int() with base 10: 'n/a'

Start at the bottom

The last line is what actually broke. The lines above are the route Python took to get there — newest call last.

Here: load_price hit the string "n/a". That is your bug, on line 2.

  • Traceback (most recent call last):the heading: the calls (functions running because another line asked) are listed oldest first, newest last
  • File "sales.py", line 8, in <module>where it started: line 8 of sales.py, at the top level of the file (<module>)
  • File "sales.py", line 2, in load_pricethe deepest call: line 2, inside the function load_price — check this line first
  • ValueError: … 'n/a'the error type and the bad value: the text 'n/a' cannot become a whole number (base 10 = ordinary digits)
Recognise Them

Seven exceptions, each triggered for real

ExceptionWhat Python saidUsually means
ValueErrorinvalid literal for int() with base 10: 'n/a'Right type, impossible value
TypeErrorcan only concatenate str (not "int") to strWrong type entirely (joining, or “concatenating”, text with a number)
KeyError'price'That key is not in the dict
IndexErrorlist index out of rangePast the end of the list
FileNotFoundError[Errno 2] No such file or directoryWrong path, or wrong working dir (the folder Python is running in)
ZeroDivisionErrordivision by zeroAn empty group you averaged
AttributeError'str' object has no attribute 'push'Wrong method for that type

The exception name is the diagnosis and the message is the symptom. KeyError: 'price' reads “I looked for the key price and it was not there.”

Catch And Cope

try the risky line, except the fallout

Wrap the line that might fail in a try block. If it raises the exception you named, the except block runs instead of crashing.

try/except is a safety net under one trick: if the trick fails in the way you expected, you land softly and the show goes on.

raw = input("Score: ") # user types "ninety" try: score = int(raw) except ValueError: print("Not a number — using 0") score = 0
  • raw = input("Score: ")input() shows the prompt and waits for typing; it always gives back text. Here the user typed "ninety"
  • try:attempt the indented line(s) below
  • score = int(raw)the risky line: int("ninety") raises a ValueError
  • except ValueError:if exactly that error happens, jump here instead of crashing
  • score = 0the fallback (a sensible replacement): the program carries on with 0
All Four Blocks

else and finally earn their place

Most people learn try/except and stop. The other two say when code runs, which is the part that prevents bugs: else runs only when nothing went wrong, finally runs every time.

try: n = int(v) except ValueError: print(f"{v!r} is not a number") else: print(f"parsed {n}") # only if nothing raised finally: print("runs either way") # parse("15") parsed 15 runs either way # parse("n/a") 'n/a' is not a number runs either way
  • # parse("15")picture these lines inside a function parse(v); v is the text to convert
  • else:good input: try worked, so else prints parsed 15
  • finally:runs in both cases — the place for clean-up
  • {v!r}bad input: except runs, and !r keeps the quotes so you can see it was text

Why bother with else

Code in try that cannot raise still gets its exceptions caught by accident. else keeps the guarded part honest.

The Sin

A bare except hides the thing you needed

A bare except is except: with no error name. It catches everything — including typos in your own code, and Ctrl-C (the keys that stop a running program). You get "something went wrong" and no way to find out what.

# never do this try: n = int(v) except: print("something went wrong") something went wrong # do this — name what you expect except ValueError as e: print(f"bad number: {e}") bad number: invalid literal for int()… 'n/a'
  • except:nothing after it: catches every error, even a misspelled variable name
  • something went wrongthe output says nothing about what or where
  • except ValueError as e:catch only ValueError, and give the caught error the name e
  • {e}printing e shows Python’s own message, so you still see the bad value 'n/a'

Catch narrow, let the rest fly

An exception you did not predict should crash loudly in development. Silencing it just moves the failure somewhere harder to find.

The Ones You'll Meet

Six exceptions worth recognising

ExceptionHappens whenExample
ValueErrorright type, wrong valueint("abc")
KeyErrordict key missingpop["Manila"]
IndexErrorlist position too bignums[99]
FileNotFoundErrorfile isn't thereopen("nope.txt")
ZeroDivisionErrordividing by zerototal / 0
TypeErrorwrong type entirely"a" + 1
Fail Early, Fail Clearly

Raise an error on bad input

Don't let a nonsense value slip downstream. Check it at the door and raise a ValueError with a message that says what went wrong. To raise is to stop on purpose and report an exception yourself.

def set_grade(g): if not 0 <= g <= 100: raise ValueError(f"out of range: {g}") return g set_grade(130) ValueError: out of range: 130
  • if not 0 <= g <= 100:“if g is not between 0 and 100” — Python allows chained comparisons like in maths
  • raise ValueError(f"out of range: {g}")stop here and report a ValueError with our own message
  • return gonly reached when the grade is fine
  • ValueError: out of range: 130the last line of the traceback (only that line is shown): the type, then our message
Your Own

Custom exceptions name your domain

A custom exception is your own error type: a class that inherits from a built-in one. Subclass the closest built-in. Callers can then catch your failure specifically, or fall back to the general one.

class PriceError(ValueError): """Raised when a price cannot be trusted.""" def check(p): if p < 0: raise PriceError(f"price cannot be negative: {p}") return p try: check(-5) except PriceError as e: print(e) price cannot be negative: -5
  • class PriceError(ValueError):a new kind of error, built on ValueError — Part A’s inheritance, used for errors
  • """Raised when…"""a docstring: the class’s one-line description; the class needs no other code
  • raise PriceError(f"…{p}")raise your own type, with a message that includes the bad price
  • except PriceError as e: print(e)catch it by name; printing e gives price cannot be negative: -5

Inherit deliberately

Because PriceError subclasses ValueError, code that only knows about ValueError still works.

Cleanup

with closes things even when you crash

A context manager is anything you can use with with: it guarantees the cleanup runs — no finally, no forgotten .close().

with is a library book with automatic return: when you leave the indented block, the book goes back — even if you tripped on the way out.

with open("sales.csv") as f: rows = f.readlines() # file is closed here, even if readlines() raised print(f.closed) True
  • with open("sales.csv") as f:open the file and call it f for the indented block; with promises to close it afterwards
  • rows = f.readlines()read every line of the file into a list (week 3)
  • print(f.closed)outside the block: True means the file is already closed

You will meet it again

Database connections, locks, timers and matplotlib figures all use the same pattern. Learn it once here.

A Judgement Call

Handle what you can fix; surface the rest

Your Turn · 6 min

Break the code, read the message

Errors are easier to learn by causing them. Run each, and name the exception before you look.

Try it now

  1. int("12.5")
  2. [1,2,3][3]
  3. {"a":1}["b"]
  4. open("nope.csv")

Check yourself

ValueError · IndexError · KeyError · FileNotFoundError. The last one names the file it could not find: check the spelling, and the working directory (the folder Python is running in).

Quick Check

Tap to reveal

Your script dies with KeyError: 'price'. Where do you look first?

A · The top line of the traceback
B · The bottom line, then the deepest file shown
C · The middle of the traceback
D · Rerun and hope

B — bottom-up.

The final line names the failure; the file directly above it is where the bad code lives. The top of a traceback is just where your program started.

Quick Check

Tap to reveal

Reading ages["Rizal"] from a dict that has no "Rizal" key raises which exception?

A · ValueError
B · IndexError
C · KeyError
D · TypeError

C — KeyError.

Missing keys raise KeyError; missing list positions raise IndexError. Use .get() when a gap is expected.

Break

  Five minutes

Back to give all this code a tidy home across files.

Part C

Organizing Code

From one long notebook to files that import each other.

You can already write functions and classes in one notebook. This part moves them into files (modules) that any notebook can import — recipes kept in a shared binder instead of on sticky notes.

A Module Is Just A File

Move reusable code into a .py

A module is a .py file of Python code. Put your functions in grades.py. Any notebook or script can then import them (bring them in by name) — one definition, used everywhere, fixed in one place.

A module is a recipe card in a shared binder: write it once, every cook (notebook) can use it, and fixing a mistake on the card fixes it for everyone.

# grades.py def average(scores): return sum(scores) / len(scores) # analysis.ipynb from grades import average average([88, 92]) # 90.0
  • # grades.pya plain file called grades.py, saved in the same folder as the notebook: this is the module
  • # analysis.ipynbyour Jupyter notebook (.ipynb), the file you work in
  • from grades import averagefind grades.py and bring the name average into this notebook
  • average([88, 92])works as if written here; 90.0 is a float because / always gives a decimal
Three Ways To Import

Pick the one that reads clearly

FormYou then writeUse when
import gradesgrades.average(x)you want the source obvious
from grades import averageaverage(x)you use one thing a lot
import numpy as npnp.mean(x)the community's short alias
Imports

Three ways in, and what each costs

All three work. The middle one is what most real code uses, because the reader can see where a name came from.

# 1 — clear, slightly verbose import sales sales.total(rows) # 2 — the usual choice from sales import total total(rows) # 3 — avoid: where did `total` come from? from sales import *

The star import problem

It can silently overwrite a name you already had, and a reader cannot tell which module a function belongs to.

The Guard

What if __name__ == "__main__" is for

A file has two lives: something you run, and something you import. This line separates them.

New word

__name__
a variable Python fills in for every file: "__main__" when you run that file directly, its own name ("sales") when another file imports it

A file can be run like a program or borrowed like a toolbox. The guard marks the lines that should happen only when it is run as a program.

# sales.py def total(rows): ... if __name__ == "__main__": # only when you run `python sales.py` print(total(load("sales.csv"))) # elsewhere: from sales import total # does NOT run the print
  • def total(rows): ...the ... just means “body left out to save space”
  • if __name__ == "__main__":true only when you type python sales.py to run this file itself
  • from sales import totalimporting sets __name__ to "sales", so the indented print is skipped

Without it

Importing the module would execute your test code, read files and print output — every time, from anywhere.

A Place For Everything

A layout a stranger could navigate

Separate raw data, code, and notebooks. A README says what the project is; requirements.txt lists what to install.

New words

folder / directory
two names for the same thing; data/ ends in / to show it is a folder
README.md
a text file explaining the project, the first thing a visitor reads (.md = Markdown, plain text with simple formatting)
requirements.txt
the list of libraries to install, one per line, e.g. pandas
project/ data/ # raw + processed src/ grades.py # reusable code notebooks/ analysis.ipynb README.md requirements.txt
Layout

Where a stranger would look

sari_project/ data/ raw/ # never edited by hand processed/ # written by your code src/ __init__.py load.py # reading files/APIs clean.py # one job per module analyse.py notebooks/ 01-explore.ipynb tests/ README.md requirements.txt

Raw data is read-only

If a cleaning step is wrong you rerun it. If you edited the raw file by hand, that is gone forever.

Notebooks explore, modules do

Anything you need twice moves into src/. A notebook is for looking, not for the logic you depend on.

README first

What this is, how to run it. Write it on day one, while you still remember.

New words

package
src/ is a package: a folder of modules. The (often empty) __init__.py file tells Python it may import from it
tests/
code that checks your code still gives the right answers
API
a way for your code to ask a website or program for data (a later week)
Script Or Import?

The __main__ guard

Code under if __name__ == "__main__": runs only when the file is launched directly — not when another file imports it. So importing never triggers side effects.

# grades.py def main(): print("run directly") if __name__ == "__main__": main()
  • def main():a function holding what the script should do when run
  • if __name__ == "__main__":call main() only when grades.py is run directly; importing it runs nothing
Quick Check

Tap to reveal

After from stats import mean, how do you call it?

A · stats.mean(x)
B · mean(x)
C · import mean(x)
D · stats(mean, x)

B — mean(x).

The from ... import name form pulls the name in directly, so you use it bare. You'd write stats.mean(x) only after a plain import stats.

This Week's Lab

Build a class, guard it, split it out

You'll write a small Student class, add methods, make it print readably, build a subclass, catch a bad value with try/except, and raise a clear error on a bad grade. ~45 minutes, all in the browser.

You'll practise

__init__, self, methods, __str__, inheritance, try/except/finally, raise, and a custom exception.

Stretch, if you want

Move your Student class into student.py in a real notebook and import it back.

Recap

Bundle, guard, organise

Carry Out

Four things from today

1 · A class bundles data with behaviour

Attributes hold, methods do, __init__ sets up. Add __repr__ or use @dataclass so it prints.

2 · Read tracebacks bottom-up

Last line = what broke. The file above it = where. That alone solves most bugs you will hit this term.

3 · Catch narrow, never bare

Name the exception you expect. Let the ones you did not predict crash loudly while you can still find them.

4 · Reusable code leaves the notebook

Into a module, behind if __name__ == "__main__", in a layout someone else could navigate.

Glossary Recap

Every new word from today, one line each

Part A · Classes

class
a blueprint for one kind of thing: what it holds and does
object / instance
one thing built from a class — one cookie from the cutter
attribute
a value stored on an object: ana.name, no brackets
method
a function inside a class, called with brackets: ana.average()
self
inside a class, “this particular object”
__init__
set-up method Python runs automatically when an object is made
__repr__ / __str__
methods that give the text shown when an object is printed
f-string
f"…" text where each {name} is replaced by its value
@dataclass
decorator that writes __init__, __repr__ and == for you
inheritance
building a subclass on a parent class; it gets all the parent’s methods
override / super()
replace a parent’s method / call the parent’s version
mutable
can be changed in place: lists, dicts, sets
comprehension
a one-line loop that builds a result: [x * 2 for x in nums]

Parts B & C · Errors and files

exception
Python’s report that something went wrong; stops the program unless handled
traceback
the error printout; read the last line first, then its line number
try / except
attempt risky code; run the except block if that named error happens
else / finally
else: only when no error; finally: always, for clean-up
bare except
except: with no name — catches everything, hides bugs
raise
stop on purpose and report an exception with your own message
custom exception
your own error class, built on a built-in one like ValueError
with
opens something and guarantees it is closed afterwards
module
a .py file whose code other files import
import / alias
bring names in from a module / a nickname given with as
package
a folder of modules, marked by an __init__.py file
__main__ guard
if __name__ == "__main__": — runs only when the file is run directly
README / requirements.txt
what the project is / which libraries to install
Before Next Week

Practice & reading

Next Week

Scientific Computing with NumPy

Arrays that do maths on thousands of numbers at once — faster and shorter than any loop you'd write by hand.

DS 208 · Programming for Data Science