What’s one thing you learned? What’s still confusing?
Mini-Project: Todo App
Build a CLI todo app with file persistence — your first program that remembers state between runs.
Modules, Packages & pip
Import modules, install packages, create virtual environments, and structure projects.
Environment Variables & Secrets: Never Hardcode API Keys
Keep API keys out of code using os.environ, .env files, and python-dotenv.
Interactive Labs for This Track
Loop Visualizer
You're a factory robot repeating the same task on an assembly line — watch how loops automate repetitive work
List Slicing
You have a playlist of 50 songs — grab just tracks 10 through 20 with a single slice expression
Sorting Algorithms
You're organizing a library of 10,000 books — which sorting method is fastest?
Ask questions, share insights
.csv, .parquet, or .jsonl on disk. Every fine-tuning dataset at OpenAI is .jsonl. Reading and writing files cleanly — with context managers and try/except — is the difference between code that handles a corrupted row gracefully and code that crashes your overnight training job at 3 AM.What does this code print? try: x = int('abc') except ValueError: print('Bad value') finally: print('Done')
open() function opens a file and returns a file object.# Basic file reading
file = open("data.txt", "r") # "r" = read mode (default)
content = file.read() # read the entire file as one string
print(content)
file.close() # ALWAYS close when doneopen() and close(), the file never gets closed. This can corrupt data and leak system resources.with Statement (Always Use This)# The safe way -- with statement handles closing automatically
with open("data.txt", "r") as file:
content = file.read()
print(content)
# file is automatically closed here, even if an error occurswith statement is called a context manager. It guarantees the file is closed when the block ends, no matter what happens. Always use with for file operations.with Actually Works (the __enter__ / __exit__ Protocol)with cm() as x: is equivalent to a try / finally: Python first calls cm.__enter__() (which sets things up and returns whatever you bind to x), then runs the body, then ALWAYS calls cm.__exit__() — even when the body raises an exception. That's the whole point. The animation below walks through five scenarios so you can see it move:__exit__ STILL runs and closes the file before the exception propagates up. This is the killer feature.__exit__ returns True: the exit hook swallows the exception entirely and execution continues past the with block. (Powerful but rare — usually you let exceptions through.)with: two stacked blocks; cleanup is LIFO — the innermost resource is released first, like a call stack popping.@contextmanager: the generator-based shortcut. Code before yield is __enter__, code in the finally after yield is __exit__.with is not a syntactic nicety — it's an exception-safe cleanup contract baked into the language. Anything that owns a resource (a file, a network socket, a database transaction, a lock) should expose itself as a context manager so callers can use it inside with and never leak.# Read entire file as one string
with open("data.txt", "r") as f:
all_text = f.read()
# Read all lines into a list
with open("data.txt", "r") as f:
lines = f.readlines() # ["line 1\n", "line 2\n", ...]
# Read line by line (memory-efficient for large files)
with open("data.txt", "r") as f:
for line in f:
print(line.strip()) # .strip() removes the trailing newline.read() would consume all your RAM.A file contains: hello world What does this print? with open("data.txt") as f: for line in f: print(line)
line includes the trailing \n newline character. Then print() adds ITS OWN newline. So you get the line text, then \n from the file, then \n from print — double-spacing! The fix: print(line.rstrip()) to strip the trailing newline, or print(line, end="") to tell print not to add its own. This is the #1 surprise when reading files line by line.HitFileNotFoundError,PermissionError, orIsADirectoryError? These are subclasses ofOSErrorand crashopen()when the path is wrong, the file is locked, or you pointed at a folder. Wrapopen()intry/except— see the error decoder for the full catalog andImportError-style import crashes too.
# Write mode ("w") -- creates file or OVERWRITES existing content
with open("output.txt", "w") as f:
f.write("First line\n")
f.write("Second line\n")
# Append mode ("a") -- adds to the end without erasing
with open("output.txt", "a") as f:
f.write("Third line (appended)\n")
# Write multiple lines at once
lines = ["Alice,95\n", "Bob,87\n", "Charlie,92\n"]
with open("grades.csv", "w") as f:
f.writelines(lines)| Mode | Description | Creates file? | Erases existing? |
|---|---|---|---|
"r" | Read only | No | No |
"w" | Write (overwrite) | Yes | Yes |
"a" | Append | Yes | No |
"r+" | Read and write | No | No |
"x" |
What happens if you open an existing file with mode 'w' and write one line?
"w" completely overwrites the file. This is a common source of accidental data loss. If you want to add to a file, use append mode "a" instead.A very common task is reading CSV-like data from files or strings:
# Parse CSV data without any external library
csv_data = """name,age,score
Alice,17,95
Bob,16,87
Charlie,18,92
Diana,17,78
Eve,16,99"""
# Split into lines, then split each line by comma
lines = csv_data.strip().split("\n")
header = lines[0].split(",")
students = []
for line in lines[1:]:
values = line.split(",")
student = {
"name": values[0],
"age": int(values[1]),
"score": int(values[2]),
}
students.append(student)
# Now we can work with structured data
for s in students:
status = "PASS" if s["score"] >= 80 else "FAIL"
print(f"{s['name']:>10} (age {s['age']}): {s['score']} - {status}")
# Filter and analyze
top_students = [s for s in students if s["score"] >= 90]
avg_score = sum(s["score"] for s in students) / len(students)
print(f"\nAverage score: {avg_score:.1f}")
print(f"Top students: {[s['name'] for s in top_students]}")json module handles JSON files, which are extremely common for configurations and API responses:import json
# Python dict to JSON string
config = {
"model": "transformer",
"layers": 12,
"learning_rate": 0.001,
"dropout": 0.1,
}
json_string = json.dumps(config, indent=2)
print(json_string)
# JSON string back to Python dict
loaded = json.loads(json_string)
print(loaded["model"]) # transformer
# Save to file
with open("config.json", "w") as f:
json.dump(config, f, indent=2)
# Load from file
with open("config.json", "r") as f:
loaded_config = json.load(f)# This crashes with FileNotFoundError
# with open("nonexistent.txt") as f:
# data = f.read()
# This crashes with ZeroDivisionError
# result = 10 / 0
# This crashes with ValueError
# number = int("not_a_number")# Catch specific errors
try:
with open("data.txt", "r") as f:
content = f.read()
print("File read successfully!")
except FileNotFoundError:
print("Error: File not found. Using default data.")
content = "default data"
except PermissionError:
print("Error: No permission to read the file.")
content = ""
print(f"Content: {content}")def safe_divide(a, b):
"""Divide a by b with error handling."""
try:
result = a / b
except ZeroDivisionError:
print("Cannot divide by zero!")
return None
except TypeError:
print(f"Invalid types: {type(a)} and {type(b)}")
return None
else:
# Runs ONLY if no exception occurred
print(f"{a} / {b} = {result}")
return result
finally:
# Runs ALWAYS, whether or not an exception occurred
print("Division operation complete.\n")
safe_divide(10, 3) # 10 / 3 = 3.333... then "complete"
safe_divide(10, 0) # "Cannot divide by zero!" then "complete"
safe_divide("a", 2) # "Invalid types..." then "complete"| Exception | When It Occurs |
|---|---|
FileNotFoundError | File does not exist |
PermissionError | No permission to read/write |
ValueError | Wrong value type (e.g., int("abc")) |
TypeError | Wrong operation on a type (e.g., "a" + 1) |
KeyError | Dictionary key does not exist |
IndexError | List index out of range |
You can create your own exception types and raise them intentionally:
# Custom exception
class InvalidScoreError(Exception):
"""Raised when a score is outside the valid range."""
pass
class DataValidationError(Exception):
"""Raised when input data fails validation."""
def __init__(self, field, value, message):
self.field = field
self.value = value
super().__init__(f"Validation failed for '{field}' (value: {value}): {message}")
def validate_score(name, score):
"""Validate a student's score."""
if not isinstance(score, (int, float)):
raise DataValidationError("score", score, "must be a number")
if score < 0 or score > 100:
raise InvalidScoreError(f"{name}'s score {score} is out of range (0-100)")
return True
# Using custom exceptions
students_raw = [
("Alice", 95),
("Bob", -5),
("Charlie", "ninety"),
("Diana", 88),
]
valid_students = []
for name, score in students_raw:
try:
validate_score(name, score)
valid_students.append({"name": name, "score": score})
except InvalidScoreError as e:
print(f"Skipping: {e}")
except DataValidationError as e:
print(f"Skipping: {e}")
print(f"\nValid students: {len(valid_students)} out of {len(students_raw)}")
for s in valid_students:
print(f" {s['name']}: {s['score']}")def load_dataset(filepath):
"""Load and parse a CSV dataset with comprehensive error handling."""
rows = []
errors = []
try:
with open(filepath, "r") as f:
lines = f.readlines()
except FileNotFoundError:
print(f"Dataset not found: {filepath}")
return [], ["File not found"]
if len(lines) < 2:
return [], ["File is empty or has no data rows"]
header = lines[0].strip().split(",")
for i, line in enumerate(lines[1:], start=2):
try:
values = line.strip().split(",")
if len(values) != len(header):
raise ValueError(f"Expected {len(header)} columns, got {len(values)}")
row = dict(zip(header, values))
rows.append(row)
except ValueError as e:
errors.append(f"Row {i}: {e}")
print(f"Loaded {len(rows)} rows ({len(errors)} errors)")
if errors:
for err in errors[:5]: # show first 5 errors
print(f" Warning: {err}")
return rows, errorsTests · Parse the CSV, handle errors gracefully, and verify your error messages are clear!
In ML, you train a model once and save it. Then load it at inference time without retraining.
import pickle, joblib, json
from sklearn.linear_model import LogisticRegression
from sklearn.preprocessing import StandardScaler
# --- Train (one time) ---
model = LogisticRegression()
scaler = StandardScaler()
# model.fit(X_train_scaled, y_train) ← in practice
# Option 1: pickle — works for any Python object
with open("model.pkl", "wb") as f:
pickle.dump(model, f)
# Option 2: joblib — better for large NumPy arrays (faster)
joblib.dump(model, "model.joblib")
joblib.dump(scaler, "scaler.joblib") # ← always save the scaler too!
# Save config / metrics as human-readable JSON
config = {"model": "LogisticRegression", "accuracy": 0.94, "threshold": 0.5}
with open("config.json", "w") as f:
json.dump(config, f, indent=2)
# --- Load (every time you serve predictions) ---
loaded_model = joblib.load("model.joblib")
loaded_scaler = joblib.load("scaler.joblib")
# prediction = loaded_model.predict(loaded_scaler.transform(new_data))This program writes a note to a file and then reads it back. But the read returns an empty string because the writer was never closed. Fix it using a context manager.
Got: hello from python
with for file operations -- the with open(path) as f: pattern guarantees the file is closed properly, even if an error occurs. Never use bare open() without with"r" reads, "w" overwrites (dangerous!), "a" appends. Accidentally using "w" on an important file erases everythingtry, handle specific exceptions in except, use finally for cleanup that must always runexcept ValueError is good. Bare except: is bad because it hides bugs by catching everything, even typos in your codeWhat is the main advantage of using 'with open(...)' over plain 'open()'?
| Create (fails if exists) |
| Yes |
| No |
ZeroDivisionError |
| Division by zero |
AttributeError | Object does not have the attribute/method |
raise InvalidScoreError(...)raise ValueError("bad")