ATOC 4815/5815

Functions and Reusable Code - Week 2

Will Chapman

CU Boulder ATOC

2026-01-01

Functions and Reusable Code

Today’s Objectives

  • Master advanced function concepts
  • Handle errors gracefully (try/except)
  • Read and write data files
  • Introduction to Object Oriented Programming
  • Build reusable atmospheric data tools

Reminders

Due Sunday at 12pm:

  • Lab 2
  • HW2

Office Hours:

Will: Tu / Th 11:15-12:15p Aerospace Cafe

Aiden: M / W 4-5p DUAN D319

DUAN Building

Why Functions Matter

The Problem: Copy-Paste Programming

Real scenario from a student’s homework:

# Calculate wind chill for Boulder
temp_boulder = 15
wind_boulder = 30
wc_boulder = temp_boulder - 0.4*(temp_boulder-10)*wind_boulder/36
print(f"Boulder wind chill: {wc_boulder}°C")

# Calculate wind chill for Denver
temp_denver = 18
wind_denver = 25
wc_denver = temp_denver - 0.4*(temp_denver-10)*wind_denver/36  # BUG! Wrong formula
print(f"Denver wind chill: {wc_denver}°C")

# Calculate wind chill for Vail
temp_vail = 5
wind_vail = 40
wc_vail = temp_vail - 0.4*(temp_vail-10)*wind_vail/36
print(f"Vail wind chill: {wc_vail}°C")

What’s wrong? Bug in line 9 (/36 should be /10), but you have to fix it in 3 places!

The Solution: Write Once, Use Everywhere

def wind_chill(temp_c, wind_kmh):
    """
    Calculate wind chill temperature (simplified formula)

    Args:
        temp_c: Air temperature in Celsius
        wind_kmh: Wind speed in km/h
    Returns:
        Wind chill temperature in Celsius
    """
    return temp_c - 0.4 * (temp_c - 10) * wind_kmh / 36

# Use it for any station
stations = [
    ("Boulder", 15, 30),
    ("Denver", 18, 25),
    ("Vail", 5, 40)
]

for name, temp, wind in stations:
    wc = wind_chill(temp, wind)
    print(f"{name} wind chill: {wc:.1f}°C")
Boulder wind chill: 13.3°C
Denver wind chill: 15.8°C
Vail wind chill: 7.2°C

Benefits: Fix bug once. Test once. Reuse everywhere.

Check Your Understanding 🤔

Predict the output:

def kelvin_to_celsius(temp_k):
    return temp_k - 273.15

temps_k = [273.15, 300, 250]
for t in temps_k:
    c = kelvin_to_celsius(t)
    print(f"{t}K = {c}°C")

Output:

273.15K = 0.0°C
300K = 26.85°C
250K = -23.15°C

Key insight: Functions work on ANY input that matches the expected type

Function Arguments & Defaults

Why Default Arguments? → Real Use Case

Scenario: You’re processing temperature data. Most files are in Celsius, but occasionally you get Fahrenheit.

Without defaults (annoying):

def read_temp_file(filename, unit):
    # Always have to specify unit!
    pass

# 99% of the time:
read_temp_file("boulder.csv", "C")
read_temp_file("denver.csv", "C")
read_temp_file("vail.csv", "C")

# Occasionally:
read_temp_file("old_data.csv", "F")

You repeat "C" everywhere even though it’s almost always Celsius.

With defaults (better):

def read_temp_file(filename, unit="C"):
    """
    Read temperature file

    Args:
        filename: Path to data file
        unit: Temperature unit (default "C")
    """
    # File reading logic here
    pass

# Simple calls:
read_temp_file("boulder.csv")  # Uses "C"
read_temp_file("denver.csv")   # Uses "C"
read_temp_file("vail.csv")     # Uses "C"

# Override when needed:
read_temp_file("old_data.csv", "F")

Default = “C” because that’s the common case!

Function Arguments: Complete Picture

