pip install openpyxl★Pure Python, no Excel required. Reads/writes xlsx, xlsm, xltx, xltm.pip install pillowRequired before you can add images — not a hard dependency.pip install lxmlUsed automatically if present; noticeably faster on large files.pip install defusedxmlGuards against billion-laughs / quadratic-blowup XML attacks.from openpyxl import Workbook, load_workbook★The only two names most scripts need.openpyxl.LXML · DEFUSEDXML · NUMPY · PANDASBooleans telling you which optional backends were detected.
wb = Workbook()★Always born with exactly one worksheet.wb = load_workbook("in.xlsx")★Opens an existing file. See card 03 for the flags.wb.save("out.xlsx")★Overwrites without warning. There is no "save changes" prompt.wb.close()Required in read-only and write-only modes; harmless otherwise.wb.template = True; wb.save("t.xltx")Save as a template — match the extension or Excel refuses it.wb.save(buf) # io.BytesIO()★Serve from Flask/Django without touching disk.load_workbook(BytesIO(blob))Reading works from any file-like object too.wb.properties.creator = "me"Document metadata:title,subject,created,lastModifiedBy.
data_only=True★Return Excel's last cached result instead of the formula text.read_only=True★Lazy loading, near-constant memory. No styling, no charts, no edits.keep_vba=TruePreserve macros in .xlsm. They survive but stay uneditable.rich_text=TrueKeep per-run formatting inside a cell. Default is False.keep_links=FalseDrop cached data from linked external workbooks.data_only=True then wb.save()destroysSaving a data-only workbook writes the values back over your formulas.load_workbook("x.xls")wrong toolLegacy .xls is a different binary format — convert it first.
ws = wb.active★The sheet Excel would open on. Index 0 unless you change it.ws = wb["Sales"]★By title.KeyErrorif it doesn't exist.wb.sheetnames★Titles in tab order, as a list of strings.ws = wb.create_sheet("Data", 0)★Second arg is the position; omit it to append at the end.ws.title = "Report"Max 31 chars;: \ / ? * [ ]are illegal.wb.remove(ws) · del wb["Sheet"]Delete the default empty "Sheet" you never used.wb.copy_worksheet(ws)Same workbook only. Copies cells/styles — not images or charts.wb.move_sheet("Data", offset=-1)Reorder tabs without recreating them.for ws in wb: ws.titleWorkbooks iterate over their worksheets.wb.index(ws) · wb.worksheetsPosition lookup and the list of sheet objects.
ws.sheet_properties.tabColor = "1072BA"Colour the tab. aRGB hex, no leading#.ws.sheet_state = "hidden"Also"visible"and"veryHidden"(invisible in the Excel UI).ws.sheet_view.showGridLines = False★The single change that makes a sheet look designed.ws.sheet_view.zoomScale = 85Saved zoom level for that sheet.wb.active = 2Which tab is selected when the file opens.ws.sheet_properties.outlinePr.summaryBelow = FalseGrouping/outline direction.
ws["A1"] = 42★A1 notation. Assigning to the sheet sets.valuedirectly.ws["A1"].value★Reading gives back the Cell;.valuegives the content.ws.cell(row=1, column=1, value=42)★Numeric addressing — both indexes start at 1, not 0.ws.cell(1, 1).valuePositional form works too; returns the cell, creating it if needed.c = ws["A1"]; c.value = "hi"Hold the Cell when you'll also set styles on it.for x in range(1,101): ws.cell(x, 1)careMerely touching a cell creates it in memory, empty or not.
c.value · c.row · c.column · c.coordinate★.columnis the number;.column_letteris the letter.c.data_typennumeric ·sstring ·fformula ·ddate ·bbool ·eerror.c.parent · c.is_dateThe owning worksheet, and whether the number format is a date.ws["A1"] = datetime.now()★Python types convert automatically; dates get a date format.c.hyperlink = "https://x.com"; c.style = "Hyperlink"The link and its blue-underline look are two separate things.c.comment = Comment("note", "author")from openpyxl.comments import Comment.ws["A1"] = Decimal("1.5")TypeErrorOnly str/int/float/bool/datetime/None are storable — cast first.
get_column_letter(28) → "AB"★The function you reach for in every generated-column loop.column_index_from_string("AB") → 28★The inverse. Both are 1-based.coordinate_to_tuple("B3") → (3, 2)Note the order: (row, column).range_boundaries("A1:C4") → (1,1,3,4)min_col, min_row, max_col, max_row — ready foriter_rows.absolute_coordinate("A1") → "$A$1"For building formula strings and defined names.quote_sheetname("My Sheet")Escapes names with spaces for use inside formulas.ws.max_row · ws.max_column · ws.dimensions★The used range;dimensionsgives it as"A1:D10".ws.max_rowcareCounts formatted-but-empty rows too — often larger than your data.
ws["A1:C2"]★Returns a tuple of row-tuples of Cells.ws["C"] · ws["C:E"]★A whole column, or a span of columns.ws[10] · ws[5:10]★A whole row, or a span of rows. Integer keys, 1-based.ws["A1":"C2"]Slice form of the same thing.for row in ws["A1:C3"]:
for c in row: …Always two levels — even a single row comes wrapped.ws.calculate_dimension()Recompute the used range rather than trusting the stored one.
ws.iter_rows(min_row=2, max_col=3)★Bounded, memory-friendly row iteration. Skip the header withmin_row=2.ws.iter_rows(values_only=True)★Yields plain tuples of values — usually what you actually want.ws.iter_cols(min_row=1, max_col=3)Column-major. Not available in read-only mode.ws.rows · ws.columns★Generators over the whole used range.columnsis read-write only.ws.values★Every row as a tuple of values — the fastest way to a list of lists.rows = ws.values; header = next(rows)★Pop the header off the generator, then loop the rest.list(ws.iter_rows(values_only=True))Materialize when you need indexing or a second pass.
ws.append(["a", 1, 2.5])★Writes to the next empty row. Fastest way to fill a sheet.ws.append({"A": 1, "C": 3})Dict keyed by column letter — leaves B empty.ws.append({1: 1, 3: 3})Or keyed by 1-based column index.for r in rows: ws.append(r)★Beats nestedws.cell(...)loops by a wide margin.for r, row in enumerate(data, start=2): …When you need explicit positions, start the count at 2 under a header.
ws.insert_rows(2, amount=3)★Insert before row 2, pushing everything down.ws.delete_rows(5, amount=2)★Delete bottom-up when looping, or your indexes shift under you.ws.insert_cols(3) · ws.delete_cols(3, 2)Same semantics, column-wise.ws.move_range("D4:F10", rows=-1, cols=2)Shift a block.translate=Truealso rewrites relative formulas.ws.delete_rows(…)careDoes not adjust formulas, merged ranges, charts or validation refs.
c.font = Font(bold=True, size=14)★Alsoname,italic,underline,strike,vertAlign.Font(color="FF0000")★aRGB hex string, no leading#. 6 or 8 digits.c.fill = PatternFill("solid", fgColor="FFFF00")★Forgettingfill_type="solid"is why your fill "did nothing".GradientFill(stop=("FFFFFF", "217346"))Two-stop gradient across the cell.c.font.bold = Trueno effectStyle objects are immutable — assign a whole newFont(...).f = copy(c.font); f.bold = True; c.font = f★from copy import copy— the correct way to tweak one attribute.
thin = Side(style="thin", color="000000")★Styles:thin,medium,thick,dashed,dotted,double…c.border = Border(bottom=thin, top=thin)★Each edge takes its own Side; omitted edges are cleared.c.alignment = Alignment(horizontal="center")★Plusvertical,indent,shrink_to_fit.Alignment(wrap_text=True)★Needed for any multi-line cell — set the row height too.Alignment(text_rotation=90)0–180, or 255 for stacked vertical text.border on a merged rangecareOnly the top-left cell carries style — border each edge cell yourself.
c.number_format = "#,##0.00"★Excel's own format codes, verbatim. Value is untouched.c.number_format = "0.00%"★Store0.42, not42— Excel multiplies by 100 for display.c.number_format = "yyyy-mm-dd"★Or"dd/mm/yyyy hh:mm". Write a realdatetimeobject.c.number_format = '"₹"#,##0'Quote literal text inside the code; escape the quotes in Python.c.number_format = "@"Force Text — stops leading zeros and long IDs being eaten.c.number_format = "#,##0;[Red]-#,##0;–"Sections: positive ; negative ; zero ; text.from openpyxl.styles.numbers import BUILTIN_FORMATSThe numbered built-ins Excel ships with.
c.style = "Title"★Built-ins:Title,Headline 1,Good,Bad,Neutral,Currency.ns = NamedStyle(name="hdr", font=Font(bold=True))★Bundle font + fill + border + format under one name.wb.add_named_style(ns); c.style = "hdr"★Register once, then apply by name — much smaller files.wb.named_stylesList what's already registered.ws.column_dimensions["A"].font = Font(bold=True)Style a whole column or row via its dimension object.styling 200k cells one by oneslowUse a NamedStyle, or row/column dimensions, instead.
from openpyxl.formatting.rule import CellIsRuleAlsoColorScaleRule,DataBarRule,IconSetRule,FormulaRule.ws.conditional_formatting.add("B2:B99", rule)★Attach a rule to a range. Excel does the evaluating, not openpyxl.CellIsRule(operator="lessThan", formula=["0"], fill=red)★formulais a list of strings, even for one value.ColorScaleRule(start_color="FFFFFF", end_color="63BE7B")Two- or three-point heat maps withstart/mid/end.DataBarRule(start_type="min", end_type="max")In-cell bars — a sparkline substitute.FormulaRule(formula=["$C2>100"], fill=amber)★Whole-row highlighting: anchor the column with$, not the row.
ws.column_dimensions["A"].width = 24★Units are characters, not pixels. There is no auto-fit.ws.row_dimensions[1].height = 28Points. Needed whenever you turn onwrap_text.ws.column_dimensions["D"].hidden = TrueHide without deleting.ws.merge_cells("A1:D1")★Only the top-left cell keeps its value; the rest become None.ws.unmerge_cells("A1:D1") · ws.merged_cells.rangesInspect before you iterate — merged blocks read as None.ws.freeze_panes = "B2"★Freezes everything above and left of B2."A2"= header row only.ws.column_dimensions.group("B", "D", outline_level=1)Collapsible column groups.max(len(str(c.value or "")) for c in col)★The standard hand-rolled auto-width idiom.
ws["D2"] = "=SUM(B2:C2)"★Written as a string starting with=. Excel computes it on open.ws["D2"].value # after writinggotchaYou get"=SUM(B2:C2)"back. openpyxl has no formula engine.load_workbook(f, data_only=True)★Reads the value Excel cached the last time it saved the file.data_only value is NoneexpectedNothing has ever computed it — a file openpyxl wrote has no cache.f"=SUM(B2:B{ws.max_row})"f-strings are the normal way to build dynamic ranges."=Sheet2!A1" · "='My Sheet'!A1"Quote sheet names containing spaces.Translator("=A1+1", origin="C1").translate_formula("C2")openpyxl.formula.translate— fill a formula down like Excel does.wb.defined_names["tax"] = DefinedName("tax", attr_text="Sheet1!$B$1")Workbook-level named ranges.from openpyxl.utils import FORMULAEThe set of function names openpyxl recognises.
from openpyxl.chart import BarChart, Reference★AlsoLineChart,PieChart,ScatterChart,AreaChart,RadarChart.data = Reference(ws, min_col=2, max_col=3, min_row=1, max_row=9)★A Reference points at cells — charts never take Python lists.chart.add_data(data, titles_from_data=True)★Include the header row and let it become the series name.chart.set_categories(Reference(ws, min_col=1, min_row=2, max_row=9))★Category labels — exclude the header here.ws.add_chart(chart, "F2")★Anchor cell for the chart's top-left corner.chart.type = "col" # or "bar"col= vertical,bar= horizontal.grouping="stacked"to stack.chart.title · chart.x_axis.title · chart.y_axis.titlePlain strings;chart.styletakes 1–48 Excel presets.chart.width = 18; chart.height = 9Centimetres, not pixels.load_workbook() then wb.save()lostCharts in the source file are not read back — they vanish on re-save.
from openpyxl.drawing.image import Image★Requirespillow; PNG/JPEG/BMP/GIF.img = Image("logo.png"); ws.add_image(img, "A1")★Floats above the grid — it is not "in" cell A1.img.width = 240; img.height = 80Pixels. Set beforeadd_image.from openpyxl.cell.rich_text import CellRichText, TextBlockMixed formatting within one cell.CellRichText(["plain ", TextBlock(InlineFont(b=True), "bold")])InlineFontusesrFontfor the font name, notname.load_workbook(f, rich_text=True)Otherwise rich text is flattened to a plain string on read.images on loadlostLike charts, images in an existing file are dropped on re-save.
from openpyxl.worksheet.table import Table, TableStyleInfoReal Excel Tables, with banding and filter buttons.t = Table(displayName="Sales", ref="A1:D20")★displayNamemust be unique and contain no spaces.t.tableStyleInfo = TableStyleInfo(name="TableStyleMedium9", showRowStripes=True)Excel's built-in style names, verbatim.ws.add_table(t)★Therefmust include a header row with unique, non-empty names.ws.auto_filter.ref = "A1:D20"★Filter buttons without a full Table.ws.auto_filter.add_filter_column(0, ["North"])Records the filter; Excel applies it. openpyxl won't hide rows for you.ws.auto_filter.add_sort_condition("B2:B20")Same story — the sort is recorded, not performed.a corrupt-file prompt on opentablesNearly always a duplicatedisplayNameor a header name that repeats.
from openpyxl.worksheet.datavalidation import DataValidationDropdowns, ranges, date and length limits.dv = DataValidation(type="list", formula1='"Yes,No"')★Inline lists need quotes inside the string. Or point at a range.ws.add_data_validation(dv); dv.add("C2:C99")★Add to the sheet first, then attach ranges.DataValidation(type="whole", operator="between", formula1="1", formula2="10")Alsodecimal,date,time,textLength,custom.dv.showErrorMessage = True; dv.error = "Pick one"Prompt and error text are separate flags.ws.protection.sheet = TrueLock the sheet;ws.protection.password = "x"to set a password.c.protection = Protection(locked=False)Unlock the cells you do want editable — all cells are locked by default.
ws.print_area = "A1:F40"★Limit what prints; accepts a list of ranges too.ws.page_setup.orientation = "landscape"★Constant also atPAGESETUP_ORIENTATION_LANDSCAPE.ws.print_title_rows = "1:1"★Repeat the header on every printed page.print_title_colstoo.ws.page_setup.fitToWidth = 1Also setws.sheet_properties.pageSetUpPr = PageSetupProperties(fitToPage=True).ws.oddHeader.center.text = "&A"Header/footer codes:&Asheet,&Ppage,&Ntotal,&Ddate.ws.page_margins.left = 0.5Inches.ws.page_setup.paperSize = ws.PAPERSIZE_A4Constants live on the Worksheet class.
load_workbook(f, read_only=True)★Lazy, streaming, near-constant memory. Cells areReadOnlyCell.wb.close()★Mandatory in read-only mode — the file handle stays open otherwise.ws.reset_dimensions()When the writer lied about the used range and rows come back empty.wb = Workbook(write_only=True)★Stream rows out.wb.activeis None — callcreate_sheet().ws.append(row) # the only way to add cellsNo random access, no reading back what you wrote.second wb.save() in write-onlyWorkbookAlreadySavedYou get exactly one save. Setfreeze_panesetc. before any cells.memory ≈ 50× file sizeplan for itA 50 MB xlsx can need ~2.5 GB in normal mode. Hence these modes.
pd.read_excel(f, engine="openpyxl")★pandas uses openpyxl for .xlsx under the hood already.pd.DataFrame(ws.values)★Straight from a sheet; slice off row 0 for the header.from openpyxl.utils.dataframe import dataframe_to_rows★The supported bridge going the other way.for r in dataframe_to_rows(df, index=False, header=True): ws.append(r)★Write a DataFrame into a sheet you're also styling.pd.ExcelWriter(f, engine="openpyxl", mode="a")★Append a sheet to an existing file;if_sheet_exists="replace".writer.book · writer.sheets["Sheet1"]Reach through pandas to the live openpyxl objects and style them.to_excel() then reopen to styleslowUseExcelWriteronce instead of writing, loading and saving again.
formula evaluationneverNo calculation engine. Use LibreOffice headless orformulas/pycel.charts & images on re-savelostNot read back in. Rebuild them, or edit a copy and keep the original.shapes, smart art, comments threadslostAnything openpyxl can't model is dropped on load + save..xls (legacy binary)unsupportedDifferent format entirely — convert to .xlsx first.password-encrypted filesunsupportedDecrypt first (e.g.msoffcrypto-tool), then open.macros are preserved, not editablekeep_vbaVBA survives the round trip as an opaque blob.pivot tablespartialExisting ones are largely preserved; you can't build one from scratch.1,048,576 rows × 16,384 colsExcel's own hard ceiling — openpyxl will happily let you exceed it.