CSV-to-Excel Report Generator
Build a Python script that turns any CSV URL into a formatted Excel report.
Introduction
30 Second Summary
Every week, teams export data as CSV files and then spend time manually formatting them into presentable reports. What if one script could do all of that for you?
In this project, you will build a Python script that downloads CSV data from any URL and automatically generates a polished Excel report with styled headers, formatted numbers, and an embedded chart.
What You'll Build
Point your script at any CSV file on the web, run it, and watch it produce a professional Excel report complete with formatted tables and a column chart.
By the end of this project, you'll have:
- A running script that fetches live CSV data from a URL and prints a data preview straight to your terminal.
- A generated Excel report with blue headers, bordered cells, auto-sized columns, and comma-formatted numbers.
- An embedded column chart that visualizes key numeric data directly in the spreadsheet.
- Secret Mission: Add conditional formatting that color-codes your numbers and a second summary sheet with aggregated statistics.
Are there any prerequisites?
You should be comfortable with basic Python syntax (variables, functions, loops). No prior experience with pandas or Excel libraries is needed.
Before We Start
Before We Start...
Before we dive in, let's lock in what you're about to build and why it matters.
You're building a Python script that automates repetitive reporting. Point it at any CSV URL and it produces a polished Excel report with styled headers, formatted numbers, and an embedded chart.
Set Up Your Project
Your report generator needs three Python packages to do its job: one for downloading data from the web, one for processing that data, and one for writing Excel files.
In this step, you'll create a project folder and install everything your script needs to run.
In this step, get ready to:
- Create a report-generator project folder.
- Install pandas, XlsxWriter, and requests.
- Verify all packages import correctly.
Create your project folder
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Visual Studio Code and press Enter to open it.
- Open the integrated terminal by pressing Ctrl+` (Windows) or Cmd+` (macOS).
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir report-generator
cd report-generator
- Confirm the folder was created by running:
ls
You should see no files listed yet, which confirms you're inside a fresh, empty folder.
- Now open this folder as your VS Code workspace. Click File in the top menu bar.
- Click Open Folder.
- Navigate to your Desktop, select the report-generator folder, and click Open.
The VS Code explorer sidebar now shows REPORT-GENERATOR at the top. Open a fresh terminal with Ctrl+` (Windows) or Cmd+` (macOS). It starts inside your project folder automatically.
Don't see the folder name in the sidebar?
Make sure you selected the report-generator folder itself, not the Desktop. The sidebar title should match the folder name.
If VS Code asked whether you trust the authors of files in this folder, click Yes, I trust the authors.
help me open my project folder in VS Code
Install Python packages
Your script depends on three libraries that don't come with Python out of the box.
- Install all three packages at once by running this command in your terminal:
pip install pandas xlsxwriter requests
You'll see download progress bars and a "Successfully installed" message when it finishes.
What are these packages?
- pandas loads and manipulates tabular data. It turns raw CSV text into structured rows and columns you can filter, sort, and analyze.
- xlsxwriter creates Excel files from scratch. It handles formatting, charts, and conditional formatting.
- requests downloads data from any URL. It handles HTTP connections so your script can fetch live CSV files from the web.
Seeing 'pip: command not found'?
Try using pip3 instead of pip. On some systems, Python 3's package manager is only accessible as pip3.
If that still doesn't work, try running python -m pip install pandas xlsxwriter requests which calls pip through your Python installation directly.
help me fix pip not being recognized in my terminal
Verify the installation
A successful install message from pip only means the files were downloaded. The real test is whether Python can actually import them.
Before you run the next command, what do you expect to see if all three packages imported successfully?
- Verify the installation by running this command:
python -c "import pandas; import xlsxwriter; import requests; print('All packages ready!')"
You should see All packages ready! printed in your terminal. This confirms Python can find and load all three libraries.
Seeing 'ModuleNotFoundError'?
This means pip installed the package to a different Python version than the one running your command. Try python3 instead of python in the command above.
If that doesn't help, re-run the install with python -m pip install pandas xlsxwriter requests to ensure the packages go to the same Python that runs your scripts.
help me fix ModuleNotFoundError after installing packages
Your project folder is ready and all three packages are installed. Next up, you'll write a script that downloads live CSV data from a URL and previews it in your terminal.
Download CSV Data and Preview It
Your packages are installed and ready to go. Now it's time to write the script that actually fetches data from the internet.
Before building any formatted report, you need to confirm your script can reach a CSV file on the web and pull its contents into Python. If the data pipeline breaks at this stage, nothing downstream will work.
In this step, get ready to:
- Create report.py with a function that downloads CSV data from a URL.
- Preview the downloaded data in your terminal to confirm the pipeline works.
Create the data download function
Your script needs to do three things: fetch raw text from a URL, convert that text into structured tabular data, and confirm it loaded correctly. pandas handles the tabular data as a DataFrame, and the requests library handles the HTTP fetch.
- In VS Code, create a new file called report.py inside your report-generator folder.
- Add the following code to report.py:
import requests
import pandas as pd
from io import StringIO
# Configuration - replace with your CSV URL
CSV_URL = "https://your-company-url.com/data.csv"
OUTPUT_FILE = "report.xlsx"
def download_data(url):
"""Download CSV data from a URL and return as a DataFrame."""
print(f"Downloading data from: {url}")
response = requests.get(url)
response.raise_for_status()
df = pd.read_csv(StringIO(response.text))
print(f"Loaded {len(df)} rows x {len(df.columns)} columns")
return df
What does this code do?
- CSV_URL holds the web address of the CSV file you want to turn into a report. Replace this with the URL of any publicly accessible CSV file.
- requests.get(url) sends an HTTP request to download the file's raw text content.
- response.raise_for_status() stops the script with a clear error if the download failed (e.g. a 404 or 500 status code).
- pd.read_csv(StringIO(response.text)) converts the downloaded text into a pandas DataFrame. StringIO wraps the text so pandas can read it as if it were a file on disk.
Find a CSV URL to use
You need any publicly accessible .csv file URL. Search for "sample CSV data" online, or use a dataset from a site like GitHub or data.gov. Copy the direct link to the raw .csv file and paste it as your CSV_URL value.
- Replace the placeholder URL in CSV_URL with the URL of a real CSV file you want to use.
- Save report.py.
- Confirm you can see report.py listed in the Explorer sidebar in VS Code.
Don't see the file in your sidebar?
- Make sure you saved the file with Cmd+S (macOS) or Ctrl+S (Windows).
- Check that the file is named exactly report.py (not report.py.txt or Report.py).
- Confirm your VS Code window has the report-generator folder open. help me check my VS Code is pointing at the right folder
Add the main block and run the script
The download_data function exists, but nothing calls it yet. A Python __main__ block is the entry point that runs when you execute the script from your terminal.
- Add the following code at the bottom of report.py, below the download_data function:
if __name__ == '__main__':
df = download_data(CSV_URL)
print("\nFirst 5 rows:")
print(df.head())
print("\nSummary statistics:")
print(df.describe())
What does this code do?
- if __name__ == '__main__': ensures this code only runs when you execute the file directly (not when another script imports it).
- df.head() prints the first 5 rows so you can eyeball whether the data looks correct.
- df.describe() generates summary statistics (mean, min, max) for every numeric column.
- Save report.py.
Before you run this, what do you expect to see printed in your terminal?
- Run the script by executing this command in your terminal:
python report.py
You should see the URL being fetched, a row and column count, the first 5 rows of data in a table, and summary statistics for numeric columns.
Seeing an error instead of data?
- If you see MissingSchema or InvalidURL, your CSV_URL value is not a valid URL. Make sure it starts with https://.
- If you see HTTPError, the URL returned an error status code. Double check that the URL points directly to a .csv file and is publicly accessible (no login required).
- If you see ParserError, the URL returned content that is not valid CSV. Try opening the URL in your browser to confirm it shows comma-separated text. help me debug my CSV download error
✔️ Awesome, I've got everything!
Great. Double check you've saved report.py and your terminal shows data.
ⓧ I'd like to double check the full code
Here is the complete report.py file at this point:
import requests
import pandas as pd
from io import StringIO
# Configuration - replace with your CSV URL
CSV_URL = "https://your-company-url.com/data.csv"
OUTPUT_FILE = "report.xlsx"
def download_data(url):
"""Download CSV data from a URL and return as a DataFrame."""
print(f"Downloading data from: {url}")
response = requests.get(url)
response.raise_for_status()
df = pd.read_csv(StringIO(response.text))
print(f"Loaded {len(df)} rows x {len(df.columns)} columns")
return df
if __name__ == '__main__':
df = download_data(CSV_URL)
print("\nFirst 5 rows:")
print(df.head())
print("\nSummary statistics:")
print(df.describe())
Your data pipeline is alive. Next up, you'll push this data into an Excel file to see how a raw, unformatted export looks before you add any styling.
Export Raw Data to Excel
Your script can fetch live CSV data and display it in the terminal. Now you need to get that data out of the terminal and into a file someone can actually open.
In this step, you'll write the DataFrame to an Excel file using pandas and XlsxWriter. The result will be raw and unformatted, but it proves the full pipeline works end-to-end.
In this step, get ready to:
- Add a function that exports your DataFrame to an Excel file.
- Run the script and confirm a .xlsx file appears in your project folder.
Add the generate_report function
Your script currently prints data to the terminal, which is great for checking, but useless for sharing. You need a function that takes a DataFrame and writes it to an Excel file.
- In report.py, add this function below the download_data function:
def generate_report(df, output_file):
"""Generate a formatted Excel report with a chart."""
# Write DataFrame to an Excel file using XlsxWriter engine
with pd.ExcelWriter(output_file, engine='xlsxwriter') as writer:
df.to_excel(writer, sheet_name='Report', index=False, startrow=1)
print(f"\nReport generated: {output_file}")
What does this code do?
pd.ExcelWriter creates an Excel file using the xlsxwriter engine. The with statement ensures the file is properly saved and closed when the block ends.
df.to_excel() writes all your DataFrame rows and columns into a sheet called Report. The startrow=1 parameter leaves row 0 empty for a title you'll add later.
Call generate_report from the main block
The function exists but nothing calls it yet. You need to wire it into your script's main block so it runs after the data is downloaded.
- In report.py, scroll down to the if __name__ == '__main__': block at the bottom of your file.
- Find this line:
print(df.describe())
- Add this line directly below it:
generate_report(df, OUTPUT_FILE)
- Save report.py.
Run the script and check the output
Before you run this, think about what you expect to see. Do you think the Excel file will look polished, or more like a raw data dump?
- Run your script by entering this command in the terminal:
python report.py
You should see your usual terminal output plus a new line: Report generated: report.xlsx.
- Confirm the file was created by running:
ls
You should see report.xlsx listed alongside report.py in the output.
- Open report.xlsx in Excel, Google Sheets, or any spreadsheet application.
The data is there, but it looks rough. Headers are plain text, columns are cramped, and numbers have no formatting. That's the gap you'll close in the next step.
Don't see report.xlsx?
- Check your terminal for error messages. A ConnectionError means the CSV URL is unreachable. Make sure CSV_URL points to a valid, publicly accessible CSV file.
- If you see ModuleNotFoundError: No module named 'xlsxwriter', the package did not install correctly. Run pip install xlsxwriter again.
- Make sure generate_report(df, OUTPUT_FILE) is indented inside the if __name__ block, not at the top level of the file.
Still stuck? Help me debug why report.xlsx is not being generated.
✔️ Awesome, I've got everything!
Great. Double check you've saved report.py and move on to the next step.
ⓧ I'd like to double check the full code
Your report.py should have these sections in order:
- Three imports at the top: requests, pandas, and StringIO.
- Two configuration constants: CSV_URL and OUTPUT_FILE.
- The download_data(url) function that fetches CSV data and returns a DataFrame.
- The generate_report(df, output_file) function that writes the DataFrame to an Excel file using pd.ExcelWriter with engine='xlsxwriter'.
- The if __name__ == '__main__': block that calls download_data, prints the head and describe, then calls generate_report.
You've got data flowing from a URL all the way into an Excel file. Next up, you'll transform this raw dump into a professional-looking report with styled headers, proper column widths, and number formatting.
Add Professional Formatting
Your script downloads data and exports it to Excel. But if you open report.xlsx right now, you'll see a raw data dump with cramped columns, plain headers, and no number formatting.
Nobody wants to receive a report that looks like a spreadsheet accident. In this step, you'll use XlsxWriter's formatting engine to add styled headers, bordered cells, number formatting, and auto-fitted columns. The result is a report that's presentation-ready.
In this step, get ready to:
- Create format objects for the title, headers, numbers, and text cells.
- Overwrite the header row and data cells with professional styling.
- Auto-fit column widths so content is readable at a glance.
Access the workbook and create format objects
XlsxWriter gives you direct access to the underlying Excel workbook and worksheet through the pandas writer object. Once you have these handles, you can create format objects that define how cells look.
- In report.py, find the generate_report function. Locate this line inside the with block:
df.to_excel(writer, sheet_name='Report', index=False, startrow=1)
- Add a comment above that line explaining why data starts at row 1, then paste the following code directly below it (still inside the with block):
# Write data starting at row 1 (row 0 reserved for title)
df.to_excel(writer, sheet_name='Report', index=False, startrow=1)
workbook = writer.book
worksheet = writer.sheets['Report']
# Title
title_format = workbook.add_format({
'bold': True,
'font_size': 16,
'font_color': '#1F4E79'
})
worksheet.write('A1', 'Data Report', title_format)
# Header formatting
header_format = workbook.add_format({
'bold': True,
'bg_color': '#4472C4',
'font_color': 'white',
'border': 1,
'align': 'center',
'valign': 'vcenter'
})
for col_num, col_name in enumerate(df.columns):
worksheet.write(1, col_num, col_name, header_format)
What does this code do?
This code grabs direct access to the XlsxWriter engine so you can format cells beyond what pandas offers.
- writer.book gives you the workbook object. writer.sheets['Report'] gives you the worksheet where pandas wrote your data.
- workbook.add_format() creates reusable style objects. You define a format once and apply it to as many cells as you need.
- The header loop overwrites the plain header row (row index 1) with the same column names, now styled with blue backgrounds and white bold text.
- Make sure the original df.to_excel() line is replaced by the version with the comment above it. You should not have a duplicate.
- Save report.py.
- Run the script to see your first formatting changes by running:
python report.py
- Open report.xlsx. You should see "Data Report" as a large title in cell A1, and your column headers now have bold white text on a blue background.
Headers still look plain?
- Make sure all the new code is indented inside the with block. If it's outside the block, the workbook is already closed and formats won't apply.
- Check that you're writing headers to row index 1 (the second row), matching where pandas placed the original headers with startrow=1.
- Close the .xlsx file before re-running the script. Excel locks the file while it's open, preventing Python from writing to it.
Why aren't my XlsxWriter formats showing up in the output file?
Format the data cells
Your headers look great, but the data cells below them are still plain. Numbers need comma formatting, and every cell needs a border so the table reads as a cohesive grid.
- In report.py, add this code directly below the header loop (the for col_num, col_name line you just added):
# Cell formats
number_format = workbook.add_format({'num_format': '#,##0.00', 'border': 1})
text_format = workbook.add_format({'border': 1})
# Write formatted data cells
for row_idx in range(len(df)):
for col_idx, col in enumerate(df.columns):
cell_value = df.iloc[row_idx, col_idx]
if pd.isna(cell_value):
worksheet.write(row_idx + 2, col_idx, '', text_format)
elif pd.api.types.is_numeric_dtype(df[col]):
worksheet.write_number(row_idx + 2, col_idx, float(cell_value), number_format)
else:
worksheet.write_string(row_idx + 2, col_idx, str(cell_value), text_format)
What does this code do?
- number_format displays numbers with comma separators and two decimal places (e.g. 1,234.56). text_format adds a border to non-numeric cells.
- The nested loop visits every cell. It checks whether each column is numeric using pd.api.types.is_numeric_dtype(), then writes with the matching format.
- row_idx + 2 accounts for the title in row 0 and headers in row 1. Data begins in row 2.
- NaN values are written as empty strings so blank cells still get a border instead of showing "nan".
Auto-fit column widths
Even with beautiful formatting, cramped columns make data hard to read. This last piece calculates the widest value in each column and sets the column width to match.
- Still in report.py, add this code directly below the data-cell loop you just wrote:
# Auto-fit column widths
for i, col in enumerate(df.columns):
max_length = max(df[col].astype(str).map(len).max(), len(col)) + 2
worksheet.set_column(i, i, min(max_length, 30))
What does this code do?
- For each column, it finds the longest string (comparing all values and the header name) and adds 2 characters of padding.
- worksheet.set_column() sets the width. The min(..., 30) cap prevents absurdly wide columns from a single long value.
- Save report.py.
Before you run the script, predict what the output file will look like. Will the headers still be blue? Will numbers show commas? Will columns be wider?
- Run the script to generate your fully formatted report:
python report.py
- Open report.xlsx. You should see bold white text on blue headers, bordered cells throughout, numbers displayed with comma separators (e.g. 1,234.56), and columns sized to fit their content.
Numbers showing without commas?
- Confirm your data-cell loop uses write_number() for numeric columns and passes number_format as the last argument.
- Check that the elif condition uses pd.api.types.is_numeric_dtype(df[col]) with the column name, not the individual cell value.
- If the file won't overwrite, close it in Excel first. Excel locks open files.
Help me debug why my number formatting isn't applying in XlsxWriter.
✔️ Awesome, I've got everything!
Great. Double check you've saved your file before moving on.
ⓧ I'd like to double check the full code
Your report.py should now contain these sections in order. Scroll up and confirm each is present:
- Imports and configuration at the top (unchanged).
- download_data() function (unchanged).
- generate_report() function with the with block containing: df.to_excel() with comment, workbook/worksheet access, title format + write, header format + loop, number/text formats, data-cell nested loop, and auto-fit loop.
- __main__ block (unchanged).
Make sure all the formatting code is indented inside the with block, before the print statement.
Your report now looks like something you'd actually send to a stakeholder. The headers pop, numbers are readable, and columns aren't squished. Next up, you'll add a chart that turns the data into a visual story right inside the spreadsheet.
Add a Chart
Your report now has professional formatting. Headers are styled, columns are properly sized, and numbers are easy to read. But stakeholders expect more than a table.
A visual summary embedded directly in the spreadsheet makes the report self-contained and presentation-ready. In this step, you'll add a column chart that plots your numeric data automatically.
In this step, get ready to:
- Create a column chart and configure its data series.
- Style the chart with a title, axis labels, and dimensions.
- Insert the chart into the worksheet and verify it appears in the output file.
Create the chart and configure a data series
XlsxWriter can generate Excel charts entirely from Python. You tell it which cells hold labels and which hold values, and it builds the chart object before writing the file.
- In report.py, find the auto-fit column widths section at the bottom of the with block. Add the following code below it, still inside the with pd.ExcelWriter(...) block:
# Add a column chart
numeric_cols = df.select_dtypes(include='number').columns.tolist()
if numeric_cols:
chart = workbook.add_chart({'type': 'column'})
data_col = df.columns.get_loc(numeric_cols[0])
num_rows = min(len(df), 10)
chart.add_series({
'name': numeric_cols[0],
'categories': ['Report', 2, 0, num_rows + 1, 0],
'values': ['Report', 2, data_col, num_rows + 1, data_col],
})
What does this code do?
df.select_dtypes(include='number') filters for only numeric columns in your DataFrame. The chart needs numbers to plot, so this finds them automatically.
workbook.add_chart({'type': 'column'}) creates a new column chart object. The add_series() call tells the chart where its data lives: categories (x-axis labels) come from column 0, and values come from the first numeric column. Both reference rows 2 through num_rows + 1 to cap the chart at 10 rows for readability.
Style the chart and insert it into the worksheet
The chart exists but has no title, no axis labels, and no position on the sheet. You need to style it and place it next to the data.
- Directly below the chart.add_series() block you just added, paste the following styling and insertion code:
chart.set_title({'name': f'{numeric_cols[0]} Overview'})
chart.set_x_axis({'name': df.columns[0]})
chart.set_y_axis({'name': numeric_cols[0]})
chart.set_style(10)
chart.set_size({'width': 520, 'height': 320})
worksheet.insert_chart(1, len(df.columns) + 1, chart)
What does this code do?
chart.set_title() adds a heading above the chart. set_x_axis() and set_y_axis() label what each axis represents.
chart.set_style(10) applies a built-in Excel style (style 10 is a clean, professional look). chart.set_size() sets the chart dimensions to 520x320 pixels.
worksheet.insert_chart(1, len(df.columns) + 1, chart) places the chart at row 1, one column past the end of your data. This positions it neatly to the right of the table.
- Save report.py.
Seeing an indentation error?
The chart code must be indented at the same level as the auto-fit section (two levels of indentation inside the with block). The styling lines inside the if numeric_cols: block need one extra indent level.
Check that numeric_cols = df.select_dtypes(...) aligns with the for i, col in enumerate(df.columns): line above it.
Help me fix my chart indentation in report.py
Run the script and verify the chart appears
Before you run this, what do you think the Excel file will look like now? Will the chart appear on the same sheet as the data, or on a separate sheet?
- Run the script by entering this command in your terminal:
python report.py
- Open report.xlsx in Excel, Google Sheets, or LibreOffice Calc.
A column chart appears to the right of your data table on the same sheet. It plots the first numeric column values against the labels from your first column. The chart has a title, labeled axes, and professional styling.
Don't see a chart in the file?
Make sure the worksheet.insert_chart() line is inside the if numeric_cols: block. Without it, the chart is created but never placed on the sheet.
If your CSV has no numeric columns, the chart is skipped entirely. Check your data source has at least one column of numbers.
Help me figure out why my chart isn't appearing in the Excel file
✔️ Awesome, I've got everything!
Great. Make sure you've saved report.py and confirmed the chart appears in your Excel file.
ⓧ I'd like to double check the full code
Your report.py should contain these sections in order. Scroll through your file and confirm each one is present:
- Imports (requests, pandas, StringIO) and configuration constants (CSV_URL, OUTPUT_FILE).
- The download_data(url) function that fetches and parses CSV data.
- The generate_report(df, output_file) function containing: DataFrame export, title formatting, header formatting, cell formatting, auto-fit column widths, and the chart code you just added.
- Inside the chart section: numeric_cols detection, workbook.add_chart(), chart.add_series(), styling calls (set_title, set_x_axis, set_y_axis, set_style, set_size), and worksheet.insert_chart().
- The __main__ block that calls download_data(), prints the preview, and calls generate_report().
Secret mission
Add Conditional Formatting and a Summary Sheet
Your report has clean formatting and a chart, but real business reports highlight key values visually and include aggregated insights. In this mission, you'll add a color-coded gradient to your numbers and a second summary sheet with statistics.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your resources for future use, slim them down, or delete them entirely.
Resources you used:
- report-generator project folder on your Desktop (contains report.py and report.xlsx).
- Python packages installed via pip: pandas, xlsxwriter, requests.
Keep everything running
No action needed. Choose this if you plan to reuse the script with different CSV URLs.
- Your report-generator folder stays on your Desktop, ready to use anytime.
- To generate a report for different data, open report.py and change the CSV_URL variable to a new URL. Run the script again to produce a fresh report.
- The installed Python packages (pandas, xlsxwriter, requests) remain available for other Python projects on your machine.
Pause - I'll come back to this later
Delete the generated output file to free up disk space while keeping the script for later.
- Delete report.xlsx from your report-generator folder. You can regenerate it at any time by running python report.py again.
- Keep report.py and the installed packages so you can pick up where you left off.
Delete - I don't want to use this again
Remove all project files and uninstall the packages to return your machine to its previous state.
- Delete the project folder by running this command in your terminal:
macOS
rm -rf ~/Desktop/report-generator
Windows
rmdir /s /q $HOME\Desktop\report-generator
- Uninstall the Python packages by running:
pip uninstall pandas xlsxwriter requests -y
- Confirm the folder is gone by checking your Desktop. You should no longer see report-generator listed there.
That's a Wrap!
That's a Wrap!
Nice work! You just built a complete data automation pipeline that turns raw CSV data into polished, presentation-ready Excel reports.
You've learned how to:
- Fetch live CSV data from a URL using Python's requests library and parse it into a pandas DataFrame for programmatic analysis.
- Generate a professionally formatted Excel report with styled headers, auto-fitted columns, number formatting, and an embedded column chart using XlsxWriter.
- Build a reusable automation script that can be pointed at any CSV URL to produce business-ready output in seconds.
- Secret Mission: Added conditional formatting with color scales and a multi-sheet summary dashboard with aggregated statistics.
Ready to quiz yourself?