def format_temp(temp_c, unit="C", decimals=1, include_symbol=True):
    """
    Format temperature for display

    Args:
        temp_c: Temperature in Celsius (REQUIRED)
        unit: Display unit, "C" or "F" (optional, default "C")
        decimals: Decimal places (optional, default 1)
        include_symbol: Show degree symbol (optional, default True)
    """
    # Convert if needed
    if unit == "F":
        value = temp_c * 9/5 + 32
    else:
        value = temp_c

    # Format
    symbol = f"°{unit}" if include_symbol else ""
    return f"{value:.{decimals}f}{symbol}"

# Different ways to call:
print(format_temp(20))                           # 20.0°C
print(format_temp(20, "F"))                      # 68.0°F
print(format_temp(20, "F", 2))                   # 68.00°F
print(format_temp(20, decimals=0))               # 20°C
print(format_temp(20, "F", include_symbol=False)) # 68.0
20.0°C
68.0°F
68.00°F
20°C
68.0

Try It Yourself 💻

With your neighbor (3 min): Write a function to classify wind speed

def classify_wind(speed_kmh, scale="beaufort"):
    """
    Classify wind speed

    Args:
        speed_kmh: Wind speed in km/h
        scale: Classification scale (default "beaufort")
    Returns:
        str: Classification like "Calm", "Breeze", "Gale", etc.
    """
    # Your code here!
    pass

# Test it:
print(classify_wind(5))    # Should return "Light air" or similar
print(classify_wind(30))   # Should return "Fresh breeze" or similar
print(classify_wind(75))   # Should return "Gale" or similar

Solution approach: Use if/elif chain with speed thresholds

Multiple Returns & Tuple Unpacking

Why Return Multiple Values?

Real scenario: Calculate temperature statistics for quality control

def temp_stats(temps):
    """
    Calculate temperature statistics

    Args:
        temps: List of temperature measurements
    Returns:
        tuple: (min, max, mean, std_dev)
    """
    import statistics

    min_temp = min(temps)
    max_temp = max(temps)
    mean_temp = statistics.mean(temps)
    std_temp = statistics.stdev(temps) if len(temps) > 1 else 0

    return min_temp, max_temp, mean_temp, std_temp

# Use it:
temps = [15, 18, 22, 19, 16, 21, 17]
t_min, t_max, t_mean, t_std = temp_stats(temps)

print(f"Temperature range: {t_min}°C to {t_max}°C")
print(f"Mean: {t_mean:.1f}°C ± {t_std:.1f}°C")

# Quality check: Flag if range > 2 standard deviations
if (t_max - t_min) > 2 * t_std:
    print("⚠️  Large temperature range - check data quality")
Temperature range: 15°C to 22°C
Mean: 18.3°C ± 2.6°C
⚠️  Large temperature range - check data quality

Tuple Unpacking Tricks

You can ignore values you don’t need:

# Only care about mean and std dev
_, _, mean, std = temp_stats([15, 18, 22, 19, 16])
print(f"Mean ± SD: {mean:.1f} ± {std:.1f}°C")
Mean ± SD: 18.0 ± 2.7°C

You can unpack into a single variable:

# Get all stats as a tuple
stats = temp_stats([15, 18, 22, 19, 16])
print(f"All stats: {stats}")
print(f"Min temp: {stats[0]}°C")
All stats: (15, 22, 18, 2.7386127875258306)
Min temp: 15°C

Python technically returns ONE thing (a tuple):

result = temp_stats([15, 18, 22, 19, 16])
print(f"Type: {type(result)}")  # tuple
print(f"Length: {len(result)}")  # 4
Type: <class 'tuple'>
Length: 4

Check Your Understanding 🤔

What will this print?

def analyze_wind(speed_kmh):
    beaufort = int(speed_kmh / 5)  # Simplified Beaufort scale
    category = "Calm" if beaufort == 0 else "Windy"
    return beaufort, category

# Call it:
b, cat = analyze_wind(25)
print(f"Beaufort: {b}, Category: {cat}")

Answer:

Beaufort: 5, Category: Windy

Calculation: 25 / 5 = 5, and 5 != 0 so category is “Windy”

Function Composition

Building Complex Tools from Simple Parts

