Python · SQL Server · Excel Reporting

Export SQL Server Data to Excel Using Python, pandas and openpyxl

A complete Step 05 workflow that reads the latest furnace events from SQF_DB, converts them into a pandas DataFrame, and produces a timestamped, filtered and professionally formatted Excel workbook.

SQL Server source pandas + pyodbc openpyxl formatting Furnace event report

Lab Overview

Python Learning SeriesEstimated time: 60 minutesDifficulty: Intermediate

Prerequisites / What You’ll Need

  • SQL Server table containing report data
  • Python 3.x with pyodbc, pandas and openpyxl
  • Write access to a local report folder
Quick answer

Connect to SQL Server with pyodbc, execute a newest-first SELECT, convert the cursor results to a pandas DataFrame, create a unique .xlsx path with pathlib, and export through pd.ExcelWriter(engine="openpyxl"). Use openpyxl to freeze the header, add filters, style cells, format dates/numbers and size columns.

  • SQL Server supplies the latest 100 furnace events.
  • pandas handles tabular data; openpyxl controls workbook presentation.
  • Timestamped filenames prevent normal report runs from overwriting each other.

What This SQL Server-to-Excel Python Tutorial Covers

This guide targets developers and automation engineers searching for a reliable way to export SQL Server query results to Excel with Python. It combines pyodbc for SQL access, pandas for tabular data, and openpyxl for professional XLSX formatting.

Related Search Topics

export SQL query results to Excel Python · SQL Server to pandas DataFrame to Excel · format Excel report with openpyxl · automate SQL Server Excel reports

1. Export SQL Server Data to Excel: Reporting Flow

This program converts operational SQL data into a user-friendly Excel report. It validates the source table, reads a controlled batch, preserves SQL column names, writes the data, applies presentation formatting and closes the database connection regardless of success or failure.

Plant Reporting • SQL-to-Excel Architecture

SQL Server → pandas → openpyxl → Excel

Convert operational SQL data into a formatted Excel report by reading a controlled record set, building a DataFrame, writing XLSX and applying workbook formatting.

1

Connect SQL Server

Open SQF_DB through pyodbc and verify the source table.

pyodbc.connect(...)
2

Query Records

Read the latest controlled batch from dbo.tblEvent.

SELECT TOP (100) ... ORDER BY DT DESC
3

Build DataFrame

Preserve SQL column names while moving rows into pandas.

df = pd.DataFrame(...)
4

Create Report Path

Generate a unique XLSX filename in the report folder.

D:\SQF_Reports\ SQF_Report_....xlsx
5

Write Excel

Export the DataFrame through the openpyxl Excel engine.

df.to_excel(..., engine="openpyxl")
6

Format & Verify

Freeze header, filter, format numbers/dates and save the workbook.

freeze_panes = "A2" AutoFilter / widths save()
pyodbcSQL Server read
pandasDataFrame
openpyxlExcel formatting
XLSXFinal report
Architecture:SQL Server→pyodbc→DataFrame→XLSX→openpyxl→Report

Code-rendered diagram: the reporting layers are separated so learners can understand where SQL retrieval ends and Excel presentation begins.

Lab Manual Index: 8 Guided Exercises

Every section of this tutorial carries its own lab manual — objective, procedure, a line-by-line explanation, copy-paste practice code, the exact console output to expect, a checkpoint and a troubleshooting table. Work through them in order; each lab builds on the checkpoint before it. Total bench time is about 63 minutes.

2. Requirements and Configuration

install_requirements.cmd
python -m pip install --upgrade pip
python -m pip install pyodbc pandas openpyxl
01_settings.py
from pathlib import Path

SERVER = r"SOFTWELL\WINCC"
DATABASE = "SQF_DB"
TABLE = "dbo.tblEvent"
DRIVER = "ODBC Driver 17 for SQL Server"

# Export the latest 100 records
TOP_RECORDS = 100

# Excel report folder
OUTPUT_FOLDER = Path(r"D:\SQF_Reports")

The raw server/path strings preserve Windows backslashes. The program needs SQL SELECT access plus write permission for D:\SQF_Reports. Keep connection and output settings in trusted configuration for production deployments.

Data handling: generated reports may contain operational or production information. Apply folder permissions, retention rules and approved distribution controls; do not write sensitive reports into a broadly shared directory.

3. Connect to SQL Server and Verify the Table

02_connect.py
import pyodbc

connection_string = (
    f"DRIVER={{{DRIVER}}};"
    f"SERVER={SERVER};"
    f"DATABASE={DATABASE};"
    "Trusted_Connection=yes;"
    "TrustServerCertificate=yes;"
)

connection = pyodbc.connect(connection_string, timeout=15)
cursor = connection.cursor()
03_verify_table.py
cursor.execute("""
    SELECT COUNT(*)
    FROM INFORMATION_SCHEMA.TABLES
    WHERE TABLE_SCHEMA = 'dbo'
      AND TABLE_NAME = 'tblEvent'
""")

table_available = cursor.fetchone()[0]

if table_available == 0:
    raise RuntimeError("Table dbo.tblEvent is not available.")

The existence check fails early with a clear message instead of letting the later query produce a less focused error. In a production report, also validate required columns and compatible types or manage the schema through versioned migrations.

Lab Manual 3 — Connect to SQL Server and Verify the Table

8 min

Objective

Open the exact SQL Server instance and database the report will read, and confirm the source table exists before a single row or workbook is produced.

