[{"data":1,"prerenderedAt":2091},["ShallowReactive",2],{"doc:\u002Fgetting-started-with-python-excel-automation\u002Fusing-openpyxl-for-excel-file-manipulation\u002Fread-excel-rows-into-dictionaries-with-openpyxl":3,"surround:\u002Fgetting-started-with-python-excel-automation\u002Fusing-openpyxl-for-excel-file-manipulation\u002Fread-excel-rows-into-dictionaries-with-openpyxl":2056,"ancestors:\u002Fgetting-started-with-python-excel-automation\u002Fusing-openpyxl-for-excel-file-manipulation\u002Fread-excel-rows-into-dictionaries-with-openpyxl":2065,"children:\u002Fgetting-started-with-python-excel-automation\u002Fusing-openpyxl-for-excel-file-manipulation\u002Fread-excel-rows-into-dictionaries-with-openpyxl":2070,"related:\u002Fgetting-started-with-python-excel-automation\u002Fusing-openpyxl-for-excel-file-manipulation\u002Fread-excel-rows-into-dictionaries-with-openpyxl":2071},{"id":4,"title":5,"body":6,"dateModified":2023,"datePublished":2023,"description":2024,"extension":2025,"faq":2026,"keyword":2035,"kind":2036,"meta":2037,"navigation":267,"path":2046,"seo":2047,"slug":2050,"stem":2051,"topics":2052,"type":2054,"__hash__":2055},"docs\u002Fgetting-started-with-python-excel-automation\u002Fusing-openpyxl-for-excel-file-manipulation\u002Fread-excel-rows-into-dictionaries-with-openpyxl\u002Findex.md","Read Excel Rows into Dictionaries with openpyxl",{"type":7,"value":8,"toc":2010},"minimark",[9,24,29,54,64,229,233,239,441,458,462,465,914,933,940,1006,1175,1186,1190,1193,1373,1380,1384,1391,1649,1656,1818,1822,1851,1855,1923,1927,1934,1938,1945,1949,1955,1961,1967,1973,1977,2006],[10,11,12,13,17,18,23],"p",{},"Plenty of scripts that read a spreadsheet do not need a DataFrame. They loop over the rows once — send an email per line, call an API per customer, check each record against a rule — and what they want is what ",[14,15,16],"code",{},"csv.DictReader"," gives for CSV files: one dictionary per row, keyed by the column header. openpyxl has no DictReader, but the pattern takes a few lines, works on sheets of any size in read-only mode, and keeps a small script free of pandas and its import time. This page is part of ",[19,20,22],"a",{"href":21},"\u002Fgetting-started-with-python-excel-automation\u002Fusing-openpyxl-for-excel-file-manipulation\u002F","Using openpyxl for Excel File Manipulation",".",[25,26,28],"h2",{"id":27},"prerequisites","Prerequisites",[30,31,36],"pre",{"className":32,"code":33,"language":34,"meta":35,"style":35},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install openpyxl\n","bash","",[14,37,38],{"__ignoreMap":35},[39,40,43,47,51],"span",{"class":41,"line":42},"line",1,[39,44,46],{"class":45},"sMTad","pip",[39,48,50],{"class":49},"srMev"," install",[39,52,53],{"class":49}," openpyxl\n",[10,55,56,59,60,63],{},[14,57,58],{},"customers.xlsx"," has a header row and five data rows, with the usual problems of a real export: a blank row in the middle, an unlabelled column holding notes, a second column also called ",[14,61,62],{},"Name",", padded text, and a footer line under the data.",[65,66,74,75,74,79,74,83,74,91,74,100,74,104,74,111,74,115,74,119,74,123,74,128,74,133,74,137,74,139,74,142,74,145,74,149,74,151,74,155,74,157,74,160,74,163,74,167,74,169,74,172,74,174,74,177,74,183,74,189,74,192,74,196,74,202,74,205,74,210,74,215,74,223],"svg",{"viewBox":67,"role":68,"ariaLabelledBy":69,"xmlns":72,"style":73},"66 0 628 164","img",[70,71],"rd-dicts-t","rd-dicts-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:628px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[76,77,78],"title",{"id":70},"A header row plus each data row becomes one dictionary",[80,81,82],"desc",{"id":71},"On the left a worksheet with a header row ID and Plan and two data rows. On the right the same data as two Python dictionaries keyed by the header names.",[84,85],"rect",{"x":86,"y":87,"width":88,"height":89,"fill":90},"66","0","628","164","#ffffff",[84,92],{"x":93,"y":94,"width":95,"height":96,"rx":87,"fill":97,"stroke":98,"style":99},"82.4","40.0","32.0","24.0","#e7ebef","var(--line,#cdd5e6)","stroke-width:1px",[84,101],{"x":102,"y":94,"width":103,"height":96,"rx":87,"fill":97,"stroke":98,"style":99},"114.4","54.4",[105,106,110],"text",{"x":107,"y":108,"style":109},"141.6","57.0","font-size:11px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","A",[84,112],{"x":113,"y":94,"width":114,"height":96,"rx":87,"fill":97,"stroke":98,"style":99},"168.8","57.5",[105,116,118],{"x":117,"y":108,"style":109},"197.6","B",[84,120],{"x":93,"y":121,"width":95,"height":122,"rx":87,"fill":97,"stroke":98,"style":99},"64.0","28.0",[105,124,127],{"x":125,"y":126,"style":109},"98.4","83.0","1",[84,129],{"x":102,"y":121,"width":103,"height":122,"rx":87,"fill":130,"stroke":131,"style":132},"#f0f4ff","var(--brand,#5b5cf0)","stroke-width:1.5px",[105,134,136],{"x":107,"y":126,"style":135},"font-size:12px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","ID",[84,138],{"x":113,"y":121,"width":114,"height":122,"rx":87,"fill":130,"stroke":131,"style":132},[105,140,141],{"x":117,"y":126,"style":135},"Plan",[84,143],{"x":93,"y":144,"width":95,"height":122,"rx":87,"fill":97,"stroke":98,"style":99},"92.0",[105,146,148],{"x":125,"y":147,"style":109},"111.0","2",[84,150],{"x":102,"y":144,"width":103,"height":122,"rx":87,"fill":90,"stroke":98,"style":99},[105,152,154],{"x":107,"y":147,"style":153},"font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","1001",[84,156],{"x":113,"y":144,"width":114,"height":122,"rx":87,"fill":90,"stroke":98,"style":99},[105,158,159],{"x":117,"y":147,"style":153},"Pro",[84,161],{"x":93,"y":162,"width":95,"height":122,"rx":87,"fill":97,"stroke":98,"style":99},"120.0",[105,164,166],{"x":125,"y":165,"style":109},"139.0","3",[84,168],{"x":102,"y":162,"width":103,"height":122,"rx":87,"fill":90,"stroke":98,"style":99},[105,170,171],{"x":107,"y":165,"style":153},"1002",[84,173],{"x":113,"y":162,"width":114,"height":122,"rx":87,"fill":90,"stroke":98,"style":99},[105,175,176],{"x":117,"y":165,"style":153},"Basic",[84,178],{"x":179,"y":94,"width":180,"height":122,"rx":87,"fill":181,"stroke":182,"style":132},"491.2","186.3","#d9f4f1","var(--teal,#0f9488)",[105,184,188],{"x":185,"y":186,"style":187},"584.4","59.0","font-size:12px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","{'ID': 1001, 'Plan': 'Pro'}",[84,190],{"x":179,"y":191,"width":180,"height":122,"rx":87,"fill":181,"stroke":182,"style":132},"68.0",[105,193,195],{"x":185,"y":194,"style":187},"87.0","{'ID': 1002, 'Plan': 'Basic'}",[105,197,201],{"x":198,"y":199,"style":200},"154.4","29.0","font-size:12.5px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","before",[105,203,204],{"x":185,"y":199,"style":200},"after",[41,206],{"x1":207,"y1":191,"x2":208,"y2":191,"stroke":131,"style":209},"236.3","477.2","stroke-width:2px",[211,212],"polygon",{"points":213,"fill":214},"481.2,68.0 472.2,73.0 472.2,63.0","#5b5cf0",[84,216],{"x":217,"y":218,"width":219,"height":220,"rx":221,"fill":90,"stroke":98,"style":222},"268.8","35.0","179.9","22.0","11","stroke-width:1.2px",[105,224,228],{"x":225,"y":226,"style":227},"358.8","50.0","font-size:11.5px;font-weight:400;fill:var(--brand-strong,#4338ca);text-anchor:middle;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace","dict(zip(header, row))",[25,230,232],{"id":231},"step-1-the-basic-pattern","Step 1 — The basic pattern",[10,234,235,238],{},[14,236,237],{},"iter_rows(values_only=True)"," yields each row as a tuple of plain values instead of cell objects. Take the first tuple as the header and zip it with every tuple after it:",[30,240,244],{"className":241,"code":242,"language":243,"meta":35,"style":35},"language-python shiki shiki-themes github-light github-dark-high-contrast","from openpyxl import load_workbook\n\nwb = load_workbook(\"customers.xlsx\", read_only=True, data_only=True)\nws = wb[\"Customers\"]\nrows = ws.iter_rows(values_only=True)\nheader = next(rows)\nprint(header)\nrecords = [dict(zip(header, row)) for row in rows]\nwb.close()\nprint(len(records), \"records\")\nprint(records[0])\n","python",[14,245,246,262,269,309,326,346,360,369,404,410,428],{"__ignoreMap":35},[39,247,248,252,256,259],{"class":41,"line":42},[39,249,251],{"class":250},"s-kum","from",[39,253,255],{"class":254},"skGVy"," openpyxl ",[39,257,258],{"class":250},"import",[39,260,261],{"class":254}," load_workbook\n",[39,263,265],{"class":41,"line":264},2,[39,266,268],{"emptyLinePlaceholder":267},true,"\n",[39,270,272,275,278,281,284,287,291,293,297,299,302,304,306],{"class":41,"line":271},3,[39,273,274],{"class":254},"wb ",[39,276,277],{"class":250},"=",[39,279,280],{"class":254}," load_workbook(",[39,282,283],{"class":49},"\"customers.xlsx\"",[39,285,286],{"class":254},", ",[39,288,290],{"class":289},"sa561","read_only",[39,292,277],{"class":250},[39,294,296],{"class":295},"sP0c6","True",[39,298,286],{"class":254},[39,300,301],{"class":289},"data_only",[39,303,277],{"class":250},[39,305,296],{"class":295},[39,307,308],{"class":254},")\n",[39,310,312,315,317,320,323],{"class":41,"line":311},4,[39,313,314],{"class":254},"ws ",[39,316,277],{"class":250},[39,318,319],{"class":254}," wb[",[39,321,322],{"class":49},"\"Customers\"",[39,324,325],{"class":254},"]\n",[39,327,329,332,334,337,340,342,344],{"class":41,"line":328},5,[39,330,331],{"class":254},"rows ",[39,333,277],{"class":250},[39,335,336],{"class":254}," ws.iter_rows(",[39,338,339],{"class":289},"values_only",[39,341,277],{"class":250},[39,343,296],{"class":295},[39,345,308],{"class":254},[39,347,349,352,354,357],{"class":41,"line":348},6,[39,350,351],{"class":254},"header ",[39,353,277],{"class":250},[39,355,356],{"class":295}," next",[39,358,359],{"class":254},"(rows)\n",[39,361,363,366],{"class":41,"line":362},7,[39,364,365],{"class":295},"print",[39,367,368],{"class":254},"(header)\n",[39,370,372,375,377,380,383,386,389,392,395,398,401],{"class":41,"line":371},8,[39,373,374],{"class":254},"records ",[39,376,277],{"class":250},[39,378,379],{"class":254}," [",[39,381,382],{"class":295},"dict",[39,384,385],{"class":254},"(",[39,387,388],{"class":295},"zip",[39,390,391],{"class":254},"(header, row)) ",[39,393,394],{"class":250},"for",[39,396,397],{"class":254}," row ",[39,399,400],{"class":250},"in",[39,402,403],{"class":254}," rows]\n",[39,405,407],{"class":41,"line":406},9,[39,408,409],{"class":254},"wb.close()\n",[39,411,413,415,417,420,423,426],{"class":41,"line":412},10,[39,414,365],{"class":295},[39,416,385],{"class":254},[39,418,419],{"class":295},"len",[39,421,422],{"class":254},"(records), ",[39,424,425],{"class":49},"\"records\"",[39,427,308],{"class":254},[39,429,431,433,436,438],{"class":41,"line":430},11,[39,432,365],{"class":295},[39,434,435],{"class":254},"(records[",[39,437,87],{"class":295},[39,439,440],{"class":254},"])\n",[10,442,443,444,447,448,450,451,454,455,457],{},"Two things in the first row show why the basic version is not enough: the unlabelled column arrives as a key of ",[14,445,446],{},"None",", and the second ",[14,449,62],{}," column silently overwrites the first, because a dictionary cannot hold the same key twice. The record for Arden Ltd has ",[14,452,453],{},"'Name': 'Arden'"," — the value from the later column. The count is also wrong — nine records for four customers: the blank row and the two empty rows before the footer became records full of ",[14,456,446],{},", and the footer became a record whose ID is a sentence.",[25,459,461],{"id":460},"step-2-a-dictreader-that-handles-real-sheets","Step 2 — A DictReader that handles real sheets",[10,463,464],{},"Wrap the pattern in a generator that fixes the header once and filters the rows as they stream past:",[30,466,468],{"className":241,"code":467,"language":243,"meta":35,"style":35},"def excel_dict_reader(path, sheet=None, header_row=1):\n    wb = load_workbook(path, read_only=True, data_only=True)\n    try:\n        ws = wb[sheet] if sheet else wb.worksheets[0]\n        rows = ws.iter_rows(min_row=header_row, values_only=True)\n        raw = next(rows)\n        header, seen = [], {}\n        for i, name in enumerate(raw, start=1):\n            name = str(name).strip() if name is not None else f\"column_{i}\"\n            seen[name] = seen.get(name, 0) + 1\n            header.append(name if seen[name] == 1 else f\"{name}_{seen[name]}\")\n        for row in rows:\n            if all(v is None or (isinstance(v, str) and not v.strip()) for v in row):\n                continue\n            yield {k: (v.strip() if isinstance(v, str) else v) for k, v in zip(header, row)}\n    finally:\n        wb.close()\n\nfor rec in excel_dict_reader(\"customers.xlsx\"):\n    print(rec[\"ID\"], rec[\"Name\"], rec[\"Name_2\"], rec[\"column_6\"])\n",[14,469,470,496,522,530,556,581,592,602,627,676,697,741,753,804,810,848,856,862,867,884],{"__ignoreMap":35},[39,471,472,475,479,482,484,486,489,491,493],{"class":41,"line":42},[39,473,474],{"class":250},"def",[39,476,478],{"class":477},"s_Opv"," excel_dict_reader",[39,480,481],{"class":254},"(path, sheet",[39,483,277],{"class":250},[39,485,446],{"class":295},[39,487,488],{"class":254},", header_row",[39,490,277],{"class":250},[39,492,127],{"class":295},[39,494,495],{"class":254},"):\n",[39,497,498,501,503,506,508,510,512,514,516,518,520],{"class":41,"line":264},[39,499,500],{"class":254},"    wb ",[39,502,277],{"class":250},[39,504,505],{"class":254}," load_workbook(path, ",[39,507,290],{"class":289},[39,509,277],{"class":250},[39,511,296],{"class":295},[39,513,286],{"class":254},[39,515,301],{"class":289},[39,517,277],{"class":250},[39,519,296],{"class":295},[39,521,308],{"class":254},[39,523,524,527],{"class":41,"line":271},[39,525,526],{"class":250},"    try",[39,528,529],{"class":254},":\n",[39,531,532,535,537,540,543,546,549,552,554],{"class":41,"line":311},[39,533,534],{"class":254},"        ws ",[39,536,277],{"class":250},[39,538,539],{"class":254}," wb[sheet] ",[39,541,542],{"class":250},"if",[39,544,545],{"class":254}," sheet ",[39,547,548],{"class":250},"else",[39,550,551],{"class":254}," wb.worksheets[",[39,553,87],{"class":295},[39,555,325],{"class":254},[39,557,558,561,563,565,568,570,573,575,577,579],{"class":41,"line":328},[39,559,560],{"class":254},"        rows ",[39,562,277],{"class":250},[39,564,336],{"class":254},[39,566,567],{"class":289},"min_row",[39,569,277],{"class":250},[39,571,572],{"class":254},"header_row, ",[39,574,339],{"class":289},[39,576,277],{"class":250},[39,578,296],{"class":295},[39,580,308],{"class":254},[39,582,583,586,588,590],{"class":41,"line":348},[39,584,585],{"class":254},"        raw ",[39,587,277],{"class":250},[39,589,356],{"class":295},[39,591,359],{"class":254},[39,593,594,597,599],{"class":41,"line":362},[39,595,596],{"class":254},"        header, seen ",[39,598,277],{"class":250},[39,600,601],{"class":254}," [], {}\n",[39,603,604,607,610,612,615,618,621,623,625],{"class":41,"line":371},[39,605,606],{"class":250},"        for",[39,608,609],{"class":254}," i, name ",[39,611,400],{"class":250},[39,613,614],{"class":295}," enumerate",[39,616,617],{"class":254},"(raw, ",[39,619,620],{"class":289},"start",[39,622,277],{"class":250},[39,624,127],{"class":295},[39,626,495],{"class":254},[39,628,629,632,634,637,640,642,645,648,651,654,657,660,663,667,670,673],{"class":41,"line":406},[39,630,631],{"class":254},"            name ",[39,633,277],{"class":250},[39,635,636],{"class":295}," str",[39,638,639],{"class":254},"(name).strip() ",[39,641,542],{"class":250},[39,643,644],{"class":254}," name ",[39,646,647],{"class":250},"is",[39,649,650],{"class":250}," not",[39,652,653],{"class":295}," None",[39,655,656],{"class":250}," else",[39,658,659],{"class":250}," f",[39,661,662],{"class":49},"\"column_",[39,664,666],{"class":665},"sSjpA","{",[39,668,669],{"class":254},"i",[39,671,672],{"class":665},"}",[39,674,675],{"class":49},"\"\n",[39,677,678,681,683,686,688,691,694],{"class":41,"line":412},[39,679,680],{"class":254},"            seen[name] ",[39,682,277],{"class":250},[39,684,685],{"class":254}," seen.get(name, ",[39,687,87],{"class":295},[39,689,690],{"class":254},") ",[39,692,693],{"class":250},"+",[39,695,696],{"class":295}," 1\n",[39,698,699,702,704,707,710,713,715,717,720,722,725,727,730,732,735,737,739],{"class":41,"line":430},[39,700,701],{"class":254},"            header.append(name ",[39,703,542],{"class":250},[39,705,706],{"class":254}," seen[name] ",[39,708,709],{"class":250},"==",[39,711,712],{"class":295}," 1",[39,714,656],{"class":250},[39,716,659],{"class":250},[39,718,719],{"class":49},"\"",[39,721,666],{"class":665},[39,723,724],{"class":254},"name",[39,726,672],{"class":665},[39,728,729],{"class":49},"_",[39,731,666],{"class":665},[39,733,734],{"class":254},"seen[name]",[39,736,672],{"class":665},[39,738,719],{"class":49},[39,740,308],{"class":254},[39,742,744,746,748,750],{"class":41,"line":743},12,[39,745,606],{"class":250},[39,747,397],{"class":254},[39,749,400],{"class":250},[39,751,752],{"class":254}," rows:\n",[39,754,756,759,762,765,767,769,772,775,778,781,784,786,789,791,794,796,799,801],{"class":41,"line":755},13,[39,757,758],{"class":250},"            if",[39,760,761],{"class":295}," all",[39,763,764],{"class":254},"(v ",[39,766,647],{"class":250},[39,768,653],{"class":295},[39,770,771],{"class":250}," or",[39,773,774],{"class":254}," (",[39,776,777],{"class":295},"isinstance",[39,779,780],{"class":254},"(v, ",[39,782,783],{"class":295},"str",[39,785,690],{"class":254},[39,787,788],{"class":250},"and",[39,790,650],{"class":250},[39,792,793],{"class":254}," v.strip()) ",[39,795,394],{"class":250},[39,797,798],{"class":254}," v ",[39,800,400],{"class":250},[39,802,803],{"class":254}," row):\n",[39,805,807],{"class":41,"line":806},14,[39,808,809],{"class":250},"                continue\n",[39,811,813,816,819,821,824,826,828,830,832,835,837,840,842,845],{"class":41,"line":812},15,[39,814,815],{"class":250},"            yield",[39,817,818],{"class":254}," {k: (v.strip() ",[39,820,542],{"class":250},[39,822,823],{"class":295}," isinstance",[39,825,780],{"class":254},[39,827,783],{"class":295},[39,829,690],{"class":254},[39,831,548],{"class":250},[39,833,834],{"class":254}," v) ",[39,836,394],{"class":250},[39,838,839],{"class":254}," k, v ",[39,841,400],{"class":250},[39,843,844],{"class":295}," zip",[39,846,847],{"class":254},"(header, row)}\n",[39,849,851,854],{"class":41,"line":850},16,[39,852,853],{"class":250},"    finally",[39,855,529],{"class":254},[39,857,859],{"class":41,"line":858},17,[39,860,861],{"class":254},"        wb.close()\n",[39,863,865],{"class":41,"line":864},18,[39,866,268],{"emptyLinePlaceholder":267},[39,868,870,872,875,877,880,882],{"class":41,"line":869},19,[39,871,394],{"class":250},[39,873,874],{"class":254}," rec ",[39,876,400],{"class":250},[39,878,879],{"class":254}," excel_dict_reader(",[39,881,283],{"class":49},[39,883,495],{"class":254},[39,885,887,890,893,896,899,902,904,907,909,912],{"class":41,"line":886},20,[39,888,889],{"class":295},"    print",[39,891,892],{"class":254},"(rec[",[39,894,895],{"class":49},"\"ID\"",[39,897,898],{"class":254},"], rec[",[39,900,901],{"class":49},"\"Name\"",[39,903,898],{"class":254},[39,905,906],{"class":49},"\"Name_2\"",[39,908,898],{"class":254},[39,910,911],{"class":49},"\"column_6\"",[39,913,440],{"class":254},[10,915,916,917,920,921,924,925,928,929,932],{},"The header is cleaned once: a missing name becomes ",[14,918,919],{},"column_6",", and a repeated name gets a suffix, ",[14,922,923],{},"Name_2",", so no value is lost. Blank rows are skipped — including rows of empty strings, which some exporters write instead of truly empty cells — and text values are stripped, which fixes ",[14,926,927],{},"\"  Dunmore  \"",". Because it is a generator, the workbook is read one row at a time and closed by the ",[14,930,931],{},"finally"," block when the loop ends, even if the loop body raises.",[10,934,935,936,939],{},"The footer is still there: ",[14,937,938],{},"Exported 2026-09-28"," comes through as a record with that text as its ID. Stop at the first row that does not look like data, or drop records whose key field is not the right type:",[30,941,943],{"className":241,"code":942,"language":243,"meta":35,"style":35},"records = [r for r in excel_dict_reader(\"customers.xlsx\") if isinstance(r[\"ID\"], int)]\nprint([r[\"ID\"] for r in records])\n",[14,944,945,985],{"__ignoreMap":35},[39,946,947,949,951,954,956,959,961,963,965,967,969,971,974,976,979,982],{"class":41,"line":42},[39,948,374],{"class":254},[39,950,277],{"class":250},[39,952,953],{"class":254}," [r ",[39,955,394],{"class":250},[39,957,958],{"class":254}," r ",[39,960,400],{"class":250},[39,962,879],{"class":254},[39,964,283],{"class":49},[39,966,690],{"class":254},[39,968,542],{"class":250},[39,970,823],{"class":295},[39,972,973],{"class":254},"(r[",[39,975,895],{"class":49},[39,977,978],{"class":254},"], ",[39,980,981],{"class":295},"int",[39,983,984],{"class":254},")]\n",[39,986,987,989,992,994,997,999,1001,1003],{"class":41,"line":264},[39,988,365],{"class":295},[39,990,991],{"class":254},"([r[",[39,993,895],{"class":49},[39,995,996],{"class":254},"] ",[39,998,394],{"class":250},[39,1000,958],{"class":254},[39,1002,400],{"class":250},[39,1004,1005],{"class":254}," records])\n",[65,1007,74,1013,74,1016,74,1019,74,1024,74,1031,74,1042,74,1048,74,1053,74,1058,74,1061,74,1064,74,1071,74,1077,74,1081,74,1085,74,1089,74,1092,74,1095,74,1101,74,1106,74,1109,74,1112,74,1116,74,1120,74,1123,74,1126,74,1133,74,1138,74,1141,74,1144,74,1148,74,1151,74,1156,74,1162,74,1167,74,1170,74,1173],{"viewBox":1008,"role":68,"ariaLabelledBy":1009,"xmlns":72,"style":1012},"4 0 752 160",[1010,1011],"rd-filter-t","rd-filter-d","width:100%;max-width:752px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[76,1014,1015],{"id":1010},"What the reader does to each row",[80,1017,1018],{"id":1011},"Each row streams from the sheet, blank rows are skipped, the row is zipped with the cleaned header, text is stripped, and rows whose ID is not an integer are dropped.",[84,1020],{"x":1021,"y":87,"width":1022,"height":1023,"fill":90},"4","752","160",[84,1025],{"x":1026,"y":1027,"width":1028,"height":1029,"rx":1030,"fill":97,"stroke":98,"style":209},"20.0","16.0","115.2","128.0","12",[1032,1033,1035],"g",{"transform":1034},"translate(65.6,46.5) scale(1.000)",[1036,1037],"path",{"d":1038,"fill":1039,"stroke":1040,"style":1041},"M4 4h16v16H4z M4 9h16 M4 14h16 M10 4v16","none","var(--text,#172033)","stroke-width:1.80px;stroke-linecap:round;stroke-linejoin:round",[105,1043,1047],{"x":1044,"y":1045,"style":1046},"77.6","93.0","font-size:13.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","Stream row",[105,1049,1052],{"x":1044,"y":1050,"style":1051},"112.5","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","values_only=True",[41,1054],{"x1":1055,"y1":1056,"x2":1057,"y2":1056,"stroke":131,"style":209},"140.2","80.0","162.2",[211,1059],{"points":1060,"fill":214},"166.2,80.0 157.2,85.0 157.2,75.0",[84,1062],{"x":1063,"y":1027,"width":1028,"height":1029,"rx":1030,"fill":130,"stroke":131,"style":209},"171.2",[1032,1065,1067],{"transform":1066},"translate(216.8,37.8) scale(1.000)",[1036,1068],{"d":1069,"fill":1039,"stroke":1070,"style":1041},"M3 5h18l-7 8v6l-4 2v-8z","var(--brand-strong,#4338ca)",[105,1072,1076],{"x":1073,"y":1074,"style":1075},"228.8","84.2","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Skip blank",[105,1078,1080],{"x":1073,"y":1079,"style":1051},"103.8","all None or",[105,1082,1084],{"x":1073,"y":1083,"style":1051},"121.2","empty text",[41,1086],{"x1":1087,"y1":1056,"x2":1088,"y2":1056,"stroke":131,"style":209},"291.4","313.4",[211,1090],{"points":1091,"fill":214},"317.4,80.0 308.4,85.0 308.4,75.0",[84,1093],{"x":1094,"y":1027,"width":1028,"height":1029,"rx":1030,"fill":130,"stroke":131,"style":209},"322.4",[1032,1096,1098],{"transform":1097},"translate(368.0,29.0) scale(1.000)",[1036,1099],{"d":1100,"fill":1039,"stroke":1070,"style":1041},"M10 14a4 4 0 0 0 5.7 0l3-3a4 4 0 0 0-5.7-5.7l-1 1 M14 10a4 4 0 0 0-5.7 0l-3 3a4 4 0 0 0 5.7 5.7l1-1",[105,1102,1105],{"x":1103,"y":1104,"style":1075},"380.0","75.5","Zip with",[105,1107,1108],{"x":1103,"y":1045,"style":1075},"header",[105,1110,1111],{"x":1103,"y":1050,"style":1051},"column_6,",[105,1113,1115],{"x":1103,"y":1114,"style":1051},"130.0","Name_2 ...",[41,1117],{"x1":1118,"y1":1056,"x2":1119,"y2":1056,"stroke":131,"style":209},"442.6","464.6",[211,1121],{"points":1122,"fill":214},"468.6,80.0 459.6,85.0 459.6,75.0",[84,1124],{"x":1125,"y":1027,"width":1028,"height":1029,"rx":1030,"fill":181,"stroke":182,"style":209},"473.6",[1032,1127,1129],{"transform":1128},"translate(519.2,37.8) scale(1.000)",[1036,1130],{"d":1131,"fill":1039,"stroke":1132,"style":1041},"M5 12.5l4.5 4.5L19 7","var(--teal-ink,#0b6157)",[105,1134,1137],{"x":1135,"y":1074,"style":1136},"531.2","font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Strip text",[105,1139,1140],{"x":1135,"y":1079,"style":1051},"' Dunmore ' →",[105,1142,1143],{"x":1135,"y":1083,"style":1051},"'Dunmore'",[41,1145],{"x1":1146,"y1":1056,"x2":1147,"y2":1056,"stroke":131,"style":209},"593.8","615.8",[211,1149],{"points":1150,"fill":214},"619.8,80.0 610.8,85.0 610.8,75.0",[84,1152],{"x":1153,"y":1027,"width":1028,"height":1029,"rx":1030,"fill":1154,"stroke":1155,"style":209},"624.8","#fdefd8","var(--gold,#b4740a)",[1032,1157,1159],{"transform":1158},"translate(670.4,29.0) scale(1.000)",[1036,1160],{"d":1069,"fill":1039,"stroke":1161,"style":1041},"var(--gold-ink,#7a4e06)",[105,1163,1166],{"x":1164,"y":1104,"style":1165},"682.4","font-size:13.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Keep real",[105,1168,1169],{"x":1164,"y":1045,"style":1165},"rows",[105,1171,1172],{"x":1164,"y":1050,"style":1051},"ID must be an",[105,1174,981],{"x":1164,"y":1114,"style":1051},[10,1176,1177,1178,1181,1182,1185],{},"The ",[14,1179,1180],{},"header_row"," argument covers reports with a title above the table: ",[14,1183,1184],{},"excel_dict_reader(\"report.xlsx\", header_row=4)"," starts reading at row 4, treats it as the header and ignores everything above. When the header row moves from one export to the next, find it first by scanning the top rows for a known column name, then pass the row number in.",[25,1187,1189],{"id":1188},"step-3-build-a-lookup-keyed-by-id","Step 3 — Build a lookup keyed by ID",[10,1191,1192],{},"The most common use of row dictionaries is a lookup: given an ID from somewhere else, fetch that customer's details. Build a dictionary of dictionaries, and refuse duplicate keys rather than let the last one win:",[30,1194,1196],{"className":241,"code":1195,"language":243,"meta":35,"style":35},"customers = {}\nfor rec in records:\n    if rec[\"ID\"] in customers:\n        raise ValueError(f\"duplicate ID {rec['ID']}\")\n    customers[rec[\"ID\"]] = rec\n\nprint(customers[1004][\"Name\"], customers[1004][\"Renewal\"])\ndue = [c[\"Name\"] for c in customers.values() if c[\"Renewal\"].strftime(\"%Y-%m\") == \"2026-10\"]\nprint(\"renewing in October:\", due)\n",[14,1197,1198,1208,1219,1236,1269,1284,1288,1315,1361],{"__ignoreMap":35},[39,1199,1200,1203,1205],{"class":41,"line":42},[39,1201,1202],{"class":254},"customers ",[39,1204,277],{"class":250},[39,1206,1207],{"class":254}," {}\n",[39,1209,1210,1212,1214,1216],{"class":41,"line":264},[39,1211,394],{"class":250},[39,1213,874],{"class":254},[39,1215,400],{"class":250},[39,1217,1218],{"class":254}," records:\n",[39,1220,1221,1224,1227,1229,1231,1233],{"class":41,"line":271},[39,1222,1223],{"class":250},"    if",[39,1225,1226],{"class":254}," rec[",[39,1228,895],{"class":49},[39,1230,996],{"class":254},[39,1232,400],{"class":250},[39,1234,1235],{"class":254}," customers:\n",[39,1237,1238,1241,1244,1246,1249,1252,1254,1257,1260,1263,1265,1267],{"class":41,"line":311},[39,1239,1240],{"class":250},"        raise",[39,1242,1243],{"class":295}," ValueError",[39,1245,385],{"class":254},[39,1247,1248],{"class":250},"f",[39,1250,1251],{"class":49},"\"duplicate ID ",[39,1253,666],{"class":665},[39,1255,1256],{"class":254},"rec[",[39,1258,1259],{"class":49},"'ID'",[39,1261,1262],{"class":254},"]",[39,1264,672],{"class":665},[39,1266,719],{"class":49},[39,1268,308],{"class":254},[39,1270,1271,1274,1276,1279,1281],{"class":41,"line":328},[39,1272,1273],{"class":254},"    customers[rec[",[39,1275,895],{"class":49},[39,1277,1278],{"class":254},"]] ",[39,1280,277],{"class":250},[39,1282,1283],{"class":254}," rec\n",[39,1285,1286],{"class":41,"line":348},[39,1287,268],{"emptyLinePlaceholder":267},[39,1289,1290,1292,1295,1298,1301,1303,1306,1308,1310,1313],{"class":41,"line":362},[39,1291,365],{"class":295},[39,1293,1294],{"class":254},"(customers[",[39,1296,1297],{"class":295},"1004",[39,1299,1300],{"class":254},"][",[39,1302,901],{"class":49},[39,1304,1305],{"class":254},"], customers[",[39,1307,1297],{"class":295},[39,1309,1300],{"class":254},[39,1311,1312],{"class":49},"\"Renewal\"",[39,1314,440],{"class":254},[39,1316,1317,1320,1322,1325,1327,1329,1331,1334,1336,1339,1341,1344,1346,1349,1352,1354,1356,1359],{"class":41,"line":371},[39,1318,1319],{"class":254},"due ",[39,1321,277],{"class":250},[39,1323,1324],{"class":254}," [c[",[39,1326,901],{"class":49},[39,1328,996],{"class":254},[39,1330,394],{"class":250},[39,1332,1333],{"class":254}," c ",[39,1335,400],{"class":250},[39,1337,1338],{"class":254}," customers.values() ",[39,1340,542],{"class":250},[39,1342,1343],{"class":254}," c[",[39,1345,1312],{"class":49},[39,1347,1348],{"class":254},"].strftime(",[39,1350,1351],{"class":49},"\"%Y-%m\"",[39,1353,690],{"class":254},[39,1355,709],{"class":250},[39,1357,1358],{"class":49}," \"2026-10\"",[39,1360,325],{"class":254},[39,1362,1363,1365,1367,1370],{"class":41,"line":406},[39,1364,365],{"class":295},[39,1366,385],{"class":254},[39,1368,1369],{"class":49},"\"renewing in October:\"",[39,1371,1372],{"class":254},", due)\n",[10,1374,1375,1376,1379],{},"Dates arrive as ",[14,1377,1378],{},"datetime"," objects, because openpyxl converts cells with a date format; a value stored as text stays a string. Check the type before calling date methods on values from sheets people edit by hand.",[25,1381,1383],{"id":1382},"step-4-load-rows-into-a-dataclass","Step 4 — Load rows into a dataclass",[10,1385,1386,1387,1390],{},"Dictionaries are loose: a typo in a key is a ",[14,1388,1389],{},"KeyError"," at runtime, and nothing says which fields exist. For anything bigger than a throwaway script, convert each row into a dataclass that names the fields and their types:",[30,1392,1394],{"className":241,"code":1393,"language":243,"meta":35,"style":35},"from dataclasses import dataclass\nfrom datetime import datetime\n\n@dataclass(frozen=True)\nclass Customer:\n    id: int\n    name: str\n    email: str | None\n    plan: str\n    renewal: datetime\n\ndef to_customer(r):\n    return Customer(id=r[\"ID\"], name=r[\"Name\"], email=r[\"Email\"], plan=r[\"Plan\"], renewal=r[\"Renewal\"])\n\ntyped = [to_customer(r) for r in records]\nprint(typed[2])\nmissing_email = [c.name for c in typed if c.email is None]\nprint(\"no email:\", missing_email)\n",[14,1395,1396,1408,1420,1424,1440,1450,1461,1469,1482,1489,1494,1498,1508,1573,1577,1596,1607,1637],{"__ignoreMap":35},[39,1397,1398,1400,1403,1405],{"class":41,"line":42},[39,1399,251],{"class":250},[39,1401,1402],{"class":254}," dataclasses ",[39,1404,258],{"class":250},[39,1406,1407],{"class":254}," dataclass\n",[39,1409,1410,1412,1415,1417],{"class":41,"line":264},[39,1411,251],{"class":250},[39,1413,1414],{"class":254}," datetime ",[39,1416,258],{"class":250},[39,1418,1419],{"class":254}," datetime\n",[39,1421,1422],{"class":41,"line":271},[39,1423,268],{"emptyLinePlaceholder":267},[39,1425,1426,1429,1431,1434,1436,1438],{"class":41,"line":311},[39,1427,1428],{"class":477},"@dataclass",[39,1430,385],{"class":254},[39,1432,1433],{"class":289},"frozen",[39,1435,277],{"class":250},[39,1437,296],{"class":295},[39,1439,308],{"class":254},[39,1441,1442,1445,1448],{"class":41,"line":328},[39,1443,1444],{"class":250},"class",[39,1446,1447],{"class":45}," Customer",[39,1449,529],{"class":254},[39,1451,1452,1455,1458],{"class":41,"line":348},[39,1453,1454],{"class":295},"    id",[39,1456,1457],{"class":254},": ",[39,1459,1460],{"class":295},"int\n",[39,1462,1463,1466],{"class":41,"line":362},[39,1464,1465],{"class":254},"    name: ",[39,1467,1468],{"class":295},"str\n",[39,1470,1471,1474,1476,1479],{"class":41,"line":371},[39,1472,1473],{"class":254},"    email: ",[39,1475,783],{"class":295},[39,1477,1478],{"class":250}," |",[39,1480,1481],{"class":295}," None\n",[39,1483,1484,1487],{"class":41,"line":406},[39,1485,1486],{"class":254},"    plan: ",[39,1488,1468],{"class":295},[39,1490,1491],{"class":41,"line":412},[39,1492,1493],{"class":254},"    renewal: datetime\n",[39,1495,1496],{"class":41,"line":430},[39,1497,268],{"emptyLinePlaceholder":267},[39,1499,1500,1502,1505],{"class":41,"line":743},[39,1501,474],{"class":250},[39,1503,1504],{"class":477}," to_customer",[39,1506,1507],{"class":254},"(r):\n",[39,1509,1510,1513,1516,1519,1521,1524,1526,1528,1530,1532,1534,1536,1538,1541,1543,1545,1548,1550,1553,1555,1557,1560,1562,1565,1567,1569,1571],{"class":41,"line":755},[39,1511,1512],{"class":250},"    return",[39,1514,1515],{"class":254}," Customer(",[39,1517,1518],{"class":289},"id",[39,1520,277],{"class":250},[39,1522,1523],{"class":254},"r[",[39,1525,895],{"class":49},[39,1527,978],{"class":254},[39,1529,724],{"class":289},[39,1531,277],{"class":250},[39,1533,1523],{"class":254},[39,1535,901],{"class":49},[39,1537,978],{"class":254},[39,1539,1540],{"class":289},"email",[39,1542,277],{"class":250},[39,1544,1523],{"class":254},[39,1546,1547],{"class":49},"\"Email\"",[39,1549,978],{"class":254},[39,1551,1552],{"class":289},"plan",[39,1554,277],{"class":250},[39,1556,1523],{"class":254},[39,1558,1559],{"class":49},"\"Plan\"",[39,1561,978],{"class":254},[39,1563,1564],{"class":289},"renewal",[39,1566,277],{"class":250},[39,1568,1523],{"class":254},[39,1570,1312],{"class":49},[39,1572,440],{"class":254},[39,1574,1575],{"class":41,"line":806},[39,1576,268],{"emptyLinePlaceholder":267},[39,1578,1579,1582,1584,1587,1589,1591,1593],{"class":41,"line":812},[39,1580,1581],{"class":254},"typed ",[39,1583,277],{"class":250},[39,1585,1586],{"class":254}," [to_customer(r) ",[39,1588,394],{"class":250},[39,1590,958],{"class":254},[39,1592,400],{"class":250},[39,1594,1595],{"class":254}," records]\n",[39,1597,1598,1600,1603,1605],{"class":41,"line":850},[39,1599,365],{"class":295},[39,1601,1602],{"class":254},"(typed[",[39,1604,148],{"class":295},[39,1606,440],{"class":254},[39,1608,1609,1612,1614,1617,1619,1621,1623,1626,1628,1631,1633,1635],{"class":41,"line":858},[39,1610,1611],{"class":254},"missing_email ",[39,1613,277],{"class":250},[39,1615,1616],{"class":254}," [c.name ",[39,1618,394],{"class":250},[39,1620,1333],{"class":254},[39,1622,400],{"class":250},[39,1624,1625],{"class":254}," typed ",[39,1627,542],{"class":250},[39,1629,1630],{"class":254}," c.email ",[39,1632,647],{"class":250},[39,1634,653],{"class":295},[39,1636,325],{"class":254},[39,1638,1639,1641,1643,1646],{"class":41,"line":864},[39,1640,365],{"class":295},[39,1642,385],{"class":254},[39,1644,1645],{"class":49},"\"no email:\"",[39,1647,1648],{"class":254},", missing_email)\n",[10,1650,1651,1652,23],{},"The mapping function is the one place where sheet headers meet Python names, so a renamed column is fixed in one line. A dataclass does not check types at runtime; when the data must be validated as it is loaded — email formats, allowed plans, required fields — use pydantic instead, as shown in ",[19,1653,1655],{"href":1654},"\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fvalidate-excel-rows-with-pydantic\u002F","Validate Excel Rows with Pydantic",[65,1657,74,1662,74,1665,74,1668,74,1671,74,1676,74,1683,74,1689,74,1694,74,1699,74,1705,74,1708,74,1712,74,1717,74,1721,74,1728,74,1731,74,1736,74,1740,74,1744,74,1748,74,1753,74,1757,74,1762,74,1765,74,1768,74,1774,74,1777,74,1783,74,1788,74,1792,74,1796,74,1801,74,1804,74,1809,74,1813],{"viewBox":1658,"role":68,"ariaLabelledBy":1659,"xmlns":72,"style":1012},"4 0 752 224",[1660,1661],"rd-choice-t","rd-choice-d",[76,1663,1664],{"id":1660},"Dictionaries, dataclasses or a DataFrame",[80,1666,1667],{"id":1661},"Three panels comparing row dictionaries, dataclasses and a pandas DataFrame for reading Excel rows, with the strengths and weaknesses of each.",[84,1669],{"x":1021,"y":87,"width":1022,"height":1670,"fill":90},"224",[84,1672],{"x":1026,"y":1027,"width":1673,"height":1674,"rx":1675,"fill":130,"stroke":131,"style":209},"228.0","192.0","14",[1032,1677,1679],{"transform":1678},"translate(121.0,29.0) scale(1.083)",[1036,1680],{"d":1681,"fill":1039,"stroke":1070,"style":1682},"M9 8l-4 4 4 4 M15 8l4 4-4 4","stroke-width:1.66px;stroke-linecap:round;stroke-linejoin:round",[105,1684,1688],{"x":1685,"y":1686,"style":1687},"134.0","78.0","font-size:14px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Row dictionaries",[41,1690],{"x1":1691,"y1":1692,"x2":1693,"y2":1692,"stroke":131,"style":99},"36.0","91.0","232.0",[105,1695,1698],{"x":1696,"y":147,"style":1697},"60.0","font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:start","no dependencies beyond",[1032,1700,1702],{"transform":1701},"translate(38.5,99.5) scale(0.625)",[1036,1703],{"d":1131,"fill":1039,"stroke":182,"style":1704},"stroke-width:2.88px;stroke-linecap:round;stroke-linejoin:round",[105,1706,1707],{"x":1696,"y":1029,"style":1697},"openpyxl",[105,1709,1711],{"x":1696,"y":1710,"style":1697},"154.0","streams sheets of any size",[1032,1713,1715],{"transform":1714},"translate(38.5,142.5) scale(0.625)",[1036,1716],{"d":1131,"fill":1039,"stroke":182,"style":1704},[105,1718,1720],{"x":1696,"y":1719,"style":1697},"180.0","typos in keys fail at runtime",[1032,1722,1724],{"transform":1723},"translate(38.5,168.5) scale(0.625)",[1036,1725],{"d":1726,"fill":1039,"stroke":1727,"style":1704},"M6 6l12 12 M18 6L6 18","var(--accent,#f43f8f)",[84,1729],{"x":1730,"y":1027,"width":1673,"height":1674,"rx":1675,"fill":181,"stroke":182,"style":209},"266.0",[1032,1732,1734],{"transform":1733},"translate(367.0,29.0) scale(1.083)",[1036,1735],{"d":1131,"fill":1039,"stroke":1132,"style":1682},[105,1737,1739],{"x":1103,"y":1686,"style":1738},"font-size:14px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Dataclasses",[41,1741],{"x1":1742,"y1":1692,"x2":1743,"y2":1692,"stroke":182,"style":99},"282.0","478.0",[105,1745,1747],{"x":1746,"y":147,"style":1697},"306.0","named, documented fields",[1032,1749,1751],{"transform":1750},"translate(284.5,99.5) scale(0.625)",[1036,1752],{"d":1131,"fill":1039,"stroke":182,"style":1704},[105,1754,1756],{"x":1746,"y":1755,"style":1697},"137.0","one place maps headers to",[1032,1758,1760],{"transform":1759},"translate(284.5,125.5) scale(0.625)",[1036,1761],{"d":1131,"fill":1039,"stroke":182,"style":1704},[105,1763,1764],{"x":1746,"y":1710,"style":1697},"names",[105,1766,1767],{"x":1746,"y":1719,"style":1697},"no validation on their own",[1032,1769,1771],{"transform":1770},"translate(284.5,168.5) scale(0.625)",[1036,1772],{"d":1773,"fill":1039,"stroke":1155,"style":1704},"M12 3.5l9.5 17h-19z M12 10v5 M12 17.8v.4",[84,1775],{"x":1776,"y":1027,"width":1673,"height":1674,"rx":1675,"fill":1154,"stroke":1155,"style":209},"512.0",[1032,1778,1780],{"transform":1779},"translate(613.0,29.0) scale(1.083)",[1036,1781],{"d":1782,"fill":1039,"stroke":1161,"style":1682},"M3 5h18v14H3z M3 10h18 M3 15h18 M9 5v14",[105,1784,1787],{"x":1785,"y":1686,"style":1786},"626.0","font-size:14px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","pandas DataFrame",[41,1789],{"x1":1790,"y1":1692,"x2":1791,"y2":1692,"stroke":1155,"style":99},"528.0","724.0",[105,1793,1795],{"x":1794,"y":147,"style":1697},"552.0","fast column-wise maths",[1032,1797,1799],{"transform":1798},"translate(530.5,99.5) scale(0.625)",[1036,1800],{"d":1131,"fill":1039,"stroke":182,"style":1704},[105,1802,1803],{"x":1794,"y":1755,"style":1697},"grouping, joins, pivots",[1032,1805,1807],{"transform":1806},"translate(530.5,125.5) scale(0.625)",[1036,1808],{"d":1131,"fill":1039,"stroke":182,"style":1704},[105,1810,1812],{"x":1794,"y":1811,"style":1697},"163.0","whole sheet in memory",[1032,1814,1816],{"transform":1815},"translate(530.5,151.5) scale(0.625)",[1036,1817],{"d":1726,"fill":1039,"stroke":1727,"style":1704},[25,1819,1821],{"id":1820},"compared-with-pandas","Compared with pandas",[10,1823,1824,1825,1828,1829,286,1831,286,1833,1835,1836,1839,1840,1843,1844,1846,1847,1850],{},"pandas does the same in one line — ",[14,1826,1827],{},"pd.read_excel(path).to_dict(\"records\")"," — and for data that is going to be grouped, joined or summarised, a DataFrame is the better tool. The dictionary approach wins when the work is per-row and the script is small: there is no pandas import, rows stream instead of loading at once, and values keep the types openpyxl gives them (Python ",[14,1830,981],{},[14,1832,783],{},[14,1834,1378],{},") instead of NumPy types that need converting before they go into JSON or an API call. ",[14,1837,1838],{},"to_dict(\"records\")"," also turns empty cells into ",[14,1841,1842],{},"NaN",", a float, where openpyxl gives ",[14,1845,446],{},"; code that checks ",[14,1848,1849],{},"if value is None"," behaves differently on the two.",[25,1852,1854],{"id":1853},"common-pitfalls","Common pitfalls",[1856,1857,1858,1870,1883,1893,1910],"ul",{},[1859,1860,1861,1865,1866,1869],"li",{},[1862,1863,1864],"strong",{},"Duplicate headers."," ",[14,1867,1868],{},"dict(zip(...))"," keeps the last value for a repeated key. Rename duplicates before zipping.",[1859,1871,1872,1875,1876,1879,1880,23],{},[1862,1873,1874],{},"Formulas instead of values."," Without ",[14,1877,1878],{},"data_only=True",", formula cells return strings such as ",[14,1881,1882],{},"=B2*1.2",[1859,1884,1885,1892],{},[1862,1886,1887,1888,1891],{},"Treating ",[14,1889,1890],{},"max_row"," as the end of the data."," Formatted empty rows and footers extend it; filter rows by content instead.",[1859,1894,1895,1898,1899,1902,1903,1905,1906,1909],{},[1862,1896,1897],{},"Not closing read-only workbooks."," Read-only mode keeps the file open until ",[14,1900,1901],{},"close()",". The generator above closes it in ",[14,1904,931],{},"; if you break out of the loop early, the generator is closed when it is garbage-collected, or call ",[14,1907,1908],{},".close()"," on it yourself.",[1859,1911,1912,1915,1916,1918,1919,23],{},[1862,1913,1914],{},"Merged cells."," Only the top-left cell of a merged range has a value; the others read as ",[14,1917,446],{},". See ",[19,1920,1922],{"href":1921},"\u002Fgetting-started-with-python-excel-automation\u002Fusing-openpyxl-for-excel-file-manipulation\u002Funmerge-cells-and-fill-values-with-openpyxl\u002F","Unmerge Cells and Fill Values with openpyxl",[25,1924,1926],{"id":1925},"performance-notes","Performance notes",[10,1928,1929,1930,1933],{},"In read-only mode memory stays flat however long the sheet is, because only the current row is held. Speed is roughly that of openpyxl's parser — tens of thousands of rows per second — which is similar to ",[14,1931,1932],{},"read_excel"," with the openpyxl engine, since pandas uses the same parser underneath. Collecting every record into a list, as Step 3 does, brings the memory back; for very large sheets keep the processing inside the loop.",[25,1935,1937],{"id":1936},"conclusion","Conclusion",[10,1939,1940,1941,1944],{},"Read the header row with ",[14,1942,1943],{},"next(iter_rows(values_only=True))",", clean it once — name the blanks, suffix the duplicates — and zip it with every following row, skipping blank rows and stripping text as they stream past. Filter out footers by checking the key field, build ID lookups that refuse duplicates, and move to dataclasses or pydantic models once the script grows. Reach for pandas when the work is about columns rather than rows.",[25,1946,1948],{"id":1947},"frequently-asked-questions","Frequently asked questions",[10,1950,1951,1954],{},[1862,1952,1953],{},"Does openpyxl have a DictReader like the csv module?","\nNo, but the pattern is short: take the first row from ws.iter_rows(values_only=True) as the header and zip it with each following row. Wrapping that in a generator gives a DictReader equivalent that streams the sheet.",[10,1956,1957,1960],{},[1862,1958,1959],{},"How do I skip empty rows when reading with openpyxl?","\nCheck each row before converting it: skip it when every value is None or an empty string. Relying on max_row does not work, because formatted empty rows at the bottom count as part of the sheet.",[10,1962,1963,1966],{},[1862,1964,1965],{},"Why do some of my dictionary keys come out as None?","\nThe header cell for that column is empty. Replace missing header names with a placeholder such as column_6 before zipping, otherwise every unlabelled column shares the key None and only the last one survives.",[10,1968,1969,1972],{},[1862,1970,1971],{},"Is this faster than pandas read_excel?","\nRoughly the same for parsing, since pandas uses openpyxl for .xlsx files. The gains are a lighter import, flat memory use when streaming, and plain Python types instead of NumPy ones.",[25,1974,1976],{"id":1975},"related","Related",[1856,1978,1979,1985,1992,1999],{},[1859,1980,1981,1982,1984],{},"Up one level: ",[19,1983,22],{"href":21}," — the cell-level API these readers are built on.",[1859,1986,1987,1991],{},[19,1988,1990],{"href":1989},"\u002Fgetting-started-with-python-excel-automation\u002Fusing-openpyxl-for-excel-file-manipulation\u002Fiterate-over-rows-and-columns-with-openpyxl\u002F","Iterate Over Rows and Columns with openpyxl"," — every way to walk a sheet.",[1859,1993,1994,1998],{},[19,1995,1997],{"href":1996},"\u002Fgetting-started-with-python-excel-automation\u002Fwriting-dataframes-to-excel-with-pandas\u002Fwrite-a-list-of-dictionaries-to-excel-without-pandas\u002F","Write a List of Dictionaries to Excel Without pandas"," — the same records going back out.",[1859,2000,2001,2005],{},[19,2002,2004],{"href":2003},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-large-excel-files-in-python\u002Fspeed-up-openpyxl-with-read-only-mode\u002F","Speed Up openpyxl with Read-Only Mode"," — why streaming keeps memory flat.",[2007,2008,2009],"style",{},"html pre.shiki code .sMTad, html code.shiki .sMTad{--shiki-default:#6F42C1;--shiki-dark:#FFB757}html pre.shiki code .srMev, html code.shiki .srMev{--shiki-default:#032F62;--shiki-dark:#ADDCFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s-kum, html code.shiki .s-kum{--shiki-default:#D73A49;--shiki-dark:#FF9492}html pre.shiki code .skGVy, html code.shiki .skGVy{--shiki-default:#24292E;--shiki-dark:#F0F3F6}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}",{"title":35,"searchDepth":264,"depth":264,"links":2011},[2012,2013,2014,2015,2016,2017,2018,2019,2020,2021,2022],{"id":27,"depth":264,"text":28},{"id":231,"depth":264,"text":232},{"id":460,"depth":264,"text":461},{"id":1188,"depth":264,"text":1189},{"id":1382,"depth":264,"text":1383},{"id":1820,"depth":264,"text":1821},{"id":1853,"depth":264,"text":1854},{"id":1925,"depth":264,"text":1926},{"id":1936,"depth":264,"text":1937},{"id":1947,"depth":264,"text":1948},{"id":1975,"depth":264,"text":1976},"2026-09-29","Turn each row of a sheet into a dictionary keyed by the header — the csv.DictReader pattern for .xlsx files. Stream rows with iter_rows(values_only=True), skip blank rows, clean up missing and duplicate headers, build lookups keyed by ID, and load rows into dataclasses without pandas.","md",[2027,2029,2031,2033],{"q":1953,"a":2028},"No, but the pattern is short: take the first row from ws.iter_rows(values_only=True) as the header and zip it with each following row. Wrapping that in a generator gives a DictReader equivalent that streams the sheet.",{"q":1959,"a":2030},"Check each row before converting it: skip it when every value is None or an empty string. Relying on max_row does not work, because formatted empty rows at the bottom count as part of the sheet.",{"q":1965,"a":2032},"The header cell for that column is empty. Replace missing header names with a placeholder such as column_6 before zipping, otherwise every unlabelled column shares the key None and only the last one survives.",{"q":1971,"a":2034},"Roughly the same for parsing, since pandas uses openpyxl for .xlsx files. The gains are a lighter import, flat memory use when streaming, and plain Python types instead of NumPy ones.","read excel rows into dictionaries with openpyxl",null,{"breadcrumb":2038},[2039,2042,2045],{"name":2040,"item":2041},"Home","\u002F",{"name":2043,"item":2044},"Getting Started with Python Excel Automation","\u002Fgetting-started-with-python-excel-automation\u002F",{"name":22,"item":21},"\u002Fgetting-started-with-python-excel-automation\u002Fusing-openpyxl-for-excel-file-manipulation\u002Fread-excel-rows-into-dictionaries-with-openpyxl",{"title":2048,"description":2049},"Read Excel Rows as Dictionaries in Python with openpyxl","A DictReader for Excel: zip the header row with each row from iter_rows(values_only=True), stream large sheets in read-only mode, handle blank rows, None and duplicate headers, build ID lookups and typed dataclass records.","read-excel-rows-into-dictionaries-with-openpyxl","getting-started-with-python-excel-automation\u002Fusing-openpyxl-for-excel-file-manipulation\u002Fread-excel-rows-into-dictionaries-with-openpyxl\u002Findex",[1707,2053],"reading-excel","how-to","xxDF6wbHobF7Xh0x3YcV_kd-Lnc6JtLFSBPvvXDg0ao",[2057,2061],{"title":2058,"path":2059,"stem":2060,"children":-1},"Read a Cell Value From Excel With openpyxl","\u002Fgetting-started-with-python-excel-automation\u002Fusing-openpyxl-for-excel-file-manipulation\u002Fread-cell-value-from-excel-with-openpyxl","getting-started-with-python-excel-automation\u002Fusing-openpyxl-for-excel-file-manipulation\u002Fread-cell-value-from-excel-with-openpyxl\u002Findex",{"title":2062,"path":2063,"stem":2064,"children":-1},"Set the Active Sheet, Selected Cell and Tab Colours with openpyxl","\u002Fgetting-started-with-python-excel-automation\u002Fusing-openpyxl-for-excel-file-manipulation\u002Fset-the-active-sheet-and-tab-colour-with-openpyxl","getting-started-with-python-excel-automation\u002Fusing-openpyxl-for-excel-file-manipulation\u002Fset-the-active-sheet-and-tab-colour-with-openpyxl\u002Findex",[2066,2068],{"path":2067,"title":2043},"\u002Fgetting-started-with-python-excel-automation",{"path":2069,"title":22},"\u002Fgetting-started-with-python-excel-automation\u002Fusing-openpyxl-for-excel-file-manipulation",[],[2072,2076,2079,2082,2085,2088],{"path":2073,"title":2074,"kind":2075},"\u002Fadvanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl","Applying Conditional Formatting With openpyxl","guide",{"path":2077,"title":2078,"kind":2054},"\u002Fadvanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002Fadd-data-bars-and-colour-scales-with-openpyxl","Add Data Bars and Colour Scales with openpyxl",{"path":2080,"title":2081,"kind":2054},"\u002Fadvanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002Fadd-icon-sets-with-openpyxl","Add Icon Sets to Excel Cells with openpyxl",{"path":2083,"title":2084,"kind":2054},"\u002Fadvanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002Fapply-a-colour-scale-per-row-or-per-column-with-openpyxl","Apply a Colour Scale per Row or per Column with openpyxl",{"path":2086,"title":2087,"kind":2054},"\u002Fadvanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002Fcontrol-conditional-formatting-rule-order-and-stop-if-true","Control Conditional Formatting Rule Order and Stop If True",{"path":2089,"title":2090,"kind":2054},"\u002Fadvanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002Fhighlight-an-entire-row-based-on-a-cell-value-with-openpyxl","Highlight an Entire Row Based on a Cell Value with openpyxl",1790972070056]