Build a CSV Data Quality Auditor
Build a Python CLI that audits CSV ticket data and writes JSON findings.
Introduction
30 Second Summary
A support-ticket export can look tidy until one broken value stops the whole process. Every unchecked row then becomes a question mark for the team relying on that data.
In this project, you will build a Python auditor with a command-line interface for support-ticket CSV files. It preserves malformed rows as findings in a readable JSON report.
What You'll Build
Your finished auditor turns a messy ticket export into a completed six-row audit with every invalid record preserved for review.
By the end of this project, you'll have:
- A terminal summary for a clean four-row dataset with an average resolution time of 26.25 minutes.
- A structured JSON report that preserves four malformed rows as row-level findings.
- A public GitHub repository backed by a repeatable unit-test suite. Its README presents your debugging process as an engineering story.
- Secret Mission: Add a GitHub Actions workflow that runs the tests automatically on every push and pull request.
Are there any prerequisites?
You need basic Python experience with functions, dictionaries, loops, and exceptions.
Your Windows computer needs Python 3.15.0, Visual Studio Code, Git, plus a GitHub account.
No third-party Python packages are required.
Before We Start
Before any hands-on work begins, lock in the goal: a Python command-line auditor that checks required fields, allowed priorities, duplicate ticket IDs, and resolution durations. This validation turns unreliable CSV ticket data into a structured JSON report so an internal engineering workflow can continue safely.
Verify and Initialize Your Windows Workspace
Reliable automation starts with a known Python runtime. Your auditor depends on version 3.15.0 so its behavior stays reproducible.
A trustworthy Git history also needs a traceable commit identity. This step verifies your tools before it creates the repository that will hold the auditor.
In this step, get ready to:
- Verify Python, Git, Visual Studio Code, and PowerShell command-line access.
- Confirm your global Git commit identity.
- Prepare the local repository structure on the main branch.
Verify your development tools
PowerShell can confirm each tool through its command-line entry point. A successful check proves that the required tools are available to your project.
- Press the Windows key to open Windows search.
- Type PowerShell into the search field.
- Press Enter to open PowerShell.
- Check the installed tools by running these commands:
python --version
git --version
code --version
What do these checks prove?
- The python check confirms which Python runtime handles the auditor.
- The git check confirms that version control is available.
- The code check confirms that PowerShell can communicate with Visual Studio Code.
- Git for Windows 2.29 or later supports Git Credential Manager for future HTTPS authentication with GitHub.
✔️ All three checks pass
Good progress. Your runtime, version control, and editor can all respond from PowerShell.
- Confirm that Python reports 3.15.0.
- Confirm that Git reports 2.29 or later.
- Confirm that Visual Studio Code prints its version details.
ⓧ I see an older version
An outdated runtime can change how the project behaves. Bring each failed requirement up to the required version before continuing.
- Install Python 3.15.0 from the official Python Downloads page if Python reports a different version.
- Update Git from the official Git for Windows page if Git reports a version below 2.29.
- Close the current PowerShell window after the update finishes.
- Repeat the Windows search steps above to open a fresh PowerShell window.
- Run the three checks again from the command block above.
Continue when Python reports 3.15.0 and Git reports 2.29 or later.
ⓧ A command is not found
A missing command means that PowerShell cannot reach the corresponding tool. Repair the missing command before you create the project.
- Install Python 3.15.0 from the official Python Downloads page if python is unavailable.
- Install Git for Windows if git is unavailable.
- Use the official Visual Studio Code command-line guide to make code available if that command is missing.
- Close the current PowerShell window after repairing the missing tool.
- Repeat the Windows search steps above to open a fresh PowerShell window.
- Run the three checks again from the command block above.
Each command should print version information before you continue.
Still blocked by a tool check?
Close every PowerShell window after installing or updating a tool. A fresh window reloads the command paths used by your terminal.
Use this prompt if a command still fails: Help me diagnose which Windows development tool PowerShell cannot find.
Set your Git commit identity
Git attaches a configured name and email address to each commit. Confirming both values now keeps your future project history attributable.
- Check your global Git identity by running these commands:
git config --global user.name
git config --global user.email
What do these settings control?
The user.name setting supplies the author name for future commits. The user.email setting supplies the associated commit email.
The --global option stores these values for repositories created under your Windows account.
✔️ Both values are displayed
Your Git identity is already configured. The two commands should show your chosen commit name and email address.
ⓧ A value is empty
An empty line means that Git has no global value for that setting. Add each missing value before creating the repository.
- Replace YOUR_NAME with your real commit name in the first line.
- Replace YOUR_EMAIL with your commit email in the second line.
- Remove any line for a value that was already correct.
- Save each missing value by running the remaining command:
git config --global user.name "YOUR_NAME"
git config --global user.email "YOUR_EMAIL"
What do these commands change?
Each command writes one global identity setting for Git. Replacing the placeholder before running the command ensures that future commits carry your real identity.
- Confirm the saved identity by running the checks again:
git config --global user.name
git config --global user.email
What should this confirm?
Both commands read the values that Git now stores globally. Each command should print one non-empty value.
Your name and email should now appear on separate lines. This identity is ready for the commits you create later.
Create and initialize the repository
The repository structure separates input data, tests, and engineering notes before implementation begins. Git will manage future changes from the main branch.
- Move to your Desktop by running this command:
Set-Location ~/Desktop
What does this command do?
The Set-Location command changes PowerShell's current folder. Starting from your Desktop makes the new project easy to find.
- Create the csv-data-quality-auditor folder and move into it by running these commands:
New-Item -ItemType Directory -Path csv-data-quality-auditor
Set-Location csv-data-quality-auditor
What do these commands do?
The New-Item command creates the project folder on your Desktop. The Set-Location command makes that folder your current PowerShell location.
PowerShell displays the new directory details. Its prompt should now end with csv-data-quality-auditor.
- Create the three project subfolders and list them by running these commands:
New-Item -ItemType Directory -Path data, tests, docs
Get-ChildItem -Directory
How is the workspace organized?
- The data folder will hold the clean and malformed CSV inputs.
- The tests folder will hold the automated validation tests.
- The docs folder will hold your engineering notes.
- The Get-ChildItem command lists the folders so you can verify their creation immediately.
You should see data, docs, and tests in the PowerShell output.
The next commands create the Git metadata on the main branch. Visual Studio Code opens the current csv-data-quality-auditor folder in a new window.
- Initialize the repository and open the workspace by running these commands:
git init -b main
code .
What just happened?
- The git command creates repository metadata inside csv-data-quality-auditor.
- The -b option names the initial branch main.
- The code command opens the current folder represented by ..
Visual Studio Code should open with csv-data-quality-auditor at the top of the left sidebar. The three empty subfolders should appear beneath it.
Did the repository fail to open?
Confirm that the PowerShell prompt ends with csv-data-quality-auditor before running the initialization commands. A different prompt location creates the repository in the wrong folder.
Use this prompt if the folder or branch looks wrong: Help me fix my local Git repository location.
Before you run the final check, do you expect all five commands to return a value?
- Switch back to PowerShell.
- Recheck the installed tools and commit identity by running these commands:
python --version
git --version
git config --global user.name
git config --global user.email
code --version
What does the final check prove?
These commands confirm that the pinned runtime, version-control client, commit identity, and editor command remain available after the repository setup.
You should see Python 3.15.0 and Git 2.29 or later. Your name, email, and Visual Studio Code version should also print.
- Return to the Visual Studio Code window from earlier.
- Check the left sidebar for data, docs, and tests beneath csv-data-quality-auditor.
- Check the lower status bar for the main branch.
You should see the empty project structure in the left sidebar. The status bar should identify main as the active branch.
Your workspace is ready for implementation. Next, you will give the auditor a clean CSV file and produce its first successful summary.
Produce a Clean-Data Summary
Your workspace is ready. The next risk is adding validation before you know the basic data path works.
A small CSV dataset gives your Python command-line tool a predictable baseline. Four complete records prove the full path from file loading to terminal summary.
In this step, get ready to:
- Create a clean CSV dataset with four complete ticket records.
- Build the first Python summary script.
- Run the script to confirm the row count and average.
Create the clean ticket dataset
A CSV header gives every column a stable name. Each clean row supplies every value that the first calculation needs.
- Select the data folder in the Visual Studio Code file tree.
- Create clean_tickets.csv inside data with the new-file icon at the top of the file tree.
- Add the four clean ticket records by pasting this dataset:
ticket_id,category,priority,resolution_minutes,owner
T-1001,Login,high,45,Ana
T-1002,Billing,medium,30,Malik
T-1003,Access,low,10,Jordan
T-1004,Billing,high,20,Priya
What does this dataset contain?
- The header names the five fields that describe each support ticket.
- Each ticket_id uniquely identifies one ticket in this clean sample.
- The resolution_minutes field supplies the numeric values used by the average calculation.
- Every row contains an owner because this dataset represents the successful path.
- Save data/clean_tickets.csv.
- Confirm that data/clean_tickets.csv appears in the file tree.
Dataset saved in the wrong place?
- Confirm that clean_tickets.csv is nested inside the data folder.
- Check that the filename ends with .csv.
Help me place clean_tickets.csv inside the correct project folder.
✔️ Awesome, I've got everything!
Your clean dataset now matches the project baseline.
ⓧ I'd like to double check the full code
ticket_id,category,priority,resolution_minutes,owner
T-1001,Login,high,45,Ana
T-1002,Billing,medium,30,Malik
T-1003,Access,low,10,Jordan
T-1004,Billing,high,20,Priya
Build the summary script
The script keeps file loading separate from average calculation. Its main() function connects those parts to the input path supplied in PowerShell.
- Select the csv-data-quality-auditor folder at the top of the file tree.
- Create auditor.py inside csv-data-quality-auditor with the new-file icon.
- Add the first complete version of the auditor by pasting this code:
import csv
import sys
from pathlib import Path
def load_rows(input_path: Path) -> list[dict[str, str]]:
with input_path.open("r", encoding="utf-8", newline="") as csv_file:
return list(csv.DictReader(csv_file))
def calculate_average(rows: list[dict[str, str]]) -> float:
total_minutes = sum(int(row["resolution_minutes"]) for row in rows)
return total_minutes / len(rows)
def main() -> None:
input_path = Path(sys.argv[1])
rows = load_rows(input_path)
average = calculate_average(rows)
print(f"Processed {len(rows)} rows")
print(f"Average resolution time: {average} minutes")
if __name__ == "__main__":
main()
What does this code do?
- The csv module provides the dictionary-based CSV reader.
- The sys module provides the input path from the command line.
- Path handles the CSV file path.
- load_rows() returns each CSV record as a dictionary.
- calculate_average() converts every duration directly with int().
- The direct conversion keeps this first version focused on clean input.
- main() prints the number of processed rows.
- main() also prints the calculated average.
- Save auditor.py.
- Confirm that auditor.py appears beside the data folder in the file tree.
Script file in the wrong folder?
- Confirm that auditor.py sits directly inside csv-data-quality-auditor.
- Check that the filename ends with .py.
- Compare every function name with the full-file reference below.
Help me compare my auditor.py file with the clean-data summary script.
✔️ Awesome, I've got everything!
Your first auditor script now contains the complete clean-data path.
ⓧ I'd like to double check the full code
import csv
import sys
from pathlib import Path
def load_rows(input_path: Path) -> list[dict[str, str]]:
with input_path.open("r", encoding="utf-8", newline="") as csv_file:
return list(csv.DictReader(csv_file))
def calculate_average(rows: list[dict[str, str]]) -> float:
total_minutes = sum(int(row["resolution_minutes"]) for row in rows)
return total_minutes / len(rows)
def main() -> None:
input_path = Path(sys.argv[1])
rows = load_rows(input_path)
average = calculate_average(rows)
print(f"Processed {len(rows)} rows")
print(f"Average resolution time: {average} minutes")
if __name__ == "__main__":
main()
Run the clean-data audit
Before you run the audit, which two summary lines do you expect from these four clean records?
- Run the clean-data audit in the PowerShell terminal from earlier by entering:
python auditor.py data/clean_tickets.csv
What does this command do?
- python starts the verified Python runtime.
- auditor.py selects the script to execute.
- data/clean_tickets.csv supplies the input path expected by main().
You should see Processed 4 rows on the first line. You should see Average resolution time: 26.25 minutes on the second line.
That is your first working audit. The script now turns a known CSV file into a verified terminal summary.
Summary command not working?
- Confirm that the PowerShell terminal is inside csv-data-quality-auditor.
- Check that auditor.py sits beside the data folder.
- Confirm that every resolution_minutes value in the clean dataset contains digits only.
Help me diagnose why the clean-data auditor does not print the expected summary.
Your clean-data path works from input file to terminal output. Next up, you will test this simple script against ticket data that contains realistic problems.
Make the Naive Script Fail
Your clean CSV path now produces the correct summary for four complete records. That success only proves the script works when every value matches its assumptions.
Real operational exports can contain conflicting IDs, unsupported labels, nonnumeric durations, or blank fields. This step puts those cases through the unchanged script and captures the resulting traceback as engineering evidence.
In this step, get ready to:
- Create a six-row dataset with four realistic data-quality problems.
- Reproduce the unhandled integer-conversion failure.
- Record the failure as an engineering requirement.
Create a malformed ticket dataset
A pressure-test dataset combines valid records with the kinds of mistakes found in operational exports. The six rows below include four separate data-quality problems.
- Return to the Explorer sidebar from earlier.
- Select the data folder.
- Click the New File icon at the top of the Explorer sidebar.
- Name the file tickets.csv.
- Populate data/tickets.csv by pasting the dataset below:
ticket_id,category,priority,resolution_minutes,owner
T-1001,Login,high,45,Ana
T-1002,Billing,medium,30,Malik
T-1003,Login,urgent,18,Jordan
T-1002,Access,low,25,Priya
T-1005,Billing,high,not_available,Sam
T-1006,Access,low,12,
What Problems Are Hidden Here?
- The third record uses urgent as an unsupported priority.
- The fourth record repeats ticket ID T-1002.
- The fifth record uses not_available where the script expects a number.
- The sixth record leaves the owner field blank.
- Save data/tickets.csv.
- Confirm tickets.csv is listed beneath data in the Explorer sidebar.
You now have a realistic pressure test that still fits on one screen. Both CSV files should be visible beneath the data folder.
Does the Dataset Look Wrong?
- Check that the header and each ticket occupy separate lines.
- Confirm the fifth record contains not_available in the duration column.
- Confirm the final record ends with a comma after 12 to preserve the blank owner field.
Ask for help checking the file format:
✔️ Awesome, I've got everything!
Your malformed dataset matches the six-row pressure test.
ⓧ I'd like to double check the full code
ticket_id,category,priority,resolution_minutes,owner
T-1001,Login,high,45,Ana
T-1002,Billing,medium,30,Malik
T-1003,Login,urgent,18,Jordan
T-1002,Access,low,25,Priya
T-1005,Billing,high,not_available,Sam
T-1006,Access,low,12,
What Should Match?
Compare the header and all six records with your saved file. The repeated ID, unsupported priority, nonnumeric duration, and blank owner must remain exactly as shown.
Run the malformed dataset
A traceback follows the chain of function calls that led to a failure. Its final frame identifies the line where Python could no longer continue.
Before you run this, do you think all six rows can make it through the unchanged calculation?
- Test the unchanged script against the malformed data by running this command in the PowerShell terminal:
python auditor.py data/tickets.csv
What Should You See?
The terminal shows a traceback ending in a ValueError. The traceback points back to the direct int(row["resolution_minutes"]) conversion in calculate_average().
- The unsupported urgent priority reaches the calculation because the naive script does not validate priorities.
- The duplicate T-1002 reaches the calculation because the naive script does not compare identifiers.
- The value not_available reaches int() and raises the exception.
- The final record is never processed.
That failure is useful. You have reproduced the exact weakness that clean data kept hidden.
- Read the traceback from its final line upward.
- Return to auditor.py from earlier.
- Locate total_minutes = sum(int(row["resolution_minutes"]) for row in rows) inside calculate_average().
The failure points to the conversion performed for each row. Python reaches not_available before it can calculate the summary.
Did the Script Avoid the Intended Failure?
- Confirm the terminal command points to data/tickets.csv instead of data/clean_tickets.csv.
- Check that the fifth data record still contains not_available.
- Confirm auditor.py still uses the direct int(row["resolution_minutes"]) conversion.
Help me reproduce the intended failure.
Record the failure and requirement
Engineering notes preserve the trigger, the observed behavior, and the requirement that follows from it. This creates debugging evidence another developer can review.
- Return to the Explorer sidebar.
- Select the docs folder from earlier.
- Click the New File icon at the top of the Explorer sidebar.
- Name the file engineering-notes.md.
- Record the evidence in docs/engineering-notes.md by pasting the notes below:
# Engineering Notes
## Failure reproduced
The first implementation converted every `resolution_minutes` value directly with `int()`. The value `not_available` raised a `ValueError`, so later records were never checked.
## Requirement
One bad row must not stop the full audit. Invalid records must become reportable findings.
What Do These Notes Capture?
The failure section records the exact value that triggered the exception. It also records the consequence for later rows.
The requirement turns that observation into a measurable goal. Future versions must preserve invalid records as findings while continuing the audit.
- Save docs/engineering-notes.md.
- Confirm the file contains the Failure reproduced and Requirement headings.
Your debugging evidence now has a permanent home in the repository. The notes connect one malformed value to a concrete engineering requirement.
✔️ Awesome, I've got everything!
Your engineering notes capture the reproduced failure and the requirement it established.
ⓧ I'd like to double check the full code
# Engineering Notes
## Failure reproduced
The first implementation converted every `resolution_minutes` value directly with `int()`. The value `not_available` raised a `ValueError`, so later records were never checked.
## Requirement
One bad row must not stop the full audit. Invalid records must become reportable findings.
What Should Match?
Compare both headings and their paragraphs with your saved file. The notes must name the triggering value and state that every row still needs an audit result.
Before you rerun the unchanged script, do you expect the documented requirement to change its behavior?
- Verify the naive failure remains reproducible by running this command in the PowerShell terminal:
python auditor.py data/tickets.csv
What Does This Check Prove?
You should see the same ValueError at the direct integer conversion. The notes document the weakness without changing auditor.py.
The repeated failure confirms that the malformed input reliably reproduces the problem. You now have a stable starting point for the resilient implementation.
The weakness is now reproduced and documented. Next up, you will separate validation from aggregation so malformed rows become findings instead of a fatal exception.
Turn the Script into a Resilient Auditor
Your malformed CSV already exposed the direct integer conversion in the Python script. One invalid duration stopped every later row from being checked.
Now you will add an input validation boundary around each row. The command-line interface will preserve every problem in a structured JSON report.
In this step, get ready to:
- Separate row validation from summary calculations.
- Add command-line options for report output and verbose logging.
- Process every malformed record without crashing.
Replace the crash path with validation
A resilient auditor treats malformed values as data-quality findings. Each function gets one job so a problem in one row cannot stop the remaining rows.
- In the auditor.py tab in Visual Studio Code from earlier, press Ctrl+A to select the existing code.
- Replace the selected code with all of the following sections in the order shown.
import argparse
import csv
import json
import logging
from pathlib import Path
REQUIRED_FIELDS = (
"ticket_id",
"category",
"priority",
"resolution_minutes",
"owner",
)
VALID_PRIORITIES = {"low", "medium", "high"}
LOGGER = logging.getLogger(__name__)
What establishes the audit rules?
- The imports provide command-line parsing, CSV reading, JSON writing, logging, and file-path handling.
- REQUIRED_FIELDS defines the columns that every input file must contain.
- VALID_PRIORITIES provides the accepted values for each ticket priority.
- LOGGER records row-level details when verbose mode is active.
def load_rows(input_path: Path) -> list[dict[str, str | None]]:
"""Load ticket rows and verify that the required headers exist."""
with input_path.open("r", encoding="utf-8", newline="") as csv_file:
reader = csv.DictReader(csv_file)
fieldnames = set(reader.fieldnames or [])
missing_headers = [
field for field in REQUIRED_FIELDS if field not in fieldnames
]
if missing_headers:
missing = ", ".join(missing_headers)
raise ValueError(f"CSV is missing required headers: {missing}")
return list(reader)
How does loading stay safe?
- csv.DictReader maps each ticket row to its column names.
- missing_headers finds required columns that are absent from the input file.
- The function stops with a focused error when the file structure cannot support a reliable audit.
def validate_row(
row: dict[str, str | None],
row_number: int,
seen_ids: set[str],
) -> tuple[dict[str, str], list[str], int | None]:
"""Normalize one row and return its validation findings."""
cleaned = {
field: str(row.get(field) or "").strip()
for field in REQUIRED_FIELDS
}
problems: list[str] = []
for field in REQUIRED_FIELDS:
if not cleaned[field]:
problems.append(f"missing required field: {field}")
ticket_id = cleaned["ticket_id"]
if ticket_id:
if ticket_id in seen_ids:
problems.append(f"duplicate ticket_id: {ticket_id}")
else:
seen_ids.add(ticket_id)
How are missing and duplicate values found?
- cleaned converts missing values to empty strings before trimming whitespace.
- problems collects every issue found in the current row.
- seen_ids tracks earlier ticket identifiers so later duplicates become findings.
priority = cleaned["priority"].lower()
cleaned["priority"] = priority
if priority and priority not in VALID_PRIORITIES:
allowed = ", ".join(sorted(VALID_PRIORITIES))
problems.append(
f"invalid priority: {priority}; expected one of {allowed}"
)
duration: int | None = None
raw_duration = cleaned["resolution_minutes"]
if raw_duration:
try:
duration = int(raw_duration)
if duration < 0:
problems.append("resolution_minutes must be non-negative")
except ValueError:
problems.append(
f"resolution_minutes is not an integer: {raw_duration}"
)
LOGGER.debug("Validated CSV row %s with %s issue(s)", row_number, len(problems))
return cleaned, problems, duration
How does row validation prevent crashes?
- The priority check compares normalized values with VALID_PRIORITIES.
- The duration conversion catches ValueError inside the row validator.
- Invalid durations become problem messages while later records remain available for auditing.
- LOGGER.debug() records the row number and issue count during a verbose run.
def audit_rows(rows: list[dict[str, str | None]]) -> dict[str, object]:
"""Audit loaded rows and return a JSON-serializable report."""
seen_ids: set[str] = set()
issues: list[dict[str, object]] = []
priority_counts: dict[str, int] = {}
valid_rows = 0
resolution_total = 0
for row_number, row in enumerate(rows, start=2):
cleaned, row_problems, duration = validate_row(
row, row_number, seen_ids
)
if row_problems:
issues.append(
{
"row": row_number,
"ticket_id": cleaned["ticket_id"] or None,
"problems": row_problems,
}
)
continue
How are invalid rows preserved?
- enumerate() starts at row two because row one contains the CSV headers.
- Each invalid row adds its row number, ticket identifier, and problem list to issues.
- continue keeps invalid values out of the valid-row calculations.
valid_rows += 1
priority = cleaned["priority"]
priority_counts[priority] = priority_counts.get(priority, 0) + 1
resolution_total += duration or 0
average_resolution = (
round(resolution_total / valid_rows, 2) if valid_rows else None
)
return {
"summary": {
"total_rows": len(rows),
"valid_rows": valid_rows,
"invalid_rows": len(issues),
"average_resolution_minutes": average_resolution,
"valid_priority_counts": dict(sorted(priority_counts.items())),
},
"issues": issues,
}
How is the summary calculated?
- Only valid rows contribute to the priority counts and resolution total.
- average_resolution avoids division by zero when every row is invalid.
- The returned dictionary separates overall totals from row-level findings.
def write_report(report: dict[str, object], output_path: Path) -> None:
"""Write the audit report as formatted JSON."""
output_path.parent.mkdir(parents=True, exist_ok=True)
with output_path.open("w", encoding="utf-8") as report_file:
json.dump(report, report_file, indent=2)
report_file.write("\n")
How is the report written?
- output_path.parent.mkdir() creates the report directory when it does not exist.
- json.dump() writes the report with indentation so each finding is readable.
- The final newline keeps the generated file tidy in editors and command-line tools.
def build_parser() -> argparse.ArgumentParser:
"""Create the command-line parser."""
parser = argparse.ArgumentParser(
description="Audit ticket CSV data and write a JSON report."
)
parser.add_argument("input", type=Path, help="Path to the input CSV file")
parser.add_argument(
"--output",
type=Path,
default=Path("reports/audit_report.json"),
help="Path for the JSON report",
)
parser.add_argument(
"--verbose",
action="store_true",
help="Show row-level validation logs",
)
return parser
What does the parser control?
- input captures the CSV path supplied after the script name.
- --output lets the learner choose where the JSON report is written.
- --verbose activates row-level diagnostic messages.
def main() -> int:
"""Run the command-line application."""
args = build_parser().parse_args()
logging.basicConfig(
level=logging.DEBUG if args.verbose else logging.INFO,
format="%(levelname)s: %(message)s",
)
try:
rows = load_rows(args.input)
report = audit_rows(rows)
write_report(report, args.output)
except (OSError, ValueError, csv.Error) as error:
LOGGER.error("Audit failed: %s", error)
return 1
summary = report["summary"]
print(f"Processed {summary['total_rows']} rows")
print(f"Valid rows: {summary['valid_rows']}")
print(f"Invalid rows: {summary['invalid_rows']}")
print(
"Average resolution time: "
f"{summary['average_resolution_minutes']} minutes"
)
print(f"Report written to {args.output}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
How does the application stay resilient?
- main() connects parsing, loading, auditing, report writing, and terminal output.
- The exception handler converts file-level failures into a controlled exit code.
- Row-level data problems remain inside the report because validate_row() already handled them.
- Save auditor.py by pressing Ctrl+S.
✔️ Awesome, I've got everything!
Your saved file now contains the complete validation, reporting, logging, and command-line flow.
ⓧ I'd like to double check the full code
- Compare your saved auditor.py with this complete reference.
import argparse
import csv
import json
import logging
from pathlib import Path
REQUIRED_FIELDS = (
"ticket_id",
"category",
"priority",
"resolution_minutes",
"owner",
)
VALID_PRIORITIES = {"low", "medium", "high"}
LOGGER = logging.getLogger(__name__)
def load_rows(input_path: Path) -> list[dict[str, str | None]]:
"""Load ticket rows and verify that the required headers exist."""
with input_path.open("r", encoding="utf-8", newline="") as csv_file:
reader = csv.DictReader(csv_file)
fieldnames = set(reader.fieldnames or [])
missing_headers = [
field for field in REQUIRED_FIELDS if field not in fieldnames
]
if missing_headers:
missing = ", ".join(missing_headers)
raise ValueError(f"CSV is missing required headers: {missing}")
return list(reader)
def validate_row(
row: dict[str, str | None],
row_number: int,
seen_ids: set[str],
) -> tuple[dict[str, str], list[str], int | None]:
"""Normalize one row and return its validation findings."""
cleaned = {
field: str(row.get(field) or "").strip()
for field in REQUIRED_FIELDS
}
problems: list[str] = []
for field in REQUIRED_FIELDS:
if not cleaned[field]:
problems.append(f"missing required field: {field}")
ticket_id = cleaned["ticket_id"]
if ticket_id:
if ticket_id in seen_ids:
problems.append(f"duplicate ticket_id: {ticket_id}")
else:
seen_ids.add(ticket_id)
priority = cleaned["priority"].lower()
cleaned["priority"] = priority
if priority and priority not in VALID_PRIORITIES:
allowed = ", ".join(sorted(VALID_PRIORITIES))
problems.append(
f"invalid priority: {priority}; expected one of {allowed}"
)
duration: int | None = None
raw_duration = cleaned["resolution_minutes"]
if raw_duration:
try:
duration = int(raw_duration)
if duration < 0:
problems.append("resolution_minutes must be non-negative")
except ValueError:
problems.append(
f"resolution_minutes is not an integer: {raw_duration}"
)
LOGGER.debug("Validated CSV row %s with %s issue(s)", row_number, len(problems))
return cleaned, problems, duration
def audit_rows(rows: list[dict[str, str | None]]) -> dict[str, object]:
"""Audit loaded rows and return a JSON-serializable report."""
seen_ids: set[str] = set()
issues: list[dict[str, object]] = []
priority_counts: dict[str, int] = {}
valid_rows = 0
resolution_total = 0
for row_number, row in enumerate(rows, start=2):
cleaned, row_problems, duration = validate_row(
row, row_number, seen_ids
)
if row_problems:
issues.append(
{
"row": row_number,
"ticket_id": cleaned["ticket_id"] or None,
"problems": row_problems,
}
)
continue
valid_rows += 1
priority = cleaned["priority"]
priority_counts[priority] = priority_counts.get(priority, 0) + 1
resolution_total += duration or 0
average_resolution = (
round(resolution_total / valid_rows, 2) if valid_rows else None
)
return {
"summary": {
"total_rows": len(rows),
"valid_rows": valid_rows,
"invalid_rows": len(issues),
"average_resolution_minutes": average_resolution,
"valid_priority_counts": dict(sorted(priority_counts.items())),
},
"issues": issues,
}
def write_report(report: dict[str, object], output_path: Path) -> None:
"""Write the audit report as formatted JSON."""
output_path.parent.mkdir(parents=True, exist_ok=True)
with output_path.open("w", encoding="utf-8") as report_file:
json.dump(report, report_file, indent=2)
report_file.write("\n")
def build_parser() -> argparse.ArgumentParser:
"""Create the command-line parser."""
parser = argparse.ArgumentParser(
description="Audit ticket CSV data and write a JSON report."
)
parser.add_argument("input", type=Path, help="Path to the input CSV file")
parser.add_argument(
"--output",
type=Path,
default=Path("reports/audit_report.json"),
help="Path for the JSON report",
)
parser.add_argument(
"--verbose",
action="store_true",
help="Show row-level validation logs",
)
return parser
def main() -> int:
"""Run the command-line application."""
args = build_parser().parse_args()
logging.basicConfig(
level=logging.DEBUG if args.verbose else logging.INFO,
format="%(levelname)s: %(message)s",
)
try:
rows = load_rows(args.input)
report = audit_rows(rows)
write_report(report, args.output)
except (OSError, ValueError, csv.Error) as error:
LOGGER.error("Audit failed: %s", error)
return 1
summary = report["summary"]
print(f"Processed {summary['total_rows']} rows")
print(f"Valid rows: {summary['valid_rows']}")
print(f"Invalid rows: {summary['invalid_rows']}")
print(
"Average resolution time: "
f"{summary['average_resolution_minutes']} minutes"
)
print(f"Report written to {args.output}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
How to use this reference
This reference shows the exact cumulative file for this step. Matching it removes indentation or naming drift before the audit run.
Run the complete audit
The malformed dataset now tests all four validation paths in one run. Verbose mode also proves that the auditor reaches every CSV row.
Before you run the audit, do you expect the invalid duration to stop the script again?
- Run the resilient auditor from the project terminal with this command:
python auditor.py data/tickets.csv --output reports/audit_report.json --verbose
What does this command do?
- data/tickets.csv supplies the six-record malformed dataset.
- --output reports/audit_report.json selects the generated report path.
- --verbose enables the row-level debug messages configured in auditor.py.
You should see row-level debug messages followed by Processed 6 rows, Valid rows: 2, and Invalid rows: 4.
That is the failure fixed. The auditor now reaches every record and writes the findings instead of ending with a traceback.
- In the Explorer sidebar, expand the reports folder.
- Select audit_report.json to inspect the generated report.
You should see a summary with two valid rows and four invalid rows. The issues list should preserve the invalid priority, duplicate ticket ID, non-numeric duration, and missing owner.
Audit still failing?
- Check that every indented continuation stayed inside its matching function when you replaced auditor.py.
- Confirm the input path matches the existing data/tickets.csv file.
- Compare your function names with validate_row(), audit_rows(), and write_report().
Help me diagnose why my resilient CSV auditor still fails or produces the wrong row counts.
Your auditor now converts unreliable ticket data into a complete report. Next, you will protect that behavior with tests before documenting and publishing the project.
Test, Document, and Publish the Project
Your resilient auditor now processes every ticket row. It preserves malformed records as structured findings instead of crashing.
A program that works once still needs evidence that another developer can trust. In this step, you will add unit tests. You will document your decisions before publishing the project with Git and GitHub.
In this step, get ready to:
- Test valid records plus malformed values and duplicate identifiers.
- Document usage plus debugging evidence and career talking points.
- Commit the finished project and publish it in a public GitHub repository.
Add the unit tests
Automated tests turn the auditor's expected behavior into repeatable checks. Each test passes rows directly to audit_rows() before checking the returned report.
- Select the tests folder in the Visual Studio Code Explorer.
- Select New File... in the Explorer title bar.
- Enter __init__.py as the file name.
- Add the test-package description by copying this code into tests/__init__.py:
"""Tests for the CSV data quality auditor."""
Why Does This File Matter?
Python 3.15 test discovery searches test subdirectories that contain __init__.py. This file makes the tests directory discoverable as a package.
- Save tests/__init__.py.
- Confirm that __init__.py appears inside the tests folder in Explorer.
File in the Wrong Folder?
Check that Explorer shows __init__.py nested under tests. Move the file into that folder if it appears beside auditor.py.
Help me place tests/__init__.py correctly.
- Select the tests folder in Explorer.
- Select New File... in the Explorer title bar.
- Enter test_auditor.py as the file name.
- Add the imports plus the valid-row test setup by copying this code into tests/test_auditor.py:
import unittest
from auditor import audit_rows
class AuditorTests(unittest.TestCase):
def test_all_valid_rows_are_summarized(self) -> None:
rows = [
{
"ticket_id": "T-1",
"category": "Login",
"priority": "high",
"resolution_minutes": "20",
"owner": "Ana",
},
{
"ticket_id": "T-2",
"category": "Billing",
"priority": "low",
"resolution_minutes": "10",
"owner": "Malik",
},
]
report = audit_rows(rows)
What Does This Test Set Up?
- The imports provide Python's test framework plus the project function under test.
- The first test supplies two complete rows with valid priorities and numeric durations.
- The report variable holds the summary returned by audit_rows().
- Find the report = audit_rows(rows) line in tests/test_auditor.py.
- Add these assertions immediately below that line:
self.assertEqual(report["summary"]["valid_rows"], 2)
self.assertEqual(report["summary"]["invalid_rows"], 0)
self.assertEqual(
report["summary"]["average_resolution_minutes"], 15.0
)
What Do These Assertions Prove?
The assertions prove that both rows remain valid. They also confirm that the auditor calculates the expected average of 15.0 minutes.
- Save tests/test_auditor.py.
- Return to the PowerShell terminal from earlier.
- Run the current test suite with this command:
python -m unittest -v
What Does This Command Do?
Python discovers tests in the project before running them with detailed output. The current suite contains the valid-row test.
You should see one test complete with OK.
Test Not Discovered?
- Confirm that both test files are inside the tests folder.
- Check that the method name starts with test_.
- Check that PowerShell is running inside the csv-data-quality-auditor folder.
Help me debug why python -m unittest -v does not find or pass my test.
- Add the invalid-value test below test_all_valid_rows_are_summarized() in tests/test_auditor.py by copying this code:
def test_invalid_priority_and_duration_are_reported(self) -> None:
rows = [
{
"ticket_id": "T-1",
"category": "Login",
"priority": "urgent",
"resolution_minutes": "not_available",
"owner": "Ana",
}
]
report = audit_rows(rows)
problems = report["issues"][0]["problems"]
self.assertEqual(report["summary"]["invalid_rows"], 1)
self.assertTrue(any("invalid priority" in item for item in problems))
self.assertTrue(any("not an integer" in item for item in problems))
What Does This Test Cover?
- The row combines the unsupported priority urgent with the non-numeric duration not_available.
- The assertions confirm that one row becomes invalid.
- The problem checks prove that both validation findings survive in the report.
- Save tests/test_auditor.py.
- Run the expanded suite with this command:
python -m unittest -v
What Should This Run Prove?
Test discovery now runs the valid-row test plus the malformed-value test. Both checks should finish with OK.
Malformed-Value Test Failing?
Compare the dictionary keys with the fields accepted by audit_rows(). Check the spelling of urgent and not_available.
Help me diagnose the invalid-value test failure.
- Add the duplicate-ID test below test_invalid_priority_and_duration_are_reported() in tests/test_auditor.py by copying this code:
def test_duplicate_ticket_id_is_reported(self) -> None:
rows = [
{
"ticket_id": "T-1",
"category": "Login",
"priority": "high",
"resolution_minutes": "20",
"owner": "Ana",
},
{
"ticket_id": "T-1",
"category": "Access",
"priority": "low",
"resolution_minutes": "10",
"owner": "Jordan",
},
]
report = audit_rows(rows)
problems = report["issues"][0]["problems"]
self.assertEqual(report["summary"]["valid_rows"], 1)
self.assertEqual(report["summary"]["invalid_rows"], 1)
self.assertIn("duplicate ticket_id: T-1", problems)
if __name__ == "__main__":
unittest.main()
What Does the Final Test Prove?
- Both input rows reuse T-1 as their ticket identifier.
- The first occurrence remains valid.
- The second occurrence becomes an issue with the exact duplicate finding.
- Save tests/test_auditor.py.
Before you run the complete suite, do you expect all three validation scenarios to pass?
- Run all discovered tests with this command:
python -m unittest -v
What Does the Complete Run Check?
The command runs all three methods in AuditorTests. Together they cover clean summaries plus invalid values and duplicate identifiers.
You should see three tests finish with OK. Your auditor now has repeatable evidence for its core validation behavior.
Seeing a Test Failure?
- Read the failing test name near the top of the terminal output.
- Compare that method with the matching block in the full-code reference below.
- Confirm that auditor.py still contains the resilient audit_rows() implementation from the previous step.
Help me understand and fix my unit-test failure.
✔️ Awesome, I've got everything!
Great work. Save both test files before moving on.
ⓧ I'd like to double check the full code
"""Tests for the CSV data quality auditor."""
What Should This File Contain?
The package file contains one module docstring. Its presence allows test discovery to search the tests directory.
import unittest
from auditor import audit_rows
class AuditorTests(unittest.TestCase):
def test_all_valid_rows_are_summarized(self) -> None:
rows = [
{
"ticket_id": "T-1",
"category": "Login",
"priority": "high",
"resolution_minutes": "20",
"owner": "Ana",
},
{
"ticket_id": "T-2",
"category": "Billing",
"priority": "low",
"resolution_minutes": "10",
"owner": "Malik",
},
]
report = audit_rows(rows)
self.assertEqual(report["summary"]["valid_rows"], 2)
self.assertEqual(report["summary"]["invalid_rows"], 0)
self.assertEqual(
report["summary"]["average_resolution_minutes"], 15.0
)
def test_invalid_priority_and_duration_are_reported(self) -> None:
rows = [
{
"ticket_id": "T-1",
"category": "Login",
"priority": "urgent",
"resolution_minutes": "not_available",
"owner": "Ana",
}
]
report = audit_rows(rows)
problems = report["issues"][0]["problems"]
self.assertEqual(report["summary"]["invalid_rows"], 1)
self.assertTrue(any("invalid priority" in item for item in problems))
self.assertTrue(any("not an integer" in item for item in problems))
def test_duplicate_ticket_id_is_reported(self) -> None:
rows = [
{
"ticket_id": "T-1",
"category": "Login",
"priority": "high",
"resolution_minutes": "20",
"owner": "Ana",
},
{
"ticket_id": "T-1",
"category": "Access",
"priority": "low",
"resolution_minutes": "10",
"owner": "Jordan",
},
]
report = audit_rows(rows)
problems = report["issues"][0]["problems"]
self.assertEqual(report["summary"]["valid_rows"], 1)
self.assertEqual(report["summary"]["invalid_rows"], 1)
self.assertIn("duplicate ticket_id: T-1", problems)
if __name__ == "__main__":
unittest.main()
What Should the Full Suite Contain?
The file imports audit_rows() before defining three test methods. Each method protects one required behavior from future regressions.
Finish the project documentation
Documentation gives another developer the context that source code cannot provide on its own. Your files will explain how to run the auditor plus how you debugged and strengthened it.
- Select the csv-data-quality-auditor folder at the top of Explorer.
- Select New File... in the Explorer title bar.
- Enter .gitignore as the file name.
- Exclude generated Python files plus local reports by copying this code into .gitignore:
__pycache__/
*.py[cod]
reports/
What Does This File Exclude?
The first two patterns exclude generated Python cache files. The final pattern keeps locally generated reports outside the repository.
- Save .gitignore.
- Confirm that .gitignore appears beside auditor.py in Explorer.
Gitignore Name Looks Wrong?
Confirm that the file name begins with a dot. Remove any automatic .txt extension if one was added.
Help me verify my .gitignore file.
- Select the csv-data-quality-auditor folder at the top of Explorer.
- Select New File... in the Explorer title bar.
- Enter README.md as the file name.
- Add the project overview plus usage instructions by copying this first section into README.md:
# CSV Data Quality Auditor
A Python command-line tool that validates operational ticket data and writes a structured JSON audit report.
## What it checks
- Missing required fields
- Priorities outside `low`, `medium`, and `high`
- Duplicate ticket IDs
- Missing, non-numeric, or negative resolution durations
## Requirements
- Python 3.15.0
- No third-party Python packages
## Run the auditor
```powershell
python auditor.py data/clean_tickets.csv
python auditor.py data/tickets.csv --output reports/audit_report.json --verbose
```
The second command processes six rows, keeps two valid rows in the summary, and reports four invalid rows without stopping the audit.
What Does This Section Explain?
- The opening names the problem solved by the project.
- The checklist states the validation rules in plain language.
- The usage examples show both the clean path and the malformed-data path.
- Save README.md.
- Confirm that the editor shows the What it checks and Run the auditor headings.
- Add the testing plus project-structure sections below the existing content by copying this code:
## Run the tests
```powershell
python -m unittest -v
```
## Project structure
- `auditor.py`: command-line interface and audit logic
- `data/`: clean and malformed sample inputs
- `tests/`: unit tests for validation behavior
- `docs/engineering-notes.md`: debugging evidence and career talking points
- `reports/`: generated JSON output, excluded from Git
## Engineering focus
This project demonstrates automation, CSV processing, modular validation, error handling, logging, unit testing, documentation, Git, and GitHub workflows.
What Does the Final README Add?
The testing section gives reviewers a repeatable verification command. The project map explains where the implementation plus data and evidence live.
- Save README.md.
- Confirm that the final visible heading is Engineering focus.
Markdown Layout Looks Broken?
Check that every PowerShell block has an opening and closing line of three backticks. Confirm that each heading starts on its own line.
Help me repair the README formatting.
- Return to docs/engineering-notes.md from the earlier debugging step.
- Replace its contents with the requirement through resume sections below:
# Engineering Notes
## Requirement
Process every ticket row, summarize valid records, and preserve invalid records as actionable findings instead of stopping at the first error.
## Failure reproduced
The first implementation converted every `resolution_minutes` value directly with `int()`. The value `not_available` raised a `ValueError`, so later records were never checked.
## Fix
The final implementation separates loading, row validation, aggregation, and report writing. Validation catches data-quality problems at row level and allows the audit to continue.
## Verification
The malformed dataset contains six rows. The completed audit reports two valid rows and four invalid rows. Unit tests cover valid input, invalid priority and duration values, and duplicate ticket IDs.
## Resume-ready bullets
- Developed a modular Python command-line tool that audits CSV records and generates structured JSON reports for internal data-quality workflows.
- Debugged an unhandled data-conversion failure, added row-level validation, and prevented malformed input from stopping batch processing.
- Added automated unit tests, implementation documentation, and Git history to improve maintainability and reproducibility.
What Evidence Does This Preserve?
The notes connect the original ValueError to the final row-level validation design. The resume bullets convert that technical work into concise evidence of impact.
- Save docs/engineering-notes.md.
- Confirm that the file now starts with the requirement before the reproduced failure.
- Add the interview story below the resume bullets by copying this section:
## Interview talking points
Situation: Operational ticket exports could contain malformed values that caused a basic processing script to stop.
Task: Build a reusable auditor that processes the full file and explains every rejected row.
Action: Reproduced the failure, traced it to direct integer conversion, separated validation from aggregation, added structured findings, and protected the behavior with tests.
Result: The tool processes all six sample records, summarizes two valid rows, and reports four invalid rows without crashing.
How Is the Story Structured?
The four lines organize the debugging work into situation plus task and action plus result. This keeps the explanation concrete during an interview.
- Save docs/engineering-notes.md.
- Confirm that the final result mentions all six sample records plus two valid rows and four invalid rows.
Notes Missing Earlier Evidence?
Compare the requirement and failure sections carefully before saving. The final file must retain the original not_available evidence.
Help me compare my engineering notes.
✔️ Awesome, I've got everything!
Your usage guide and engineering evidence are ready. Save all three documentation files.
ⓧ I'd like to double check the full code
__pycache__/
*.py[cod]
reports/
What Should Git Ignore?
The file excludes Python cache artifacts plus the generated reports directory.
# CSV Data Quality Auditor
A Python command-line tool that validates operational ticket data and writes a structured JSON audit report.
## What it checks
- Missing required fields
- Priorities outside `low`, `medium`, and `high`
- Duplicate ticket IDs
- Missing, non-numeric, or negative resolution durations
## Requirements
- Python 3.15.0
- No third-party Python packages
## Run the auditor
```powershell
python auditor.py data/clean_tickets.csv
python auditor.py data/tickets.csv --output reports/audit_report.json --verbose
```
The second command processes six rows, keeps two valid rows in the summary, and reports four invalid rows without stopping the audit.
## Run the tests
```powershell
python -m unittest -v
```
## Project structure
- `auditor.py`: command-line interface and audit logic
- `data/`: clean and malformed sample inputs
- `tests/`: unit tests for validation behavior
- `docs/engineering-notes.md`: debugging evidence and career talking points
- `reports/`: generated JSON output, excluded from Git
## Engineering focus
This project demonstrates automation, CSV processing, modular validation, error handling, logging, unit testing, documentation, Git, and GitHub workflows.
What Should the README Cover?
The complete README covers validation behavior plus requirements and usage. It also explains tests plus the project structure and engineering focus.
# Engineering Notes
## Requirement
Process every ticket row, summarize valid records, and preserve invalid records as actionable findings instead of stopping at the first error.
## Failure reproduced
The first implementation converted every `resolution_minutes` value directly with `int()`. The value `not_available` raised a `ValueError`, so later records were never checked.
## Fix
The final implementation separates loading, row validation, aggregation, and report writing. Validation catches data-quality problems at row level and allows the audit to continue.
## Verification
The malformed dataset contains six rows. The completed audit reports two valid rows and four invalid rows. Unit tests cover valid input, invalid priority and duration values, and duplicate ticket IDs.
## Resume-ready bullets
- Developed a modular Python command-line tool that audits CSV records and generates structured JSON reports for internal data-quality workflows.
- Debugged an unhandled data-conversion failure, added row-level validation, and prevented malformed input from stopping batch processing.
- Added automated unit tests, implementation documentation, and Git history to improve maintainability and reproducibility.
## Interview talking points
Situation: Operational ticket exports could contain malformed values that caused a basic processing script to stop.
Task: Build a reusable auditor that processes the full file and explains every rejected row.
Action: Reproduced the failure, traced it to direct integer conversion, separated validation from aggregation, added structured findings, and protected the behavior with tests.
Result: The tool processes all six sample records, summarizes two valid rows, and reports four invalid rows without crashing.
What Should the Notes Preserve?
The final notes preserve the original failure before documenting the fix and verification. They close with resume evidence plus an interview-ready debugging story.
Commit and publish the repository
A commit records the tested project as one reproducible snapshot. Publishing that history makes the auditor visible to reviewers through a public repository.
- Return to the PowerShell terminal in Visual Studio Code.
- Stage the project files plus create the finished snapshot by running these commands:
git add .
git commit -m "Build CSV data quality auditor"
What Do These Commands Record?
- The first command stages the project files allowed by .gitignore.
- The second command records those staged files with the project commit message.
- The generated reports directory remains local.
Git should confirm that it created the commit and list the tracked files. The commit summary should omit reports/audit_report.json.
Commit Did Not Complete?
- Confirm that the global Git name and email from Step 1 are still configured.
- Save every edited file in Visual Studio Code.
- Run the two commit commands again after correcting the reported cause.
Help me troubleshoot my Git commit.
- Sign in to GitHub in your browser.
- Select New repository.
- Enter csv-data-quality-auditor as the repository name.
- Choose the public visibility option.
- Leave every repository initialization option disabled.
- Select Create repository.
You should reach the Quick Setup page for an empty public repository. Leaving the initialization options disabled prevents an unnecessary merge conflict.
- Copy the HTTPS repository URL shown on the Quick Setup page into REMOTE-URL.
- Configure origin plus verify the stored URL by running these commands:
git remote add origin [[REMOTE_URL="REMOTE-URL"]]
git remote -v
What Do These Remote Commands Do?
The first command connects your local repository to the empty GitHub repository under the name origin. The second command prints the configured remote for verification.
You should see origin listed with the same HTTPS URL for fetching and pushing.
Remote URL Incorrect?
Compare both printed URLs with the HTTPS address on the repository's Quick Setup page. A copied space or missing repository suffix prevents the push from reaching the correct destination.
Help me correct my Git remote.
Git for Windows may request GitHub authentication during the first HTTPS push. The browser prompt is expected, so complete it before returning to PowerShell.
Before you push, do you expect GitHub to show the complete project on the main branch?
- Push the local main branch by running this command:
git push -u origin main
What Does the First Push Do?
The command uploads the local main branch to origin. It also records that remote branch as the upstream for future pushes.
- Complete the Git Credential Manager browser prompt if it opens.
- Return to the GitHub repository page after the push finishes.
- Refresh the repository page.
- Confirm that the selected branch is main.
- Confirm that auditor.py plus tests and README.md are visible.
- Confirm that docs/engineering-notes.md is available inside the docs folder.
Your public repository now shows the tested auditor plus its documentation on main. That is the complete portfolio result another developer can inspect and run.
Push or Authentication Failed?
- Complete the Git Credential Manager browser flow if PowerShell is waiting for authentication.
- Confirm that git remote -v shows the intended public repository URL.
- Confirm that the empty GitHub repository is named csv-data-quality-auditor.
Help me troubleshoot my first GitHub push.
You have turned a working script into evidence that others can verify. The public repository now combines resilient validation plus automated tests and a documented debugging story.
Secret mission
Automate Tests with GitHub Actions
A local test suite only protects the project when someone remembers to run it. Add a GitHub Actions workflow that tests every push and pull request. A green workflow run proves each shared validation change still passes the full suite.
Clean Up Your Resources
Clean Up Your Resources
Choose whether to keep the local project plus its public repository, pause the automation, or delete both copies. GitHub Actions usage stays at $0 for this public repository on a standard GitHub-hosted runner.
Resources you used:
- The local csv-data-quality-auditor Git repository containing your source files, tests, sample data, documentation, and commit history.
- Generated JSON reports inside reports/.
- The public GitHub repository named csv-data-quality-auditor.
- The GitHub Actions workflow stored at .github/workflows/tests.yml.
Keep everything running
No action is needed. Choose this if you want to keep the project available as a portfolio example or continue improving its validation logic.
- Your local csv-data-quality-auditor repository keeps the complete project history.
- Your public repository remains available on main.
- Future pushes and pull requests continue to run the Python tests workflow automatically.
- Standard GitHub-hosted workflow runs remain at $0 while the repository stays public.
Pause - I'll come back to this later
Pause automatic test runs while keeping the local project and public portfolio repository intact. Future pushes stop triggering the workflow after its file is removed from main.
- Return to the csv-data-quality-auditor folder already open in Visual Studio Code.
- Locate .github/workflows/tests.yml in the file tree.
- Remove .github/workflows/tests.yml from the local repository.
- Record the removal in a follow-up Git commit with the message Pause automatic tests.
- Push the follow-up commit to origin through the Git controls in Visual Studio Code.
- Return to the public repository on GitHub.
- Confirm that .github/workflows/tests.yml no longer appears on main.
You have paused the automation while preserving the auditor as a public portfolio project.
Delete - I don't want to use this again
Remove the public repository plus every local project file if you no longer need the auditor. Deleting both copies is permanent, so this option fits only when you are certain you no longer need the portfolio project.
- Return to the public csv-data-quality-auditor repository on GitHub.
- Open the repository settings page.
- Use the repository deletion control for csv-data-quality-auditor.
- Complete GitHub's confirmation prompt for csv-data-quality-auditor.
GitHub should no longer host the repository. This removes the remote workflow plus the project files stored on main.
- Switch back to the csv-data-quality-auditor folder in Visual Studio Code.
- Use the folder context menu to reveal csv-data-quality-auditor in File Explorer.
- Close Visual Studio Code.
- Delete the csv-data-quality-auditor folder from File Explorer.
- Search File Explorer for csv-data-quality-auditor.
You should see no matching project folder. The source files, generated reports, local Git history, and workflow file are now removed from your computer.
Nice Work!
Nice Work!
You did it: your Python command-line application audits messy CSV ticket records without crashing. Each run writes a structured JSON report that separates valid rows from actionable findings.
What you learned:
- Built a resilient data auditor that processes every ticket row. Its report summarizes clean records while preserving malformed records as row-level findings.
- Reproduced the ValueError caused by direct int() conversion. Refactored the script into modular functions for loading data. Added separate functions for validation. Added separate functions for aggregation plus report writing.
- Protected the validation behavior with unit testing that covers clean rows. The suite also covers malformed values plus duplicate identifiers. Published the tested project through Git in a public GitHub repository. Its README.md plus docs/engineering-notes.md capture usage instructions, debugging evidence, resume bullets, and interview talking points.
- Secret Mission: Added continuous integration with GitHub Actions. The workflow runs the complete unit-test suite on every push or pull request. A green workflow run proves the automated check works.
Ready to quiz yourself?