Procedure

  1. Open ODBC Data Sources (64-bit), go to the Drivers tab and copy the driver name exactly as Windows shows it.
  2. Edit SERVER, DATABASE, TABLE and DRIVER in the settings block.
  3. Run the connection and table check together, before any SELECT is written.
  4. Confirm the printed server and database names match what you intended.

What each line does

CodeMeaning in this lab
r"SOFTWELL\WINCC"Raw string. Without the leading r, Python reads \W as an escape sequence and the named instance breaks.
f"DRIVER={{{DRIVER}}};"Three braces produce one literal {, the driver name, and one literal } — ODBC requires the braces.
Trusted_Connection=yesUses the Windows account running the script, so no password sits in the report file.
TrustServerCertificate=yesSkips certificate validation. Fine inside a closed training lab, not on a plant server.
timeout=15Login timeout in seconds, so a scheduled report fails fast instead of hanging overnight.
INFORMATION_SCHEMA.TABLESANSI catalog view. A count of zero means the table is missing or invisible to your login.
raise RuntimeError(...)Stops with a readable message instead of letting the failure surface later as a confusing SELECT error.

Copy-paste practice code

02_connection_test.py
import pyodbc

print("Installed ODBC drivers:")
for driver in pyodbc.drivers():
    print("  -", driver)

connection = pyodbc.connect(connection_string, timeout=15)
cursor = connection.cursor()

cursor.execute("SELECT @@SERVERNAME, DB_NAME(), SYSTEM_USER")
server_name, database_name, login_name = cursor.fetchone()
print("Server  :", server_name)
print("Database:", database_name)
print("Login   :", login_name)

cursor.execute(f"SELECT COUNT(*) FROM {TABLE}")
print("Rows available:", cursor.fetchone()[0])

connection.close()

TABLE is a fixed constant in your own script, never user input, so it is safe inside the f-string. Data values always stay in parameters.

Expected result

console output
Installed ODBC drivers:
  - SQL Server
  - ODBC Driver 17 for SQL Server
Server  : SOFTWELL\WINCC
Database: SQF_DB
Login   : SOFTWELL\Trainer
Rows available: 100

Checkpoint

Pass when: The server and database names match your constants, and the row count is greater than zero. A count of zero means there is nothing to report on yet.

If it fails

SymptomWhat to check
IM002 — data source name not foundDriver string misspelled, or 32-bit driver installed while 64-bit Python is running. Compare with the printed list.
08001 — SQL Server does not existWrong instance name, SQL Server Browser stopped, or TCP/IP disabled in SQL Server Configuration Manager.
Table dbo.tblEvent is not availableConnected to the wrong database, wrong schema, or the login has no rights on the object.
Rows available: 0The table exists but is empty. Run the insert practical (Code 02) before generating a report.

4. Read the Latest SQL Records

04_select_query.py
select_query = f"""
    SELECT TOP ({TOP_RECORDS})
        DT, TM, SQF_No, ChargeNo,
        Event_From, Event_To,
        Temp_Set, Temp_Act,
        Cp_Set, Cp_Act,
        Oil_Set, Oil_Act,
        Jacket_Set, Jacket_Act,
        Fan_Status
    FROM dbo.tblEvent
    ORDER BY DT DESC, TM DESC
"""

cursor.execute(select_query)

The query returns newest records first. TOP_RECORDS is an integer constant controlled by the program, so its interpolation is not accepting user SQL. If the limit becomes external input, validate it as a bounded integer or use a supported bound parameter.

Add a unique descending tie-breaker such as Event_ID DESC when two records can share the same DT and TM.

Lab Manual 4 — Read the Latest SQL Records for the Report

7 min

Objective

Retrieve exactly the columns the report needs, in the order the Excel sheet will show them, sorted so the newest event is row one.

Procedure

  1. Set TOP_RECORDS = 5 for the first run so a mistake is cheap.
  2. Run the query and print the raw cursor rows before pandas or Excel is involved.
  3. Confirm the column order matches the order you want in the workbook.
  4. Raise TOP_RECORDS to 100 once the small batch looks right.

What each line does

CodeMeaning in this lab
f"""...TOP ({TOP_RECORDS})..."""The limit is an integer constant you control, inserted with an f-string. Never insert user-supplied text this way.
DT, TM, SQF_No, ...Columns are named explicitly, so the Excel column order is fixed and the report does not change when the table gains a column.
ORDER BY DT DESC, TM DESCBoth date and time descend, so the newest event lands in row 2 of the sheet, directly under the header.
cursor.execute(select_query)Runs the statement. Nothing is read into memory until fetchall() is called.
Column positionsThis order is what makes columns G to N the numeric ones in Section 8. Change the SELECT and you must change those letters.

Copy-paste practice code

04_query_preview.py
TOP_RECORDS = 5

cursor.execute(select_query)

print("Column order for the Excel sheet:")
for position, column in enumerate(cursor.description, start=1):
    letter = chr(64 + position)
    print(f"  {letter}: {column[0]}")

print("\nRows:")
for index, row in enumerate(cursor.fetchall(), start=1):
    print(index, row.DT, row.TM, row.SQF_No, row.Event_To, row.Temp_Act)

The letter column in this output is the one to keep. It tells you which spreadsheet columns the number formatting in Section 8 will apply to.

Expected result

