What Most People Miss About How to Import Excel File in Python

Why does pd.read_excel('sales.xlsx') return a KeyError on 'Sheet1'? Why do dates from column D2:D15 show up as floats instead of datetime objects? Why does it work fine in Jupyter but crash in your production script with a PermissionError?

The answer isn’t version mismatches or missing packages — it’s that Excel files aren’t just tables. They’re layered objects: formatting, merged cells, hidden rows, named ranges, and multiple sheets with inconsistent headers. And Python doesn’t care about your Excel ribbon. It only sees the raw XML or OLE structure — unless you tell it exactly how to look.

Quick Answer

Use pandas.read_excel() for simple cases — but always specify sheet_name, dtype, and parse_dates; for complex files (merged headers, multiple tables, password-protected sheets), switch to openpyxl or xlwings to read cell-by-cell like Excel itself does. Never rely on default sheet inference — sheet_name=0 is safer than 'Sheet1'.

All the Methods

MethodStepsBest ForLimitations
pandas.read_excel()1. Install pandas + openpyxl
2. pd.read_excel('data.xlsx', sheet_name='Q3', usecols='A:C', skiprows=2)
Clean, flat tables with consistent headersFails on merged cells, formulas, or protected sheets
openpyxl (cell-level)1. Load workbook: wb = load_workbook('report.xlsx')
2. Access sheet: ws = wb['Summary']
3. Read A1: ws['A1'].value or range B2:C10
Files with merged headers, formulas, or custom formattingNo automatic dtype conversion — you’ll parse dates manually
xlwings (Excel engine)1. Launch Excel app: app = xw.App(visible=False)
2. Open book: wb = app.books.open('dashboard.xlsm')
3. Read range: wb.sheets[0].range('A1:E20').options(pd.DataFrame).value
Macros, .xlsm files, or when you need Excel’s calculation engineWindows-only (unless using xlwings-server), slower, requires Excel installed
pyxlsb (for .xlsb)1. Install pyxlsb
2. readxlsb('archive.xlsb', sheet_name='RawData')
Large binary Excel files (.xlsb) common in finance teamsNo support for formulas or charts — only values
pandas + ExcelFile1. exc = pd.ExcelFile('multi_sheet.xlsx')
2. exc.sheet_names → ['Sales', 'Targets', 'Notes']
3. df = exc.parse('Sales', header=1)
Reading multiple sheets *once*, then reusing the parsed file objectStill uses same underlying engines — won’t fix parsing bugs

Method 1 Deep Dive

Let’s say your team drops q3_forecast.xlsx every Friday at 9 a.m. It has three sheets: Actuals, Forecast, and Notes. The Actuals sheet starts at row 4 — rows 1–3 contain title, department name, and a blank line. Column headers sit in row 4: A4 = 'Region', B4 = 'Product', C4 = 'Units Sold', D4 = 'Revenue', E4 = 'Date'. But E5:E12 contains dates like 2024-09-01, 2024-09-08 — and pandas reads them as strings unless told otherwise.

Here’s what *actually* works:

import pandas as pd
df = pd.read_excel(
'q3_forecast.xlsx',
sheet_name='Actuals',
skiprows=3, # skip first 3 rows
usecols='A:E', # only read columns A through E
parse_dates=['Date'], # convert column 'Date' to datetime64
dtype={'Region': 'category', 'Units Sold': 'int32'}
)

That skiprows=3 is critical — without it, pandas treats row 4 as the header and shifts all data down. You’ll get NaN in the first row and misaligned types. Also: never use header=4 expecting row 4 to be header — header is zero-indexed, so header=3 means “use row 4”, but skiprows=3 is clearer and less error-prone.

Surprising tip: If your Excel file has filters applied (like AutoFilter dropdowns in row 4), pandas ignores them — but openpyxl will see the filter state. So if your analyst filtered to ‘APAC’ before saving, read_excel() still reads *all* rows. No filtering survives export.

Method 2 Deep Dive

Now imagine exec_summary.xlsx — opened by your CFO. It has merged cells across A1:B2 (“Q3 Performance”), a logo in cell E1, and actual numbers starting at A5. Columns are inconsistent: A5 = 'North', B5 = 'Q3 Target', C5 = 'Q3 Actual', D5 = 'Variance'. But row 6 has no ‘North’ label — it’s merged with A5. Pandas gives up here. Enter openpyxl.

Install it: pip install openpyxl. Then:

from openpyxl import load_workbook
wb = load_workbook('exec_summary.xlsx')
ws = wb['Summary']

# Read merged cell A1:B2 manually
top_left = ws['A1']
value = top_left.value # returns 'Q3 Performance'

# Read data block starting at A5
data = []
for row in ws.iter_rows(min_row=5, max_row=12, min_col=1, max_col=4, values_only=True):
data.append(row)
df = pd.DataFrame(data, columns=['Region', 'Target', 'Actual', 'Variance'])

Notice values_only=True — without it, you get Cell objects, not values. And min_row=5 skips the merged title and logo rows. This method also lets you inspect formatting: ws['C6'].number_format returns '$#,##0.00' — useful if you need to preserve currency display logic.

Keyboard shortcut tip: In Excel, press Alt + D + E to open the “Edit Links” dialog — a quick way to check if your .xlsx pulls data from external sources (like SQL or another workbook). If it does, read_excel() won’t refresh those links. Only xlwings can trigger recalculation.

Cheat Sheet

TaskCode SnippetNotes
Read first sheet, skip 2 rowspd.read_excel(f, skiprows=2)Always test with nrows=5 first
Read specific columns onlyusecols='A,C:E'Saves memory on wide files
Force date parsingparse_dates=['Order Date'], date_parser=lambda x: pd.to_datetime(x, format='%Y-%m-%d')Fixes ambiguous formats like '01/02/23'
List all sheet namespd.ExcelFile(f).sheet_namesRun this *before* parsing — avoids KeyError
Read cell A1 from 'Metrics' sheetws = load_workbook(f)['Metrics']; ws['A1'].valueWorks even if A1 is merged or formatted
Skip blank rows automaticallydf.dropna(how='all')Add after read_excel() — pandas doesn’t auto-skip
Handle password-protected filesxlwings only — app.api.Workbooks.Open(..., Password='secret')No pure-Python solution exists
Tom Bradley

Tom Bradley

Tom has 15 years of experience in office management and supply chain optimization. He shares practical tips for running efficient workplaces.