The Power of Composition:

# Step 1: Simple helper functions
def celsius_to_kelvin(temp_c):
    """Convert Celsius to Kelvin"""
    return temp_c + 273.15

def calculate_saturation_vp(temp_k):
    """Calculate saturation vapor pressure (simplified)"""
    return 611.2 * (2.718 ** (17.67 * (temp_k - 273.15) / (temp_k - 29.65)))

def calculate_relative_humidity(temp_c, dewpoint_c):
    """Calculate relative humidity from temp and dewpoint"""
    temp_k = celsius_to_kelvin(temp_c)
    dewpoint_k = celsius_to_kelvin(dewpoint_c)

    es = calculate_saturation_vp(temp_k)
    e = calculate_saturation_vp(dewpoint_k)

    return 100 * (e / es)

# Use the composed function:
temp = 20
dewpoint = 15
rh = calculate_relative_humidity(temp, dewpoint)
print(f"At {temp}°C with dewpoint {dewpoint}°C:")
print(f"Relative Humidity: {rh:.1f}%")
At 20°C with dewpoint 15°C:
Relative Humidity: 72.9%

Composition Benefits

Each function does ONE thing:

  • celsius_to_kelvin: unit conversion
  • calculate_saturation_vp: physics calculation
  • calculate_relative_humidity: combines the pieces

Benefits:

  1. Test individually: Verify celsius_to_kelvin works before using it in complex calculations
  2. Reuse pieces: celsius_to_kelvin used in many other functions
  3. Debug easily: If RH is wrong, check each component separately
  4. Swap implementations: Upgrade calculate_saturation_vp formula without touching other code

Real example:

# Reuse celsius_to_kelvin in different context
def potential_temperature(temp_c, pressure_hpa):
    """Calculate potential temperature"""
    temp_k = celsius_to_kelvin(temp_c)
    theta = temp_k * (1000 / pressure_hpa) ** 0.286
    return theta - 273.15  # Back to Celsius

print(f"Potential temp: {potential_temperature(15, 850):.1f}°C")
Potential temp: 28.7°C

Error Handling

What Errors Look Like

We need to SEE errors to understand them:

ValueError:

temp_str = "cold"
temp = float(temp_str)
ValueError: could not convert string to float: 'cold'

TypeError:

temp = "20"
result = temp + 5
TypeError: can only concatenate str (not "int") to str

FileNotFoundError:

with open("missing.csv") as f:
    data = f.read()
FileNotFoundError: [Errno 2] No such file or directory: 'missing.csv'

ZeroDivisionError:

temps = []
mean = sum(temps) / len(temps)
ZeroDivisionError: division by zero

Key insight: Error messages tell you WHAT went wrong and WHERE

Handling Errors: try/except Pattern

Instead of crashing, catch the error and respond gracefully:

def safe_read_temperature(temp_str):
    """
    Safely convert temperature string to float

    Args:
        temp_str: Temperature as string
    Returns:
        float or None: Temperature value, or None if invalid
    """
    try:
        temp = float(temp_str)

        # Validate range
        if not (-100 <= temp <= 60):
            print(f"⚠️  Temperature {temp}°C outside valid range")
            return None

        return temp

    except ValueError:
        print(f"❌ Cannot convert '{temp_str}' to number")
        return None

# Test with various inputs:
print(safe_read_temperature("20.5"))    # Works: 20.5
print(safe_read_temperature("cold"))    # Handles error: None
print(safe_read_temperature("100"))     # Validates: None (too hot)
print(safe_read_temperature("-50"))     # Works: -50.0
20.5
❌ Cannot convert 'cold' to number
None
⚠️  Temperature 100.0°C outside valid range
None
-50.0

Error Handling in File I/O

Real scenario: Reading data files that might not exist