console output
Column order for the Excel sheet:
  A: DT
  B: TM
  C: SQF_No
  D: ChargeNo
  E: Event_From
  F: Event_To
  G: Temp_Set
  H: Temp_Act
  I: Cp_Set
  J: Cp_Act
  K: Oil_Set
  L: Oil_Act
  M: Jacket_Set
  N: Jacket_Act
  O: Fan_Status

Rows:
1 2026-09-19 12:43:00 3 Charge Discharged 96.21
2 2026-09-19 12:42:00 3 Cooling 148.77

Checkpoint

Pass when: Fifteen columns appear in the expected order, G to N are the eight numeric process columns, and the timestamps descend from the newest record.

If it fails

SymptomWhat to check
Invalid column nameThe table schema differs from the example. Compare the SELECT list with INFORMATION_SCHEMA.COLUMNS.
Rows return in the wrong orderDT or TM is stored as text, so it sorts alphabetically. Use proper date and time types.
Identical timestamps swap between runsTies have no defined order. Add a unique descending key such as Event_ID DESC.
Numeric formatting lands on the wrong columnsThe SELECT order changed but the numeric_columns letters in Section 8 did not.

5. Convert Cursor Results into a DataFrame

05_build_dataframe.py
import pandas as pd

column_names = [column[0] for column in cursor.description]
rows = cursor.fetchall()

if not rows:
    raise RuntimeError("No records are available in dbo.tblEvent.")

dataframe = pd.DataFrame.from_records(rows, columns=column_names)

cursor.description supplies the selected SQL column names. The row list and names become one labeled pandas DataFrame. The empty check prevents creating a misleading blank report.

fetchall() is appropriate for 100 rows. Large exports should use chunked queries/writes or a streaming strategy so the full result does not occupy memory twice as cursor rows and DataFrame data.

Lab Manual 5 — Convert Cursor Results into a DataFrame

6 min

Objective

Build a labelled pandas DataFrame from the cursor result and stop the program cleanly when the query returned nothing, rather than writing an empty workbook.

Procedure

  1. Build the DataFrame from the small batch of Lab 4.
  2. Print shape and dtypes.
  3. Note which numeric columns arrived as object — those will not right-align in Excel as numbers.
  4. Test the empty guard by pointing the query at a filter that returns no rows.

What each line does

CodeMeaning in this lab
cursor.descriptionMetadata for the returned columns. Element zero of each tuple is the column name.
cursor.fetchall()Reads the remaining rows into memory. Safe here because TOP_RECORDS limits the query.
if not rows:An empty result stops the run. Without this, the script would create a workbook with a header and no data.
pd.DataFrame.from_records(...)Combines the row tuples with the column names into a labelled table.
columns=column_namesWithout this, pandas numbers the columns 0 to 14 and the Excel header row would be numbers instead of names.

Copy-paste practice code

05_inspect_dataframe.py
print("Shape (rows, columns):", dataframe.shape)

print("\nData types:")
print(dataframe.dtypes.to_string())

print("\nMissing values per column:")
print(dataframe.isna().sum().to_string())

print("\nFirst 3 rows as they will appear in Excel:")
print(dataframe.head(3).to_string(index=False))

Any numeric column showing dtype object will be written to Excel as text, so the 0.00 number format in Section 8 will have no visible effect on it.

Expected result

console output
Shape (rows, columns): (100, 15)

Data types:
DT             object
TM             object
SQF_No          int64
Temp_Set      float64
Temp_Act      float64
Fan_Status     object

Missing values per column:
DT            0
Temp_Act      0
Fan_Status    0

Checkpoint

Pass when: The row count matches TOP_RECORDS, all fifteen columns carry their SQL names, and the eight process columns are numeric rather than object.

If it fails

SymptomWhat to check
Excel header row shows 0, 1, 2 ...The columns= argument was dropped from from_records().
RuntimeError: No records are availableThe table is empty for this query. This is the guard working correctly, not a bug.
Numbers appear left-aligned in ExcelThat column is dtype object, so Excel stores it as text. Apply pd.to_numeric() before writing.
MemoryError on a large tablefetchall() loads everything. Keep the TOP limit or read in chunks.

6. Create a Unique Excel Report Path

06_report_path.py
from datetime import datetime

OUTPUT_FOLDER.mkdir(parents=True, exist_ok=True)

# Date, time, seconds and microseconds prevent overwriting
timestamp = datetime.now().strftime("%d-%m-%Y_%H-%M-%S-%f")

excel_file = OUTPUT_FOLDER / f"SQF_Event_Report_{timestamp}.xlsx"

Path.mkdir(..., exist_ok=True) creates the output folder and any missing parents without failing when it already exists. Date, time and microseconds make filename collisions unlikely during normal sequential runs.

For multi-process or distributed jobs, use a UUID or atomic file-creation pattern. Consider starting filenames with YYYY-MM-DD so ordinary alphabetical sorting also follows date order.

Lab Manual 6 — Create a Unique Excel Report Path

6 min

Objective

Produce a report path that is guaranteed to be new on every run, and create the output folder if it does not yet exist.

Procedure

  1. Set OUTPUT_FOLDER to a folder you own — not a network share for the first run.
  2. Run the path builder twice in a row and compare the two filenames.
  3. Open the folder in Explorer and confirm it was created automatically.

What each line does

