Week 4 Code Examples

Files, JSON, APIs & Exception Handling — With Runnable Examples

This week you cross a major milestone: your programs start talking to the outside world. Files let you save data between runs. JSON is the format almost every API uses. HTTP requests let Python fetch live data from the internet.

How to use this page: Copy any example into a .py file and run it. The file I/O and JSON examples work immediately. The HTTP example requires pip install requests first.

1. Dictionaries — Key-Value Pairs

A dictionary stores data as named pairs: a key and its value. Instead of accessing data by position (like a list), you access it by name.

Key concept: Dictionaries are how Python represents structured data — the same way JSON does. Mastering dicts is essential for working with any API.

Creating and accessing a dictionary

person = { "name": "Alice", "age": 28, "city": "Austin", "active": True } print(person["name"]) # Alice print(person["age"]) # 28 # .get() returns None (or a default) instead of crashing if key is missing print(person.get("email")) # None print(person.get("email", "N/A")) # N/A

Adding, updating, and deleting keys

profile = {"username": "bob42", "score": 100} profile["email"] = "bob@example.com" # add new key profile["score"] = 150 # update existing key profile.pop("email") # remove a key print(profile) # {'username': 'bob42', 'score': 150}

Looping over a dictionary

config = {"host": "localhost", "port": 8080, "debug": True} # Loop over keys for key in config: print(key) # Loop over key-value pairs (most common) for key, value in config.items(): print(f" {key}: {value}") # Just values for value in config.values(): print(value)

Nested dictionaries — representing complex data

API responses are often dictionaries inside dictionaries. Practice navigating them:

user = { "name": "Carol", "address": { "city": "Dallas", "state": "TX", "zip": "75201" }, "scores": [88, 92, 79] } print(user["address"]["city"]) # Dallas print(user["address"]["state"]) # TX print(user["scores"][0]) # 88 (first score)

2. Reading and Writing Files

Files let your program persist data — the information survives after the program closes, and is available the next time it runs. Python's built-in open() function handles both reading and writing.

Key concept: Always use the with open(...) as f: pattern. It automatically closes the file when the block ends, even if an error occurs.

Writing to a file

# "w" mode — write (creates the file, or overwrites it if it exists) with open("notes.txt", "w") as f: f.write("First line\n") f.write("Second line\n") # "a" mode — append (adds to the end without erasing existing content) with open("notes.txt", "a") as f: f.write("Third line\n")

Reading from a file

# Read the entire file as one string with open("notes.txt", "r") as f: contents = f.read() print(contents) # Read line by line — memory-efficient for large files with open("notes.txt", "r") as f: for line in f: print(line.strip()) # .strip() removes the trailing newline # Read all lines into a list with open("notes.txt", "r") as f: lines = f.readlines() print(lines) # ['First line\n', 'Second line\n', 'Third line\n']

Handling a missing file

try: with open("settings.txt", "r") as f: data = f.read() except FileNotFoundError: print("File not found — using defaults.") data = ""

3. Exception Handling — try / except

Exceptions are errors that happen while your program runs. Without handling them, a single bad input or network hiccup crashes your entire program. try/except lets you catch specific errors and recover gracefully.

Key concept: Catch specific exception types, not everything at once. This way you know exactly what went wrong and can respond appropriately.

Catching specific exceptions

# ValueError — wrong type of value (e.g. int("hello")) try: number = int(input("Enter a number: ")) print(f"Double: {number * 2}") except ValueError: print("That wasn't a valid number.") # ZeroDivisionError — dividing by zero try: result = 100 / int(input("Divisor: ")) print(f"Result: {result}") except ZeroDivisionError: print("Can't divide by zero.") except ValueError: print("That wasn't a number.")

The else and finally clauses

try: value = int(input("Enter a positive number: ")) if value <= 0: raise ValueError("Must be positive") result = 100 / value except ValueError as e: print(f"Input error: {e}") else: # Runs only if NO exception was raised print(f"100 / {value} = {result:.2f}") finally: # ALWAYS runs — great for cleanup print("Done.")