def load_station_data(station_name):
    """
    Load temperature data for a weather station

    Args:
        station_name: Name of station (e.g., "Boulder")
    Returns:
        list: Temperature readings, or empty list if file not found
    """
    filename = f"{station_name}.csv"

    try:
        with open(filename, 'r') as f:
            temps = []
            for line in f:
                temp_str = line.strip()
                temp = safe_read_temperature(temp_str)
                if temp is not None:
                    temps.append(temp)
            return temps

    except FileNotFoundError:
        print(f"📂 Station file '{filename}' not found")
        print(f"   Available stations: Boulder.csv, Denver.csv, Vail.csv")
        return []

    except PermissionError:
        print(f"🔒 Permission denied reading '{filename}'")
        return []

# Use it:
boulder_temps = load_station_data("Boulder")
if boulder_temps:
    print(f"Loaded {len(boulder_temps)} readings")
else:
    print("No data to analyze")

Debugging Exercise 🐛

Find and fix the bugs:

def calculate_average_temp(filename):
    """Calculate average temperature from file"""
    with open(filename) as f:
        temps = []
        for line in f:
            temp = float(line)  # BUG 1: What if line has text?
            temps.append(temp)

    average = sum(temps) / len(temps)  # BUG 2: What if temps is empty?
    return average

result = calculate_average_temp("Boulder.csv")
print(f"Average: {result}°C")

Bugs:

  1. float(line) will crash if line contains non-numeric text → Add try/except
  2. len(temps) could be 0 → Check if temps is empty first

Fixed version:

def calculate_average_temp(filename):
    try:
        with open(filename) as f:
            temps = []
            for line in f:
                try:
                    temp = float(line.strip())
                    temps.append(temp)
                except ValueError:
                    continue  # Skip bad lines

        if not temps:
            return None

        return sum(temps) / len(temps)
    except FileNotFoundError:
        return None

File I/O

Why Files? → Saving Your Work

Real scenario: You’ve QC’d 1000 temperature measurements. Don’t lose that work!

Writing data:

# After quality control:
cleaned_temps = [
    20.5, 21.2, 19.8, 22.1, 20.9
]

# Save it!
with open("qc_temps.txt", "w") as f:
    for temp in cleaned_temps:
        f.write(f"{temp}\n")

print("✓ Data saved to qc_temps.txt")

Benefits:

  • Can close Python and come back later
  • Share with collaborators
  • Document your processing
  • Backup your work

Reading data back:

# Later, read it back:
with open("qc_temps.txt", "r") as f:
    temps = []
    for line in f:
        temp = float(line.strip())
        temps.append(temp)

print(f"Loaded {len(temps)} measurements")
print(f"Mean: {sum(temps)/len(temps):.1f}°C")

The with statement:

  • Opens the file
  • Automatically closes it (even if errors!)
  • Prevents file corruption

File Modes & the with Statement

# File modes:
with open("data.txt", "r") as f:   # Read (file must exist)
    content = f.read()

with open("data.txt", "w") as f:   # Write (overwrites existing!)
    f.write("New data")

with open("data.txt", "a") as f:   # Append (adds to end)
    f.write("More data\n")

Why with? Automatic cleanup:

# Without with (BAD - file might not close if error):
f = open("data.txt", "r")
data = f.read()
f.close()  # Might not execute if error above!

# With with (GOOD - always closes):
with open("data.txt", "r") as f:
    data = f.read()
# File automatically closed here

Common mistake:

# WRONG - file handle leaked:
data = open("data.txt").read()  # File never closed!

# RIGHT - use with:
with open("data.txt") as f:
    data = f.read()

CSV Files: Structured Data

CSV = Comma Separated Values (spreadsheet-like data)

import csv

# Write CSV with headers:
stations = [
    {"name": "Boulder", "lat": 40.01, "lon": -105.25, "elevation": 1655},
    {"name": "Denver", "lat": 39.74, "lon": -104.99, "elevation": 1609},
    {"name": "Vail", "lat": 39.64, "lon": -106.37, "elevation": 2476}
]

with open("stations.csv", "w") as f:
    writer = csv.DictWriter(f, fieldnames=["name", "lat", "lon", "elevation"])
    writer.writeheader()  # Write column names
    writer.writerows(stations)

print("✓ Saved to stations.csv")

The file looks like:

name,lat,lon,elevation
Boulder,40.01,-105.25,1655
Denver,39.74,-104.99,1609
Vail,39.64,-106.37,2476

Reading CSV Files

import csv

# Read CSV as dictionaries (easier!):
with open("stations.csv", "r") as f:
    reader = csv.DictReader(f)

    for row in reader:
        name = row["name"]
        elev = float(row["elevation"])
        print(f"{name}: {elev}m elevation")

# Output:
# Boulder: 1655.0m elevation
# Denver: 1609.0m elevation
# Vail: 2476.0m elevation

Why CSV over plain text?

  • Structure: Columns have names
  • Multiple fields: Store name, lat, lon, elevation together
  • Standard format: Excel, pandas, xarray all read CSV
  • Human readable: Can open in any text editor

When you’ll use this:

  • Saving QC’d station data
  • Exporting results for papers/reports
  • Sharing data with collaborators
  • Input to other analysis tools

Try It Yourself 💻

With your neighbor (5 min): Complete this function

import csv

def save_temp_timeseries(filename, station_name, times, temps):
    """
    Save temperature time series to CSV

    Args:
        filename: Output CSV path
        station_name: Station name
        times: List of time strings
        temps: List of temperature values
    """
    # Your code here!
    # 1. Open file for writing
    # 2. Create CSV writer with fieldnames ["time", "station", "temp_c"]
    # 3. Write header
    # 4. Write each time/temp pair as a row
    pass

# Test it:
times = ["2026-01-15 12:00", "2026-01-15 13:00", "2026-01-15 14:00"]
temps = [20.5, 21.2, 22.1]
save_temp_timeseries("boulder_temps.csv", "Boulder", times, temps)

Hint: Loop through zip(times, temps) to get pairs

Object-Oriented Programming

The Problem: Dictionaries Fall Apart at Scale

Imagine managing 100 weather stations with dictionaries:

# Station 1
boulder = {
    "name": "Boulder",
    "elevation": 1655,
    "temps": [],
    "winds": [],
    "status": "active"
}

# Station 2
denver = {
    "name": "Denver",
    "eleevation": 1609,  # TYPO! Will fail later
    "temps": [],
    "status": "active"
    # Forgot winds!
}

# Add temperature
boulder["temps"].append(20.5)
denver["tmps"].append(22.3)  # TYPO! Creates new key silently

# Calculate mean
mean_boulder = sum(boulder["temps"]) / len(boulder["temps"])
mean_denver = sum(denver["temps"]) / len(denver["temps"])  # CRASH! Key missing

Problems:

  • Typos: eleevation, tmps fail silently or crash randomly
  • Inconsistency: Some stations have winds, others don’t
  • No validation: Can do boulder["temps"] = "cold" (nonsense!)
  • Scattered logic: Mean calculation repeated everywhere

The Solution: Classes

A class is a blueprint for creating objects with guaranteed structure:

class WeatherStation:
    """A weather monitoring station"""

    def __init__(self, name, elevation):
        # Every station MUST have these
        self.name = name
        self.elevation = elevation
        self.temps = []
        self.winds = []
        self.status = "active"

    def add_temp(self, temp):
        """Add temperature measurement"""
        if not (-100 <= temp <= 60):
            raise ValueError(f"Temperature {temp}°C out of range")
        self.temps.append(temp)

    def mean_temp(self):
        """Calculate mean temperature"""
        if not self.temps:
            return None
        return sum(self.temps) / len(self.temps)

# Create stations:
boulder = WeatherStation("Boulder", 1655)
denver = WeatherStation("Denver", 1609)

# Use them:
boulder.add_temp(20.5)
denver.add_temp(22.3)

print(f"Boulder mean: {boulder.mean_temp()}°C")
print(f"Denver mean: {denver.mean_temp()}°C")
Boulder mean: 20.5°C
Denver mean: 22.3°C

Understanding self

Most confusing concept for beginners! Let’s visualize:

class WeatherStation:
    def __init__(self, name, elevation):
        self.name = name          # "self" means "THIS station"
        self.elevation = elevation
        self.temps = []

    def add_temp(self, temp):
        self.temps.append(temp)   # "self.temps" means "THIS station's temps"