CodeMeaning in this lab
Path(r"D:\SQF_Reports")pathlib.Path handles Windows separators safely. The raw string keeps the backslashes literal.
mkdir(parents=True, exist_ok=True)Creates the folder and any missing parents, and does nothing if it already exists — so a rerun is safe.
strftime("%d-%m-%Y_%H-%M-%S-%f")Day, month, year, hour, minute, second and microseconds. The microseconds are what make two runs in the same second distinct.
OUTPUT_FOLDER / f"..."The / operator joins paths. It is safer than string concatenation, which loses or doubles separators.
.xlsxThe extension must match the writer engine. openpyxl writes xlsx, not the legacy xls format.

Copy-paste practice code

06_path_drill.py
from datetime import datetime
from pathlib import Path

OUTPUT_FOLDER = Path(r"D:\SQF_Reports")
OUTPUT_FOLDER.mkdir(parents=True, exist_ok=True)

for attempt in range(1, 4):
    timestamp = datetime.now().strftime("%d-%m-%Y_%H-%M-%S-%f")
    excel_file = OUTPUT_FOLDER / f"SQF_Event_Report_{timestamp}.xlsx"
    print(attempt, excel_file.name)

print("\nFolder exists:", OUTPUT_FOLDER.exists())
print("Folder is writable:", OUTPUT_FOLDER.is_dir())
print("Existing reports:", len(list(OUTPUT_FOLDER.glob("SQF_Event_Report_*.xlsx"))))

Three filenames generated in the same second must still differ. If they do not, the microsecond field is missing from the format string.

Expected result

console output
1 SQF_Event_Report_19-09-2026_12-44-07-481293.xlsx
2 SQF_Event_Report_19-09-2026_12-44-07-481664.xlsx
3 SQF_Event_Report_19-09-2026_12-44-07-482011.xlsx

Folder exists: True
Folder is writable: True
Existing reports: 0

Checkpoint

Pass when: All three filenames differ, the folder is created automatically, and the path prints with correct Windows separators.

If it fails

SymptomWhat to check
FileNotFoundError on the driveThe drive letter does not exist on this PC. Use a local folder such as C:\SoftwellLabs\reports.
PermissionError creating the folderThe Windows account cannot write there. Choose a user-owned folder, not a protected system path.
Two runs produce the same filenameThe %f microsecond field is missing from strftime.
Reports pile up foreverExpected. Add a retention step that deletes reports older than an agreed number of days.

7. Write a pandas DataFrame to Excel with openpyxl

07_write_excel.py
with pd.ExcelWriter(
    excel_file,
    engine="openpyxl",
    datetime_format="DD-MM-YYYY HH:MM:SS",
) as writer:
    dataframe.to_excel(
        writer,
        sheet_name="Event Report",
        index=False,
    )
    worksheet = writer.sheets["Event Report"]

The context manager saves and closes the workbook automatically. index=False prevents pandas’ row index from becoming an unwanted Excel column. writer.sheets exposes the openpyxl worksheet for detailed formatting before the file is finalized.

Lab Manual 7 — Write the DataFrame to an Excel Workbook

7 min

Objective

Write the DataFrame into a real xlsx workbook with a named sheet, and hold the worksheet object so Section 8 can format it before the file is saved.

Procedure

  1. Write the workbook with the with block exactly as shown.
  2. Open the file in Excel and confirm the sheet name and header row.
  3. Delete the file, remove index=False, run again and compare — then restore it.

What each line does

CodeMeaning in this lab
with pd.ExcelWriter(...) as writerThe context manager saves and closes the workbook when the block ends. Remove it and the file stays empty or locked.
engine="openpyxl"Selects the writer that produces xlsx and exposes the worksheet object for formatting.
datetime_format="DD-MM-YYYY HH:MM:SS"Excel number format applied to datetime cells as they are written.
sheet_name="Event Report"Names the tab. Without it the sheet is called Sheet1, which looks unfinished in a delivered report.
index=FalseSuppresses the pandas index. Without this, column A becomes 0, 1, 2 and every later column letter shifts by one.
writer.sheets["Event Report"]Retrieves the live openpyxl worksheet so freeze panes, fills and widths can be applied before saving.

Copy-paste practice code

07_write_and_check.py
from openpyxl import load_workbook

with pd.ExcelWriter(
    excel_file,
    engine="openpyxl",
    datetime_format="DD-MM-YYYY HH:MM:SS",
) as writer:
    dataframe.to_excel(writer, sheet_name="Event Report", index=False)

# Re-open the saved file and prove what actually landed on disk
workbook = load_workbook(excel_file)
worksheet = workbook["Event Report"]

print("Sheet names :", workbook.sheetnames)
print("Dimensions  :", worksheet.dimensions)
print("Header row  :", [cell.value for cell in worksheet[1]])
print("Data rows   :", worksheet.max_row - 1)
workbook.close()

Re-opening the saved file is the only honest check. A script that prints “report created” without reading the file back has proved nothing.

Expected result

console output
Sheet names : ['Event Report']
Dimensions  : A1:O101
Header row  : ['DT', 'TM', 'SQF_No', 'ChargeNo', 'Event_From', 'Event_To', 'Temp_Set', 'Temp_Act', 'Cp_Set', 'Cp_Act', 'Oil_Set', 'Oil_Act', 'Jacket_Set', 'Jacket_Act', 'Fan_Status']
Data rows   : 100

Checkpoint

Pass when: The sheet is named correctly, column A holds DT and not an index, and the data row count matches the DataFrame length.

If it fails

SymptomWhat to check
Column A contains 0, 1, 2 ...index=False was omitted. Every formatting letter in Section 8 is now shifted by one column.
ModuleNotFoundError: openpyxlInstall it with python -m pip install openpyxl inside the active environment.
The file is created but emptyThe with block was replaced with a bare call, so the workbook was never saved.
PermissionError while writingThe workbook is open in Excel. Close it and run again.