Raising your own exceptions

def set_age(age): if not isinstance(age, int): raise TypeError("Age must be an integer") if age < 0 or age > 150: raise ValueError(f"Age {age} is not realistic") return age try: set_age(-5) except ValueError as e: print(f"Error: {e}") # Error: Age -5 is not realistic

4. JSON — The Language of APIs

JSON (JavaScript Object Notation) is a text format for storing structured data. It looks almost identical to Python dictionaries and lists — and that's exactly what Python converts it to and from.

Key concept: json.dumps() converts a Python object to a JSON string. json.loads() converts a JSON string back to a Python object. json.dump() / json.load() do the same directly to/from files.

Python ↔ JSON

import json # Python dict → JSON string data = {"name": "Alice", "score": 95, "tags": ["python", "ai"]} json_string = json.dumps(data, indent=2) print(json_string) # { # "name": "Alice", # "score": 95, # "tags": ["python", "ai"] # } # JSON string → Python dict back = json.loads(json_string) print(back["name"]) # Alice print(type(back)) #

Saving and loading JSON files

import json # Save to file favorites = ["Austin", "Dallas", "Seattle"] with open("favorites.json", "w") as f: json.dump(favorites, f, indent=2) # Load from file with open("favorites.json", "r") as f: loaded = json.load(f) print(loaded) # ['Austin', 'Dallas', 'Seattle'] print(type(loaded)) #

The "load or default" pattern

A very common real-world pattern — load saved data if it exists, start fresh if it doesn't:

import json def load_history(filename): try: with open(filename, "r") as f: return json.load(f) except FileNotFoundError: return [] # first run — no file yet, start with empty list def save_history(filename, history): with open(filename, "w") as f: json.dump(history, f, indent=2) history = load_history("search_history.json") history.append({"query": "python tutorials", "timestamp": "2026-03-10"}) save_history("search_history.json", history)

5. Environment Variables and API Keys

API keys are passwords — they should never be written directly in your code (which could be accidentally shared or published). Instead, store them as environment variables and read them at runtime with os.getenv().

Security rule: Never put a real API key in source code. Always use environment variables. Anyone who can read your code can steal a key that's hardcoded in it.
import os # Read an environment variable api_key = os.getenv("MY_API_KEY") # Provide a default for optional variables debug_mode = os.getenv("DEBUG", "false") # Fail fast if a required variable is missing api_key = os.getenv("MY_API_KEY") if api_key is None: print("Error: MY_API_KEY environment variable is not set.") print("Set it with: export MY_API_KEY=your_key_here") exit(1) print(f"Key loaded: {api_key[:4]}...") # show first 4 chars only, never full key

Setting environment variables (terminal)

# Mac / Linux export MY_API_KEY=abc123xyz # Windows Command Prompt set MY_API_KEY=abc123xyz # Windows PowerShell $env:MY_API_KEY="abc123xyz" # Both: verify it's set echo $MY_API_KEY # Mac/Linux echo %MY_API_KEY% # Windows CMD

6. HTTP Requests — Fetching Data from the Web

The requests library lets Python send HTTP requests to web APIs and receive data back, just like a browser does when you visit a URL.

Install first: Run pip install requests in your terminal before using this module.

A basic GET request

import requests # GET request — fetch data from a URL response = requests.get("https://api.github.com") print(response.status_code) # 200 = OK print(response.headers["Content-Type"]) # Parse the JSON response body into a Python dict data = response.json() print(data["current_user_url"])

GET request with query parameters

import requests # Pass parameters as a dict — requests builds the URL automatically # This becomes: https://api.example.com/search?q=python&limit=5 params = { "q": "python", "limit": 5, } response = requests.get("https://api.example.com/search", params=params) print(response.url) # shows the full URL that was sent print(response.status_code)

