Yes, Python can work with Excel — but most people assume it means replacing Excel entirely, when the real power is *orchestrating* Excel from Python while keeping human-readable formatting intact.
The Setup
You just got a weekly sales report emailed as
Q2_Sales_Report.xlsx. It’s not clean: merged headers, blank rows, inconsistent date formats, and totals buried mid-sheet. Finance needs this loaded into Power BI by noon — but first, you need to extract, validate, and restructure it.
Here’s what Sheet1 actually looks like (A1:E10):
| Region |
Sales Rep |
Deal Date |
Amount ($) |
Status |
| North |
Sarah Chen |
2024-03-15 |
$45,200 |
Closed |
| North |
Sarah Chen |
2024-03-18 |
$12,750 |
Closed |
| South |
Diego Mora |
Mar 22, 2024 |
$38,900 |
Closed |
| South |
Diego Mora |
2024/04/01 |
$26,100 |
Pending |
| East |
Priya Patel |
04/05/2024 |
$52,300 |
Closed |
| East |
Priya Patel |
2024-04-10 |
$18,450 |
Closed |
| West |
Marcus Lee |
April 12, 2024 |
$63,700 |
Closed |
| West |
Marcus Lee |
2024-04-15 |
$29,800 |
Pending |
Notice how
Deal Date isn’t uniform? That’s your first red flag. Also, there’s no header row at A1 — the real data starts at A3. And yes, row 2 is blank. (Trust me, I learned this the hard way.)
The Challenge
You need to:
- Read only rows 3–10 (skip merged title and blank row)
- Parse all four date formats into ISO standard (YYYY-MM-DD)
- Strip dollar signs and commas from Amount
- Keep Status exactly as-is (no lowercasing)
- Write cleaned output back to a new sheet named
Cleaned_Data, starting at A1
The tricky part?
pandas.read_excel() will choke on the mixed dates unless you tell it
parse_dates=False — and even then, it won’t auto-detect
'Mar 22, 2024'. You’ll get NaT values and silent failures.
Walking Through It
We’ll use
openpyxl for reading (preserves raw cell values) and
pandas for transformation. Install both:
pip install openpyxl pandas
Step 1: Load workbook *without* parsing dates:
wb = load_workbook('Q2_Sales_Report.xlsx')
ws = wb['Sheet1']
Step 2: Extract raw values from A3:E10 manually:
data = []
for row in ws.iter_rows(min_row=3, max_row=10, min_col=1, max_col=5, values_only=True):
data.append(row)
That gives you a list of tuples — no date coercion yet. Now feed into pandas:
df = pd.DataFrame(data, columns=['Region', 'Sales Rep', 'Deal Date', 'Amount ($)', 'Status'])
Step 3: Clean dates. This is where most fail. Don’t use
pd.to_datetime() blindly. Instead:
from dateutil import parser
def safe_parse_date(x):
try:
return parser.parse(str(x)).strftime('%Y-%m-%d')
except:
return None
df['Deal Date'] = df['Deal Date'].apply(safe_parse_date)
Step 4: Clean amount:
df['Amount ($)'] = df['Amount ($)'].str.replace(r'[$,]', '', regex=True).astype(float)
Before writing back, create a new sheet:
wb.create_sheet('Cleaned_Data')
clean_ws = wb['Cleaned_Data']
Now write the DataFrame using
openpyxl’s
append() — not
to_excel(). Why? Because
to_excel() opens a new workbook and kills formatting, formulas, and macros. We want to keep the original file intact.
clean_ws.append(df.columns.tolist())
for r in dataframe_to_rows(df, index=False, header=False):
clean_ws.append(r)
wb.save('Q2_Sales_Report.xlsx')
Note:
dataframe_to_rows() is from
openpyxl.utils.dataframe — import it.
Here’s the cleaned table (A1:E9 on
Cleaned_Data):
| Region |
Sales Rep |
Deal Date |
Amount ($) |
Status |
| North |
Sarah Chen |
2024-03-15 |
45200.0 |
Closed |
| North |
Sarah Chen |
2024-03-18 |
12750.0 |
Closed |
| South |
Diego Mora |
2024-03-22 |
38900.0 |
Closed |
| South |
Diego Mora |
2024-04-01 |
26100.0 |
Pending |
| East |
Priya Patel |
2024-04-05 |
52300.0 |
Closed |
| East |
Priya Patel |
2024-04-10 |
18450.0 |
Closed |
| West |
Marcus Lee |
2024-04-12 |
63700.0 |
Closed |
| West |
Marcus Lee |
2024-04-15 |
29800.0 |
Pending |
The Result
Your
Cleaned_Data sheet is now ready for Power BI ingestion — all dates standardized, numbers numeric, no hidden characters, and zero manual copy-paste. Bonus: the original
Sheet1 stays untouched, so your boss can still review raw entries.
What Could Go Wrong
- You used
pd.read_excel() with header=0 — this forces pandas to treat row 1 as headers, but your actual headers are in row 3. Result: garbage column names like Unnamed: 0, and misaligned data. Fix: always inspect ws.max_row and ws.min_row before assuming.
- You saved with
df.to_excel() instead of openpyxl — this overwrites your entire file, deleting all other sheets, formulas, and conditional formatting. The file opens fine, but your finance team loses their dashboard tabs. Alt+Shift+F2 (Excel’s “Recover Unsaved Workbooks”) won’t help here — it’s gone.
- You parsed dates with
pd.to_datetime(..., errors='coerce') and ignored warnings — this silently replaces bad dates with NaT, which becomes 1900-01-00 when written back to Excel. Your April deals suddenly appear in 1900. Check df['Deal Date'].isna().sum() before saving.
Here’s your quick-reference cheat sheet for next time:
| Task |
Right Tool |
Key Shortcut / Tip |
| Read messy Excel with merged cells |
openpyxl.load_workbook() |
ws.iter_rows(min_row=3, values_only=True) |
| Parse inconsistent dates |
dateutil.parser.parse() |
Wrap in try/except — never rely on coerce |
| Write back without breaking formulas |
openpyxl.Workbook + append() |
Never df.to_excel() on an existing file |
| Check for hidden blanks or spaces |
df.applymap(lambda x: str(x).strip() if isinstance(x, str) else x) |
Run df.dtypes — if object appears where you expect float64, investigate |