Guide
Getting Started With Python Excel AutomationDeep dive

Read a Cell Value From Excel With openpyxl

Read Excel cell values with openpyxl: load_workbook, ws['B2'].value, ws.cell(), iter_rows, ranges, and data_only for cached formula results instead of formula text.

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.

Addressing cells in an A1:C3 grid with openpyxl A three-by-three grid shows columns A, B, C and rows 1 to 3; the cell B2 can be read with ws["B2"] or ws.cell(row=2, column=2), and data_only=True returns the cached result of a formula instead of its text. A B C 1 2 3 B2 =B3*10 Two ways to reach B2 ws["B2"].value ws.cell(row=2, column=2) data_only=True returns the cached result, not the formula text

Prerequisites

Install openpyxl:

Bash
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:

Python
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:

Python
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:

Python
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:

Python
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:

Python
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:

Python
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

SymptomCauseFix
Got a <Cell> repr, not the valueRead the cell object, not its dataAppend .value: ws["B2"].value
IndexError or wrong cell with cell()Used 0-based indicesopenpyxl is 1-based; top-left is row=1, column=1
data_only=True returns NoneFile has no cached formula resultsOpen and save the file in Excel first; openpyxl won't compute
MemoryError on a huge fileLoaded the whole workbook into RAMUse load_workbook(path, read_only=True) and stream iter_rows
Edits don't stick after read_onlyread_only mode is non-writableDrop 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:

Reading cell objects compared with reading values Iterating without values_only produces a Cell object per cell, carrying coordinate and style information you may not need. Passing values_only=True yields plain tuples of values, which is substantially faster on a large range. cell objects coordinate, style, number format needed for styling work slower on large ranges values_only=True plain Python values much faster what most reads want
Python
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:

Python
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

Three load modes and what a cell read gives back The default returns formula text. data_only returns the cached number, or None when nothing has calculated the file. read_only returns lightweight values and cannot be written to. default formula text editable full styling data_only=True cached numbers None if never calculated harvesting values read_only=True streamed values no writing large files

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 .valuews["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.