# Create two INDEPENDENT stations:
boulder = WeatherStation("Boulder", 1655)
denver = WeatherStation("Denver", 1609)

# Add data to Boulder:
boulder.add_temp(20)
boulder.add_temp(21)

# Add data to Denver:
denver.add_temp(25)

# They're SEPARATE!
print(f"Boulder temps: {boulder.temps}")  # [20, 21]
print(f"Denver temps: {denver.temps}")    # [25]
Boulder temps: [20, 21]
Denver temps: [25]

Think of self as “this specific object”:

  • boulder.add_temp(20) → Inside method, self refers to boulder
  • denver.add_temp(25) → Inside method, self refers to denver

Each object has its OWN data!

Classes vs Dictionaries: Side-by-Side

Dictionary (loosely structured):

# Create
station = {
    "name": "Boulder",
    "temps": []
}

# Add temp
station["temps"].append(20)

# Typo creates NEW key silently!
station["tmps"].append(21)  # BUG

# Calculate mean
if station["temps"]:
    mean = sum(station["temps"]) / len(station["temps"])

Problems:

  • No structure enforcement
  • Typos fail silently
  • Logic scattered
  • No validation

Class (well structured):

class WeatherStation:
    def __init__(self, name):
        self.name = name
        self.temps = []

    def add_temp(self, temp):
        self.temps.append(temp)

    def mean_temp(self):
        if not self.temps:
            return None
        return sum(self.temps) / len(self.temps)

# Create
station = WeatherStation("Boulder")

# Add temp
station.add_temp(20)

# Typo is caught!
station.add_tmps(21)  # AttributeError!

# Calculate mean
mean = station.mean_temp()  # Clean!

Benefits:

  • Guaranteed structure
  • Typos caught immediately
  • Logic bundled with data
  • Can add validation

When to Use Classes vs Functions/Dicts

Use a FUNCTION when:

  • ✅ Simple transformation: input → output
  • ✅ No state to maintain
  • ✅ Pure calculation

Example: celsius_to_fahrenheit(temp)

Use a DICTIONARY when:

  • ✅ One-off data grouping
  • ✅ Simple key-value lookup
  • ✅ No behavior attached

Example: {"station": "Boulder", "temp": 20}

Use a CLASS when:

  • ✅ Data + behavior together
  • ✅ Need validation/rules
  • ✅ Multiple instances with same structure
  • ✅ Need initialization logic

Example: WeatherStation with add_temp(), mean_temp(), etc.

Real-world example:

# Class for weather station (complex, stateful):
class WeatherStation:
    def __init__(self, name, lat, lon):
        self.name = name
        self.lat = lat
        self.lon = lon
        self.observations = []

    def add_observation(self, temp, wind, time):
        # Validation, storage, QC...
        pass

    def daily_summary(self):
        # Complex calculation...
        pass

# Function for unit conversion (simple, stateless):
def celsius_to_fahrenheit(temp_c):
    return temp_c * 9/5 + 32

Building a Real Weather Data Class

class WeatherObservation:
    """Single weather observation with validation"""

    def __init__(self, time, temp_c, wind_kt, pressure_hpa):
        # Validate all inputs
        if not (-50 <= temp_c <= 50):
            raise ValueError(f"Temperature {temp_c}°C out of range")
        if wind_kt < 0:
            raise ValueError("Wind speed cannot be negative")
        if not (800 <= pressure_hpa <= 1100):
            raise ValueError(f"Pressure {pressure_hpa}hPa out of range")

        # Store validated data
        self.time = time
        self.temp_c = temp_c
        self.wind_kt = wind_kt
        self.pressure_hpa = pressure_hpa

    def temp_f(self):
        """Temperature in Fahrenheit"""
        return self.temp_c * 9/5 + 32

    def is_freezing(self):
        """Check if below freezing"""
        return self.temp_c <= 0

    def __str__(self):
        return f"{self.time}: {self.temp_c}°C, {self.wind_kt}kt, {self.pressure_hpa}hPa"

