How to Read Excel Files With Pandas: A Step-by-Step Guide
pandas.read_excel() turns a spreadsheet into a DataFrame in a single line. The hard part isn't the read — it's making that read survive a scheduled job, where sheets get renamed, exports prepend junk rows, numbers arrive as text, and files occasionally go missing. This guide builds a reliable reader one capability at a time, so you finish with a wrapper you can drop into a real pipeline. It sits under Reading Excel Files with Pandas, which maps every read_excel parameter; here we walk them in order. Every snippet is runnable — an early step writes a small workbook, so you can paste each example and watch it work.
Prerequisites
You need Python 3.8 or newer and two libraries. pandas doesn't parse Excel itself — it delegates to an engine, and openpyxl handles modern .xlsx files:
pip install pandas openpyxl
A few notes before you start:
- File format decides the engine.
openpyxlcovers.xlsx. Legacy.xlsneedsxlrd==1.2.0(xlrd 2.0 dropped.xlssupport), and binary.xlsbneedspyxlsborcalamine. - No spreadsheet software is required. pandas reads the file bytes directly, so this works headless on a server with no Excel or LibreOffice installed.
- A little pandas familiarity helps. You should be comfortable with a DataFrame and
.head(), but every example is self-contained.
Step 1: Create a sample workbook
So every example below runs as-is, generate a small workbook with a Raw_Data sheet:
import pandas as pd
sample = pd.DataFrame({
"Order_ID": [1001, 1002, 1003, 1004],
"SKU": ["A-100", "B-200", "A-100", "C-300"],
"Quantity": [3, 1, 5, 2],
"Unit_Price": [19.99, 49.50, 19.99, 8.75],
"Region": ["North", "South", "North", "West"],
"Transaction_Date": ["2024-01-05", "2024-01-06", "2024-01-07", "2024-01-08"],
})
sample.to_excel("sales_data.xlsx", sheet_name="Raw_Data", index=False)
Step 2: Read a sheet into a DataFrame
With no extra arguments, read_excel reads the first sheet and treats row 0 as the header:
import pandas as pd
df = pd.read_excel("sales_data.xlsx")
print(df.head())
Step 3: Target a specific sheet and columns
Real workbooks mix metadata, summaries, and raw data across tabs. Name the tab with sheet_name, and load only the columns you need with usecols to keep memory down on wide files — reading specific columns with usecols covers every form that argument accepts. nrows caps how many rows are read:
df = pd.read_excel(
"sales_data.xlsx",
sheet_name="Raw_Data",
usecols=["Order_ID", "SKU", "Quantity", "Unit_Price"],
nrows=1000,
)
Step 4: Skip metadata rows and realign the header
Automated exports often prepend a title and a timestamp above the real header row. Recreate that layout in a second sheet, then use skiprows to drop the junk and header to pick the real header:
# Add an "Export" sheet whose header sits below two title rows
with pd.ExcelWriter("sales_data.xlsx", engine="openpyxl", mode="a") as writer:
pd.DataFrame([
["Monthly Export", None, None, None],
["Generated 2024-02-01", None, None, None],
["Order_ID", "SKU", "Quantity", "Unit_Price"],
[2001, "A-100", 4, 19.99],
[2002, "B-200", 2, 49.50],
]).to_excel(writer, sheet_name="Export", index=False, header=False)
df = pd.read_excel(
"sales_data.xlsx",
sheet_name="Export",
skiprows=2, # drop the title and timestamp rows
header=0, # the next row becomes the column names
)
print(df.columns.tolist())
Step 5: Control dtypes and parse dates
Excel stores dates as serial numbers and frequently coerces numbers to text. Pin the types you depend on and parse date columns explicitly, so downstream aggregation doesn't silently break:
df = pd.read_excel(
"sales_data.xlsx",
sheet_name="Raw_Data",
parse_dates=["Transaction_Date"],
dtype={
"Quantity": "int64",
"Unit_Price": "float64",
"Region": "category",
},
)
print(df.dtypes)
Step 6: Wrap it in a reusable, defensive reader
In a scheduled job, the read should fail loudly with a clear message rather than crash with a stack trace. This wrapper checks the file exists, surfaces a missing-engine error in plain language, and falls back to a sibling CSV if one is present:
import pandas as pd
from pathlib import Path
def read_excel_safe(path, **kwargs):
"""Read an Excel file with clear errors and a CSV fallback."""
path = Path(path)
if not path.exists():
raise FileNotFoundError(f"Source not found: {path}")
try:
return pd.read_excel(path, engine="openpyxl", **kwargs)
except ImportError as exc:
raise RuntimeError("Install the Excel engine: pip install openpyxl") from exc
except Exception:
csv_fallback = path.with_suffix(".csv")
if csv_fallback.exists():
return pd.read_csv(csv_fallback)
raise
df = read_excel_safe("sales_data.xlsx", sheet_name="Raw_Data")
print(f"Loaded {len(df)} rows")
Common pitfalls and gotchas
ModuleNotFoundError: No module named 'openpyxl'→ pandas doesn't bundle an Excel engine; install it withpip install openpyxl.ValueError: Excel file format cannot be determined→ passengine="openpyxl", or check the file isn't actually a renamed CSV or HTML export saved with an.xlsxextension.- An engine error on a
.xlsbfile → binary workbooks need a dedicated engine; addengine="pyxlsb"(orengine="calamine"). - Columns misaligned after a read → re-check the
skiprowscount first. Merged or two-level header cells often needheader=[0, 1]to read as aMultiIndexinstead of collapsing into one row. - ID columns lose leading zeros or show as
1.0e+05→ pandas inferred a numeric dtype. Read those columns as text withdtype={"Order_ID": "string"}to preserve the exact value. - Blank trailing columns arrive as
Unnamed: 5→ the sheet has stray formatting past the data. Restrict the read withusecolsso those phantom columns never materialize.
Performance and scale notes
read_excel loads the entire sheet into memory and has no chunksize option, so for large workbooks the goal is to read less:
- Inspect the tabs without loading any data, then read only what you need:
print(pd.ExcelFile("sales_data.xlsx").sheet_names)
- Pass
usecolsandsheet_nameso you never materialize columns or tabs you won't use. .xlsxparsing is CPU-bound on the XML. If a file is read on every run, convert it once to Parquet or CSV — both parse far faster and CSV can be streamed in chunks withread_csv(..., chunksize=...).- For a big directory of files, read them one at a time in a loop rather than all at once; the same approach scales up in combine multiple Excel files into one.
Read raw, then decide
The single habit that prevents most spreadsheet-reading problems is to read with dtype=object and convert deliberately afterwards. Inference is convenient and lossy: it strips leading zeros from identifiers, turns a column with one stray note into text, and guesses at date formats. Reading raw costs one argument and puts every one of those decisions where you can see and test it.
Print the shape after every read
One line — print(df.shape) — after each read is the cheapest sanity check available, and it catches the wrong sheet, the wrong header row and a truncated export immediately rather than three transformations later.
Log what the run actually did
Row counts at each boundary, what was filled, what was quarantined, how long it took: five or six lines per run turn a question about a number into a lookup. The value is not in reading them on a good day but in having them on a bad one, when a total has moved and nobody can say whether the source changed, the cleaning changed, or a filter was added. A job that records its own behaviour is one that can be debugged after the fact rather than re-run and watched.
Three arguments that decide the read
Frequently asked questions
Can I read an Excel file in chunks like I can with read_csv?
No. read_excel loads the entire sheet into memory and has no chunksize option. To read less, pass usecols, sheet_name, and nrows, or convert static reference data once to Parquet or CSV, which parse faster and can be streamed.
How do I skip title and timestamp rows above the real header?
Use skiprows to drop the junk rows and header to pick the real header — for example skiprows=2, header=0 reads the row immediately after the two dropped rows as the column names.
My columns are misaligned after a read — what went wrong?
Usually the skiprows count is off, so re-check it. Merged or two-level header cells often need header=[0, 1] to read as a MultiIndex rather than collapsing into one row.
How do I make a read fail with a clear message in a scheduled job?
Wrap it in a helper that checks path.exists() first, catches ImportError to surface a missing-engine message, and optionally falls back to a sibling CSV. That turns cryptic stack traces into actionable errors.
Why does my date column come in as numbers or text?
Excel stores dates as serial numbers and often coerces them to text. Pass parse_dates=["Transaction_Date"] to convert them to real datetime64 values so downstream resampling and comparisons work.
Conclusion
The defensive reader pattern — explicit path check, engine specified, CSV fallback — converts unpredictable import failures into clear, actionable error messages. Once you have that wrapper, the step-by-step additions (sheet_name, usecols, skiprows, dtype) are independent knobs you can tune per file without touching the error-handling skeleton.
Related
- Up to the parent guide: Reading Excel Files with Pandas — the full map of
read_excel, its engines, and multi-sheet workbooks. - Read specific columns from Excel with pandas — every form of the
usecolsargument you met in Step 3. - Combine multiple Excel files into one — apply the defensive reader across a whole folder of files.
- Read a cell value from Excel with openpyxl — when you need a single cell rather than a whole sheet.
- Overview: Getting Started with Python Excel Automation.