Building the App Loop
You have the pieces. Now you assemble them into one script that runs start to finish: load the data, clean it, summarize it, send it to the model, and print the analysis. Then you make it sturdy with error handling so it fails gracefully instead of crashing on the first messy file or network hiccup.
The error-handling concepts run in the playground (they are plain Python). The full script that makes a real API call runs on your machine.
What You'll Learn
- Combine load, clean, summarize, analyze, and present into one script
- Why error handling matters the moment real users and real files appear
- Use
try/exceptto catch the failures that actually happen - Validate inputs before you spend money on an API call
- Structure the script so it is easy to lift into a web app next
The whole loop, in order
Here is the complete command-line version. It pulls together everything from the previous lessons into one file, app.py. Read it top to bottom; the structure is the same four boxes from lesson one.
import os
import sys
import pandas as pd
from dotenv import load_dotenv
from anthropic import Anthropic
load_dotenv()
_client = Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])
# --- 1. Load + clean ---
def load_and_clean(path, numeric_cols, required_cols):
df = pd.read_csv(path)
for col in numeric_cols:
df[col] = pd.to_numeric(df[col], errors="coerce")
return df.dropna(subset=required_cols)
# --- 2. Summarize ---
def summarize_dataframe(df, max_rows=5):
lines = [
f"Rows: {df.shape[0]}, Columns: {df.shape[1]}",
"Columns: " + ", ".join(df.columns),
"",
"Numeric summary:",
df.describe().round(2).to_string(),
"",
f"First {max_rows} rows:",
df.head(max_rows).to_string(index=False),
]
return "\n".join(lines)
# --- 3. Analyze ---
def analyze(df, question):
brief = summarize_dataframe(df)
prompt = (
f"Task: {question}\n"
"Give 4 to 6 bullet points, then one next step.\n"
"Rules: only use numbers in the brief; if unknown, say so.\n\n"
f"DATA BRIEF:\n{brief}\n"
)
response = _client.messages.create(
model="claude-sonnet-4-6",
max_tokens=600,
system="You are a precise data analyst. Never invent numbers.",
messages=[{"role": "user", "content": prompt}],
)
return response.content[0].text
# --- 4. Present (run the loop) ---
def main():
df = load_and_clean("sales.csv", numeric_cols=["revenue", "units"],
required_cols=["revenue", "region"])
print(f"Loaded {len(df)} clean rows.\n")
analysis = analyze(df, question="Which region is underperforming and why?")
print(analysis)
if __name__ == "__main__":
main()
Run it with python app.py and you get a written analysis of sales.csv in your terminal. That is a real, working AI data app. Everything from here is about making it sturdier and friendlier.
Why error handling now
The script above works when everything is perfect: the file exists, the columns are named right, the key is valid, the network is up. The moment another person uses it, none of that is guaranteed. They upload the wrong file. A column is missing. The key expired. The internet blips. Without handling, each of those is an ugly crash and a stack trace. With handling, each becomes a clear message the user can act on.
The shape of try / except
try runs code that might fail. except catches a specific failure and lets you respond instead of crashing. Catch specific exceptions, not a bare except, so you do not accidentally swallow real bugs.
Validate before you spend
The cheapest error to handle is the one you catch before calling the API. Check the data first: does it have rows, do the columns you need exist? If not, stop early with a clear message. No point paying for an API call on an empty or wrong file.
Wrap the risky parts
Now layer handling around the two things that genuinely fail in this app: reading the file and calling the model. Each gets a try / except that returns a friendly message rather than crashing.
def load_and_clean(path, numeric_cols, required_cols):
try:
df = pd.read_csv(path)
except FileNotFoundError:
raise SystemExit(f"Could not find the file: {path}")
except pd.errors.EmptyDataError:
raise SystemExit("That file is empty.")
for col in numeric_cols:
if col in df.columns:
df[col] = pd.to_numeric(df[col], errors="coerce")
return df.dropna(subset=required_cols)
def analyze(df, question):
brief = summarize_dataframe(df)
prompt = build_prompt(brief, question) # the prompt builder from before
try:
response = _client.messages.create(
model="claude-sonnet-4-6",
max_tokens=600,
system="You are a precise data analyst. Never invent numbers.",
messages=[{"role": "user", "content": prompt}],
)
return response.content[0].text
except Exception as e:
# Network down, bad key, rate limit, etc. Show something useful.
return f"The AI request failed: {e}"
Catching a broad Exception is acceptable for the single external API call, because there are many possible network and service errors and you mostly want to report them to the user rather than handle each differently. Everywhere else, prefer specific exceptions.
Why this structure travels well
Notice the script is built from small functions: load_and_clean, summarize_dataframe, analyze, plus validation. The main function just wires them together. That separation is deliberate. In the next lesson you replace main with a web interface, and the other functions move over completely unchanged. Good structure now means almost no rework later.
Key Takeaways
- The command-line app is the four boxes wired together: load and clean, summarize, analyze, present.
- Add error handling the moment real files and real users appear, not before shipping.
- Use
try/exceptwith specific exceptions; reserve a broadexceptfor the single external API call. - Validate the data (rows present, required columns exist) before spending money on an API call.
- Keep the work in small functions so the logic lifts straight into a web app next.