8. Format the Excel Report with openpyxl

08_format_sheet.py
from openpyxl.styles import Alignment, Font, PatternFill

# Freeze the heading row and add filters to every column
worksheet.freeze_panes = "A2"
worksheet.auto_filter.ref = worksheet.dimensions

# Format the header row
header_fill = PatternFill(fill_type="solid", fgColor="F4B183")

for cell in worksheet[1]:
    cell.font = Font(bold=True)
    cell.fill = header_fill
    cell.alignment = Alignment(horizontal="center", vertical="center")

# Format the date column
for cell in worksheet["A"][1:]:
    cell.number_format = "DD-MM-YYYY HH:MM:SS"

# Format the process-value columns
numeric_columns = [
    "G", "H",  # Temperature
    "I", "J",  # CP
    "K", "L",  # Oil
    "M", "N",  # Jacket
]

for column_letter in numeric_columns:
    for cell in worksheet[column_letter][1:]:
        cell.number_format = "0.00"
        cell.alignment = Alignment(horizontal="center")
Formatting featureBenefit
Freeze A2Keeps headings visible while scrolling
Auto filterLets users filter furnace, charge, stage and fan status
Orange bold headerCreates a visible report hierarchy
Date number formatShows consistent timestamps
0.00 numeric formatStandardizes process-value display
Calculated widths capped at 35Improves readability without extremely wide columns
09_column_widths.py
for column_cells in worksheet.columns:
    column_letter = column_cells[0].column_letter

    maximum_length = max(
        len(str(cell.value)) if cell.value is not None else 0
        for cell in column_cells
    )

    worksheet.column_dimensions[column_letter].width = min(maximum_length + 3, 35)

worksheet.row_dimensions[1].height = 25

Lab Manual 8 — Format Headers, Values and Column Widths

9 min

Objective

Turn a plain data dump into a report an engineer can actually read: frozen header, filters, formatted numbers and columns wide enough to show their values.

Procedure

  1. Apply the freeze pane and auto filter first, then reopen the file to confirm both.
  2. Apply the header fill and bold font.
  3. Apply the 0.00 number format to columns G to N and the date format to column A.
  4. Run the width loop and check that no column shows #####.

What each line does

CodeMeaning in this lab
worksheet.freeze_panes = "A2"Everything above row 2 stays visible while scrolling, so the header never leaves the screen.
worksheet.auto_filter.ref = worksheet.dimensionsAdds filter dropdowns across the whole used range, so the reader can filter by furnace or stage.
PatternFill(fill_type="solid", fgColor="F4B183")Header background colour. fill_type must be set or the colour will not appear.
worksheet[1]Row 1 — the header row. worksheet["A"][1:] is column A from row 2 down, skipping the header.
cell.number_format = "0.00"Excel display format, two decimals. It changes how the number is shown, not the stored value.
numeric_columns = ["G", ... "N"]Hard-coded letters. They are only correct while the SELECT column order and index=False stay unchanged.
min(maximum_length + 3, 35)Width fits the longest value plus padding, capped at 35 so one long text column cannot dominate the sheet.
row_dimensions[1].height = 25Gives the header room so the centred text is not cramped.

Copy-paste practice code

08_verify_formatting.py
from openpyxl import load_workbook

workbook = load_workbook(excel_file)
worksheet = workbook["Event Report"]

print("Freeze panes :", worksheet.freeze_panes)
print("Auto filter  :", worksheet.auto_filter.ref)
print("Header height:", worksheet.row_dimensions[1].height)

header = worksheet["A1"]
print("Header bold  :", header.font.bold)
print("Header fill  :", header.fill.fgColor.rgb)

print("\nColumn widths:")
for letter in ("A", "B", "C", "F", "H"):
    width = worksheet.column_dimensions[letter].width
    print(f"  {letter}: {width:.1f}")

print("\nNumber format of H2:", worksheet["H2"].number_format)
workbook.close()

This reads the formatting back out of the saved file. It is the check to run before emailing a report to a customer.

Expected result

console output
Freeze panes : A2
Auto filter  : A1:O101
Header height: 25.0
Header bold  : True
Header fill  : 00F4B183

Column widths:
  A: 22.0
  B: 11.0
  C: 9.0
  F: 20.0
  H: 11.0

Number format of H2: 0.00

Checkpoint

Pass when: Freeze panes reads A2, the filter covers the full range, the header is bold and filled, and H2 reports the 0.00 format.

If it fails

SymptomWhat to check
The header colour does not appearfill_type="solid" is missing. A fgColor alone does nothing.
Numbers still show many decimalsThat column is text, not numeric. A number format has no effect on a string.
Formatting lands on the wrong columnsThe SELECT order changed, or index=False was dropped so everything shifted one column right.
A column shows #####The width is too small for the value. Raise the cap of 35, or shorten the source text.
Formatting disappears after savingThe formatting ran outside the with pd.ExcelWriter(...) block, after the workbook was already written.

9. Error Handling and Connection Cleanup

The program separates database errors, file-permission failures and other exceptions. finally closes the SQL Server connection in every path.

10_error_handling.py
except pyodbc.Error as error:
    print("SQL Server error:")
    print(error)

except PermissionError:
    print(
        "Excel file permission error.\n"
        "Close the Excel file and run the program again."
    )

