pi = 3.1416
sqrt(2) = 1.4142
e = 2.7183
Linking Python Scripts Together - Week 5.5
CU Boulder ATOC
2026-01-01
.py filesif __name__ == "__main__" guardMidterm Grades Posted!
Office Hours:
Will: Tu 11:15-12:15p Th 9-10a Aerospace Cafe
Aiden: M / W 330-430p DUAN D319


Spotify Playlist: ATOC4815
Hot Chip - Ready for the Fall / I Feel Better

You’ve been writing everything in one notebook or one script. That works at first…
But then a .py for our Lorenze system looks like this:
lorenz63_everything.py (400 lines)
├── import statements
├── Lorenz63 class
├── plotting functions
├── utility helpers
├── analysis code
└── if __name__ == "__main__": ... (maybe)
Problems:
Lorenz63 class in a new analysis without copy-pastingA module is just a .py file that contains functions, classes, or variables.
my_project/
├── lorenz63.py ← the Lorenz63 class
├── plotting.py ← plotting helper functions
├── utils.py ← small utility functions
└── run_experiment.py ← the main script that ties it all together
Benefits:
lorenz63.py in any future projectYou already do this! Every time you write import numpy, you’re importing a module someone else wrote.
You’ve been doing this since week 1:
pi = 3.1416
sqrt(2) = 1.4142
e = 2.7183
The same syntax works for your own .py files!
Step 1: Write a file called conversions.py:
All three work. Each has trade-offs:
Import the module:
Verbose but clear where things come from.
Import specific names:
Shorter. You see exactly what you use.
General Rule: Use from module import name1, name2 for most cases. Use import module when you use many things from it (like numpy).
Predict the output:
ModuleNotFoundError: No module named 'conversions'
Why? Python can’t find conversions.py. Most common causes:
| Cause | Fix |
|---|---|
| File doesn’t exist | Create conversions.py |
| File is in a different folder | Move it, or adjust sys.path |
| Typo in the name | import conveRsions vs import conversions |
| Running from wrong directory | cd to the project folder first |
Given this project layout:
weather_project/
├── conversions.py ← has celsius_to_fahrenheit()
├── analysis.py ← your main script
└── data/
└── temps.csv
Which import works in analysis.py?
Answer: Option A
conversions.py is in the same folder as analysis.pyweather_project/ and it’s a package__name__ GuardImagine conversions.py has some test code at the bottom:
# conversions.py
def celsius_to_fahrenheit(temp_c):
return temp_c * 9/5 + 32
def fahrenheit_to_celsius(temp_f):
return (temp_f - 32) * 5/9
# Quick test
print("Testing conversions...")
print(f"0°C = {celsius_to_fahrenheit(0)}°F")
print(f"100°C = {celsius_to_fahrenheit(100)}°F")
print("All tests passed!")Key insight: When Python imports a .py file, it executes the entire file from top to bottom.
import conversions
│
├─ def celsius_to_fahrenheit(...) ← defined, stored ✓
├─ def fahrenheit_to_celsius(...) ← defined, stored ✓
├─ print("Testing conversions...") ← RUNS immediately ✗
├─ print(f"0°C = ...") ← RUNS immediately ✗
└─ print("All tests passed!") ← RUNS immediately ✗
This is why we need the __name__ guard.
__name__ VariableEvery Python file has a built-in variable called __name__.
It has two possible values:
| Situation | __name__ equals |
|---|---|
You run the file directly: python my_file.py |
"__main__" |
Someone imports the file: import my_file |
"my_file" |
Demo:
This is how a file knows whether it’s being run or imported!
Wrap any “run only when executed directly” code in this block:
# conversions.py
def celsius_to_fahrenheit(temp_c):
return temp_c * 9/5 + 32
def fahrenheit_to_celsius(temp_f):
return (temp_f - 32) * 5/9
if __name__ == "__main__":
# This only runs when you do: python conversions.py
print("Testing conversions...")
print(f"0°C = {celsius_to_fahrenheit(0)}°F")
print(f"100°C = {celsius_to_fahrenheit(100)}°F")
print("All tests passed!")Now:
| Action | What happens |
|---|---|
python conversions.py |
Functions defined + tests run |
from conversions import celsius_to_fahrenheit |
Functions defined, tests skipped |
Predict what happens:
# euler.py
import numpy as np
class Lorenz63:
def __init__(self, sigma=10, rho=28, beta=8/3, dt=0.01):
self.sigma, self.rho, self.beta, self.dt = sigma, rho, beta, dt
def tendency(self, state):
x, y, z = state
return np.array([self.sigma*(y-x), self.rho*x - y - x*z, x*y - self.beta*z])
def step(self, state):
return state + self.tendency(state) * self.dt
# "Quick test" without guard
model = Lorenz63()
state = np.array([1.0, 1.0, 1.0])
for _ in range(1000):
state = model.step(state)
print(f"Final state: {state}")Which version is correct?
Version A:
Answer: Version B
from stats import anomaly would print output every timefrom stats import anomaly just gives you the function, cleanlypython stats.py runs the testLet’s build a small multi-file project step by step:
weather_toolkit/
├── conversions.py ← temperature unit conversions
├── stats.py ← statistical analysis functions
├── plotting.py ← visualization helpers
└── run_analysis.py ← main script that ties it all together
Design principle: Each file has one job.
| File | Responsibility |
|---|---|
conversions.py |
Unit conversions (C↔︎F↔︎K) |
stats.py |
Anomalies, climatology, running mean |
plotting.py |
Standard plot templates for the course |
run_analysis.py |
Load data, call functions, produce output |
This is how real scientific code is organized — the same pattern scales from homework to research.
conversions.py# conversions.py
"""Temperature unit conversion utilities."""
def celsius_to_fahrenheit(temp_c):
"""Convert Celsius to Fahrenheit."""
return temp_c * 9/5 + 32
def celsius_to_kelvin(temp_c):
"""Convert Celsius to Kelvin."""
return temp_c + 273.15
def wind_speed_knots_to_ms(knots):
"""Convert wind speed from knots to m/s."""
return knots * 0.514444
if __name__ == "__main__":
# Quick sanity checks
assert celsius_to_fahrenheit(0) == 32
assert celsius_to_fahrenheit(100) == 212
assert celsius_to_kelvin(0) == 273.15
print("conversions.py: all checks passed")Notice:
if __name__ guard with assert statements for self-testingpython conversions.py verifies the module worksstats.py# stats.py
"""Statistical analysis functions for atmospheric data."""
import numpy as np
def compute_anomaly(data, axis=None):
"""Subtract the mean to get anomalies."""
climatology = data.mean(axis=axis, keepdims=True)
return data - climatology
def running_mean(data, window):
"""Compute running mean with given window size."""
kernel = np.ones(window) / window
return np.convolve(data, kernel, mode='valid')
def find_extremes(data):
"""Return dict with min, max, and their indices."""
return {
'min_val': data.min(),
'max_val': data.max(),
'min_idx': data.argmin(),
'max_idx': data.argmax(),
}
if __name__ == "__main__":
test_data = np.array([1.0, 2.0, 3.0, 4.0, 5.0])
anom = compute_anomaly(test_data)
assert np.isclose(anom.mean(), 0.0)
print("stats.py: all checks passed")plotting.py# plotting.py
"""Standard plotting templates for ATOC 4815."""
import matplotlib.pyplot as plt
def plot_timeseries(time, data, ylabel, title, label=None, ax=None):
"""Create a labeled time series plot."""
if ax is None:
fig, ax = plt.subplots(figsize=(9, 4))
ax.plot(time, data, linewidth=2, label=label)
ax.set_xlabel('Time')
ax.set_ylabel(ylabel)
ax.set_title(title)
ax.grid(True, alpha=0.3)
if label:
ax.legend()
return ax
def plot_comparison(time, datasets, labels, ylabel, title):
"""Plot multiple time series on the same axes."""
fig, ax = plt.subplots(figsize=(9, 4))
for data, label in zip(datasets, labels):
ax.plot(time, data, linewidth=2, label=label)
ax.set_xlabel('Time')
ax.set_ylabel(ylabel)
ax.set_title(title)
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
return fig, ax
if __name__ == "__main__":
import numpy as np
t = np.linspace(0, 24, 100)
y = 15 + 8 * np.sin(t * np.pi / 12)
plot_timeseries(t, y, 'Temp (°C)', 'Test Plot')
plt.show()
print("plotting.py: visual check passed")run_analysis.py# run_analysis.py
"""Main analysis script — ties all modules together."""
import numpy as np
from conversions import celsius_to_fahrenheit
from stats import compute_anomaly, running_mean, find_extremes
from plotting import plot_timeseries, plot_comparison
# --- Generate synthetic data ---
np.random.seed(42)
hours = np.arange(0, 72) # 3 days
temp_c = 15 + 8 * np.sin((hours - 6) * np.pi / 12) + np.random.randn(72) * 2
# --- Analysis ---
temp_f = celsius_to_fahrenheit(temp_c)
anomalies = compute_anomaly(temp_c)
smooth = running_mean(temp_c, window=6)
extremes = find_extremes(temp_c)
print(f"Max temp: {extremes['max_val']:.1f}°C at hour {extremes['max_idx']}")
print(f"Min temp: {extremes['min_val']:.1f}°C at hour {extremes['min_idx']}")
# --- Visualization ---
plot_comparison(
hours,
[temp_c, np.pad(smooth, (2, 3), constant_values=np.nan)],
['Raw', '6-hr Running Mean'],
'Temperature (°C)',
'72-Hour Boulder Temperature Analysis'
)
import matplotlib.pyplot as plt
plt.savefig('boulder_analysis.png', dpi=150, bbox_inches='tight')
plt.show()
print("Analysis complete!")Look how clean this is! The main script reads almost like English because each function has a clear name and lives in a focused module.
When you run python run_analysis.py, here’s what happens:
python run_analysis.py
│
├─ import numpy as np ← from installed packages
├─ from conversions import celsius_to_fahrenheit
│ └─ Python executes conversions.py
│ ├─ defines celsius_to_fahrenheit ← kept ✓
│ ├─ defines celsius_to_kelvin ← kept (not imported, but defined)
│ └─ if __name__ == "__main__": ... ← SKIPPED (name is "conversions")
├─ from stats import compute_anomaly, ...
│ └─ Python executes stats.py
│ ├─ import numpy as np ← numpy loaded (cached)
│ ├─ defines compute_anomaly ← kept ✓
│ └─ if __name__ == "__main__": ... ← SKIPPED
├─ from plotting import ...
│ └─ ...same pattern...
│
└─ Your analysis code runs
Every imported file is executed once, top to bottom. The __name__ guard prevents side effects.
What happens here?
ImportError: cannot import name 'get_temp' from partially initialized
module 'temperature' (most likely due to a circular import)
What went wrong?
wind.pyfrom temperature import get_temp → Python pauses wind.py and starts loading temperature.pytemperature.py: from wind import wind_chill → but wind.py isn’t done yet!wind_chill doesn’t exist yet → crashThe rule: import dependencies must flow one way — like a river, not a loop.
Before (broken): each file imports from the other
wind.py ──imports──▶ temperature.py
▲ │
└────────imports─────────┘ ← CYCLE!
After (fixed): extract the shared piece into a third file
core.py ◀── wind.py ◀── temperature.py ← one direction, no cycle ✓
PEP 8 (the Python style guide) recommends this order:
Why?
flake8) will warn if you mix them upSimple rule: stdlib → third-party → yours, with a blank line between each group.
Predict the output:
AttributeError: module 'math' has no attribute 'sqrt'
Why? Python found your math.py before the built-in math module!
Dangerous file names to avoid:
| Don’t name your file | It shadows |
|---|---|
math.py |
import math |
random.py |
import random |
numpy.py |
import numpy |
test.py |
import test (built-in) |
statistics.py |
import statistics |
Fix: Rename your file to something specific: my_math_utils.py, weather_stats.py, etc.
__init__.py FileWhen your Python project grows beyond loose .py files into an organized folder, you’ll see a special file called __init__.py. What does it do, and when do you actually need it?
“Wait, I can already import files without this…” — Yes! If you’re inside a folder, Python finds sibling files automatically:
But when you use dot notation to reach into a folder from outside, Python needs __init__.py:
lorenz_project/
├── __init__.py ← without this: ModuleNotFoundError
├── integrators.py
├── lorenz63.py
└── plotting.py
Without __init__.py:
With __init__.py (even empty):
Rule: __init__.py is needed when you reference a folder with dot notation — i.e., when you treat it as a package rather than just a directory you happen to be sitting in.
__init__.py — More Than Just EmptyAn empty __init__.py is fine to start, but it can also do useful things:
1. Convenience imports — let users skip the submodule path:
Now instead of the long path:
users can just write:
2. Package-level metadata:
3. Control what from package import * exports:
For this lab: an empty __init__.py is all you need. But as your projects grow, this file becomes your package’s front door.
The Lorenz attractor has regions where forecasts stay accurate and regions where they immediately diverge.
Your goal: Build a multi-file project that produces this figure:

Three panels showing ensembles of trajectories started from different regions of the attractor:
| Panel | Starting region | What you’ll see |
|---|---|---|
| (a) Deep left lobe [-15,1,45] | Deep in the left wing | Ring of uncertainty barely grows — highly predictable |
| (b) High left lobe [-8,-3,34] | Upper part of left wing, near transition | ensemble slightly diverves - ** medium predictability ** |
| (c) Saddle region [0,0,18] | Between the two wings | Ensemble explodes — some go left, some go right — no predictability |
This is the key insight of chaos: not all regions of a system are equally predictable. Weather models face exactly this challenge.
Refactor your midterm code into this multi-file project:
lorenz_project/
├── __init__.py ← empty, makes this a package
├── integrators.py ← euler_step() and integrate()
├── lorenz63.py ← Lorenz63 class with run() and run_ensemble()
├── plotting.py ← plot_attractor(), plot_ensemble(), plot_ensemble_panels()
└── run_lorenz_ensemble.py ← driver script — produces the 3-panel figure
Who imports whom? Read each arrow as “imports from”:
run_lorenz_ensemble.py
├── imports from ──▶ lorenz63.py
│ └── imports from ──▶ integrators.py
└── imports from ──▶ plotting.py
Notice: arrows only point downward/right — no file imports from a file that imports it back. That’s what “no circular imports” means.
Starter files are provided — each has docstrings, hints, and TODO markers. Fill them in.
integrators.pyWhat to implement:
| Function | Signature | What it does |
|---|---|---|
euler_step |
(state, tendency_fn, dt) → state |
One Forward Euler step |
integrate |
(state0, tendency_fn, dt, n_steps) → trajectory |
Full time loop, returns shape (n_steps+1, n_vars) |
lorenz63.pyWhat to implement:
| Method | What it does |
|---|---|
__init__(sigma, rho, beta) |
Store parameters |
tendency(state) |
Return \([dx/dt,\; dy/dt,\; dz/dt]\) |
run(state0, dt, n_steps) |
Integrate one trajectory (calls integrate from integrators.py) |
run_ensemble(ics, dt, n_steps) |
Integrate many trajectories from an array of initial conditions |
The ensemble method — implement it two ways:
self.run() for each one# Method 1: loop over members (straightforward)
for i in range(n_members):
ensemble[i] = self.run(ics[i], dt, n_steps)
# Method 2: loop over time only (fast!)
# states shape: (n_members, 3) — all members at once
for t in range(n_steps):
states = states + vectorized_tendency(states) * dt
plotting.pyWhat to implement:
| Function | What it does |
|---|---|
plot_attractor(ax, trajectory) |
Plot one trajectory in \(x\)-\(z\) phase space (the butterfly) |
plot_ensemble(ax, ensemble, reference) |
Plot reference attractor (light) + ensemble members (bold) |
plot_ensemble_panels(ensemble_list, reference, titles) |
Create the 3-panel figure, one panel per starting region |
Plotting tips:
trajectory[:, 0] for \(x\) and trajectory[:, 2] for \(z\)alpha=0.3)"firebrick", higher alphaplt.savefig(path, dpi=150, bbox_inches='tight') to saverun_lorenz_ensemble.pyThe driver script. This ties everything together:
Lorenz63 model with default parametersnp.random.randn(30, 3) * 0.5)model.run_ensemble() for each cloudplot_ensemble_panels() to make the 3-panel figurelorenz_ensemble_predictability.pngintegrators.py — run python -m lorenz_project.integrators and confirm it prints "all checks passed!" (tests Euler on \(dy/dt = -y\), checks answer ≈ \(e^{-1}\))lorenz63.py — run python -m lorenz_project.lorenz63 and confirm it prints "all checks passed!" (checks trajectory shape is (1001, 3) and ensemble shape is (5, 1001, 3))plotting.py — run python -m lorenz_project.plotting — a plot window should appear with a random-walk line. If you see a line, it works.run_lorenz_ensemble.py — run python -m lorenz_project.run_lorenz_ensemble — produces and saves lorenz_ensemble_predictability.pngif __name__ == "__main__": guard with tests inside itrun_ensemble implemented with Method 1 (nested loop); Method 2 (vectorized) for bonuslorenz_ensemble_predictability.png with three panels showing how predictability varies across the attractorGrading:
When your figure is done, you’ll see:
(a) Deep left lobe — The ensemble starts tight and stays together. Trajectories orbit the left wing in sync. The ring of uncertainty barely grows. Highly predictable.
(b) High left lobe — The ensemble starts tight but distorts. Some members continue looping left, others begin transitioning to the right lobe. The ring stretches into banana and boomerang shapes. Partially predictable — you know roughly where, but not which lobe.
(c) Saddle region — The ensemble starts tight but immediately explodes. Some trajectories go left, some go right. This is where the two wings meet and tiny differences get maximally amplified. No predictability.
This is exactly the problem weather forecasters face: some atmospheric states are inherently more predictable than others. Ensemble forecasting reveals where confidence is warranted and where it isn’t.
Split code into modules — each .py file should have one clear job
Import styles:
import module → module.function() (explicit)from module import function → function() (convenient)from module import * → avoid (namespace pollution)Always use if __name__ == "__main__": to guard code that should only run when the file is executed directly
Avoid pitfalls:
This pattern scales — from homework to research code to production software
__name__ Guard Cheat Sheet# my_module.py
# --- Imports at the top ---
import numpy as np
# --- Functions and classes ---
def my_function():
...
class MyClass:
...
# --- Guard: only runs when executed directly ---
if __name__ == "__main__":
# Tests, demos, or standalone behavior
result = my_function()
print(f"Self-test: {result}")Memorize this template. Every .py file you write for this course (and beyond) should follow it.
Next lectures:
This week’s lab:
lorenz_project/ files.py files + the saved figurePro tip: Build and test one file at a time. Run each self-test before moving to the next file.
Key things to remember:
.py fileimport executes the file, __name__ controls what runsintegrators → lorenz63 → plotting → run_lorenz_ensembleYou’ve got this! You already wrote all the hard code for the midterm. Now you’re organizing it and extending it.
Prof. Will Chapman
wchapman@colorado.edu
willychap.github.io
ATOC Building, CU Boulder
See you next week!

ATOC 4815/5815 - Week 5.5