# Use it:
obs = WeatherObservation("2026-01-15 12:00", 20.5, 15, 1013)
print(obs)
print(f"Freezing: {obs.is_freezing()}")
print(f"Temp (F): {obs.temp_f():.1f}°F")
2026-01-15 12:00: 20.5°C, 15kt, 1013hPa
Freezing: False
Temp (F): 68.9°F

Managing Collections with Classes

class WeatherDataset:
    """Collection of observations for a station"""

    def __init__(self, station_name):
        self.station_name = station_name
        self.observations = []

    def add(self, obs):
        """Add a validated observation"""
        if not isinstance(obs, WeatherObservation):
            raise TypeError("Must add WeatherObservation objects")
        self.observations.append(obs)

    def mean_temp(self):
        """Mean temperature across all observations"""
        if not self.observations:
            return None
        temps = [obs.temp_c for obs in self.observations]
        return sum(temps) / len(temps)

    def count_freezing(self):
        """Count freezing observations"""
        return sum(1 for obs in self.observations if obs.is_freezing())

    def summary(self):
        """Print summary statistics"""
        print(f"Station: {self.station_name}")
        print(f"Observations: {len(self.observations)}")
        print(f"Mean temp: {self.mean_temp():.1f}°C")
        print(f"Freezing days: {self.count_freezing()}")

# Use it:
boulder = WeatherDataset("Boulder")
boulder.add(WeatherObservation("12:00", 20, 10, 1013))
boulder.add(WeatherObservation("13:00", -2, 15, 1015))
boulder.add(WeatherObservation("14:00", 5, 8, 1014))
boulder.summary()
Station: Boulder
Observations: 3
Mean temp: 7.7°C
Freezing days: 1

Check Your Understanding 🤔

Predict the output:

class Station:
    def __init__(self, name):
        self.name = name
        self.count = 0

    def record(self):
        self.count += 1

s1 = Station("Boulder")
s2 = Station("Denver")

s1.record()
s1.record()
s2.record()

print(f"Boulder: {s1.count}")
print(f"Denver: {s2.count}")

Answer:

Boulder: 2
Denver: 1

Each object (s1, s2) has its OWN count variable!

Putting It All Together

Complete Workflow: Functions + Files + Classes

import csv

# Helper function
def validate_temp(temp):
    """Validate temperature is in reasonable range"""
    return -50 <= temp <= 50

# Class for data management
class StationData:
    def __init__(self, name):
        self.name = name
        self.temps = []

    def add_temp(self, temp):
        if validate_temp(temp):  # Use function
            self.temps.append(temp)
            return True
        return False

    def save_to_csv(self, filename):
        """Save data to CSV file"""
        with open(filename, 'w') as f:
            writer = csv.writer(f)
            writer.writerow(['station', 'temperature'])
            for temp in self.temps:
                writer.writerow([self.name, temp])

    def load_from_csv(self, filename):
        """Load data from CSV file"""
        try:
            with open(filename, 'r') as f:
                reader = csv.DictReader(f)
                for row in reader:
                    temp = float(row['temperature'])
                    self.add_temp(temp)  # Uses validation
        except FileNotFoundError:
            print(f"File {filename} not found")

# Use it all together:
boulder = StationData("Boulder")
boulder.add_temp(20.5)
boulder.add_temp(21.2)
boulder.add_temp(100)  # Rejected by validation!
boulder.save_to_csv("boulder_qc.csv")

# Later, load it back:
new_session = StationData("Boulder")
new_session.load_from_csv("boulder_qc.csv")
print(f"Loaded {len(new_session.temps)} measurements")

Practice Exercise 💻

Build a complete weather analysis pipeline (10 min with neighbor):

class WeatherAnalyzer:
    """Analyze weather data with QC and file I/O"""

    def __init__(self, station_name):
        self.station = station_name
        self.data = []

    def load_csv(self, filename):
        """Load temps from CSV, with error handling"""
        # TODO: Implement with try/except
        pass

    def quality_control(self):
        """Remove outliers (>3 std from mean)"""
        # TODO: Calculate mean and std
        # TODO: Filter data
        pass

    def save_clean_data(self, filename):
        """Save QC'd data to new CSV"""
        # TODO: Write cleaned data
        pass

    def summary_stats(self):
        """Print min, max, mean, count"""
        # TODO: Calculate and print stats
        pass