except Exception as error:
    print("Program stopped:")
    print(error)

finally:
    if connection is not None:
        connection.close()
        print("SQL Server connection closed.")

A locked workbook commonly triggers PermissionError when a fixed filename is reused. Unique names reduce that risk, but folder permissions, antivirus scanning, synchronization software or another process may still block creation.

Lab Manual 9 — Error Handling and Connection Cleanup

8 min

Objective

Make the report fail with one readable line instead of a stack trace, and prove the SQL Server connection is released on every path including failure.

Procedure

  1. Run the report normally and confirm the closing message appears.
  2. Open the last report in Excel, run again, and confirm the PermissionError message.
  3. Break the server name deliberately and confirm the SQL Server handler catches it.
  4. Confirm that in both failures the connection-closed message still prints.

What each line does

CodeMeaning in this lab
connection = NoneSet before the try, so finally can test it safely even when the connect call itself failed.
except pyodbc.ErrorCatches driver, login and query failures specifically.
except PermissionErrorThe common real-world failure: the previous workbook is still open in Excel, so the new file cannot be written.
except Exception as errorLast-resort handler. It must come after the specific handlers, or it would swallow them.
finally:Runs on success, on failure, and after an unhandled exit — so the connection is never left open.
if connection is not None:Guards against calling close() on a connection that was never created.

Copy-paste practice code

10_failure_drill.py
import pyodbc

BROKEN_SERVER = r"SOFTWELL\NOSUCHINSTANCE"

connection = None

try:
    connection = pyodbc.connect(
        f"DRIVER={{{DRIVER}}};"
        f"SERVER={BROKEN_SERVER};"
        f"DATABASE={DATABASE};"
        "Trusted_Connection=yes;"
        "TrustServerCertificate=yes;",
        timeout=5,
    )
    print("This line should never run.")

except pyodbc.Error as error:
    print("Handled cleanly.")
    print("SQLSTATE:", error.args[0])
    print("Message :", str(error.args[1])[:110])

finally:
    if connection is not None:
        connection.close()
    print("Finally block reached — cleanup always runs.")

Run this once so you recognise the SQLSTATE codes in a real failure. timeout=5 keeps the drill short instead of waiting for the default login timeout.

Expected result

console output
Handled cleanly.
SQLSTATE: 08001
Message : [Microsoft][ODBC Driver 17 for SQL Server]Named Pipes Provider: Could not open a connection to SQL Server
Finally block reached — cleanup always runs.

Checkpoint

Pass when: Each failure prints one handled message with no traceback, and the cleanup line appears in every case including the successful run.

If it fails

SymptomWhat to check
A full traceback still appearsThe failing call sits outside the try block.
NameError: connection is not definedconnection = None was not set before the try.
The PermissionError handler never firesIt is listed after except Exception, which catches it first. Specific handlers must come first.
The script hangs instead of failingNo login timeout. Add timeout=15 to pyodbc.connect().

10. Complete Copy-Paste-Ready Program

sql_to_excel_report.py
r"""
STEP 05: SQL Server to Excel Report

SQL Server : SOFTWELL\WINCC
Database   : SQF_DB
Table      : dbo.tblEvent
Output     : D:\SQF_Reports

Install once:
python -m pip install pyodbc pandas openpyxl
"""

from datetime import datetime
from pathlib import Path

import pandas as pd
import pyodbc
from openpyxl.styles import Alignment, Font, PatternFill

# ============================================================
# 1. SQL SERVER SETTINGS
# ============================================================
SERVER = r"SOFTWELL\WINCC"
DATABASE = "SQF_DB"
TABLE = "dbo.tblEvent"
DRIVER = "ODBC Driver 17 for SQL Server"

# Export the latest 100 records
TOP_RECORDS = 100

# Excel report folder
OUTPUT_FOLDER = Path(r"D:\SQF_Reports")

# ============================================================
# 2. CONNECT TO SQL SERVER
# ============================================================
connection = None

