[{"data":1,"prerenderedAt":1994},["ShallowReactive",2],{"doc:\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-large-excel-files-in-python\u002Freduce-the-file-size-of-an-excel-workbook-with-python":3,"surround:\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-large-excel-files-in-python\u002Freduce-the-file-size-of-an-excel-workbook-with-python":1960,"ancestors:\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-large-excel-files-in-python\u002Freduce-the-file-size-of-an-excel-workbook-with-python":1969,"children:\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-large-excel-files-in-python\u002Freduce-the-file-size-of-an-excel-workbook-with-python":1974,"related:\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-large-excel-files-in-python\u002Freduce-the-file-size-of-an-excel-workbook-with-python":1975},{"id":4,"title":5,"body":6,"dateModified":1928,"datePublished":1928,"description":1929,"extension":1930,"faq":1931,"keyword":1940,"kind":1941,"meta":1942,"navigation":101,"path":1950,"seo":1951,"slug":1953,"stem":1954,"topics":1955,"type":1958,"__hash__":1959},"docs\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-large-excel-files-in-python\u002Freduce-the-file-size-of-an-excel-workbook-with-python\u002Findex.md","Reduce the File Size of an Excel Workbook with Python",{"type":7,"value":8,"toc":1915},"minimark",[9,24,29,61,67,71,325,340,446,450,457,714,730,734,818,826,830,833,1089,1104,1222,1226,1229,1403,1417,1536,1543,1547,1550,1801,1804,1808,1843,1847,1850,1854,1860,1866,1872,1878,1882,1911],[10,11,12,13,17,18,23],"p",{},"A 6 MB monthly report is a nuisance: mail systems reject it, shared drives sync it slowly, and people open it with a sigh. Most advice about shrinking Excel files is folklore — \"remove formatting\", \"save as binary\" — and most of it guesses at the cause. A workbook is a ZIP of parts, so the cause can be ",[14,15,16],"em",{},"measured",": list the parts, see which ones are large after compression, and fix those. In practice, one or two culprits account for nearly all the size. This page measures a deliberately bloated workbook, removes the bloat step by step with the size after each step, and ends with the habits that keep generated reports small in the first place. It belongs to ",[19,20,22],"a",{"href":21},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-large-excel-files-in-python\u002F","Working with Large Excel Files in Python",".",[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 pillow pandas\n","bash","",[37,38,39],"code",{"__ignoreMap":35},[40,41,44,48,52,55,58],"span",{"class":42,"line":43},"line",1,[40,45,47],{"class":46},"sMTad","pip",[40,49,51],{"class":50},"srMev"," install",[40,53,54],{"class":50}," openpyxl",[40,56,57],{"class":50}," pillow",[40,59,60],{"class":50}," pandas\n",[10,62,63,66],{},[37,64,65],{},"monthly_report.xlsx"," contains the usual suspects: 5,000 rows of real data; a fill colour applied to 55,000 empty rows below it (someone formatted \"the whole column\"); 3,000 cells with individual font colours; a 1600 × 1600 photo inserted at full resolution; and a hidden sheet with 20,000 rows of last year's data.",[25,68,70],{"id":69},"step-1-measure-the-parts","Step 1 — Measure the parts",[30,72,76],{"className":73,"code":74,"language":75,"meta":35,"style":35},"language-python shiki shiki-themes github-light github-dark-high-contrast","import os\nimport zipfile\n\ndef part_sizes(path, top=6):\n    z = zipfile.ZipFile(path)\n    rows = sorted(((i.filename, i.file_size, i.compress_size) for i in z.infolist()), key=lambda t: -t[2])\n    print(f\"{path}: {os.path.getsize(path) \u002F 1e6:.2f} MB\")\n    for name, raw, packed in rows[:top]:\n        print(f\"  {name:28} {raw \u002F 1e3:9,.0f} kB raw  {packed \u002F 1e3:9,.0f} kB in the file\")\n\npart_sizes(\"monthly_report.xlsx\")\n","python",[37,77,78,88,96,103,126,137,185,235,249,309,314],{"__ignoreMap":35},[40,79,80,84],{"class":42,"line":43},[40,81,83],{"class":82},"s-kum","import",[40,85,87],{"class":86},"skGVy"," os\n",[40,89,91,93],{"class":42,"line":90},2,[40,92,83],{"class":82},[40,94,95],{"class":86}," zipfile\n",[40,97,99],{"class":42,"line":98},3,[40,100,102],{"emptyLinePlaceholder":101},true,"\n",[40,104,106,109,113,116,119,123],{"class":42,"line":105},4,[40,107,108],{"class":82},"def",[40,110,112],{"class":111},"s_Opv"," part_sizes",[40,114,115],{"class":86},"(path, top",[40,117,118],{"class":82},"=",[40,120,122],{"class":121},"sP0c6","6",[40,124,125],{"class":86},"):\n",[40,127,129,132,134],{"class":42,"line":128},5,[40,130,131],{"class":86},"    z ",[40,133,118],{"class":82},[40,135,136],{"class":86}," zipfile.ZipFile(path)\n",[40,138,140,143,145,148,151,154,157,160,163,167,170,173,176,179,182],{"class":42,"line":139},6,[40,141,142],{"class":86},"    rows ",[40,144,118],{"class":82},[40,146,147],{"class":121}," sorted",[40,149,150],{"class":86},"(((i.filename, i.file_size, i.compress_size) ",[40,152,153],{"class":82},"for",[40,155,156],{"class":86}," i ",[40,158,159],{"class":82},"in",[40,161,162],{"class":86}," z.infolist()), ",[40,164,166],{"class":165},"sa561","key",[40,168,169],{"class":82},"=lambda",[40,171,172],{"class":86}," t: ",[40,174,175],{"class":82},"-",[40,177,178],{"class":86},"t[",[40,180,181],{"class":121},"2",[40,183,184],{"class":86},"])\n",[40,186,188,191,194,197,200,204,207,210,213,215,218,221,224,227,229,232],{"class":42,"line":187},7,[40,189,190],{"class":121},"    print",[40,192,193],{"class":86},"(",[40,195,196],{"class":82},"f",[40,198,199],{"class":50},"\"",[40,201,203],{"class":202},"sSjpA","{",[40,205,206],{"class":86},"path",[40,208,209],{"class":202},"}",[40,211,212],{"class":50},": ",[40,214,203],{"class":202},[40,216,217],{"class":86},"os.path.getsize(path) ",[40,219,220],{"class":82},"\u002F",[40,222,223],{"class":121}," 1e6",[40,225,226],{"class":82},":.2f",[40,228,209],{"class":202},[40,230,231],{"class":50}," MB\"",[40,233,234],{"class":86},")\n",[40,236,238,241,244,246],{"class":42,"line":237},8,[40,239,240],{"class":82},"    for",[40,242,243],{"class":86}," name, raw, packed ",[40,245,159],{"class":82},[40,247,248],{"class":86}," rows[:top]:\n",[40,250,252,255,257,259,262,264,267,270,272,275,278,280,283,286,288,291,293,296,298,300,302,304,307],{"class":42,"line":251},9,[40,253,254],{"class":121},"        print",[40,256,193],{"class":86},[40,258,196],{"class":82},[40,260,261],{"class":50},"\"  ",[40,263,203],{"class":202},[40,265,266],{"class":86},"name",[40,268,269],{"class":82},":28",[40,271,209],{"class":202},[40,273,274],{"class":202}," {",[40,276,277],{"class":86},"raw ",[40,279,220],{"class":82},[40,281,282],{"class":121}," 1e3",[40,284,285],{"class":82},":9,.0f",[40,287,209],{"class":202},[40,289,290],{"class":50}," kB raw  ",[40,292,203],{"class":202},[40,294,295],{"class":86},"packed ",[40,297,220],{"class":82},[40,299,282],{"class":121},[40,301,285],{"class":82},[40,303,209],{"class":202},[40,305,306],{"class":50}," kB in the file\"",[40,308,234],{"class":86},[40,310,312],{"class":42,"line":311},10,[40,313,102],{"emptyLinePlaceholder":101},[40,315,317,320,323],{"class":42,"line":316},11,[40,318,319],{"class":86},"part_sizes(",[40,321,322],{"class":50},"\"monthly_report.xlsx\"",[40,324,234],{"class":86},[10,326,327,328,331,332,335,336,23],{},"The compressed size of each part is what you pay for. Here the photo — ",[37,329,330],{},"xl\u002Fmedia\u002Fimage1.png"," — is 5.8 MB of a 6.6 MB file, because PNG data is already compressed and ZIP cannot shrink it further. The two sheets are 3.9 MB and 2.9 MB of XML but only about 0.4 and 0.3 MB once compressed; ",[37,333,334],{},"styles.xml"," is 400 kB raw and 18 kB compressed. Without measuring, most people would have started on the formatting. Inspecting packages is covered in ",[19,337,339],{"href":338},"\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Finspect-the-xml-inside-an-xlsx-file-with-python\u002F","Inspect the XML Inside an .xlsx File with Python",[341,342,350,351,350,355,350,359,350,367,350,377,350,383,350,390,350,395,350,398,350,402,350,407,350,412,350,415,350,419,350,422,350,426,350,429,350,432,350,435,350,440],"svg",{"viewBox":343,"role":344,"ariaLabelledBy":345,"xmlns":348,"style":349},"4 0 744 206","img",[346,347],"fs-parts-t","fs-parts-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:744px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[352,353,354],"title",{"id":346},"Where the 6.6 MB actually is",[356,357,358],"desc",{"id":347},"Compressed size of each part in the bloated workbook. The embedded photo takes 5.8 MB. The data sheet takes about 0.41 MB, the hidden archive sheet 0.33 MB and the styles part 0.02 MB.",[360,361],"rect",{"x":362,"y":363,"width":364,"height":365,"fill":366},"4","0","744","206","#ffffff",[360,368],{"x":369,"y":370,"width":371,"height":372,"rx":373,"fill":374,"stroke":375,"style":376},"20.0","16.0","246.7","28.0","7","#e7ebef","var(--line,#cdd5e6)","stroke-width:1px",[378,379,330],"text",{"x":380,"y":381,"style":382},"143.4","34.5","font-size:12.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle",[360,384],{"x":385,"y":370,"width":386,"height":372,"rx":373,"fill":387,"stroke":388,"style":389},"276.7","395.1","#fce9e9","var(--danger,#dc2626)","stroke-width:1.5px",[378,391,394],{"x":392,"y":381,"style":393},"679.8","font-size:12px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:start","5.84 MB",[360,396],{"x":369,"y":397,"width":371,"height":372,"rx":373,"fill":374,"stroke":375,"style":376},"56.0",[378,399,401],{"x":380,"y":400,"style":382},"74.5","worksheets\u002Fsheet1.xml (Data)",[360,403],{"x":385,"y":397,"width":404,"height":372,"rx":373,"fill":405,"stroke":406,"style":389},"27.8","#fdefd8","var(--gold,#b4740a)",[378,408,411],{"x":409,"y":400,"style":410},"312.5","font-size:12px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:start","0.41 MB",[360,413],{"x":369,"y":414,"width":371,"height":372,"rx":373,"fill":374,"stroke":375,"style":376},"96.0",[378,416,418],{"x":380,"y":417,"style":382},"114.5","worksheets\u002Fsheet2.xml (Old data)",[360,420],{"x":385,"y":414,"width":421,"height":372,"rx":373,"fill":405,"stroke":406,"style":389},"22.4",[378,423,425],{"x":424,"y":417,"style":410},"307.2","0.33 MB",[360,427],{"x":369,"y":428,"width":371,"height":372,"rx":373,"fill":374,"stroke":375,"style":376},"136.0",[378,430,334],{"x":380,"y":431,"style":382},"154.5",[360,433],{"x":385,"y":428,"width":434,"height":372,"rx":373,"fill":374,"stroke":375,"style":389},"6.0",[378,436,439],{"x":437,"y":431,"style":438},"290.7","font-size:12px;font-weight:700;fill:var(--text,#172033);text-anchor:start","0.02 MB",[378,441,445],{"x":442,"y":443,"style":444},"380.0","186.0","font-size:12.5px;font-weight:400;fill:var(--muted,#5b6780);text-anchor:middle","Compressed sizes — what each part adds to the file on disk.",[25,447,449],{"id":448},"step-2-delete-the-formatted-empty-rows","Step 2 — Delete the formatted empty rows",[10,451,452,453,456],{},"Cells that only carry formatting still exist in the sheet XML, and they push Excel's used range — and ",[37,454,455],{},"max_row"," — far below the data:",[30,458,460],{"className":73,"code":459,"language":75,"meta":35,"style":35},"from openpyxl import load_workbook\n\ndef last_data_row(ws):\n    last = 0\n    for i, row in enumerate(ws.iter_rows(values_only=True), start=1):\n        if any(v is not None for v in row):\n            last = i\n    return last\n\nwb = load_workbook(\"monthly_report.xlsx\")\nws = wb[\"Data\"]\ndata_end = last_data_row(ws)\nprint(\"max_row:\", ws.max_row, \"| last row with data:\", data_end)\nws.delete_rows(data_end + 1, ws.max_row - data_end)\nwb.save(\"step1_rows.xlsx\")\nprint(f\"{os.path.getsize('step1_rows.xlsx') \u002F 1e6:.2f} MB\")\n",[37,461,462,475,479,489,499,535,566,576,584,588,602,618,629,649,669,680],{"__ignoreMap":35},[40,463,464,467,470,472],{"class":42,"line":43},[40,465,466],{"class":82},"from",[40,468,469],{"class":86}," openpyxl ",[40,471,83],{"class":82},[40,473,474],{"class":86}," load_workbook\n",[40,476,477],{"class":42,"line":90},[40,478,102],{"emptyLinePlaceholder":101},[40,480,481,483,486],{"class":42,"line":98},[40,482,108],{"class":82},[40,484,485],{"class":111}," last_data_row",[40,487,488],{"class":86},"(ws):\n",[40,490,491,494,496],{"class":42,"line":105},[40,492,493],{"class":86},"    last ",[40,495,118],{"class":82},[40,497,498],{"class":121}," 0\n",[40,500,501,503,506,508,511,514,517,519,522,525,528,530,533],{"class":42,"line":128},[40,502,240],{"class":82},[40,504,505],{"class":86}," i, row ",[40,507,159],{"class":82},[40,509,510],{"class":121}," enumerate",[40,512,513],{"class":86},"(ws.iter_rows(",[40,515,516],{"class":165},"values_only",[40,518,118],{"class":82},[40,520,521],{"class":121},"True",[40,523,524],{"class":86},"), ",[40,526,527],{"class":165},"start",[40,529,118],{"class":82},[40,531,532],{"class":121},"1",[40,534,125],{"class":86},[40,536,537,540,543,546,549,552,555,558,561,563],{"class":42,"line":139},[40,538,539],{"class":82},"        if",[40,541,542],{"class":121}," any",[40,544,545],{"class":86},"(v ",[40,547,548],{"class":82},"is",[40,550,551],{"class":82}," not",[40,553,554],{"class":121}," None",[40,556,557],{"class":82}," for",[40,559,560],{"class":86}," v ",[40,562,159],{"class":82},[40,564,565],{"class":86}," row):\n",[40,567,568,571,573],{"class":42,"line":187},[40,569,570],{"class":86},"            last ",[40,572,118],{"class":82},[40,574,575],{"class":86}," i\n",[40,577,578,581],{"class":42,"line":237},[40,579,580],{"class":82},"    return",[40,582,583],{"class":86}," last\n",[40,585,586],{"class":42,"line":251},[40,587,102],{"emptyLinePlaceholder":101},[40,589,590,593,595,598,600],{"class":42,"line":311},[40,591,592],{"class":86},"wb ",[40,594,118],{"class":82},[40,596,597],{"class":86}," load_workbook(",[40,599,322],{"class":50},[40,601,234],{"class":86},[40,603,604,607,609,612,615],{"class":42,"line":316},[40,605,606],{"class":86},"ws ",[40,608,118],{"class":82},[40,610,611],{"class":86}," wb[",[40,613,614],{"class":50},"\"Data\"",[40,616,617],{"class":86},"]\n",[40,619,621,624,626],{"class":42,"line":620},12,[40,622,623],{"class":86},"data_end ",[40,625,118],{"class":82},[40,627,628],{"class":86}," last_data_row(ws)\n",[40,630,632,635,637,640,643,646],{"class":42,"line":631},13,[40,633,634],{"class":121},"print",[40,636,193],{"class":86},[40,638,639],{"class":50},"\"max_row:\"",[40,641,642],{"class":86},", ws.max_row, ",[40,644,645],{"class":50},"\"| last row with data:\"",[40,647,648],{"class":86},", data_end)\n",[40,650,652,655,658,661,664,666],{"class":42,"line":651},14,[40,653,654],{"class":86},"ws.delete_rows(data_end ",[40,656,657],{"class":82},"+",[40,659,660],{"class":121}," 1",[40,662,663],{"class":86},", ws.max_row ",[40,665,175],{"class":82},[40,667,668],{"class":86}," data_end)\n",[40,670,672,675,678],{"class":42,"line":671},15,[40,673,674],{"class":86},"wb.save(",[40,676,677],{"class":50},"\"step1_rows.xlsx\"",[40,679,234],{"class":86},[40,681,683,685,687,689,691,693,696,699,702,704,706,708,710,712],{"class":42,"line":682},16,[40,684,634],{"class":121},[40,686,193],{"class":86},[40,688,196],{"class":82},[40,690,199],{"class":50},[40,692,203],{"class":202},[40,694,695],{"class":86},"os.path.getsize(",[40,697,698],{"class":50},"'step1_rows.xlsx'",[40,700,701],{"class":86},") ",[40,703,220],{"class":82},[40,705,223],{"class":121},[40,707,226],{"class":82},[40,709,209],{"class":202},[40,711,231],{"class":50},[40,713,234],{"class":86},[10,715,716,718,719,722,723,725,726,23],{},[37,717,455],{}," was 60,000 while the data ends at row 5,000. Deleting the tail in one ",[37,720,721],{},"delete_rows"," call removed 55,000 formatted cells and about 0.28 MB. The bigger benefit is often not the size but the behaviour: Excel scrolls to row 60,000, prints blank pages and makes Ctrl+End useless until the tail is gone. Why ",[37,724,455],{}," overstates the data is explained in ",[19,727,729],{"href":728},"\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-openpyxl-max-row-counting-empty-rows\u002F","Fix openpyxl max_row Counting Empty Rows",[25,731,733],{"id":732},"step-3-remove-or-move-archive-sheets","Step 3 — Remove or move archive sheets",[30,735,737],{"className":73,"code":736,"language":75,"meta":35,"style":35},"wb = load_workbook(\"step1_rows.xlsx\")\nprint([(s.title, s.sheet_state, s.max_row) for s in wb.worksheets])\nwb.remove(wb[\"Old data\"])\nwb.save(\"step2_sheet.xlsx\")\nprint(f\"{os.path.getsize('step2_sheet.xlsx') \u002F 1e6:.2f} MB\")\n",[37,738,739,751,768,778,787],{"__ignoreMap":35},[40,740,741,743,745,747,749],{"class":42,"line":43},[40,742,592],{"class":86},[40,744,118],{"class":82},[40,746,597],{"class":86},[40,748,677],{"class":50},[40,750,234],{"class":86},[40,752,753,755,758,760,763,765],{"class":42,"line":90},[40,754,634],{"class":121},[40,756,757],{"class":86},"([(s.title, s.sheet_state, s.max_row) ",[40,759,153],{"class":82},[40,761,762],{"class":86}," s ",[40,764,159],{"class":82},[40,766,767],{"class":86}," wb.worksheets])\n",[40,769,770,773,776],{"class":42,"line":98},[40,771,772],{"class":86},"wb.remove(wb[",[40,774,775],{"class":50},"\"Old data\"",[40,777,184],{"class":86},[40,779,780,782,785],{"class":42,"line":105},[40,781,674],{"class":86},[40,783,784],{"class":50},"\"step2_sheet.xlsx\"",[40,786,234],{"class":86},[40,788,789,791,793,795,797,799,801,804,806,808,810,812,814,816],{"class":42,"line":128},[40,790,634],{"class":121},[40,792,193],{"class":86},[40,794,196],{"class":82},[40,796,199],{"class":50},[40,798,203],{"class":202},[40,800,695],{"class":86},[40,802,803],{"class":50},"'step2_sheet.xlsx'",[40,805,701],{"class":86},[40,807,220],{"class":82},[40,809,223],{"class":121},[40,811,226],{"class":82},[40,813,209],{"class":202},[40,815,231],{"class":50},[40,817,234],{"class":86},[10,819,820,821,825],{},"Hidden sheets are invisible to readers and fully paid for in the file. Last year's 20,000 rows cost about a third of a megabyte; in real workbooks, hidden \"data\" and \"calc\" sheets are often the largest parts after images. If the data is still needed, keep it in a separate file — or better, Parquet, as in ",[19,822,824],{"href":823},"\u002Fadvanced-data-transformation-and-cleaning\u002Freading-excel-with-polars-and-arrow\u002Fconvert-excel-files-to-parquet-with-python\u002F","Convert Excel Files to Parquet with Python"," — and link to it from the report.",[25,827,829],{"id":828},"step-4-shrink-the-image","Step 4 — Shrink the image",[10,831,832],{},"The photo is 1600 pixels square and displayed as a thumbnail. Resize it to the size it is shown at. openpyxl keeps images on load, but re-encoding them is simplest by rewriting the image part inside the package:",[30,834,836],{"className":73,"code":835,"language":75,"meta":35,"style":35},"import io\nfrom PIL import Image\n\nsrc = zipfile.ZipFile(\"step2_sheet.xlsx\")\nphoto = Image.open(io.BytesIO(src.read(\"xl\u002Fmedia\u002Fimage1.png\")))\nresized = io.BytesIO()\nphoto.resize((600, 600), Image.LANCZOS).save(resized, \"PNG\", optimize=True)\nwith zipfile.ZipFile(\"step3_image.xlsx\", \"w\", zipfile.ZIP_DEFLATED) as out:\n    for info in src.infolist():\n        data = resized.getvalue() if info.filename == \"xl\u002Fmedia\u002Fimage1.png\" else src.read(info.filename)\n        out.writestr(info.filename, data)\nprint(f\"{os.path.getsize('step3_image.xlsx') \u002F 1e6:.2f} MB\")\ncheck = load_workbook(\"step3_image.xlsx\")\nprint(\"images still present:\", len(check[\"Data\"]._images), \"| rows:\", check[\"Data\"].max_row)\n",[37,837,838,845,858,862,876,892,902,938,967,979,1007,1012,1043,1056],{"__ignoreMap":35},[40,839,840,842],{"class":42,"line":43},[40,841,83],{"class":82},[40,843,844],{"class":86}," io\n",[40,846,847,849,852,855],{"class":42,"line":90},[40,848,466],{"class":82},[40,850,851],{"class":121}," PIL",[40,853,854],{"class":82}," import",[40,856,857],{"class":86}," Image\n",[40,859,860],{"class":42,"line":98},[40,861,102],{"emptyLinePlaceholder":101},[40,863,864,867,869,872,874],{"class":42,"line":105},[40,865,866],{"class":86},"src ",[40,868,118],{"class":82},[40,870,871],{"class":86}," zipfile.ZipFile(",[40,873,784],{"class":50},[40,875,234],{"class":86},[40,877,878,881,883,886,889],{"class":42,"line":128},[40,879,880],{"class":86},"photo ",[40,882,118],{"class":82},[40,884,885],{"class":86}," Image.open(io.BytesIO(src.read(",[40,887,888],{"class":50},"\"xl\u002Fmedia\u002Fimage1.png\"",[40,890,891],{"class":86},")))\n",[40,893,894,897,899],{"class":42,"line":139},[40,895,896],{"class":86},"resized ",[40,898,118],{"class":82},[40,900,901],{"class":86}," io.BytesIO()\n",[40,903,904,907,910,913,915,918,921,924,927,929,932,934,936],{"class":42,"line":187},[40,905,906],{"class":86},"photo.resize((",[40,908,909],{"class":121},"600",[40,911,912],{"class":86},", ",[40,914,909],{"class":121},[40,916,917],{"class":86},"), Image.",[40,919,920],{"class":121},"LANCZOS",[40,922,923],{"class":86},").save(resized, ",[40,925,926],{"class":50},"\"PNG\"",[40,928,912],{"class":86},[40,930,931],{"class":165},"optimize",[40,933,118],{"class":82},[40,935,521],{"class":121},[40,937,234],{"class":86},[40,939,940,943,945,948,950,953,956,959,961,964],{"class":42,"line":237},[40,941,942],{"class":82},"with",[40,944,871],{"class":86},[40,946,947],{"class":50},"\"step3_image.xlsx\"",[40,949,912],{"class":86},[40,951,952],{"class":50},"\"w\"",[40,954,955],{"class":86},", zipfile.",[40,957,958],{"class":121},"ZIP_DEFLATED",[40,960,701],{"class":86},[40,962,963],{"class":82},"as",[40,965,966],{"class":86}," out:\n",[40,968,969,971,974,976],{"class":42,"line":251},[40,970,240],{"class":82},[40,972,973],{"class":86}," info ",[40,975,159],{"class":82},[40,977,978],{"class":86}," src.infolist():\n",[40,980,981,984,986,989,992,995,998,1001,1004],{"class":42,"line":311},[40,982,983],{"class":86},"        data ",[40,985,118],{"class":82},[40,987,988],{"class":86}," resized.getvalue() ",[40,990,991],{"class":82},"if",[40,993,994],{"class":86}," info.filename ",[40,996,997],{"class":82},"==",[40,999,1000],{"class":50}," \"xl\u002Fmedia\u002Fimage1.png\"",[40,1002,1003],{"class":82}," else",[40,1005,1006],{"class":86}," src.read(info.filename)\n",[40,1008,1009],{"class":42,"line":316},[40,1010,1011],{"class":86},"        out.writestr(info.filename, data)\n",[40,1013,1014,1016,1018,1020,1022,1024,1026,1029,1031,1033,1035,1037,1039,1041],{"class":42,"line":620},[40,1015,634],{"class":121},[40,1017,193],{"class":86},[40,1019,196],{"class":82},[40,1021,199],{"class":50},[40,1023,203],{"class":202},[40,1025,695],{"class":86},[40,1027,1028],{"class":50},"'step3_image.xlsx'",[40,1030,701],{"class":86},[40,1032,220],{"class":82},[40,1034,223],{"class":121},[40,1036,226],{"class":82},[40,1038,209],{"class":202},[40,1040,231],{"class":50},[40,1042,234],{"class":86},[40,1044,1045,1048,1050,1052,1054],{"class":42,"line":631},[40,1046,1047],{"class":86},"check ",[40,1049,118],{"class":82},[40,1051,597],{"class":86},[40,1053,947],{"class":50},[40,1055,234],{"class":86},[40,1057,1058,1060,1062,1065,1067,1070,1073,1075,1078,1081,1084,1086],{"class":42,"line":651},[40,1059,634],{"class":121},[40,1061,193],{"class":86},[40,1063,1064],{"class":50},"\"images still present:\"",[40,1066,912],{"class":86},[40,1068,1069],{"class":121},"len",[40,1071,1072],{"class":86},"(check[",[40,1074,614],{"class":50},[40,1076,1077],{"class":86},"]._images), ",[40,1079,1080],{"class":50},"\"| rows:\"",[40,1082,1083],{"class":86},", check[",[40,1085,614],{"class":50},[40,1087,1088],{"class":86},"].max_row)\n",[10,1090,1091,1092,1095,1096,1099,1100,23],{},"The file drops from 5.99 MB to 0.73 MB. The image keeps its name and format (PNG), so no other part of the package needs to change, and openpyxl loads the result without complaint. For photographs, JPEG is far smaller still — the same 600-pixel image is 36 kB as JPEG against 552 kB as PNG — but changing the format means changing the part's name and content type, which is easiest done when the report is ",[14,1093,1094],{},"generated",": resize and convert before calling ",[37,1097,1098],{},"add_image",", as described in ",[19,1101,1103],{"href":1102},"\u002Fformatting-and-charting-excel-reports-with-python\u002Finserting-images-and-logos-into-excel\u002Fresize-and-position-images-in-excel-with-openpyxl\u002F","Resize and Position Images in Excel with openpyxl",[341,1105,350,1111,350,1114,350,1117,350,1121,350,1126,350,1132,350,1136,350,1140,350,1143,350,1147,350,1150,350,1154,350,1157,350,1160,350,1163,350,1169,350,1173,350,1177,350,1181,350,1190,350,1195,350,1200,350,1204,350,1209,350,1213,350,1218],{"viewBox":1106,"role":344,"ariaLabelledBy":1107,"xmlns":348,"style":1110},"4 0 748 252",[1108,1109],"fs-steps-t","fs-steps-d","width:100%;max-width:748px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[352,1112,1113],{"id":1108},"File size after each step",[356,1115,1116],{"id":1109},"The workbook shrinks from 6.61 MB to 6.33 MB after deleting the formatted empty rows, to 5.99 MB after removing the archive sheet, and to 0.73 MB after resizing the photo.",[360,1118],{"x":362,"y":363,"width":1119,"height":1120,"fill":366},"748","252",[42,1122],{"x1":1123,"y1":1124,"x2":1125,"y2":1124,"stroke":375,"style":376},"60.7","214.0","736.0",[378,1127,1131],{"x":1128,"y":1129,"style":1130},"34.3","218.0","font-size:11px;font-weight:400;fill:var(--muted,#5b6780);text-anchor:middle","0 MB",[42,1133],{"x1":1123,"y1":1134,"x2":1125,"y2":1134,"stroke":375,"style":1135},"166.5","stroke-width:1px;stroke-dasharray:5 4",[378,1137,1139],{"x":1128,"y":1138,"style":1130},"170.5","2 MB",[42,1141],{"x1":1123,"y1":1142,"x2":1125,"y2":1142,"stroke":375,"style":1135},"119.0",[378,1144,1146],{"x":1128,"y":1145,"style":1130},"123.0","4 MB",[42,1148],{"x1":1123,"y1":1149,"x2":1125,"y2":1149,"stroke":375,"style":1135},"71.5",[378,1151,1153],{"x":1128,"y":1152,"style":1130},"75.5","6 MB",[42,1155],{"x1":1123,"y1":1156,"x2":1125,"y2":1156,"stroke":375,"style":1135},"24.0",[378,1158,1159],{"x":1128,"y":372,"style":1130},"8 MB",[42,1161],{"x1":1123,"y1":1124,"x2":1125,"y2":1124,"stroke":1162,"style":389},"var(--muted,#5b6780)",[378,1164,1168],{"x":1165,"y":1166,"style":1167},"145.1","232.0","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","original",[378,1170,1172],{"x":1171,"y":1166,"style":1167},"313.9","empty rows",[378,1174,1176],{"x":1175,"y":1166,"style":1167},"482.8","archive sheet",[378,1178,1180],{"x":1179,"y":1166,"style":1167},"651.6","photo resized",[360,1182],{"x":1183,"y":1184,"width":1185,"height":1186,"rx":1187,"fill":1188,"stroke":1189,"style":389},"127.1","57.0","36.0","157.0","3","#f0f4ff","var(--brand,#5b5cf0)",[378,1191,1194],{"x":1165,"y":1192,"style":1193},"51.0","font-size:10.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","6.61",[360,1196],{"x":1197,"y":1198,"width":1185,"height":1199,"rx":1187,"fill":1188,"stroke":1189,"style":389},"295.9","63.7","150.3",[378,1201,1203],{"x":1171,"y":1202,"style":1193},"57.7","6.33",[360,1205],{"x":1206,"y":1207,"width":1185,"height":1208,"rx":1187,"fill":1188,"stroke":1189,"style":389},"464.8","71.7","142.3",[378,1210,1212],{"x":1175,"y":1211,"style":1193},"65.7","5.99",[360,1214],{"x":1215,"y":1216,"width":1185,"height":1217,"rx":1187,"fill":1188,"stroke":1189,"style":389},"633.6","196.7","17.3",[378,1219,1221],{"x":1179,"y":1220,"style":1193},"190.7","0.73",[25,1223,1225],{"id":1224},"step-5-fixes-that-barely-help","Step 5 — Fixes that barely help",[10,1227,1228],{},"Two popular remedies did little here, and it is worth knowing why:",[30,1230,1232],{"className":73,"code":1231,"language":75,"meta":35,"style":35},"from openpyxl.styles import Font\n\nwb = load_workbook(\"step3_image.xlsx\")\nws = wb[\"Data\"]\nfor r in range(2, 3001):\n    ws.cell(r, 3).font = Font(color=\"000000\")\nwb.save(\"step4_styles.xlsx\")\nprint(\"styles.xml raw:\", zipfile.ZipFile(\"step4_styles.xlsx\").getinfo(\"xl\u002Fstyles.xml\").file_size, \"bytes\")\ntry:\n    wb.save(\"step4_again.xlsx\")\nexcept ValueError as exc:\n    print(\"second save:\", exc)\n",[37,1233,1234,1246,1250,1262,1274,1297,1322,1331,1359,1367,1377,1391],{"__ignoreMap":35},[40,1235,1236,1238,1241,1243],{"class":42,"line":43},[40,1237,466],{"class":82},[40,1239,1240],{"class":86}," openpyxl.styles ",[40,1242,83],{"class":82},[40,1244,1245],{"class":86}," Font\n",[40,1247,1248],{"class":42,"line":90},[40,1249,102],{"emptyLinePlaceholder":101},[40,1251,1252,1254,1256,1258,1260],{"class":42,"line":98},[40,1253,592],{"class":86},[40,1255,118],{"class":82},[40,1257,597],{"class":86},[40,1259,947],{"class":50},[40,1261,234],{"class":86},[40,1263,1264,1266,1268,1270,1272],{"class":42,"line":105},[40,1265,606],{"class":86},[40,1267,118],{"class":82},[40,1269,611],{"class":86},[40,1271,614],{"class":50},[40,1273,617],{"class":86},[40,1275,1276,1278,1281,1283,1286,1288,1290,1292,1295],{"class":42,"line":128},[40,1277,153],{"class":82},[40,1279,1280],{"class":86}," r ",[40,1282,159],{"class":82},[40,1284,1285],{"class":121}," range",[40,1287,193],{"class":86},[40,1289,181],{"class":121},[40,1291,912],{"class":86},[40,1293,1294],{"class":121},"3001",[40,1296,125],{"class":86},[40,1298,1299,1302,1304,1307,1309,1312,1315,1317,1320],{"class":42,"line":139},[40,1300,1301],{"class":86},"    ws.cell(r, ",[40,1303,1187],{"class":121},[40,1305,1306],{"class":86},").font ",[40,1308,118],{"class":82},[40,1310,1311],{"class":86}," Font(",[40,1313,1314],{"class":165},"color",[40,1316,118],{"class":82},[40,1318,1319],{"class":50},"\"000000\"",[40,1321,234],{"class":86},[40,1323,1324,1326,1329],{"class":42,"line":187},[40,1325,674],{"class":86},[40,1327,1328],{"class":50},"\"step4_styles.xlsx\"",[40,1330,234],{"class":86},[40,1332,1333,1335,1337,1340,1343,1345,1348,1351,1354,1357],{"class":42,"line":237},[40,1334,634],{"class":121},[40,1336,193],{"class":86},[40,1338,1339],{"class":50},"\"styles.xml raw:\"",[40,1341,1342],{"class":86},", zipfile.ZipFile(",[40,1344,1328],{"class":50},[40,1346,1347],{"class":86},").getinfo(",[40,1349,1350],{"class":50},"\"xl\u002Fstyles.xml\"",[40,1352,1353],{"class":86},").file_size, ",[40,1355,1356],{"class":50},"\"bytes\"",[40,1358,234],{"class":86},[40,1360,1361,1364],{"class":42,"line":251},[40,1362,1363],{"class":82},"try",[40,1365,1366],{"class":86},":\n",[40,1368,1369,1372,1375],{"class":42,"line":311},[40,1370,1371],{"class":86},"    wb.save(",[40,1373,1374],{"class":50},"\"step4_again.xlsx\"",[40,1376,234],{"class":86},[40,1378,1379,1382,1385,1388],{"class":42,"line":316},[40,1380,1381],{"class":82},"except",[40,1383,1384],{"class":121}," ValueError",[40,1386,1387],{"class":82}," as",[40,1389,1390],{"class":86}," exc:\n",[40,1392,1393,1395,1397,1400],{"class":42,"line":620},[40,1394,190],{"class":121},[40,1396,193],{"class":86},[40,1398,1399],{"class":50},"\"second save:\"",[40,1401,1402],{"class":86},", exc)\n",[10,1404,1405,1406,1408,1409,1412,1413,1416],{},"Resetting the 3,000 individual font colours does not shrink ",[37,1407,334],{},": openpyxl keeps every style it has loaded, used or not, and writes them all out again — and the part was only 18 kB compressed to begin with. Unless a workbook has tens of thousands of distinct styles (the \"too many different cell formats\" error), styles are not where the size is. The second surprise is a bug worth knowing: saving the ",[14,1410,1411],{},"same"," loaded workbook twice fails with ",[37,1414,1415],{},"ValueError: I\u002FO operation on closed file",", because openpyxl reads the image data from the original file only once. Load, change and save once per pass — which is why each step above reloads from the previous file.",[341,1418,350,1424,350,1427,350,1430,350,1434,350,1442,350,1452,350,1458,350,1462,350,1468,350,1474,350,1478,350,1483,350,1487,350,1492,350,1495,350,1502,350,1507,350,1511,350,1515,350,1520,350,1523,350,1528,350,1531],{"viewBox":1419,"role":344,"ariaLabelledBy":1420,"xmlns":348,"style":1423},"4 0 752 207",[1421,1422],"fs-worth-t","fs-worth-d","width:100%;max-width:752px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[352,1425,1426],{"id":1421},"Which size fixes are worth the effort",[356,1428,1429],{"id":1422},"Resizing images and moving archive sheets out saved megabytes and hundreds of kilobytes. Deleting formatted empty rows saved a few hundred kilobytes and fixed the used range. Resetting cell styles saved nothing measurable, and saving as xlsb is not possible from Python.",[360,1431],{"x":362,"y":363,"width":1432,"height":1433,"fill":366},"752","207",[360,1435],{"x":369,"y":370,"width":1436,"height":1437,"rx":1438,"fill":1439,"stroke":1440,"style":1441},"351.0","175.0","14","#d9f4f1","var(--teal,#0f9488)","stroke-width:2px",[1443,1444,1446],"g",{"transform":1445},"translate(182.5,29.0) scale(1.083)",[206,1447],{"d":1448,"fill":1449,"stroke":1450,"style":1451},"M5 12.5l4.5 4.5L19 7","none","var(--teal-ink,#0b6157)","stroke-width:1.66px;stroke-linecap:round;stroke-linejoin:round",[378,1453,1457],{"x":1454,"y":1455,"style":1456},"195.5","78.0","font-size:14px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Worth doing",[42,1459],{"x1":1185,"y1":1460,"x2":1461,"y2":1460,"stroke":1440,"style":376},"91.0","355.0",[378,1463,1467],{"x":1464,"y":1465,"style":1466},"60.0","111.0","font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:start","resize \u002F JPEG images (−5.3 MB)",[1443,1469,1471],{"transform":1470},"translate(38.5,99.5) scale(0.625)",[206,1472],{"d":1448,"fill":1449,"stroke":1440,"style":1473},"stroke-width:2.88px;stroke-linecap:round;stroke-linejoin:round",[378,1475,1477],{"x":1464,"y":1476,"style":1466},"137.0","move archive sheets out (−0.33 MB)",[1443,1479,1481],{"transform":1480},"translate(38.5,125.5) scale(0.625)",[206,1482],{"d":1448,"fill":1449,"stroke":1440,"style":1473},[378,1484,1486],{"x":1464,"y":1485,"style":1466},"163.0","delete formatted empty rows (−0.28 MB)",[1443,1488,1490],{"transform":1489},"translate(38.5,151.5) scale(0.625)",[206,1491],{"d":1448,"fill":1449,"stroke":1440,"style":1473},[360,1493],{"x":1494,"y":370,"width":1436,"height":1437,"rx":1438,"fill":405,"stroke":406,"style":1441},"389.0",[1443,1496,1498],{"transform":1497},"translate(551.5,29.0) scale(1.083)",[206,1499],{"d":1500,"fill":1449,"stroke":1501,"style":1451},"M12 3.5l9.5 17h-19z M12 10v5 M12 17.8v.4","var(--gold-ink,#7a4e06)",[378,1503,1506],{"x":1504,"y":1455,"style":1505},"564.5","font-size:14px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Rarely worth it",[42,1508],{"x1":1509,"y1":1460,"x2":1510,"y2":1460,"stroke":406,"style":376},"405.0","724.0",[378,1512,1514],{"x":1513,"y":1465,"style":1466},"429.0","resetting cell styles (≈ 0)",[1443,1516,1518],{"transform":1517},"translate(407.5,99.5) scale(0.625)",[206,1519],{"d":1500,"fill":1449,"stroke":406,"style":1473},[378,1521,1522],{"x":1513,"y":1476,"style":1466},".xlsb (Python cannot write it)",[1443,1524,1526],{"transform":1525},"translate(407.5,125.5) scale(0.625)",[206,1527],{"d":1500,"fill":1449,"stroke":406,"style":1473},[378,1529,1530],{"x":1513,"y":1485,"style":1466},"zipping an .xlsx again (already compressed)",[1443,1532,1534],{"transform":1533},"translate(407.5,151.5) scale(0.625)",[206,1535],{"d":1500,"fill":1449,"stroke":406,"style":1473},[10,1537,1538,1539,1542],{},"Saving as ",[37,1540,1541],{},".xlsb"," is sometimes suggested too; it does shrink large sheet XML, but Python cannot write that format, and images are the same size in either.",[25,1544,1546],{"id":1545},"step-6-keep-generated-reports-small-from-the-start","Step 6 — Keep generated reports small from the start",[10,1548,1549],{},"Most bloat is created by the script that builds the report, so prevention is cheap:",[30,1551,1553],{"className":73,"code":1552,"language":75,"meta":35,"style":35},"from openpyxl import Workbook\nfrom openpyxl.drawing.image import Image as XLImage\n\nthumb = Image.open(\"site_photo.png\").convert(\"RGB\")\nthumb.thumbnail((600, 600))\nthumb.save(\"site_photo_small.jpg\", quality=85)\n\nlean = Workbook()\nws = lean.active\nfor r in range(1, 5001):\n    ws.append([r, f\"Item {r % 300}\", float(r) * 1.5, \"North\"])\nfor col in \"ABCD\":\n    ws.column_dimensions[col].width = 12        # widths, not formatted cells\nws.add_image(XLImage(\"site_photo_small.jpg\"), \"F2\")\nlean.save(\"monthly_report_lean.xlsx\")\npart_sizes(\"monthly_report_lean.xlsx\", top=3)\n",[37,1554,1555,1566,1583,1587,1608,1622,1642,1646,1656,1665,1686,1732,1746,1760,1774,1784],{"__ignoreMap":35},[40,1556,1557,1559,1561,1563],{"class":42,"line":43},[40,1558,466],{"class":82},[40,1560,469],{"class":86},[40,1562,83],{"class":82},[40,1564,1565],{"class":86}," Workbook\n",[40,1567,1568,1570,1573,1575,1578,1580],{"class":42,"line":90},[40,1569,466],{"class":82},[40,1571,1572],{"class":86}," openpyxl.drawing.image ",[40,1574,83],{"class":82},[40,1576,1577],{"class":86}," Image ",[40,1579,963],{"class":82},[40,1581,1582],{"class":86}," XLImage\n",[40,1584,1585],{"class":42,"line":98},[40,1586,102],{"emptyLinePlaceholder":101},[40,1588,1589,1592,1594,1597,1600,1603,1606],{"class":42,"line":105},[40,1590,1591],{"class":86},"thumb ",[40,1593,118],{"class":82},[40,1595,1596],{"class":86}," Image.open(",[40,1598,1599],{"class":50},"\"site_photo.png\"",[40,1601,1602],{"class":86},").convert(",[40,1604,1605],{"class":50},"\"RGB\"",[40,1607,234],{"class":86},[40,1609,1610,1613,1615,1617,1619],{"class":42,"line":128},[40,1611,1612],{"class":86},"thumb.thumbnail((",[40,1614,909],{"class":121},[40,1616,912],{"class":86},[40,1618,909],{"class":121},[40,1620,1621],{"class":86},"))\n",[40,1623,1624,1627,1630,1632,1635,1637,1640],{"class":42,"line":139},[40,1625,1626],{"class":86},"thumb.save(",[40,1628,1629],{"class":50},"\"site_photo_small.jpg\"",[40,1631,912],{"class":86},[40,1633,1634],{"class":165},"quality",[40,1636,118],{"class":82},[40,1638,1639],{"class":121},"85",[40,1641,234],{"class":86},[40,1643,1644],{"class":42,"line":187},[40,1645,102],{"emptyLinePlaceholder":101},[40,1647,1648,1651,1653],{"class":42,"line":237},[40,1649,1650],{"class":86},"lean ",[40,1652,118],{"class":82},[40,1654,1655],{"class":86}," Workbook()\n",[40,1657,1658,1660,1662],{"class":42,"line":251},[40,1659,606],{"class":86},[40,1661,118],{"class":82},[40,1663,1664],{"class":86}," lean.active\n",[40,1666,1667,1669,1671,1673,1675,1677,1679,1681,1684],{"class":42,"line":311},[40,1668,153],{"class":82},[40,1670,1280],{"class":86},[40,1672,159],{"class":82},[40,1674,1285],{"class":121},[40,1676,193],{"class":86},[40,1678,532],{"class":121},[40,1680,912],{"class":86},[40,1682,1683],{"class":121},"5001",[40,1685,125],{"class":86},[40,1687,1688,1691,1693,1696,1698,1701,1704,1707,1709,1711,1713,1716,1719,1722,1725,1727,1730],{"class":42,"line":316},[40,1689,1690],{"class":86},"    ws.append([r, ",[40,1692,196],{"class":82},[40,1694,1695],{"class":50},"\"Item ",[40,1697,203],{"class":202},[40,1699,1700],{"class":86},"r ",[40,1702,1703],{"class":82},"%",[40,1705,1706],{"class":121}," 300",[40,1708,209],{"class":202},[40,1710,199],{"class":50},[40,1712,912],{"class":86},[40,1714,1715],{"class":121},"float",[40,1717,1718],{"class":86},"(r) ",[40,1720,1721],{"class":82},"*",[40,1723,1724],{"class":121}," 1.5",[40,1726,912],{"class":86},[40,1728,1729],{"class":50},"\"North\"",[40,1731,184],{"class":86},[40,1733,1734,1736,1739,1741,1744],{"class":42,"line":620},[40,1735,153],{"class":82},[40,1737,1738],{"class":86}," col ",[40,1740,159],{"class":82},[40,1742,1743],{"class":50}," \"ABCD\"",[40,1745,1366],{"class":86},[40,1747,1748,1751,1753,1756],{"class":42,"line":631},[40,1749,1750],{"class":86},"    ws.column_dimensions[col].width ",[40,1752,118],{"class":82},[40,1754,1755],{"class":121}," 12",[40,1757,1759],{"class":1758},"s-wDw","        # widths, not formatted cells\n",[40,1761,1762,1765,1767,1769,1772],{"class":42,"line":651},[40,1763,1764],{"class":86},"ws.add_image(XLImage(",[40,1766,1629],{"class":50},[40,1768,524],{"class":86},[40,1770,1771],{"class":50},"\"F2\"",[40,1773,234],{"class":86},[40,1775,1776,1779,1782],{"class":42,"line":671},[40,1777,1778],{"class":86},"lean.save(",[40,1780,1781],{"class":50},"\"monthly_report_lean.xlsx\"",[40,1783,234],{"class":86},[40,1785,1786,1788,1790,1792,1795,1797,1799],{"class":42,"line":682},[40,1787,319],{"class":86},[40,1789,1781],{"class":50},[40,1791,912],{"class":86},[40,1793,1794],{"class":165},"top",[40,1796,118],{"class":82},[40,1798,1187],{"class":121},[40,1800,234],{"class":86},[10,1802,1803],{},"Insert images at display size and as JPEG for photos; format ranges that contain data rather than whole columns; keep archive data out of the report; and set column widths instead of styling empty cells. The rebuilt report holds the same data and picture in 0.16 MB — about one fortieth of the original — with the JPEG thumbnail taking 35 kB.",[25,1805,1807],{"id":1806},"common-pitfalls","Common pitfalls",[1809,1810,1811,1819,1825,1831,1837],"ul",{},[1812,1813,1814,1818],"li",{},[1815,1816,1817],"strong",{},"Guessing the cause."," Measure part sizes first; images and hidden data usually dominate.",[1812,1820,1821,1824],{},[1815,1822,1823],{},"Formatting whole columns."," Styled empty cells extend the used range and add XML.",[1812,1826,1827,1830],{},[1815,1828,1829],{},"Saving a loaded workbook twice."," With images, the second save fails; reload between saves.",[1812,1832,1833,1836],{},[1815,1834,1835],{},"Expecting style clean-up to shrink files."," openpyxl keeps unused styles; the gain is usually tiny anyway.",[1812,1838,1839,1842],{},[1815,1840,1841],{},"Recompressing to a different image format in place."," Changing PNG to JPEG inside the package requires updating the part name and content type; convert when generating instead.",[25,1844,1846],{"id":1845},"conclusion","Conclusion",[10,1848,1849],{},"Measure the compressed size of every part in the package before changing anything — in most bloated workbooks an oversized image or a hidden data sheet is nearly all of the size. Resize images to their display size (JPEG for photos), delete formatted rows below the data, move archive sheets out, and do not expect much from style clean-up. Reload between saves when the workbook contains images, and build reports lean from the start so there is nothing to shrink.",[25,1851,1853],{"id":1852},"frequently-asked-questions","Frequently asked questions",[10,1855,1856,1859],{},[1815,1857,1858],{},"Why is my Excel file so large?","\nUsually because of embedded images at full resolution, hidden sheets with old data, or formatting applied to whole columns. Open the .xlsx as a ZIP with zipfile and compare the compressed size of each part to see which one it is.",[10,1861,1862,1865],{},[1815,1863,1864],{},"How do I compress the images in an Excel file with Python?","\nRead the image from xl\u002Fmedia inside the package, resize it with Pillow to the size it is displayed at, and write it back under the same name and format. For new reports, resize and convert photos to JPEG before inserting them with openpyxl.",[10,1867,1868,1871],{},[1815,1869,1870],{},"Does removing formatting reduce Excel file size?","\nRemoving formatting from empty rows below the data helps, because those cells are stored in the sheet. Changing the styles of cells that hold data rarely helps, since styles compress well and openpyxl keeps loaded styles anyway.",[10,1873,1874,1877],{},[1815,1875,1876],{},"Why does openpyxl say \"I\u002FO operation on closed file\" when saving?","\nThe workbook contains images and has already been saved once since it was loaded; openpyxl reads image data from the source file only for the first save. Load the workbook again before saving a second time.",[25,1879,1881],{"id":1880},"related","Related",[1809,1883,1884,1890,1897,1904],{},[1812,1885,1886,1887,1889],{},"Up one level: ",[19,1888,22],{"href":21}," — big files, read and written efficiently.",[1812,1891,1892,1896],{},[19,1893,1895],{"href":1894},"\u002Fgetting-started-with-python-excel-automation\u002Fusing-openpyxl-for-excel-file-manipulation\u002Fdelete-empty-rows-and-columns-with-openpyxl\u002F","Delete Empty Rows and Columns with openpyxl"," — removing blank rows inside the data too.",[1812,1898,1899,1903],{},[19,1900,1902],{"href":1901},"\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Fzip-large-excel-attachments-before-emailing\u002F","Zip Large Excel Attachments Before Emailing"," — when the file is as small as it gets.",[1812,1905,1906,1910],{},[19,1907,1909],{"href":1908},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fprotecting-and-sharing-excel-workbooks\u002Fremove-hidden-data-before-sharing-an-excel-file\u002F","Remove Hidden Data Before Sharing an Excel File"," — hidden sheets as a privacy issue.",[1912,1913,1914],"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 .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}html pre.shiki code .s-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}",{"title":35,"searchDepth":90,"depth":90,"links":1916},[1917,1918,1919,1920,1921,1922,1923,1924,1925,1926,1927],{"id":27,"depth":90,"text":28},{"id":69,"depth":90,"text":70},{"id":448,"depth":90,"text":449},{"id":732,"depth":90,"text":733},{"id":828,"depth":90,"text":829},{"id":1224,"depth":90,"text":1225},{"id":1545,"depth":90,"text":1546},{"id":1806,"depth":90,"text":1807},{"id":1845,"depth":90,"text":1846},{"id":1852,"depth":90,"text":1853},{"id":1880,"depth":90,"text":1881},"2026-10-02","Find out what makes a workbook big by measuring its parts, then shrink it where it counts: downscale oversized images, delete formatted-but-empty rows that inflate the used range, remove archive sheets, and know which popular fixes barely help. Includes measured before-and-after sizes, an openpyxl bug that breaks saving twice, and habits that keep generated reports small.","md",[1932,1934,1936,1938],{"q":1858,"a":1933},"Usually because of embedded images at full resolution, hidden sheets with old data, or formatting applied to whole columns. Open the .xlsx as a ZIP with zipfile and compare the compressed size of each part to see which one it is.",{"q":1864,"a":1935},"Read the image from xl\u002Fmedia inside the package, resize it with Pillow to the size it is displayed at, and write it back under the same name and format. For new reports, resize and convert photos to JPEG before inserting them with openpyxl.",{"q":1870,"a":1937},"Removing formatting from empty rows below the data helps, because those cells are stored in the sheet. Changing the styles of cells that hold data rarely helps, since styles compress well and openpyxl keeps loaded styles anyway.",{"q":1876,"a":1939},"The workbook contains images and has already been saved once since it was loaded; openpyxl reads image data from the source file only for the first save. Load the workbook again before saving a second time.","reduce the file size of an excel workbook with python",null,{"breadcrumb":1943},[1944,1946,1949],{"name":1945,"item":220},"Home",{"name":1947,"item":1948},"Advanced Data Transformation and Cleaning for Excel Automation","\u002Fadvanced-data-transformation-and-cleaning\u002F",{"name":22,"item":21},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-large-excel-files-in-python\u002Freduce-the-file-size-of-an-excel-workbook-with-python",{"title":5,"description":1952},"Shrink large xlsx files with Python: measure each part inside the package, resize embedded images, delete empty formatted rows, drop hidden archive sheets, why style clean-up rarely helps, the openpyxl save-twice image error, and building lean reports.","reduce-the-file-size-of-an-excel-workbook-with-python","advanced-data-transformation-and-cleaning\u002Fworking-with-large-excel-files-in-python\u002Freduce-the-file-size-of-an-excel-workbook-with-python\u002Findex",[1956,1957],"large-files","openpyxl","how-to","cgDnX5OTPYe6lOeOpX-yvxqoI1VmiuQO5rpdrams2Y4",[1961,1965],{"title":1962,"path":1963,"stem":1964,"children":-1},"Reduce Memory When Reading Excel with pandas","\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-large-excel-files-in-python\u002Freduce-memory-when-reading-excel-with-pandas","advanced-data-transformation-and-cleaning\u002Fworking-with-large-excel-files-in-python\u002Freduce-memory-when-reading-excel-with-pandas\u002Findex",{"title":1966,"path":1967,"stem":1968,"children":-1},"Remove Duplicate Rows Across Many Large Excel Files","\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-large-excel-files-in-python\u002Fremove-duplicate-rows-across-many-large-excel-files","advanced-data-transformation-and-cleaning\u002Fworking-with-large-excel-files-in-python\u002Fremove-duplicate-rows-across-many-large-excel-files\u002Findex",[1970,1972],{"path":1971,"title":1947},"\u002Fadvanced-data-transformation-and-cleaning",{"path":1973,"title":22},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-large-excel-files-in-python",[],[1976,1979,1982,1985,1988,1991],{"path":1977,"title":1978,"kind":1958},"\u002Fadvanced-data-transformation-and-cleaning\u002Freading-excel-with-polars-and-arrow\u002Fspeed-up-pandas-excel-reads-with-the-calamine-engine","Speed Up pandas Excel Reads with the calamine Engine",{"path":1980,"title":1981,"kind":1958},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fprofile-a-slow-excel-report-script","Profile a Slow Excel Report Script in Python",{"path":1983,"title":1984,"kind":1958},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fbuilding-excel-reports-with-xlsxwriter\u002Fwrite-a-million-rows-to-excel-with-xlsxwriter-constant-memory","Write a Million Rows to Excel with xlsxwriter",{"path":1986,"title":1987,"kind":1958},"\u002Fgetting-started-with-python-excel-automation\u002Fchoosing-a-python-excel-library\u002Fbenchmark-python-excel-read-and-write-speed","Benchmark Python Excel Read and Write Speed",{"path":1989,"title":1990,"kind":1958},"\u002Fadvanced-data-transformation-and-cleaning\u002Fmoving-data-between-excel-and-databases\u002Fexport-a-large-database-table-to-excel-in-chunks","Export a Large Database Table to Excel in Chunks",{"path":1992,"title":1993,"kind":1958},"\u002Fadvanced-data-transformation-and-cleaning\u002Fmoving-data-between-excel-and-databases\u002Fquery-excel-files-with-sql-using-duckdb","Query Excel Files with SQL Using DuckDB",1790972106752]