# Test it:
analyzer = WeatherAnalyzer("Boulder")
analyzer.load_csv("raw_temps.csv")
analyzer.quality_control()
analyzer.save_clean_data("clean_temps.csv")
analyzer.summary_stats()

Hint: Use functions from earlier! Reuse temp_stats(), safe_read_temperature(), etc.

Looking Ahead

Why Classes Matter for Next Week

Next week: NumPy arrays and pandas DataFrames

Both are CLASSES you’ll use constantly:

import pandas as pd
import numpy as np

# DataFrame is a class (like WeatherDataset but way more powerful):
df = pd.read_csv("stations.csv")
df.mean()  # Method, just like station.mean_temp()
df.plot()  # Method for visualization

# Array is a class:
temps = np.array([20, 21, 19, 22])
temps.mean()  # Method
temps.std()   # Method

# YOU'LL USE THEIR METHODS CONSTANTLY:
df.groupby('station').mean()
df.to_csv('results.csv')
temps.reshape(2, 2)

Understanding classes now makes pandas/NumPy make sense!

You’re learning the LANGUAGE the scientific Python ecosystem speaks.

Preview of Next Week

  • NumPy arrays - Fast numerical computing
  • Vectorized operations - No more loops!
  • Matplotlib - Publication-quality plots
  • Pandas DataFrames - Spreadsheet-like data
  • Real atmospheric data - NetCDF files, time series
  • First analysis project - End-to-end workflow

Assignment Checklist

Lab 2 + HW2 due Sunday at 12pm

You should now be able to:

  • ✅ Write functions with defaults and multiple returns
  • ✅ Compose small functions into larger tools
  • ✅ Handle errors with try/except
  • ✅ Read and write CSV files
  • ✅ Create classes for atmospheric data
  • ✅ Validate data in __init__
  • ✅ Build complete analysis pipelines

HW2 includes:

  • Function composition for atmospheric calculations
  • Error handling in file I/O
  • Building a WeatherStation class
  • CSV data processing
  • End-to-end analysis script

Resources and Support

Available to you:

  • Lab notebooks with examples
  • Office hours (Will: Tu/Th 11:15-12:15, Aiden: M/W 4-5p)
  • Discussion channels
  • Example code from today’s slides

Remember:

  • Functions make code reusable
  • Error handling makes code robust
  • Classes make code organized
  • Files make work persistent

Good Python Habits

Good Python Habits

  1. Write functions early - If you copy-paste, write a function instead
  2. Use descriptive names - calculate_wind_chill not calc_wc or f
  3. Add docstrings ALWAYS - Explain Args and Returns
  4. Handle expected errors - Use try/except for file I/O, conversions
  5. Use with open() for files - Prevents resource leaks
  6. Validate in __init__ - Catch bad data early
  7. One function, one job - Compose small pieces into larger tools
  8. Test edge cases - Empty lists, missing files, invalid inputs
  9. DRY: Don’t Repeat Yourself - Repeated code → function
  10. Ask “When would this break?” - Then add error handling

Review: Key Concepts

Functions:

  • Default arguments make common cases simple
  • Return multiple values with tuples
  • Compose small functions into complex tools

Error Handling:

  • Use try/except for expected failures
  • Give helpful error messages
  • Don’t let your program crash silently

Files:

  • Always use with open() for automatic cleanup
  • CSV for structured data
  • Error handling for missing files

Classes:

  • Bundle data and behavior
  • Validate in __init__
  • Each object is independent

Questions?

Contact

Prof. Will Chapman

📧 wchapman@colorado.edu

🌐 willychap.github.io

🏢 ATOC Building, CU Boulder

Office Hours:

  • Will: Tu / Th 11:15-12:15p
  • Aiden: M / W 4-5p

See you next week for NumPy & Pandas!