Build a Sales Data Analyzer with Python
Parse, clean, and analyze messy CSV sales data using only Python.
Introduction
30 Second Summary
Every business generates sales data, but raw data is messy. Missing fields, typos, and duplicates make it impossible to trust a spreadsheet at face value.
In this project, you will build a Python script that reads a messy CSV file of sales transactions, cleans the data, computes business metrics, and outputs a formatted report.
What You'll Build
A command-line data analyzer that processes sales transactions from a CSV file, handles real-world data quality issues, and generates both a terminal report and a JSON export.
By the end of this project, you'll have:
- A sample CSV dataset with intentionally messy data to simulate real-world quality issues.
- A Python script that loads, validates, cleans, and analyzes sales transactions.
- A formatted terminal report showing total revenue, best-selling products, and category breakdowns, plus a JSON export file.
- Secret Mission: Add command-line filtering so users can analyze sales by date range or category.
Are there any prerequisites?
This project assumes you have programming experience in another language (such as Java) and Python 3.14.3 already installed on your machine. No external libraries are needed.
Set Up Your Project
Before you can analyze any data, you need a home for your project files. In this step you will create a project folder, confirm Python is ready, and set up two starter files.
One file is a CSV dataset filled with intentionally messy data. The other is the Python script where all of your analysis logic will live.
In this step, get ready to:
- Create a project folder and open it in VS Code.
- Create a sample dataset with messy data to simulate real-world quality issues.
- Create the main script file with imports and a placeholder function.
Create the project folder and open it in VS Code
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal and press Enter to open it.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir sales-analyzer
cd sales-analyzer
- Confirm the folder exists by running:
ls
You should see an empty listing (no files yet) with no errors, confirming you are inside the sales-analyzer folder.
- Open this folder in VS Code by running:
code .
VS Code opens with sales-analyzer as the active workspace.
Terminal doesn't recognize the code command?
On macOS, open VS Code manually, press Cmd+Shift+P to open the Command Palette, then select Shell Command: Install 'code' command in PATH. Restart your terminal and try code . again.
On Windows, the VS Code installer usually adds the command automatically. If it is missing, reinstall VS Code and make sure the Add to PATH checkbox is selected.
help me fix the code command not found error in my terminal
- Verify Python is available by running this command in the VS Code integrated terminal (open it with Ctrl+` on both macOS and Windows):
python --version
You should see Python 3.14.3 printed in the terminal.
Seeing a different version or command not found?
If your terminal shows a version lower than 3.14.3, or if it says the command is not recognized, make sure Python 3.14.3 is installed and added to your system PATH.
On macOS you may need to run python3 --version instead, because the default python may point to an older system install.
help me get Python 3.14 working in my terminal
Create the sample dataset
Real-world data is rarely clean. Your sample dataset includes rows with missing fields, a non-numeric price value, and a duplicate entry so you can practice defensive data handling later.
- In VS Code, hover over the sales-analyzer folder name in the Explorer sidebar on the left.
- Click the New File icon (a page with a + sign).
- Type sales_data.csv and press Enter.
- Paste the following data into the new file:
order_id,order_date,product,category,price,quantity
1001,2024-01-15,Wireless Mouse,Electronics,29.99,2
1002,2024-01-15,USB-C Cable,Electronics,12.99,5
1003,2024-01-16,Notebook A5,Stationery,4.99,10
1004,2024-01-16,Wireless Mouse,Electronics,29.99,1
1005,2024-01-17,Desk Lamp,Home Office,45.00,
1006,2024-01-17,Mechanical Keyboard,Electronics,89.99,1
1007,2024-01-18,Notebook A5,Stationery,4.99,3
1008,2024-01-18,Monitor Stand,Home Office,34.99,2
1009,2024-01-19,USB-C Cable,Electronics,twelve,2
1010,2024-01-19,Wireless Mouse,Electronics,29.99,3
1011,2024-01-20,Desk Lamp,Home Office,45.00,1
1012,2024-01-20,Mechanical Keyboard,Electronics,89.99,2
1013,2024-01-21,Notebook A5,Stationery,4.99,
1014,2024-01-21,USB-C Cable,Electronics,12.99,4
1015,2024-01-22,Monitor Stand,Home Office,34.99,1
1001,2024-01-15,Wireless Mouse,Electronics,29.99,2
What makes this data messy?
Three problems are hiding in this dataset. Your script will need to handle all of them gracefully.
- Orders 1005 and 1013 have empty quantity fields (look at the trailing comma with nothing after it).
- Order 1009 has twelve as the price instead of a number.
- The last row is a duplicate of order 1001.
- Save the file with Cmd+S (macOS) or Ctrl+S (Windows).
You should see sales_data.csv listed in the Explorer sidebar with no unsaved-changes dot on its tab.
Don't see the file in the sidebar?
Make sure you created the file inside the sales-analyzer folder, not in a parent directory. If the Explorer sidebar is collapsed, click the top icon on the left activity bar (two overlapping pages) to expand it.
help me find my sales_data.csv file in VS Code
✔️ Awesome, I've got everything!
Great. Make sure the file is saved before moving on.
ⓧ I'd like to double check the full code
order_id,order_date,product,category,price,quantity
1001,2024-01-15,Wireless Mouse,Electronics,29.99,2
1002,2024-01-15,USB-C Cable,Electronics,12.99,5
1003,2024-01-16,Notebook A5,Stationery,4.99,10
1004,2024-01-16,Wireless Mouse,Electronics,29.99,1
1005,2024-01-17,Desk Lamp,Home Office,45.00,
1006,2024-01-17,Mechanical Keyboard,Electronics,89.99,1
1007,2024-01-18,Notebook A5,Stationery,4.99,3
1008,2024-01-18,Monitor Stand,Home Office,34.99,2
1009,2024-01-19,USB-C Cable,Electronics,twelve,2
1010,2024-01-19,Wireless Mouse,Electronics,29.99,3
1011,2024-01-20,Desk Lamp,Home Office,45.00,1
1012,2024-01-20,Mechanical Keyboard,Electronics,89.99,2
1013,2024-01-21,Notebook A5,Stationery,4.99,
1014,2024-01-21,USB-C Cable,Electronics,12.99,4
1015,2024-01-22,Monitor Stand,Home Office,34.99,1
1001,2024-01-15,Wireless Mouse,Electronics,29.99,2
Create the main script file
Every Python project needs an entry point. This starter file imports the three standard library modules you will use throughout the project and defines a placeholder main() function.
- In the Explorer sidebar, hover over sales-analyzer and click the New File icon again.
- Type analyzer.py and press Enter.
- Paste the following code into the new file:
import csv
import json
import sys
def main():
pass
if __name__ == "__main__":
main()
What does this code do?
- csv lets you read and write CSV files without manually splitting commas.
- json handles exporting your analysis results to a JSON file.
- sys provides access to command-line arguments and the ability to exit the script on errors.
- The if __name__ == "__main__" block ensures main() only runs when you execute the file directly, not when it is imported as a module.
- Save the file with Cmd+S (macOS) or Ctrl+S (Windows).
- In the integrated terminal, run the script to confirm there are no syntax errors:
python analyzer.py
The command should complete silently with no output and no errors. That means Python found and executed your file successfully.
Seeing a SyntaxError or ModuleNotFoundError?
Double-check that you pasted the code exactly as shown. A common mistake is missing the colon after def main() or misspelling one of the import names.
help me debug my analyzer.py syntax error
✔️ Awesome, I've got everything!
Great. Make sure both files are saved before moving on.
ⓧ I'd like to double check the full code
import csv
import json
import sys
def main():
pass
if __name__ == "__main__":
main()
Your project skeleton is ready. Next up, you will write the function that loads this CSV into memory and cleans out the bad rows.
Load and Clean the Data
Your project folder is set up with a sample dataset and a script skeleton. Now it's time to bring that data into Python and deal with the mess.
Real-world data is rarely clean. Your sales_data.csv file has missing values, non-numeric prices, and duplicate entries. In this step, you'll write two functions that load the raw CSV data and filter out the bad rows before any analysis happens.
In this step, get ready to:
- Write a function that reads the CSV file into Python dictionaries.
- Write a function that validates each row and rejects messy data.
- Run the script and confirm it catches all three problem rows.
Write the load_sales_data function
Python's csv.DictReader reads each row of a CSV file as a dictionary, using the header row as keys. This means you can access fields by name (like row["product"]) instead of by index number.
- In analyzer.py, find the line that says def main():. Add the following function above it:
def load_sales_data(filename):
"""Read CSV file and return a list of dictionaries."""
data = []
try:
with open(filename, newline='', encoding='utf-8') as csvfile:
reader = csv.DictReader(csvfile)
for row in reader:
data.append(row)
except FileNotFoundError:
print(f"Error: '{filename}' not found.")
sys.exit(1)
print(f"Loaded {len(data)} raw records.")
return data
What does this code do?
- with open(filename, newline='', encoding='utf-8') opens the file safely. The with keyword guarantees the file is closed even if an error occurs.
- newline='' is required by the csv module on Windows to prevent blank rows appearing in your data.
- csv.DictReader(csvfile) automatically uses the first row as field names, so each row becomes a dictionary like {"order_id": "1001", "product": "Wireless Mouse", ...}.
- The try/except FileNotFoundError block catches the case where the CSV file doesn't exist and exits with a helpful message.
- Save analyzer.py.
- You won't see any output yet. The final substep connects this function to main() so you can run it.
Why newline='' for CSV files?
The Python docs state: "If csvfile is a file object, it should be opened with newline=''." Without it, the csv module on Windows can misinterpret line endings and insert blank rows between your data.
Write the clean_sales_data function
Loading data is only half the job. The cleaning function needs to catch three types of problems: duplicate orders, missing fields, and values that aren't valid numbers.
- In analyzer.py, add the following function below load_sales_data and above def main()::
def clean_sales_data(data):
"""Validate and clean sales records. Returns (clean_records, rejection_log)."""
clean = []
rejections = []
seen_ids = set()
for row in data:
order_id = row.get("order_id", "").strip()
# Check for duplicate order IDs
if order_id in seen_ids:
rejections.append(f"Order {order_id}: duplicate entry")
continue
seen_ids.add(order_id)
# Check required fields are present
if not row.get("price", "").strip() or not row.get("quantity", "").strip():
rejections.append(f"Order {order_id}: missing price or quantity")
continue
# Validate numeric fields
try:
price = float(row["price"])
quantity = int(row["quantity"])
except ValueError:
rejections.append(f"Order {order_id}: non-numeric price or quantity")
continue
if price <= 0 or quantity <= 0:
rejections.append(f"Order {order_id}: price or quantity not positive")
continue
clean.append({
"order_id": order_id,
"order_date": row.get("order_date", "").strip(),
"product": row.get("product", "").strip(),
"category": row.get("category", "").strip(),
"price": price,
"quantity": quantity,
})
print(f"Valid records: {len(clean)} | Rejected: {len(rejections)}")
for reason in rejections:
print(f" - {reason}")
return clean
What does this code do?
This function runs each row through a gauntlet of checks. If a row fails any check, it gets logged to the rejections list and skipped with continue.
- seen_ids is a set() that tracks which order IDs have already appeared. Sets give instant lookup, so checking for duplicates is fast.
- The try/except ValueError block attempts to convert price to a float and quantity to an int. If either conversion fails (like the word "twelve" for a price), Python raises a ValueError and the row is rejected.
- Only rows that pass every check get appended to the clean list with properly typed values (strings stay strings, numbers become actual numbers).
- Save analyzer.py.
- You won't see output yet. The next substep wires both functions into main() and runs the script.
Seeing a red underline in VS Code?
If your editor highlights an indentation error, make sure every line inside the for loop uses exactly 4 spaces of indentation per level. Mixing tabs and spaces causes Python to reject the file.
help me fix an indentation error in my clean_sales_data function
Run the script and verify the output
Both functions are written. Now you need to call them from main() so Python actually executes them when you run the script.
- In analyzer.py, scroll down to the main() function.
- Find this line:
def main():
pass
- Replace pass with the two function calls below, so main() now looks like this:
def main():
data = load_sales_data("sales_data.csv")
clean = clean_sales_data(data)
- Save analyzer.py.
- Run the script by entering this command in your terminal:
python analyzer.py
You should see output that looks like this:
Loaded 16 raw records.
Valid records: 13 | Rejected: 3
- Order 1005: missing price or quantity
- Order 1009: non-numeric price or quantity
- Order 1001: duplicate entry
What should I see?
The script correctly identified all three problems in your dataset:
- Order 1005 has an empty quantity field (the comma with nothing after it).
- Order 1009 has "twelve" instead of a number for its price.
- The last row is a duplicate of order 1001 (same order ID repeated).
Seeing a FileNotFoundError?
This means Python can't find sales_data.csv in the current directory. Make sure your terminal is inside the sales-analyzer folder before running the command.
help me fix a FileNotFoundError when running my analyzer script
✔️ Awesome, I've got everything!
Great. Double check you've saved analyzer.py before moving on.
ⓧ I'd like to double check the full code
Here's the complete analyzer.py file at this point. Compare it with yours and fix any differences.
import csv
import json
import sys
def load_sales_data(filename):
"""Read CSV file and return a list of dictionaries."""
data = []
try:
with open(filename, newline='', encoding='utf-8') as csvfile:
reader = csv.DictReader(csvfile)
for row in reader:
data.append(row)
except FileNotFoundError:
print(f"Error: '{filename}' not found.")
sys.exit(1)
print(f"Loaded {len(data)} raw records.")
return data
def clean_sales_data(data):
"""Validate and clean sales records. Returns (clean_records, rejection_log)."""
clean = []
rejections = []
seen_ids = set()
for row in data:
order_id = row.get("order_id", "").strip()
# Check for duplicate order IDs
if order_id in seen_ids:
rejections.append(f"Order {order_id}: duplicate entry")
continue
seen_ids.add(order_id)
# Check required fields are present
if not row.get("price", "").strip() or not row.get("quantity", "").strip():
rejections.append(f"Order {order_id}: missing price or quantity")
continue
# Validate numeric fields
try:
price = float(row["price"])
quantity = int(row["quantity"])
except ValueError:
rejections.append(f"Order {order_id}: non-numeric price or quantity")
continue
if price <= 0 or quantity <= 0:
rejections.append(f"Order {order_id}: price or quantity not positive")
continue
clean.append({
"order_id": order_id,
"order_date": row.get("order_date", "").strip(),
"product": row.get("product", "").strip(),
"category": row.get("category", "").strip(),
"price": price,
"quantity": quantity,
})
print(f"Valid records: {len(clean)} | Rejected: {len(rejections)}")
for reason in rejections:
print(f" - {reason}")
return clean
def main():
data = load_sales_data("sales_data.csv")
clean = clean_sales_data(data)
if __name__ == "__main__":
main()
Your script can now load messy data and separate the good records from the bad ones. Next up, you'll crunch the clean data into actual business metrics.
Analyze the Data
Your script can now load a CSV and reject bad rows. You have 13 clean records ready for analysis.
In this step, you will turn those records into actionable business metrics. You will write a function that computes totals, finds the best-selling product, breaks revenue down by category, and identifies the highest-earning day.
In this step, get ready to:
- Write an analyze function that computes business metrics using dictionary accumulation patterns.
- Return results as a single dictionary and verify the calculations.
Write the analyze function
In Java, computing totals across a collection typically means declaring accumulators, writing for-each loops, and manually tracking maximums. Python offers a more expressive approach with generator expressions and the dict.get() accumulation pattern.
- In analyzer.py, find the clean_sales_data function's closing return clean line.
- Add the following analyze function below it, leaving two blank lines between the functions:
def analyze(data):
"""Compute business metrics from cleaned data."""
total_revenue = sum(row["price"] * row["quantity"] for row in data)
total_items = sum(row["quantity"] for row in data)
# Best-selling product by quantity
product_qty = {}
for row in data:
product_qty[row["product"]] = product_qty.get(row["product"], 0) + row["quantity"]
best_seller = max(product_qty, key=product_qty.get)
# Revenue by category
category_revenue = {}
for row in data:
revenue = row["price"] * row["quantity"]
category_revenue[row["category"]] = category_revenue.get(row["category"], 0) + revenue
# Average order value
avg_order_value = total_revenue / len(data) if data else 0
# Highest revenue day
daily_revenue = {}
for row in data:
revenue = row["price"] * row["quantity"]
daily_revenue[row["order_date"]] = daily_revenue.get(row["order_date"], 0) + revenue
best_day = max(daily_revenue, key=daily_revenue.get)
return {
"total_revenue": round(total_revenue, 2),
"total_items_sold": total_items,
"num_orders": len(data),
"avg_order_value": round(avg_order_value, 2),
"best_selling_product": best_seller,
"best_selling_product_qty": product_qty[best_seller],
"best_revenue_day": best_day,
"best_revenue_day_amount": round(daily_revenue[best_day], 2),
"revenue_by_category": {k: round(v, 2) for k, v in category_revenue.items()},
"product_quantities": product_qty,
}
What does this code do?
This function uses three key Python patterns you would not find in Java:
- Generator expressions inside sum() compute totals in a single line. No need for a loop variable or accumulator.
- Dictionary accumulation with dict.get(key, 0) replaces the Java pattern of checking containsKey() before inserting. It returns 0 if the key is missing, so you can add directly.
- max() with a key function finds the dictionary key whose value is the largest. product_qty.get (without parentheses) passes the method itself as the comparison function.
- The if data else 0 conditional expression prevents a division-by-zero error if the data list is empty.
Why dict.get() instead of defaultdict?
Python has convenience classes like defaultdict and Counter that handle missing keys automatically. This project uses dict.get() instead because it teaches the fundamental dictionary operation first.
Once you understand dict.get(key, default), the convenience classes make immediate sense as shortcuts on top of it.
- Save analyzer.py.
Seeing a syntax error?
- Make sure there are exactly two blank lines between clean_sales_data and analyze. Python does not require this, but VS Code may highlight a style warning if it is missing.
- Check that every line inside the function is indented with exactly four spaces. Mixing tabs and spaces causes an IndentationError.
- Verify that the return dictionary's closing brace } is on its own line, indented to the same level as the opening return {.
Help me fix a syntax error in my analyze function
Wire the analysis into main and run it
The function is ready. Now you need to call it from main() and print the results to confirm the calculations look correct.
- In analyzer.py, scroll down to the main() function.
- Find this line:
clean = clean_sales_data(data)
- Add the following two lines directly below it:
metrics = analyze(clean)
print(metrics)
What does this do?
This passes the list of cleaned records into analyze() and stores the resulting metrics dictionary in a variable called metrics. Printing it raw lets you inspect all the computed values before you build a formatted report in the next step.
- Save analyzer.py.
- Run the script by entering this command in your terminal:
python analyzer.py
You should see the loading and cleaning output from before, followed by a dictionary printed to the terminal containing keys like total_revenue, best_selling_product, and revenue_by_category.
Seeing a NameError for analyze?
- Make sure the analyze function is defined above main() in the file. Python reads top to bottom, so the function must exist before it is called.
- Check that the function name is spelled exactly analyze in both the definition and the call in main().
Help me debug a NameError in my analyzer script
✔️ Awesome, I've got everything!
Great. Double check you have saved analyzer.py and that running python analyzer.py prints a metrics dictionary after the cleaning output.
ⓧ I'd like to double check the full code
Here is the full analyzer.py as it should look at the end of this step:
import csv
import json
import sys
def load_sales_data(filename):
"""Read CSV file and return a list of dictionaries."""
data = []
try:
with open(filename, newline='', encoding='utf-8') as csvfile:
reader = csv.DictReader(csvfile)
for row in reader:
data.append(row)
except FileNotFoundError:
print(f"Error: '{filename}' not found.")
sys.exit(1)
print(f"Loaded {len(data)} raw records.")
return data
def clean_sales_data(data):
"""Validate and clean sales records. Returns (clean_records, rejection_log)."""
clean = []
rejections = []
seen_ids = set()
for row in data:
order_id = row.get("order_id", "").strip()
if order_id in seen_ids:
rejections.append(f"Order {order_id}: duplicate entry")
continue
seen_ids.add(order_id)
if not row.get("price", "").strip() or not row.get("quantity", "").strip():
rejections.append(f"Order {order_id}: missing price or quantity")
continue
try:
price = float(row["price"])
quantity = int(row["quantity"])
except ValueError:
rejections.append(f"Order {order_id}: non-numeric price or quantity")
continue
if price <= 0 or quantity <= 0:
rejections.append(f"Order {order_id}: price or quantity not positive")
continue
clean.append({
"order_id": order_id,
"order_date": row.get("order_date", "").strip(),
"product": row.get("product", "").strip(),
"category": row.get("category", "").strip(),
"price": price,
"quantity": quantity,
})
print(f"Valid records: {len(clean)} | Rejected: {len(rejections)}")
for reason in rejections:
print(f" - {reason}")
return clean
def analyze(data):
"""Compute business metrics from cleaned data."""
total_revenue = sum(row["price"] * row["quantity"] for row in data)
total_items = sum(row["quantity"] for row in data)
# Best-selling product by quantity
product_qty = {}
for row in data:
product_qty[row["product"]] = product_qty.get(row["product"], 0) + row["quantity"]
best_seller = max(product_qty, key=product_qty.get)
# Revenue by category
category_revenue = {}
for row in data:
revenue = row["price"] * row["quantity"]
category_revenue[row["category"]] = category_revenue.get(row["category"], 0) + revenue
# Average order value
avg_order_value = total_revenue / len(data) if data else 0
# Highest revenue day
daily_revenue = {}
for row in data:
revenue = row["price"] * row["quantity"]
daily_revenue[row["order_date"]] = daily_revenue.get(row["order_date"], 0) + revenue
best_day = max(daily_revenue, key=daily_revenue.get)
return {
"total_revenue": round(total_revenue, 2),
"total_items_sold": total_items,
"num_orders": len(data),
"avg_order_value": round(avg_order_value, 2),
"best_selling_product": best_seller,
"best_selling_product_qty": product_qty[best_seller],
"best_revenue_day": best_day,
"best_revenue_day_amount": round(daily_revenue[best_day], 2),
"revenue_by_category": {k: round(v, 2) for k, v in category_revenue.items()},
"product_quantities": product_qty,
}
def main():
data = load_sales_data("sales_data.csv")
clean = clean_sales_data(data)
metrics = analyze(clean)
print(metrics)
if __name__ == "__main__":
main()
Your script now computes real business insights from clean data. Next up, you will format these metrics into a readable terminal report and export them to a JSON file.
Generate the Report and Export
Your analyze function computes all the business metrics, but right now it just dumps a raw dictionary to the terminal.
In this step, you'll build two presentation layers: a formatted terminal report using Python's f-string alignment specifiers, and a JSON export that other tools can consume.
In this step, get ready to:
- Write a formatted terminal report with aligned columns and currency formatting.
- Export the metrics dictionary to a JSON file.
- Wire the full pipeline together and run it end-to-end.
Write the display_report function
Python's f-strings support alignment specifiers that let you build table-like output in the terminal. In Java you would use String.format() with positional arguments. Python's approach is more readable because the format spec lives right next to the value.
- In analyzer.py, add the following function below your analyze function:
def display_report(metrics):
"""Print a formatted terminal report."""
print("\n" + "=" * 50)
print(f"{'SALES ANALYSIS REPORT':^50}")
print("=" * 50)
print(f"\n{'Total Revenue:':<25} ${metrics['total_revenue']:>10,.2f}")
print(f"{'Total Items Sold:':<25} {metrics['total_items_sold']:>10,}")
print(f"{'Number of Orders:':<25} {metrics['num_orders']:>10,}")
print(f"{'Avg Order Value:':<25} ${metrics['avg_order_value']:>10,.2f}")
print(f"\n{'Best Selling Product:':<25} {metrics['best_selling_product']}")
print(f"{' Quantity Sold:':<25} {metrics['best_selling_product_qty']}")
print(f"\n{'Best Revenue Day:':<25} {metrics['best_revenue_day']}")
print(f"{' Revenue:':<25} ${metrics['best_revenue_day_amount']:>10,.2f}")
print(f"\n{'Revenue by Category:'}")
print(f" {'Category':<20} {'Revenue':>12}")
print(f" {'-' * 20} {'-' * 12}")
for category, revenue in sorted(metrics["revenue_by_category"].items()):
print(f" {category:<20} ${revenue:>10,.2f}")
print("\n" + "=" * 50)
What do these format specifiers mean?
Each f-string uses a colon : followed by alignment and formatting instructions:
- :<25 left-aligns text in a 25-character-wide column.
- :>10,.2f right-aligns a number in 10 characters, adds comma separators, and formats to 2 decimal places.
- :^50 centers text in a 50-character-wide column.
You won't see a change yet. The next chunk wires this function into main().
Write the export_json function
A terminal report is great for quick inspection, but other tools (dashboards, notebooks, APIs) need structured data. The json module's json.dump function serializes a Python dictionary straight to a file.
- In analyzer.py, add the following function below display_report:
def export_json(metrics, filename):
"""Export metrics to a JSON file."""
with open(filename, 'w', encoding='utf-8') as f:
json.dump(metrics, f, indent=2)
print(f"\nReport exported to '{filename}'.")
What does this code do?
- json.dump(metrics, f, indent=2) writes the dictionary to the file as pretty-printed JSON with 2-space indentation.
- The context manager (with open(...)) ensures the file is closed properly even if an error occurs during writing.
You won't see a change yet. The next edit wires both new functions into main().
Wire the full pipeline in main()
Your main() function currently prints the raw metrics dictionary. Let's replace that with the two presentation functions you just wrote.
- In analyzer.py, scroll down to the main() function.
- Find this line:
print(metrics)
- Replace print(metrics) with the following two lines:
display_report(metrics)
export_json(metrics, "report.json")
- Save analyzer.py.
- Run the full pipeline by executing this command in your terminal:
python analyzer.py
You should see a formatted report with aligned columns, currency values, and a category breakdown table printed in your terminal.
- Confirm that a report.json file now exists in your sales-analyzer folder by running:
ls
You should see analyzer.py, sales_data.csv, and report.json listed in the output.
- Open report.json in VS Code by clicking it in the Explorer sidebar.
- Confirm it contains keys like total_revenue, best_selling_product, and revenue_by_category with properly indented JSON.
Not seeing the formatted report?
- Check that you removed the print(metrics) line and replaced it with display_report(metrics). If both lines exist, you'll see the raw dictionary followed by the formatted report.
- If you see a NameError, make sure your display_report and export_json functions are defined above main() in the file.
- If report.json is missing, check that export_json(metrics, "report.json") appears in your main() function after display_report(metrics).
Why isn't my sales report printing correctly?
✔️ Awesome, I've got everything!
Great. Double check you've saved your file.
ⓧ I'd like to double check the full code
import csv
import json
import sys
def load_sales_data(filename):
"""Read CSV file and return a list of dictionaries."""
data = []
try:
with open(filename, newline='', encoding='utf-8') as csvfile:
reader = csv.DictReader(csvfile)
for row in reader:
data.append(row)
except FileNotFoundError:
print(f"Error: '{filename}' not found.")
sys.exit(1)
print(f"Loaded {len(data)} raw records.")
return data
def clean_sales_data(data):
"""Validate and clean sales records. Returns (clean_records, rejection_log)."""
clean = []
rejections = []
seen_ids = set()
for row in data:
order_id = row.get("order_id", "").strip()
if order_id in seen_ids:
rejections.append(f"Order {order_id}: duplicate entry")
continue
seen_ids.add(order_id)
if not row.get("price", "").strip() or not row.get("quantity", "").strip():
rejections.append(f"Order {order_id}: missing price or quantity")
continue
try:
price = float(row["price"])
quantity = int(row["quantity"])
except ValueError:
rejections.append(f"Order {order_id}: non-numeric price or quantity")
continue
if price <= 0 or quantity <= 0:
rejections.append(f"Order {order_id}: price or quantity not positive")
continue
clean.append({
"order_id": order_id,
"order_date": row.get("order_date", "").strip(),
"product": row.get("product", "").strip(),
"category": row.get("category", "").strip(),
"price": price,
"quantity": quantity,
})
print(f"Valid records: {len(clean)} | Rejected: {len(rejections)}")
for reason in rejections:
print(f" - {reason}")
return clean
def analyze(data):
"""Compute business metrics from cleaned data."""
total_revenue = sum(row["price"] * row["quantity"] for row in data)
total_items = sum(row["quantity"] for row in data)
product_qty = {}
for row in data:
product_qty[row["product"]] = product_qty.get(row["product"], 0) + row["quantity"]
best_seller = max(product_qty, key=product_qty.get)
category_revenue = {}
for row in data:
revenue = row["price"] * row["quantity"]
category_revenue[row["category"]] = category_revenue.get(row["category"], 0) + revenue
avg_order_value = total_revenue / len(data) if data else 0
daily_revenue = {}
for row in data:
revenue = row["price"] * row["quantity"]
daily_revenue[row["order_date"]] = daily_revenue.get(row["order_date"], 0) + revenue
best_day = max(daily_revenue, key=daily_revenue.get)
return {
"total_revenue": round(total_revenue, 2),
"total_items_sold": total_items,
"num_orders": len(data),
"avg_order_value": round(avg_order_value, 2),
"best_selling_product": best_seller,
"best_selling_product_qty": product_qty[best_seller],
"best_revenue_day": best_day,
"best_revenue_day_amount": round(daily_revenue[best_day], 2),
"revenue_by_category": {k: round(v, 2) for k, v in category_revenue.items()},
"product_quantities": product_qty,
}
def display_report(metrics):
"""Print a formatted terminal report."""
print("\n" + "=" * 50)
print(f"{'SALES ANALYSIS REPORT':^50}")
print("=" * 50)
print(f"\n{'Total Revenue:':<25} ${metrics['total_revenue']:>10,.2f}")
print(f"{'Total Items Sold:':<25} {metrics['total_items_sold']:>10,}")
print(f"{'Number of Orders:':<25} {metrics['num_orders']:>10,}")
print(f"{'Avg Order Value:':<25} ${metrics['avg_order_value']:>10,.2f}")
print(f"\n{'Best Selling Product:':<25} {metrics['best_selling_product']}")
print(f"{' Quantity Sold:':<25} {metrics['best_selling_product_qty']}")
print(f"\n{'Best Revenue Day:':<25} {metrics['best_revenue_day']}")
print(f"{' Revenue:':<25} ${metrics['best_revenue_day_amount']:>10,.2f}")
print(f"\n{'Revenue by Category:'}")
print(f" {'Category':<20} {'Revenue':>12}")
print(f" {'-' * 20} {'-' * 12}")
for category, revenue in sorted(metrics["revenue_by_category"].items()):
print(f" {category:<20} ${revenue:>10,.2f}")
print("\n" + "=" * 50)
def export_json(metrics, filename):
"""Export metrics to a JSON file."""
with open(filename, 'w', encoding='utf-8') as f:
json.dump(metrics, f, indent=2)
print(f"\nReport exported to '{filename}'.")
def main():
data = load_sales_data("sales_data.csv")
clean = clean_sales_data(data)
metrics = analyze(clean)
display_report(metrics)
export_json(metrics, "report.json")
if __name__ == "__main__":
main()
Secret mission
Add Command-Line Filtering
Your analyzer works great on the full dataset, but what if a user only cares about Electronics sales from last week? Add command-line arguments so users can slice the data by category or date range before analysis runs.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your files for future experimentation or delete them entirely.
Resources you used:
- The sales-analyzer folder on your Desktop containing analyzer.py, sales_data.csv, and report.json.
Keep everything running
No action needed. Choose this if you plan to keep experimenting with the analyzer or extend it further.
- Your sales-analyzer folder is ready to extend with new features, additional CSV datasets, or integration with other tools.
Pause - I'll come back to this later
Since this project is entirely local with no running processes, there is nothing to pause. Your files will stay on your Desktop until you return.
- No running services or background processes to stop.
- Your sales-analyzer folder stays where it is, ready for you when you come back.
Delete - I don't want to use this again
Remove the project folder and all its contents to start fresh.
- Delete the sales-analyzer folder by running this command in your terminal:
macOS
rm -rf ~/Desktop/sales-analyzer
Windows
rmdir /s /q %USERPROFILE%\Desktop\sales-analyzer
- Confirm the folder is gone by running:
ls ~/Desktop
You should no longer see sales-analyzer in the output.
Nice Work!
Nice Work!
You just built a complete data processing pipeline in Python using nothing but the standard library. From messy CSV data to a formatted terminal report and a clean JSON export, your analyzer handles it all.
You've learned how to:
- Read and clean messy CSV data using csv.DictReader, context managers, and defensive validation with try/except.
- Compute business metrics using dictionary accumulation patterns and list comprehensions that replace verbose Java loops.
- Format terminal output with f-string alignment specifiers and export structured data to JSON using json.dump.
- Secret Mission: Added command-line argument parsing with sys.argv to filter sales data dynamically by category and date range.
Ready to quiz yourself?