Handling errors from an API

import json import requests def fetch_data(url, params=None): try: response = requests.get(url, params=params, timeout=5) response.raise_for_status() # raises HTTPError for 4xx/5xx responses return response.json() except requests.exceptions.ConnectionError: print("Error: Could not connect. Check your internet connection.") except requests.exceptions.Timeout: print("Error: The request timed out.") except requests.exceptions.HTTPError as e: print(f"Error: Server returned {e.response.status_code}") except json.JSONDecodeError: print("Error: Response was not valid JSON.") return None data = fetch_data("https://api.github.com") if data: print("Connected successfully!")

Navigating a real API response

API responses are just nested Python dicts and lists once parsed. Here's how to dig into them:

# Imagine an API returned this response (stored here as a Python dict) response_data = { "city": "Austin", "main": { "temp": 72.4, "humidity": 58, "feels_like": 71.0 }, "weather": [ {"id": 800, "description": "clear sky"} ], "wind": { "speed": 8.3 } } # Navigate the structure city = response_data["city"] temp = response_data["main"]["temp"] humidity = response_data["main"]["humidity"] description = response_data["weather"][0]["description"] # "weather" is a list wind = response_data["wind"]["speed"] print(f"{city}: {temp}°F, {description}") print(f"Humidity: {humidity}%, Wind: {wind} mph") # Use .get() for optional fields — returns None instead of crashing feels_like = response_data["main"].get("feels_like") if feels_like: print(f"Feels like: {feels_like}°F")

7. Building a Menu-Driven Program

Most real programs have a menu that keeps running until the user quits. This pattern combines a while True loop, input validation, and try/except cleanly.

def show_menu(): print("\n--- Note Saver ---") print("1. Add a note") print("2. View all notes") print("3. Clear notes") print("4. Quit") def main(): notes = [] while True: show_menu() choice = input("Choose an option: ").strip() try: option = int(choice) except ValueError: print("Please enter a number.") continue if option == 1: note = input("Enter your note: ").strip() if note: notes.append(note) print(f"Saved! ({len(notes)} total notes)") else: print("Note can't be empty.") elif option == 2: if notes: for i, note in enumerate(notes, 1): print(f" {i}. {note}") else: print("No notes yet.") elif option == 3: notes.clear() print("All notes cleared.") elif option == 4: print("Goodbye!") break else: print("Invalid option. Choose 1–4.") main()

8. Putting It All Together — Persistent Contact Book

A mini contact book that combines files, JSON, exception handling, and a menu loop. No external APIs needed — run this immediately.

import json import os FILENAME = "contacts.json" def load_contacts(): try: with open(FILENAME, "r") as f: return json.load(f) except FileNotFoundError: return {} def save_contacts(contacts): with open(FILENAME, "w") as f: json.dump(contacts, f, indent=2) def add_contact(contacts): name = input("Name: ").strip() phone = input("Phone: ").strip() if name and phone: contacts[name] = phone save_contacts(contacts) print(f"Saved {name}.") else: print("Name and phone can't be empty.") def find_contact(contacts): name = input("Search name: ").strip() if name in contacts: print(f" {name}: {contacts[name]}") else: print("Contact not found.") def list_contacts(contacts): if contacts: for name, phone in sorted(contacts.items()): print(f" {name}: {phone}") else: print("No contacts saved.") def main(): contacts = load_contacts() print(f"Loaded {len(contacts)} contact(s).") while True: print("\n1. Add 2. Find 3. List 4. Quit") choice = input("> ").strip() if choice == "1": add_contact(contacts) elif choice == "2": find_contact(contacts) elif choice == "3": list_contacts(contacts) elif choice == "4": break else: print("Enter 1, 2, 3, or 4.") main()
Notice: load_contacts() and save_contacts() each do exactly one job. The menu loop never touches the file directly — it always goes through those functions. When you need to change the file format, you change it in one place.