try:
    connection_string = (
        f"DRIVER={{{DRIVER}}};"
        f"SERVER={SERVER};"
        f"DATABASE={DATABASE};"
        "Trusted_Connection=yes;"
        "TrustServerCertificate=yes;"
    )

    connection = pyodbc.connect(connection_string, timeout=15)
    cursor = connection.cursor()

    print("=" * 65)
    print("STEP 05 - SQL SERVER TO EXCEL REPORT")
    print("=" * 65)
    print("SQL Server connection successful.")

    # ========================================================
    # 3. CHECK WHETHER THE TABLE IS AVAILABLE
    # ========================================================
    cursor.execute("""
        SELECT COUNT(*)
        FROM INFORMATION_SCHEMA.TABLES
        WHERE TABLE_SCHEMA = 'dbo'
          AND TABLE_NAME = 'tblEvent'
    """)

    table_available = cursor.fetchone()[0]

    if table_available == 0:
        raise RuntimeError("Table dbo.tblEvent is not available.")

    print("Table dbo.tblEvent verified.")

    # ========================================================
    # 4. READ THE LATEST 100 RECORDS
    # ========================================================
    select_query = f"""
        SELECT TOP ({TOP_RECORDS})
            DT, TM, SQF_No, ChargeNo,
            Event_From, Event_To,
            Temp_Set, Temp_Act,
            Cp_Set, Cp_Act,
            Oil_Set, Oil_Act,
            Jacket_Set, Jacket_Act,
            Fan_Status
        FROM dbo.tblEvent
        ORDER BY DT DESC, TM DESC
    """

    cursor.execute(select_query)

    # Read SQL column names
    column_names = [column[0] for column in cursor.description]

    # Read SQL rows
    rows = cursor.fetchall()

    if not rows:
        raise RuntimeError("No records are available in dbo.tblEvent.")

    # Convert SQL records into a pandas DataFrame
    dataframe = pd.DataFrame.from_records(rows, columns=column_names)

    print(f"{len(dataframe)} records received from SQL Server.")

    # ========================================================
    # 5. CREATE A UNIQUE EXCEL FILE NAME
    # ========================================================
    OUTPUT_FOLDER.mkdir(parents=True, exist_ok=True)

    # Date, time, seconds and microseconds prevent overwriting
    timestamp = datetime.now().strftime("%d-%m-%Y_%H-%M-%S-%f")

    excel_file = OUTPUT_FOLDER / f"SQF_Event_Report_{timestamp}.xlsx"

    # ========================================================
    # 6. WRITE SQL DATA INTO EXCEL
    # ========================================================
    with pd.ExcelWriter(
        excel_file,
        engine="openpyxl",
        datetime_format="DD-MM-YYYY HH:MM:SS",
    ) as writer:
        dataframe.to_excel(
            writer,
            sheet_name="Event Report",
            index=False,
        )

        worksheet = writer.sheets["Event Report"]

        # Freeze the heading row
        worksheet.freeze_panes = "A2"

        # Add filters to all columns
        worksheet.auto_filter.ref = worksheet.dimensions

        # Format the header
        header_fill = PatternFill(fill_type="solid", fgColor="F4B183")

        for cell in worksheet[1]:
            cell.font = Font(bold=True)
            cell.fill = header_fill
            cell.alignment = Alignment(
                horizontal="center",
                vertical="center",
            )

        # Format the date column
        for cell in worksheet["A"][1:]:
            cell.number_format = "DD-MM-YYYY HH:MM:SS"

        # Format the process-value columns
        numeric_columns = [
            "G", "H",  # Temperature
            "I", "J",  # CP
            "K", "L",  # Oil
            "M", "N",  # Jacket
        ]

        for column_letter in numeric_columns:
            for cell in worksheet[column_letter][1:]:
                cell.number_format = "0.00"
                cell.alignment = Alignment(horizontal="center")

        # Automatically adjust the column widths
        for column_cells in worksheet.columns:
            column_letter = column_cells[0].column_letter

            maximum_length = max(
                len(str(cell.value)) if cell.value is not None else 0
                for cell in column_cells
            )

            worksheet.column_dimensions[column_letter].width = min(
                maximum_length + 3,
                35,
            )

        worksheet.row_dimensions[1].height = 25

    # ========================================================
    # 7. DISPLAY THE SUCCESS MESSAGE
    # ========================================================
    print("-" * 65)
    print("Excel report created successfully.")
    print(f"Excel file: {excel_file}")
    print("=" * 65)

# ============================================================
# 8. ERROR HANDLING
# ============================================================
except pyodbc.Error as error:
    print("SQL Server error:")
    print(error)

except PermissionError:
    print(
        "Excel file permission error.\n"
        "Close the Excel file and run the program again."
    )

except Exception as error:
    print("Program stopped:")
    print(error)

# ============================================================
# 9. CLOSE THE SQL SERVER CONNECTION
# ============================================================
finally:
    if connection is not None:
        connection.close()
        print("SQL Server connection closed.")

Lab Manual 10 — Run the Complete Report Generator End to End

12 min

Objective

Execute the finished program on a clean machine so the whole workflow — connect, verify, query, DataFrame, workbook, formatting — runs from a single command and produces a delivered file.

Procedure

  1. Create a project folder and a virtual environment so lab packages stay isolated.
  2. Install pyodbc, pandas and openpyxl inside it.
  3. Save the Section 10 listing as sql_to_excel_report.py.
  4. Edit only five constants: SERVER, DATABASE, TABLE, DRIVER and OUTPUT_FOLDER.
  5. Run it, then open the generated workbook and check the header, filters and number formats.

What each line does

CodeMeaning in this lab
python -m venv .venvCreates an isolated environment so this lab never disturbs other projects on the PC.
.venv\Scripts\activateActivates it on Windows. The prompt changes to show (.venv).
pip install pyodbc pandas openpyxlAll three are required. openpyxl is the Excel engine and is not installed with pandas by default.
TOP_RECORDSStart at 20 on an unfamiliar database, then raise it to 100.
OUTPUT_FOLDERUse a local folder for the first run. Move to a network share only once the script works.

Copy-paste practice code

setup_and_run.bat
cd C:\SoftwellLabs\python-sql
python -m venv .venv
.venv\Scripts\activate
python -m pip install --upgrade pip
python -m pip install pyodbc pandas openpyxl
python sql_to_excel_report.py

On macOS or Linux, activate with source .venv/bin/activate, install the Microsoft ODBC driver for your distribution, and use a POSIX path for OUTPUT_FOLDER.

Expected result

console output
=================================================================
STEP 05 - SQL SERVER TO EXCEL REPORT
=================================================================
SQL Server connection successful.
Table dbo.tblEvent verified.
100 records received from SQL Server.
-----------------------------------------------------------------
Excel report created successfully.
Excel file: D:\SQF_Reports\SQF_Event_Report_19-09-2026_12-44-07-481293.xlsx
=================================================================
SQL Server connection closed.

