Read a Cell Value From Excel With openpyxl
When you need the value of one cell — or a precise rectangle of cells — rather than a whole DataFrame loaded with pandas, openpyxl is the direct tool. It opens a workbook, exposes every cell by coordinate, and lets you choose between a formula's text and its cached result. This guide, part of Using openpyxl for Excel File Manipulation, walks through load_workbook, both cell-access styles, iterating rows, reading a range, and the data_only flag — with every snippet building its own sample file.
Prerequisites
Install openpyxl:
pip install openpyxl
Create a sample workbook
These examples read this file. It includes a formula in column C so the data_only section has something to demonstrate:
from openpyxl import Workbook
wb = Workbook()
ws = wb.active
ws.title = "Sales"
ws["A1"], ws["B1"], ws["C1"] = "SKU", "Qty", "Total"
ws["A2"], ws["B2"], ws["C2"] = "A-100", 3, "=B2*10"
ws["A3"], ws["B3"], ws["C3"] = "B-200", 5, "=B3*10"
wb.save("sales.xlsx")
print("wrote sales.xlsx")
Read one cell by coordinate
Load the workbook, pick a worksheet, and read .value. The ["B2"] syntax mirrors the spreadsheet UI:
from openpyxl import load_workbook
wb = load_workbook("sales.xlsx")
ws = wb["Sales"] # or wb.active for the first sheet
print(ws["B2"].value) # -> 3
ws["B2"] returns a Cell object; the data lives on its .value attribute. Forgetting .value gives you the cell wrapper, not the number.
Read a cell by row and column number
ws.cell(row=2, column=2) reaches the same cell numerically — the form you want inside loops or when coordinates are computed. openpyxl is 1-based: row 1 and column 1 are the top-left cell, so row=2, column=2 is B2:
from openpyxl import load_workbook
wb = load_workbook("sales.xlsx")
ws = wb.active
print(ws.cell(row=2, column=2).value) # -> 3, same cell as B2
Iterate rows with values only
To sweep a sheet, iter_rows(values_only=True) yields plain tuples of values instead of Cell objects — less overhead and easier to unpack:
from openpyxl import load_workbook
wb = load_workbook("sales.xlsx")
ws = wb.active
for sku, qty, total in ws.iter_rows(min_row=2, values_only=True):
print(sku, qty, total)
Drop min_row=2 to include the header row. Use min_col/max_col to bound the columns you read.
Read a rectangular range
Index the worksheet with a range string to get a tuple of rows, each a tuple of cells. Pull .value from each to flatten it:
from openpyxl import load_workbook
wb = load_workbook("sales.xlsx")
ws = wb.active
values = [[cell.value for cell in row] for row in ws["A1:B3"]]
print(values) # [['SKU', 'Qty'], ['A-100', 3], ['B-200', 5]]
Formula text vs. cached result with data_only
By default openpyxl returns the formula string for a calculated cell. Pass data_only=True to get the cached result Excel last computed and stored in the file instead:
from openpyxl import load_workbook
formula = load_workbook("sales.xlsx")
print(formula.active["C2"].value) # -> '=B2*10' (the formula text)
cached = load_workbook("sales.xlsx", data_only=True)
print(cached.active["C2"].value) # -> None until Excel saved a result
The catch: openpyxl never evaluates formulas itself. data_only=True only returns a value if a real spreadsheet application — Excel, LibreOffice, or similar — opened the file, recalculated, and saved the cached results. If you need Python to trigger a live recalculation, drive the spreadsheet application itself with xlwings. A workbook that openpyxl created and saved (like our sales.xlsx) has no cached values, so data_only=True returns None for every formula cell. Open and resave the file in Excel once, and the cached results appear.
Common pitfalls
| Symptom | Cause | Fix |
|---|---|---|
Got a <Cell> repr, not the value | Read the cell object, not its data | Append .value: ws["B2"].value |
IndexError or wrong cell with cell() | Used 0-based indices | openpyxl is 1-based; top-left is row=1, column=1 |
data_only=True returns None | File has no cached formula results | Open and save the file in Excel first; openpyxl won't compute |
MemoryError on a huge file | Loaded the whole workbook into RAM | Use load_workbook(path, read_only=True) and stream iter_rows |
Edits don't stick after read_only | read_only mode is non-writable | Drop read_only when you need to modify cells |
A second subtlety: you can't have both the formula text and the cached result from one load_workbook call. Open the file twice — once plain, once with data_only=True — if you need both.
Performance and scale
For large workbooks where you only read, load_workbook(path, read_only=True) switches openpyxl to a streaming parser that loads rows lazily, keeping memory flat as the file grows. It pairs naturally with iter_rows(values_only=True). Note that read-only mode disables writing and some cell attributes, so use it only for pure extraction.
Reading a range rather than a cell
Reading cells one at a time is fine for a handful and wasteful for a table. iter_rows with
values_only=True hands back plain tuples, which is both faster and easier to work with:
from openpyxl import load_workbook
wb = load_workbook("orders.xlsx", data_only=True)
ws = wb["Orders"]
header = [c.value for c in ws[1]]
rows = [dict(zip(header, values)) for values in ws.iter_rows(min_row=2, values_only=True)]
print(len(rows), "row(s);", rows[0] if rows else "no data")
Building a dictionary per row keeps the calling code readable — row["Revenue"] rather than
row[4] — and costs little compared with the parsing itself. For a very large sheet, keeping the
integer index and skipping the dictionary is measurably faster, which is the trade the
large-file guide
goes into.
Empty, missing and never-written
Three situations look identical from a distance and behave differently. A cell that was never
written returns None. A cell holding an empty string returns "". And a cell outside the sheet's
used range still returns None rather than raising, so a typo in a coordinate produces a silent
None instead of an error:
print(repr(ws["ZZ999"].value)) # None — no error, even though nothing is there
print(ws.max_row, ws.max_column) # the bounds worth checking against
Guarding a read with the sheet's dimensions turns that silence into a message, which is worth doing whenever the coordinate comes from configuration rather than from a literal in the code.
Reads are cheap, parsing is not
Opening a workbook is the expensive part; reading a cell from an already-open sheet costs almost nothing. A script that loads the same file three times to fetch three values is paying the parse three times, and the fix is simply to keep the workbook object around. Where only a handful of cells are needed from a very large file, combining read_only=True with direct coordinate access avoids materialising the sheet at all.
Know which mode you opened in
The same read returns different things depending on how the workbook was loaded: formula text by default, cached numbers with data_only=True, and lightweight tuples in read-only mode. Most confusion about "wrong" values traces back to the load call rather than to the read, so keeping the mode visible — ideally in a small wrapper function per job — saves a great deal of puzzled debugging.
Ranges come from the data
The habit that keeps a monthly report correct is deriving every range, row count and column position
from what was just written rather than from a number typed once. ws.max_row after the write, a
header map built from row one, a table reference rebuilt on each run — each of those removes a class
of failure that is silent rather than loud. A hardcoded range does not raise when the data outgrows
it; it simply stops covering the rows nobody looks at, and the report is wrong in a way that takes
months to notice. Deriving costs a line and removes the whole category.
What the load mode returns
Frequently asked questions
Why does my cell return the formula string instead of the number?
That's the default. Reload with load_workbook(path, data_only=True) to get the cached result — provided Excel computed and saved it.
Is openpyxl 0-based or 1-based?
1-based, everywhere. ws.cell(row=1, column=1) is A1. This matches the spreadsheet UI but differs from Python list indexing.
How do I read just the value, not the cell object?
Always go through .value — ws["B2"].value or ws.cell(row=2, column=2).value. iter_rows(values_only=True) does this for you across a whole sweep.
Can openpyxl calculate formulas for me? No. It reads formula text or the cached result, but never evaluates. For live computation, use a tool that drives Excel itself, such as xlwings.
Conclusion
openpyxl gives you two equivalent ways to reach a cell — ws["B2"].value for readability and ws.cell(row=2, column=2).value for computed coordinates — both 1-based. To sweep a sheet, iter_rows(values_only=True) is fastest; to scope a region, index with a range string. The one rule to remember about formulas: openpyxl doesn't calculate, so data_only=True returns the cached result only when a spreadsheet app saved one, and None otherwise.
Related
- Up to the parent guide: Using openpyxl for Excel File Manipulation — the broader read-and-edit workflow.
- Append Data to an Existing Excel Sheet with openpyxl — write back to a file you've just opened and read.
- openpyxl vs xlsxwriter vs pandas.ExcelWriter — pick the right engine when you need to write rather than read.
- Reading Excel Files with Pandas — when you want the whole table as a DataFrame instead of individual cells.
- xlwings basics — drive Excel itself when you need formulas evaluated live.