Checkpoint

Pass when: All eight lines appear in order, the workbook opens in Excel with a frozen filtered header, and the record count in the console matches the data rows in the sheet.

If it fails

SymptomWhat to check
ModuleNotFoundError: No module named 'openpyxl'The virtual environment is not active, or pip installed into a different Python. Run where python and confirm it points inside .venv.
No records are available in dbo.tblEventThe table is empty. Run the insert practical (Code 02) first.
Excel file permission errorThe previous report is open in Excel. Close it and run again.
The report opens but looks unformattedThe formatting block ran outside the with pd.ExcelWriter(...) context and was never saved.
It runs but you cannot find the fileThe console prints the full path. Copy it from there rather than guessing the folder.

11. Verify the Report and Improve It

Acceptance checks

  • The printed file path exists and ends in .xlsx.
  • The workbook opens without a repair warning.
  • The sheet is named Event Report.
  • The header row stays visible while scrolling and all columns have filters.
  • The record count matches the SQL query result.
  • The first row is the newest event under the defined ordering.
  • Date and process-value columns display the intended formats.
ProblemLikely causeAction
ODBC driver/server errorDriver, instance, firewall or permissionsVerify connection settings and Windows identity
Table unavailableWrong database/schema or setup not runCreate/verify SQF_DB and dbo.tblEvent first
No records availableEmpty source tableInsert approved test/production data before reporting
PermissionErrorFolder denied or workbook lockedClose the file and verify output-folder rights
Dates display as text/numbersSource type or cell format mismatchInspect DataFrame dtypes and Excel number format
Slow/large workbookToo many rows or per-cell formatting costUse chunking, bounded periods and optimized formatting

Useful production upgrades

  • Wrap execution in functions and a __main__ guard.
  • Parameterize a report date range, furnace number or charge number.
  • Add a “Report Summary” sheet with generation time, filters and row count.
  • Create an Excel Table for structured filters and styles.
  • Validate the saved workbook by reopening it with openpyxl before distribution.
  • Write to a temporary file and atomically rename it after successful completion.
  • Log report ID, SQL criteria, row count, duration and output checksum.

Hands-On Lab: Generate and Validate the Furnace Report

Hands-on
Before you start
  • Use approved SQL data and a writable training output folder.
  • Install pyodbc, pandas, openpyxl and the ODBC driver.
  • Confirm dbo.tblEvent contains test records.
  • Estimated time: 25 minutes.
1

Verify connection and row order

Run the query with 20 rows and compare the first/last timestamps with SQL Server Management Studio.

The DataFrame contains the newest controlled batch in the expected order.
2

Generate the workbook

Run the complete program and record the printed output path.

A uniquely named XLSX file is created in D:\SQF_Reports.
3

Inspect visual formatting

Open the workbook and test freeze panes, filters, date display, two-decimal values and column widths.

The report is readable, filterable and free of clipped critical values.
4

Prove failure handling

Test an invalid output folder or controlled locked-file scenario, then restore the valid configuration.

The program reports the problem clearly and always closes the SQL connection.

Related Python and SQL Server Tutorials

Continue through the Softwell Python–SQL Server learning path:

Frequently asked questions

Which libraries create this SQL-to-Excel report?

pyodbc reads SQL Server, pandas stores the result in a DataFrame, and openpyxl writes and formats the XLSX workbook.

How does the script prevent overwriting reports?

The filename includes date, time, seconds and microseconds. For concurrent distributed jobs, add a UUID or use atomic unique-file creation.

Why does Excel generation raise PermissionError?

The workbook or folder may be locked, the user may lack write permission, or synchronization/security software may be holding the path. Close the file and verify folder access.

Do I need Microsoft Excel installed to run this script?

No. openpyxl writes the xlsx file directly, so the script runs on a server with no Office installation. Excel is only needed to open the finished report.

Why is the index column appearing in my workbook?

index=False was omitted from to_excel(). The pandas index then becomes column A and every formatting column letter shifts one place to the right.

My numbers show as text and the 0.00 format does nothing. Why?

That column arrived from SQL as text, so Excel stores a string. A number format only affects numeric cells. Convert with pd.to_numeric() before writing.

Where should the formatting code sit?

Inside the with pd.ExcelWriter(...) block, using the worksheet from writer.sheets. Formatting applied after the block closes is never saved.

How do I schedule this report every shift?

Use Windows Task Scheduler pointing at the virtual environment's python.exe, with a least-privilege SQL login that has SELECT only, and redirect the console output to a log file.

How do I add a second sheet, such as a summary?

Call to_excel() again inside the same with block with a different sheet_name, then fetch that sheet from writer.sheets to format it.

Reviewed by Bhawesh Kumar SinghIndustrial Automation Trainer and Industry 4.0 Consultant · Softwell Automation · 21+ years industry experience

Get the Python + SQL reporting syllabus

Share your details—a Softwell advisor will contact you with batch dates, fees and project-practice options.

No spam. Used only to share course details for this enquiry.

Automate Excel reports from industrial SQL data

Join live online, Pune classroom or corporate Industry 4.0 training.

Request Course Details
Complete Python for Industrial Automation Learning Path

Use Previous / Next for sequential training, or open any topic directly.

Verified learning pathway

Discuss Python Excel Reporting Training

Explore practical curriculum, software, hardware and batch options for this technology.

Content reviewed: 2 August 2026

☎ Call WhatsApp ✉ Email Enquire Now