[{"data":1,"prerenderedAt":2079},["ShallowReactive",2],{"doc:\u002Fautomating-reporting-workflows\u002Fserving-excel-files-from-python-web-apps\u002Fbuild-an-excel-workbook-in-memory-with-bytesio":3,"surround:\u002Fautomating-reporting-workflows\u002Fserving-excel-files-from-python-web-apps\u002Fbuild-an-excel-workbook-in-memory-with-bytesio":2071},{"id":4,"title":5,"body":6,"dateModified":2046,"datePublished":2046,"description":2047,"extension":2048,"faq":2049,"meta":2062,"navigation":198,"path":2063,"seo":2064,"slug":2067,"stem":2068,"type":2069,"__hash__":2070},"docs\u002Fautomating-reporting-workflows\u002Fserving-excel-files-from-python-web-apps\u002Fbuild-an-excel-workbook-in-memory-with-bytesio\u002Findex.md","Build an Excel Workbook in Memory with BytesIO",{"type":7,"value":8,"toc":2031},"minimark",[9,24,124,129,160,166,170,397,420,424,427,695,701,705,708,855,862,957,961,971,977,1084,1090,1156,1176,1180,1183,1279,1282,1286,1289,1545,1558,1562,1565,1790,1848,1851,1855,1909,1913,1928,1932,1937,1941,1952,1964,1970,1980,1986,1990,2027],[10,11,12,13,17,18,23],"p",{},"Writing a workbook to disk and reading it straight back is a habit from scripts, and it becomes a liability the moment code runs in a server, a container, or a Lambda. Building the file in an ",[14,15,16],"code",{},"io.BytesIO"," buffer removes the filesystem from the picture: no temporary path to collide over, no cleanup, no read-only-volume surprise. The pattern is four lines, and two of them are the ones people miss. This guide covers all three libraries, the byte-level reasons the rules exist, and where the bytes go next. It is the shared foundation under ",[19,20,22],"a",{"href":21},"\u002Fautomating-reporting-workflows\u002Fserving-excel-files-from-python-web-apps\u002F","Serving Excel Files from Python Web Apps",".",[25,26,34,35,34,39,34,43,34,50,34,57,34,66,34,72,34,75,34,79,34,82,34,86,34,91,34,96,34,101,34,106,34,109,34,113,34,119],"svg",{"viewBox":27,"role":28,"ariaLabelledBy":29,"xmlns":32,"style":33},"0 0 760 230","img",[30,31],"bio-t","bio-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:760px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[36,37,38],"title",{"id":30},"Why a workbook must be closed before it is read",[40,41,42],"desc",{"id":31},"Worksheet parts are written as they are produced, but the zip central directory that indexes them is only appended when the workbook closes, so a buffer read early has no index.",[44,45],"rect",{"x":46,"y":46,"width":47,"height":48,"fill":49},"0","760","230","#ffffff",[51,52,56],"text",{"x":53,"y":54,"style":55},"380","26","font-size:13px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","Inside the buffer, over time",[44,58],{"x":59,"y":60,"width":61,"height":62,"rx":63,"fill":64,"stroke":65},"40","52","150","46","8","#ebebfd","var(--line,#cdd5e6)",[51,67,71],{"x":68,"y":69,"style":70},"115","80","font-size:11.5px;fill:var(--text,#172033);text-anchor:middle","sheet1.xml",[44,73],{"x":74,"y":60,"width":61,"height":62,"rx":63,"fill":64,"stroke":65},"198",[51,76,78],{"x":77,"y":69,"style":70},"273","sharedStrings.xml",[44,80],{"x":81,"y":60,"width":61,"height":62,"rx":63,"fill":64,"stroke":65},"356",[51,83,85],{"x":84,"y":69,"style":70},"431","styles.xml",[44,87],{"x":88,"y":60,"width":89,"height":62,"rx":63,"fill":90,"stroke":65},"514","206","#fdefd8",[51,92,95],{"x":93,"y":69,"style":94},"617","font-size:11.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","central directory (on close)",[44,97],{"x":59,"y":98,"width":99,"height":59,"rx":63,"fill":100,"stroke":65},"118","466","#fee8f2",[51,102,105],{"x":77,"y":103,"style":104},"143","font-size:12px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","read here: parts with no index — \"not a zip file\"",[44,107],{"x":88,"y":98,"width":89,"height":59,"rx":63,"fill":108,"stroke":65},"#d9f4f1",[51,110,112],{"x":93,"y":103,"style":111},"font-size:12px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","read here: a valid file",[44,114],{"x":59,"y":115,"width":116,"height":117,"rx":63,"fill":118,"stroke":65},"172","680","38","#f0f2f5",[51,120,123],{"x":53,"y":121,"style":122},"196","font-size:12px;fill:var(--text,#172033);text-anchor:middle","then seek(0) — otherwise the read starts after the last byte and returns nothing",[125,126,128],"h2",{"id":127},"prerequisites","Prerequisites",[130,131,136],"pre",{"className":132,"code":133,"language":134,"meta":135,"style":135},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install pandas xlsxwriter openpyxl\n","bash","",[14,137,138],{"__ignoreMap":135},[139,140,143,147,151,154,157],"span",{"class":141,"line":142},"line",1,[139,144,146],{"class":145},"sMTad","pip",[139,148,150],{"class":149},"srMev"," install",[139,152,153],{"class":149}," pandas",[139,155,156],{"class":149}," xlsxwriter",[139,158,159],{"class":149}," openpyxl\n",[10,161,162,165],{},[14,163,164],{},"io"," is in the standard library, so nothing else is required.",[125,167,169],{"id":168},"the-pattern-with-pandas","The pattern, with pandas",[130,171,175],{"className":172,"code":173,"language":174,"meta":135,"style":135},"language-python shiki shiki-themes github-light github-dark-high-contrast","\"\"\"Build a workbook in memory and get its bytes.\"\"\"\nimport io\n\nimport pandas as pd\n\ndf = pd.DataFrame({\"region\": [\"North\", \"South\"], \"revenue\": [128_400.5, 96_220.0]})\n\nbuffer = io.BytesIO()\nwith pd.ExcelWriter(buffer, engine=\"xlsxwriter\") as writer:\n    df.to_excel(writer, index=False, sheet_name=\"Sales\")\nbuffer.seek(0)\n\npayload = buffer.getvalue()\nprint(f\"{len(payload):,} bytes\")\n","python",[14,176,177,182,193,200,214,219,266,271,282,308,335,345,350,361],{"__ignoreMap":135},[139,178,179],{"class":141,"line":142},[139,180,181],{"class":149},"\"\"\"Build a workbook in memory and get its bytes.\"\"\"\n",[139,183,185,189],{"class":141,"line":184},2,[139,186,188],{"class":187},"s-kum","import",[139,190,192],{"class":191},"skGVy"," io\n",[139,194,196],{"class":141,"line":195},3,[139,197,199],{"emptyLinePlaceholder":198},true,"\n",[139,201,203,205,208,211],{"class":141,"line":202},4,[139,204,188],{"class":187},[139,206,207],{"class":191}," pandas ",[139,209,210],{"class":187},"as",[139,212,213],{"class":191}," pd\n",[139,215,217],{"class":141,"line":216},5,[139,218,199],{"emptyLinePlaceholder":198},[139,220,222,225,228,231,234,237,240,243,246,249,252,254,258,260,263],{"class":141,"line":221},6,[139,223,224],{"class":191},"df ",[139,226,227],{"class":187},"=",[139,229,230],{"class":191}," pd.DataFrame({",[139,232,233],{"class":149},"\"region\"",[139,235,236],{"class":191},": [",[139,238,239],{"class":149},"\"North\"",[139,241,242],{"class":191},", ",[139,244,245],{"class":149},"\"South\"",[139,247,248],{"class":191},"], ",[139,250,251],{"class":149},"\"revenue\"",[139,253,236],{"class":191},[139,255,257],{"class":256},"sP0c6","128_400.5",[139,259,242],{"class":191},[139,261,262],{"class":256},"96_220.0",[139,264,265],{"class":191},"]})\n",[139,267,269],{"class":141,"line":268},7,[139,270,199],{"emptyLinePlaceholder":198},[139,272,274,277,279],{"class":141,"line":273},8,[139,275,276],{"class":191},"buffer ",[139,278,227],{"class":187},[139,280,281],{"class":191}," io.BytesIO()\n",[139,283,285,288,291,295,297,300,303,305],{"class":141,"line":284},9,[139,286,287],{"class":187},"with",[139,289,290],{"class":191}," pd.ExcelWriter(buffer, ",[139,292,294],{"class":293},"sa561","engine",[139,296,227],{"class":187},[139,298,299],{"class":149},"\"xlsxwriter\"",[139,301,302],{"class":191},") ",[139,304,210],{"class":187},[139,306,307],{"class":191}," writer:\n",[139,309,311,314,317,319,322,324,327,329,332],{"class":141,"line":310},10,[139,312,313],{"class":191},"    df.to_excel(writer, ",[139,315,316],{"class":293},"index",[139,318,227],{"class":187},[139,320,321],{"class":256},"False",[139,323,242],{"class":191},[139,325,326],{"class":293},"sheet_name",[139,328,227],{"class":187},[139,330,331],{"class":149},"\"Sales\"",[139,333,334],{"class":191},")\n",[139,336,338,341,343],{"class":141,"line":337},11,[139,339,340],{"class":191},"buffer.seek(",[139,342,46],{"class":256},[139,344,334],{"class":191},[139,346,348],{"class":141,"line":347},12,[139,349,199],{"emptyLinePlaceholder":198},[139,351,353,356,358],{"class":141,"line":352},13,[139,354,355],{"class":191},"payload ",[139,357,227],{"class":187},[139,359,360],{"class":191}," buffer.getvalue()\n",[139,362,364,367,370,373,376,380,383,386,389,392,395],{"class":141,"line":363},14,[139,365,366],{"class":256},"print",[139,368,369],{"class":191},"(",[139,371,372],{"class":187},"f",[139,374,375],{"class":149},"\"",[139,377,379],{"class":378},"sSjpA","{",[139,381,382],{"class":256},"len",[139,384,385],{"class":191},"(payload)",[139,387,388],{"class":187},":,",[139,390,391],{"class":378},"}",[139,393,394],{"class":149}," bytes\"",[139,396,334],{"class":191},[10,398,399,400,404,405,407,408,411,412,415,416,419],{},"Two rules govern everything else on this page. ",[401,402,403],"strong",{},"Close the writer before reading"," — the ",[14,406,287],{}," block does it — because an ",[14,409,410],{},".xlsx"," is a zip whose central directory is only appended at the end. And ",[401,413,414],{},"rewind before streaming",": after the write, the buffer's position is at the end, so anything that reads it as a stream gets zero bytes. ",[14,417,418],{},"getvalue()"," is exempt from the second rule, since it returns the whole buffer regardless of position.",[125,421,423],{"id":422},"the-same-thing-with-xlsxwriter-directly","The same thing with xlsxwriter directly",[10,425,426],{},"Skip pandas when the data is already rows, or when you want the full formatting API:",[130,428,430],{"className":172,"code":429,"language":174,"meta":135,"style":135},"import io\n\nimport xlsxwriter\n\nbuffer = io.BytesIO()\nwith xlsxwriter.Workbook(buffer, {\"in_memory\": True}) as book:\n    ws = book.add_worksheet(\"Sales\")\n    bold = book.add_format({\"bold\": True})\n    money = book.add_format({\"num_format\": \"#,##0.00\"})\n\n    ws.write_row(0, 0, [\"region\", \"revenue\"], bold)\n    for row, (region, revenue) in enumerate([(\"North\", 128_400.5), (\"South\", 96_220.0)], 1):\n        ws.write(row, 0, region)\n        ws.write(row, 1, revenue, money)\n\n    ws.set_column(0, 0, 16)\n    ws.set_column(1, 1, 14)\n    ws.freeze_panes(1, 0)\n\nbuffer.seek(0)\n",[14,431,432,438,442,449,453,461,485,499,519,538,542,565,606,616,625,630,649,667,681,686],{"__ignoreMap":135},[139,433,434,436],{"class":141,"line":142},[139,435,188],{"class":187},[139,437,192],{"class":191},[139,439,440],{"class":141,"line":184},[139,441,199],{"emptyLinePlaceholder":198},[139,443,444,446],{"class":141,"line":195},[139,445,188],{"class":187},[139,447,448],{"class":191}," xlsxwriter\n",[139,450,451],{"class":141,"line":202},[139,452,199],{"emptyLinePlaceholder":198},[139,454,455,457,459],{"class":141,"line":216},[139,456,276],{"class":191},[139,458,227],{"class":187},[139,460,281],{"class":191},[139,462,463,465,468,471,474,477,480,482],{"class":141,"line":221},[139,464,287],{"class":187},[139,466,467],{"class":191}," xlsxwriter.Workbook(buffer, {",[139,469,470],{"class":149},"\"in_memory\"",[139,472,473],{"class":191},": ",[139,475,476],{"class":256},"True",[139,478,479],{"class":191},"}) ",[139,481,210],{"class":187},[139,483,484],{"class":191}," book:\n",[139,486,487,490,492,495,497],{"class":141,"line":268},[139,488,489],{"class":191},"    ws ",[139,491,227],{"class":187},[139,493,494],{"class":191}," book.add_worksheet(",[139,496,331],{"class":149},[139,498,334],{"class":191},[139,500,501,504,506,509,512,514,516],{"class":141,"line":273},[139,502,503],{"class":191},"    bold ",[139,505,227],{"class":187},[139,507,508],{"class":191}," book.add_format({",[139,510,511],{"class":149},"\"bold\"",[139,513,473],{"class":191},[139,515,476],{"class":256},[139,517,518],{"class":191},"})\n",[139,520,521,524,526,528,531,533,536],{"class":141,"line":284},[139,522,523],{"class":191},"    money ",[139,525,227],{"class":187},[139,527,508],{"class":191},[139,529,530],{"class":149},"\"num_format\"",[139,532,473],{"class":191},[139,534,535],{"class":149},"\"#,##0.00\"",[139,537,518],{"class":191},[139,539,540],{"class":141,"line":310},[139,541,199],{"emptyLinePlaceholder":198},[139,543,544,547,549,551,553,556,558,560,562],{"class":141,"line":337},[139,545,546],{"class":191},"    ws.write_row(",[139,548,46],{"class":256},[139,550,242],{"class":191},[139,552,46],{"class":256},[139,554,555],{"class":191},", [",[139,557,233],{"class":149},[139,559,242],{"class":191},[139,561,251],{"class":149},[139,563,564],{"class":191},"], bold)\n",[139,566,567,570,573,576,579,582,584,586,588,591,593,595,597,600,603],{"class":141,"line":347},[139,568,569],{"class":187},"    for",[139,571,572],{"class":191}," row, (region, revenue) ",[139,574,575],{"class":187},"in",[139,577,578],{"class":256}," enumerate",[139,580,581],{"class":191},"([(",[139,583,239],{"class":149},[139,585,242],{"class":191},[139,587,257],{"class":256},[139,589,590],{"class":191},"), (",[139,592,245],{"class":149},[139,594,242],{"class":191},[139,596,262],{"class":256},[139,598,599],{"class":191},")], ",[139,601,602],{"class":256},"1",[139,604,605],{"class":191},"):\n",[139,607,608,611,613],{"class":141,"line":352},[139,609,610],{"class":191},"        ws.write(row, ",[139,612,46],{"class":256},[139,614,615],{"class":191},", region)\n",[139,617,618,620,622],{"class":141,"line":363},[139,619,610],{"class":191},[139,621,602],{"class":256},[139,623,624],{"class":191},", revenue, money)\n",[139,626,628],{"class":141,"line":627},15,[139,629,199],{"emptyLinePlaceholder":198},[139,631,633,636,638,640,642,644,647],{"class":141,"line":632},16,[139,634,635],{"class":191},"    ws.set_column(",[139,637,46],{"class":256},[139,639,242],{"class":191},[139,641,46],{"class":256},[139,643,242],{"class":191},[139,645,646],{"class":256},"16",[139,648,334],{"class":191},[139,650,652,654,656,658,660,662,665],{"class":141,"line":651},17,[139,653,635],{"class":191},[139,655,602],{"class":256},[139,657,242],{"class":191},[139,659,602],{"class":256},[139,661,242],{"class":191},[139,663,664],{"class":256},"14",[139,666,334],{"class":191},[139,668,670,673,675,677,679],{"class":141,"line":669},18,[139,671,672],{"class":191},"    ws.freeze_panes(",[139,674,602],{"class":256},[139,676,242],{"class":191},[139,678,46],{"class":256},[139,680,334],{"class":191},[139,682,684],{"class":141,"line":683},19,[139,685,199],{"emptyLinePlaceholder":198},[139,687,689,691,693],{"class":141,"line":688},20,[139,690,340],{"class":191},[139,692,46],{"class":256},[139,694,334],{"class":191},[10,696,697,700],{},[14,698,699],{},"{\"in_memory\": True}"," tells xlsxwriter not to use temporary files for worksheet data. Without it, a container with a read-only filesystem — a common hardening setting, and the default in some serverless runtimes — fails at write time with a permission error that looks unrelated to Excel.",[125,702,704],{"id":703},"and-with-openpyxl-when-you-are-editing","And with openpyxl, when you are editing",[10,706,707],{},"openpyxl is the library that can read an existing workbook, change it, and save it back. Both ends work with buffers:",[130,709,711],{"className":172,"code":710,"language":174,"meta":135,"style":135},"import io\n\nfrom openpyxl import load_workbook\n\n# `source` might be an upload, an S3 object, or an HTTP response body\nsource = io.BytesIO(existing_bytes)\nwb = load_workbook(source)\nws = wb.active\nws[\"B2\"] = 42\nws[\"A1\"].value = f\"Updated {ws['A1'].value}\"\n\nout = io.BytesIO()\nwb.save(out)          # save() accepts any file-like object\nout.seek(0)\n",[14,712,713,719,723,736,740,746,756,766,776,792,825,829,838,846],{"__ignoreMap":135},[139,714,715,717],{"class":141,"line":142},[139,716,188],{"class":187},[139,718,192],{"class":191},[139,720,721],{"class":141,"line":184},[139,722,199],{"emptyLinePlaceholder":198},[139,724,725,728,731,733],{"class":141,"line":195},[139,726,727],{"class":187},"from",[139,729,730],{"class":191}," openpyxl ",[139,732,188],{"class":187},[139,734,735],{"class":191}," load_workbook\n",[139,737,738],{"class":141,"line":202},[139,739,199],{"emptyLinePlaceholder":198},[139,741,742],{"class":141,"line":216},[139,743,745],{"class":744},"s-wDw","# `source` might be an upload, an S3 object, or an HTTP response body\n",[139,747,748,751,753],{"class":141,"line":221},[139,749,750],{"class":191},"source ",[139,752,227],{"class":187},[139,754,755],{"class":191}," io.BytesIO(existing_bytes)\n",[139,757,758,761,763],{"class":141,"line":268},[139,759,760],{"class":191},"wb ",[139,762,227],{"class":187},[139,764,765],{"class":191}," load_workbook(source)\n",[139,767,768,771,773],{"class":141,"line":273},[139,769,770],{"class":191},"ws ",[139,772,227],{"class":187},[139,774,775],{"class":191}," wb.active\n",[139,777,778,781,784,787,789],{"class":141,"line":284},[139,779,780],{"class":191},"ws[",[139,782,783],{"class":149},"\"B2\"",[139,785,786],{"class":191},"] ",[139,788,227],{"class":187},[139,790,791],{"class":256}," 42\n",[139,793,794,796,799,802,804,807,810,812,814,817,820,822],{"class":141,"line":310},[139,795,780],{"class":191},[139,797,798],{"class":149},"\"A1\"",[139,800,801],{"class":191},"].value ",[139,803,227],{"class":187},[139,805,806],{"class":187}," f",[139,808,809],{"class":149},"\"Updated ",[139,811,379],{"class":378},[139,813,780],{"class":191},[139,815,816],{"class":149},"'A1'",[139,818,819],{"class":191},"].value",[139,821,391],{"class":378},[139,823,824],{"class":149},"\"\n",[139,826,827],{"class":141,"line":337},[139,828,199],{"emptyLinePlaceholder":198},[139,830,831,834,836],{"class":141,"line":347},[139,832,833],{"class":191},"out ",[139,835,227],{"class":187},[139,837,281],{"class":191},[139,839,840,843],{"class":141,"line":352},[139,841,842],{"class":191},"wb.save(out)          ",[139,844,845],{"class":744},"# save() accepts any file-like object\n",[139,847,848,851,853],{"class":141,"line":363},[139,849,850],{"class":191},"out.seek(",[139,852,46],{"class":256},[139,854,334],{"class":191},[10,856,857,858,23],{},"That is the round trip a template-filling service needs: read the template from storage, populate it, return the bytes — with no file ever landing on disk. The template techniques themselves are in ",[19,859,861],{"href":860},"\u002Fautomating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Fpopulate-excel-template-without-losing-formatting\u002F","Populate an Excel template without losing formatting",[25,863,34,868,34,871,34,874,34,877,34,880,34,887,34,892,34,897,34,904,34,909,34,913,34,916,34,920,34,923,34,925,34,928,34,933,34,937,34,940,34,944,34,947,34,952,34,954],{"viewBox":864,"role":28,"ariaLabelledBy":865,"xmlns":32,"style":33},"0 0 760 222",[866,867],"bio2-t","bio2-d",[36,869,870],{"id":866},"Where in-memory bytes can go next",[40,872,873],{"id":867},"The same byte string can be returned as an HTTP download, attached to an email, uploaded to object storage, or read straight back for verification.",[44,875],{"x":46,"y":46,"width":47,"height":876,"fill":49},"222",[51,878,879],{"x":53,"y":54,"style":55},"One buffer, four destinations",[44,881],{"x":882,"y":883,"width":884,"height":885,"rx":886,"fill":64,"stroke":65},"286","82","188","66","12",[51,888,891],{"x":53,"y":889,"style":890},"112","font-size:12.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","BytesIO",[51,893,896],{"x":53,"y":894,"style":895},"134","font-size:11.5px;fill:var(--muted,#5b6780);text-anchor:middle","the finished workbook",[141,898],{"x1":882,"y1":899,"x2":900,"y2":901,"stroke":902,"style":903},"98","234","70","var(--teal,#0f9488)","stroke-width:2px",[905,906],"polygon",{"points":907,"fill":908},"234,70 246,73 241,81","#0f766e",[141,910],{"x1":882,"y1":911,"x2":900,"y2":912,"stroke":902,"style":903},"132","160",[905,914],{"points":915,"fill":908},"234,160 241,149 246,157",[141,917],{"x1":918,"y1":899,"x2":919,"y2":901,"stroke":902,"style":903},"474","526",[905,921],{"points":922,"fill":908},"526,70 514,73 519,81",[141,924],{"x1":918,"y1":911,"x2":919,"y2":912,"stroke":902,"style":903},[905,926],{"points":927,"fill":908},"526,160 519,149 514,157",[44,929],{"x":930,"y":62,"width":121,"height":931,"rx":932,"fill":108,"stroke":65},"34","50","10",[51,934,936],{"x":911,"y":935,"style":111},"76","HTTP download",[44,938],{"x":930,"y":939,"width":121,"height":931,"rx":932,"fill":108,"stroke":65},"136",[51,941,943],{"x":911,"y":942,"style":111},"166","email attachment",[44,945],{"x":946,"y":62,"width":121,"height":931,"rx":932,"fill":90,"stroke":65},"530",[51,948,951],{"x":949,"y":935,"style":950},"628","font-size:12px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","object storage",[44,953],{"x":946,"y":939,"width":121,"height":931,"rx":932,"fill":90,"stroke":65},[51,955,956],{"x":949,"y":942,"style":950},"read back to verify",[125,958,960],{"id":959},"send-the-bytes-somewhere","Send the bytes somewhere",[10,962,963,966,967,23],{},[401,964,965],{},"As an HTTP download"," — the header pair and framework specifics are in ",[19,968,970],{"href":969},"\u002Fautomating-reporting-workflows\u002Fserving-excel-files-from-python-web-apps\u002Freturn-an-excel-file-from-a-flask-download-endpoint\u002F","Return an Excel file from a Flask download endpoint",[10,972,973,976],{},[401,974,975],{},"As an email attachment",", with no temporary file:",[130,978,980],{"className":172,"code":979,"language":174,"meta":135,"style":135},"from email.message import EmailMessage\n\nmsg = EmailMessage()\nmsg[\"Subject\"] = \"Weekly sales summary\"\nmsg.set_content(\"The summary is attached.\")\nmsg.add_attachment(\n    payload,\n    maintype=\"application\",\n    subtype=\"vnd.openxmlformats-officedocument.spreadsheetml.sheet\",\n    filename=\"sales.xlsx\",\n)\n",[14,981,982,994,998,1008,1023,1033,1038,1043,1056,1068,1080],{"__ignoreMap":135},[139,983,984,986,989,991],{"class":141,"line":142},[139,985,727],{"class":187},[139,987,988],{"class":191}," email.message ",[139,990,188],{"class":187},[139,992,993],{"class":191}," EmailMessage\n",[139,995,996],{"class":141,"line":184},[139,997,199],{"emptyLinePlaceholder":198},[139,999,1000,1003,1005],{"class":141,"line":195},[139,1001,1002],{"class":191},"msg ",[139,1004,227],{"class":187},[139,1006,1007],{"class":191}," EmailMessage()\n",[139,1009,1010,1013,1016,1018,1020],{"class":141,"line":202},[139,1011,1012],{"class":191},"msg[",[139,1014,1015],{"class":149},"\"Subject\"",[139,1017,786],{"class":191},[139,1019,227],{"class":187},[139,1021,1022],{"class":149}," \"Weekly sales summary\"\n",[139,1024,1025,1028,1031],{"class":141,"line":216},[139,1026,1027],{"class":191},"msg.set_content(",[139,1029,1030],{"class":149},"\"The summary is attached.\"",[139,1032,334],{"class":191},[139,1034,1035],{"class":141,"line":221},[139,1036,1037],{"class":191},"msg.add_attachment(\n",[139,1039,1040],{"class":141,"line":268},[139,1041,1042],{"class":191},"    payload,\n",[139,1044,1045,1048,1050,1053],{"class":141,"line":273},[139,1046,1047],{"class":293},"    maintype",[139,1049,227],{"class":187},[139,1051,1052],{"class":149},"\"application\"",[139,1054,1055],{"class":191},",\n",[139,1057,1058,1061,1063,1066],{"class":141,"line":284},[139,1059,1060],{"class":293},"    subtype",[139,1062,227],{"class":187},[139,1064,1065],{"class":149},"\"vnd.openxmlformats-officedocument.spreadsheetml.sheet\"",[139,1067,1055],{"class":191},[139,1069,1070,1073,1075,1078],{"class":141,"line":310},[139,1071,1072],{"class":293},"    filename",[139,1074,227],{"class":187},[139,1076,1077],{"class":149},"\"sales.xlsx\"",[139,1079,1055],{"class":191},[139,1081,1082],{"class":141,"line":337},[139,1083,334],{"class":191},[10,1085,1086,1089],{},[401,1087,1088],{},"To object storage",", straight from the buffer:",[130,1091,1093],{"className":172,"code":1092,"language":174,"meta":135,"style":135},"import boto3\n\nboto3.client(\"s3\").upload_fileobj(\n    io.BytesIO(payload), \"reports-bucket\", \"weekly\u002Fsales.xlsx\",\n    ExtraArgs={\"ContentType\": \"application\u002Fvnd.openxmlformats-officedocument.spreadsheetml.sheet\"},\n)\n",[14,1094,1095,1102,1106,1117,1132,1152],{"__ignoreMap":135},[139,1096,1097,1099],{"class":141,"line":142},[139,1098,188],{"class":187},[139,1100,1101],{"class":191}," boto3\n",[139,1103,1104],{"class":141,"line":184},[139,1105,199],{"emptyLinePlaceholder":198},[139,1107,1108,1111,1114],{"class":141,"line":195},[139,1109,1110],{"class":191},"boto3.client(",[139,1112,1113],{"class":149},"\"s3\"",[139,1115,1116],{"class":191},").upload_fileobj(\n",[139,1118,1119,1122,1125,1127,1130],{"class":141,"line":202},[139,1120,1121],{"class":191},"    io.BytesIO(payload), ",[139,1123,1124],{"class":149},"\"reports-bucket\"",[139,1126,242],{"class":191},[139,1128,1129],{"class":149},"\"weekly\u002Fsales.xlsx\"",[139,1131,1055],{"class":191},[139,1133,1134,1137,1139,1141,1144,1146,1149],{"class":141,"line":216},[139,1135,1136],{"class":293},"    ExtraArgs",[139,1138,227],{"class":187},[139,1140,379],{"class":191},[139,1142,1143],{"class":149},"\"ContentType\"",[139,1145,473],{"class":191},[139,1147,1148],{"class":149},"\"application\u002Fvnd.openxmlformats-officedocument.spreadsheetml.sheet\"",[139,1150,1151],{"class":191},"},\n",[139,1153,1154],{"class":141,"line":221},[139,1155,334],{"class":191},[10,1157,1158,1159,1162,1163,1166,1167,1171,1172,23],{},"Setting ",[14,1160,1161],{},"ContentType"," on upload matters: without it the object is served as ",[14,1164,1165],{},"application\u002Foctet-stream",", and a browser opening a signed URL saves a file Excel may not associate correctly. The full delivery paths are in ",[19,1168,1170],{"href":1169},"\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002F","Emailing Excel Reports with smtplib"," and ",[19,1173,1175],{"href":1174},"\u002Fautomating-reporting-workflows\u002Fpublishing-excel-reports-to-cloud-storage\u002Fupload-an-excel-report-to-amazon-s3-with-boto3\u002F","Upload an Excel report to Amazon S3 with boto3",[125,1177,1179],{"id":1178},"read-the-bytes-back-to-check-them","Read the bytes back to check them",[10,1181,1182],{},"Because everything is in memory, verification is immediate and costs no I\u002FO:",[130,1184,1186],{"className":172,"code":1185,"language":174,"meta":135,"style":135},"import io\n\nimport pandas as pd\n\ncheck = pd.read_excel(io.BytesIO(payload), engine=\"openpyxl\")\nassert list(check.columns) == [\"region\", \"revenue\"]\nassert len(check) == 2\nprint(check)\n",[14,1187,1188,1194,1198,1208,1212,1231,1257,1272],{"__ignoreMap":135},[139,1189,1190,1192],{"class":141,"line":142},[139,1191,188],{"class":187},[139,1193,192],{"class":191},[139,1195,1196],{"class":141,"line":184},[139,1197,199],{"emptyLinePlaceholder":198},[139,1199,1200,1202,1204,1206],{"class":141,"line":195},[139,1201,188],{"class":187},[139,1203,207],{"class":191},[139,1205,210],{"class":187},[139,1207,213],{"class":191},[139,1209,1210],{"class":141,"line":202},[139,1211,199],{"emptyLinePlaceholder":198},[139,1213,1214,1217,1219,1222,1224,1226,1229],{"class":141,"line":216},[139,1215,1216],{"class":191},"check ",[139,1218,227],{"class":187},[139,1220,1221],{"class":191}," pd.read_excel(io.BytesIO(payload), ",[139,1223,294],{"class":293},[139,1225,227],{"class":187},[139,1227,1228],{"class":149},"\"openpyxl\"",[139,1230,334],{"class":191},[139,1232,1233,1236,1239,1242,1245,1248,1250,1252,1254],{"class":141,"line":221},[139,1234,1235],{"class":187},"assert",[139,1237,1238],{"class":256}," list",[139,1240,1241],{"class":191},"(check.columns) ",[139,1243,1244],{"class":187},"==",[139,1246,1247],{"class":191}," [",[139,1249,233],{"class":149},[139,1251,242],{"class":191},[139,1253,251],{"class":149},[139,1255,1256],{"class":191},"]\n",[139,1258,1259,1261,1264,1267,1269],{"class":141,"line":268},[139,1260,1235],{"class":187},[139,1262,1263],{"class":256}," len",[139,1265,1266],{"class":191},"(check) ",[139,1268,1244],{"class":187},[139,1270,1271],{"class":256}," 2\n",[139,1273,1274,1276],{"class":141,"line":273},[139,1275,366],{"class":256},[139,1277,1278],{"class":191},"(check)\n",[10,1280,1281],{},"This is the assertion worth keeping in a test: it catches the truncated-buffer bug, which is invisible to every other check because the byte string looks plausible and the headers are correct.",[125,1283,1285],{"id":1284},"wrap-it-in-a-function-that-returns-bytes","Wrap it in a function that returns bytes",[10,1287,1288],{},"The most useful shape for this code is a function whose input is data and whose output is bytes. It has no side effects, it is trivial to test, and every caller — a web route, a scheduled job, a queue worker — uses it unchanged:",[130,1290,1292],{"className":172,"code":1291,"language":174,"meta":135,"style":135},"\"\"\"report.py — one builder, many callers.\"\"\"\nimport io\nfrom collections.abc import Sequence\n\nimport pandas as pd\n\nXLSX_MIME = \"application\u002Fvnd.openxmlformats-officedocument.spreadsheetml.sheet\"\n\ndef workbook_bytes(frames: dict[str, pd.DataFrame], *, autofilter: bool = True) -> bytes:\n    \"\"\"Render one sheet per frame and return the finished workbook.\"\"\"\n    buffer = io.BytesIO()\n    with pd.ExcelWriter(buffer, engine=\"xlsxwriter\") as writer:\n        for sheet, frame in frames.items():\n            frame.to_excel(writer, index=False, sheet_name=sheet[:31])\n            ws = writer.sheets[sheet[:31]]\n            ws.freeze_panes(1, 0)\n            if autofilter and len(frame.columns):\n                ws.autofilter(0, 0, len(frame), len(frame.columns) - 1)\n    return buffer.getvalue()\n",[14,1293,1294,1299,1305,1317,1321,1331,1335,1346,1350,1391,1396,1405,1424,1437,1463,1478,1491,1507,1538],{"__ignoreMap":135},[139,1295,1296],{"class":141,"line":142},[139,1297,1298],{"class":149},"\"\"\"report.py — one builder, many callers.\"\"\"\n",[139,1300,1301,1303],{"class":141,"line":184},[139,1302,188],{"class":187},[139,1304,192],{"class":191},[139,1306,1307,1309,1312,1314],{"class":141,"line":195},[139,1308,727],{"class":187},[139,1310,1311],{"class":191}," collections.abc ",[139,1313,188],{"class":187},[139,1315,1316],{"class":191}," Sequence\n",[139,1318,1319],{"class":141,"line":202},[139,1320,199],{"emptyLinePlaceholder":198},[139,1322,1323,1325,1327,1329],{"class":141,"line":216},[139,1324,188],{"class":187},[139,1326,207],{"class":191},[139,1328,210],{"class":187},[139,1330,213],{"class":191},[139,1332,1333],{"class":141,"line":221},[139,1334,199],{"emptyLinePlaceholder":198},[139,1336,1337,1340,1343],{"class":141,"line":268},[139,1338,1339],{"class":256},"XLSX_MIME",[139,1341,1342],{"class":187}," =",[139,1344,1345],{"class":149}," \"application\u002Fvnd.openxmlformats-officedocument.spreadsheetml.sheet\"\n",[139,1347,1348],{"class":141,"line":273},[139,1349,199],{"emptyLinePlaceholder":198},[139,1351,1352,1355,1359,1362,1365,1368,1371,1374,1377,1379,1382,1385,1388],{"class":141,"line":284},[139,1353,1354],{"class":187},"def",[139,1356,1358],{"class":1357},"s_Opv"," workbook_bytes",[139,1360,1361],{"class":191},"(frames: dict[",[139,1363,1364],{"class":256},"str",[139,1366,1367],{"class":191},", pd.DataFrame], ",[139,1369,1370],{"class":187},"*",[139,1372,1373],{"class":191},", autofilter: ",[139,1375,1376],{"class":256},"bool",[139,1378,1342],{"class":187},[139,1380,1381],{"class":256}," True",[139,1383,1384],{"class":191},") -> ",[139,1386,1387],{"class":256},"bytes",[139,1389,1390],{"class":191},":\n",[139,1392,1393],{"class":141,"line":310},[139,1394,1395],{"class":149},"    \"\"\"Render one sheet per frame and return the finished workbook.\"\"\"\n",[139,1397,1398,1401,1403],{"class":141,"line":337},[139,1399,1400],{"class":191},"    buffer ",[139,1402,227],{"class":187},[139,1404,281],{"class":191},[139,1406,1407,1410,1412,1414,1416,1418,1420,1422],{"class":141,"line":347},[139,1408,1409],{"class":187},"    with",[139,1411,290],{"class":191},[139,1413,294],{"class":293},[139,1415,227],{"class":187},[139,1417,299],{"class":149},[139,1419,302],{"class":191},[139,1421,210],{"class":187},[139,1423,307],{"class":191},[139,1425,1426,1429,1432,1434],{"class":141,"line":352},[139,1427,1428],{"class":187},"        for",[139,1430,1431],{"class":191}," sheet, frame ",[139,1433,575],{"class":187},[139,1435,1436],{"class":191}," frames.items():\n",[139,1438,1439,1442,1444,1446,1448,1450,1452,1454,1457,1460],{"class":141,"line":363},[139,1440,1441],{"class":191},"            frame.to_excel(writer, ",[139,1443,316],{"class":293},[139,1445,227],{"class":187},[139,1447,321],{"class":256},[139,1449,242],{"class":191},[139,1451,326],{"class":293},[139,1453,227],{"class":187},[139,1455,1456],{"class":191},"sheet[:",[139,1458,1459],{"class":256},"31",[139,1461,1462],{"class":191},"])\n",[139,1464,1465,1468,1470,1473,1475],{"class":141,"line":627},[139,1466,1467],{"class":191},"            ws ",[139,1469,227],{"class":187},[139,1471,1472],{"class":191}," writer.sheets[sheet[:",[139,1474,1459],{"class":256},[139,1476,1477],{"class":191},"]]\n",[139,1479,1480,1483,1485,1487,1489],{"class":141,"line":632},[139,1481,1482],{"class":191},"            ws.freeze_panes(",[139,1484,602],{"class":256},[139,1486,242],{"class":191},[139,1488,46],{"class":256},[139,1490,334],{"class":191},[139,1492,1493,1496,1499,1502,1504],{"class":141,"line":651},[139,1494,1495],{"class":187},"            if",[139,1497,1498],{"class":191}," autofilter ",[139,1500,1501],{"class":187},"and",[139,1503,1263],{"class":256},[139,1505,1506],{"class":191},"(frame.columns):\n",[139,1508,1509,1512,1514,1516,1518,1520,1522,1525,1527,1530,1533,1536],{"class":141,"line":669},[139,1510,1511],{"class":191},"                ws.autofilter(",[139,1513,46],{"class":256},[139,1515,242],{"class":191},[139,1517,46],{"class":256},[139,1519,242],{"class":191},[139,1521,382],{"class":256},[139,1523,1524],{"class":191},"(frame), ",[139,1526,382],{"class":256},[139,1528,1529],{"class":191},"(frame.columns) ",[139,1531,1532],{"class":187},"-",[139,1534,1535],{"class":256}," 1",[139,1537,334],{"class":191},[139,1539,1540,1543],{"class":141,"line":683},[139,1541,1542],{"class":187},"    return",[139,1544,360],{"class":191},[10,1546,1547,1548,1550,1551,1554,1555,1557],{},"Returning ",[14,1549,1387],{}," rather than the buffer removes the ",[14,1552,1553],{},"seek"," question from every call site: bytes have no position. Callers that need a stream wrap them in a fresh ",[14,1556,891],{},", which is cheap and unambiguous.",[125,1559,1561],{"id":1560},"measure-what-a-build-actually-costs","Measure what a build actually costs",[10,1563,1564],{},"Before deciding whether in-memory generation scales for your case, measure one build. The numbers are usually smaller than people fear and occasionally much larger:",[130,1566,1568],{"className":172,"code":1567,"language":174,"meta":135,"style":135},"import io\nimport time\nimport tracemalloc\n\nimport pandas as pd\n\ndf = pd.DataFrame({\"id\": range(50_000), \"value\": [i * 1.5 for i in range(50_000)]})\n\ntracemalloc.start()\nstart = time.perf_counter()\npayload = workbook_bytes({\"Data\": df})\nelapsed = time.perf_counter() - start\n_current, peak = tracemalloc.get_traced_memory()\ntracemalloc.stop()\n\nprint(f\"{len(payload)\u002F1e6:.1f} MB file, {elapsed:.2f}s, peak {peak\u002F1e6:.1f} MB\")\n",[14,1569,1570,1576,1583,1590,1594,1604,1608,1661,1665,1670,1680,1695,1710,1720,1725,1729],{"__ignoreMap":135},[139,1571,1572,1574],{"class":141,"line":142},[139,1573,188],{"class":187},[139,1575,192],{"class":191},[139,1577,1578,1580],{"class":141,"line":184},[139,1579,188],{"class":187},[139,1581,1582],{"class":191}," time\n",[139,1584,1585,1587],{"class":141,"line":195},[139,1586,188],{"class":187},[139,1588,1589],{"class":191}," tracemalloc\n",[139,1591,1592],{"class":141,"line":202},[139,1593,199],{"emptyLinePlaceholder":198},[139,1595,1596,1598,1600,1602],{"class":141,"line":216},[139,1597,188],{"class":187},[139,1599,207],{"class":191},[139,1601,210],{"class":187},[139,1603,213],{"class":191},[139,1605,1606],{"class":141,"line":221},[139,1607,199],{"emptyLinePlaceholder":198},[139,1609,1610,1612,1614,1616,1619,1621,1624,1626,1629,1632,1635,1638,1640,1643,1646,1649,1651,1654,1656,1658],{"class":141,"line":268},[139,1611,224],{"class":191},[139,1613,227],{"class":187},[139,1615,230],{"class":191},[139,1617,1618],{"class":149},"\"id\"",[139,1620,473],{"class":191},[139,1622,1623],{"class":256},"range",[139,1625,369],{"class":191},[139,1627,1628],{"class":256},"50_000",[139,1630,1631],{"class":191},"), ",[139,1633,1634],{"class":149},"\"value\"",[139,1636,1637],{"class":191},": [i ",[139,1639,1370],{"class":187},[139,1641,1642],{"class":256}," 1.5",[139,1644,1645],{"class":187}," for",[139,1647,1648],{"class":191}," i ",[139,1650,575],{"class":187},[139,1652,1653],{"class":256}," range",[139,1655,369],{"class":191},[139,1657,1628],{"class":256},[139,1659,1660],{"class":191},")]})\n",[139,1662,1663],{"class":141,"line":273},[139,1664,199],{"emptyLinePlaceholder":198},[139,1666,1667],{"class":141,"line":284},[139,1668,1669],{"class":191},"tracemalloc.start()\n",[139,1671,1672,1675,1677],{"class":141,"line":310},[139,1673,1674],{"class":191},"start ",[139,1676,227],{"class":187},[139,1678,1679],{"class":191}," time.perf_counter()\n",[139,1681,1682,1684,1686,1689,1692],{"class":141,"line":337},[139,1683,355],{"class":191},[139,1685,227],{"class":187},[139,1687,1688],{"class":191}," workbook_bytes({",[139,1690,1691],{"class":149},"\"Data\"",[139,1693,1694],{"class":191},": df})\n",[139,1696,1697,1700,1702,1705,1707],{"class":141,"line":347},[139,1698,1699],{"class":191},"elapsed ",[139,1701,227],{"class":187},[139,1703,1704],{"class":191}," time.perf_counter() ",[139,1706,1532],{"class":187},[139,1708,1709],{"class":191}," start\n",[139,1711,1712,1715,1717],{"class":141,"line":352},[139,1713,1714],{"class":191},"_current, peak ",[139,1716,227],{"class":187},[139,1718,1719],{"class":191}," tracemalloc.get_traced_memory()\n",[139,1721,1722],{"class":141,"line":363},[139,1723,1724],{"class":191},"tracemalloc.stop()\n",[139,1726,1727],{"class":141,"line":627},[139,1728,199],{"emptyLinePlaceholder":198},[139,1730,1731,1733,1735,1737,1739,1741,1743,1745,1748,1751,1754,1756,1759,1761,1764,1767,1769,1772,1774,1777,1779,1781,1783,1785,1788],{"class":141,"line":632},[139,1732,366],{"class":256},[139,1734,369],{"class":191},[139,1736,372],{"class":187},[139,1738,375],{"class":149},[139,1740,379],{"class":378},[139,1742,382],{"class":256},[139,1744,385],{"class":191},[139,1746,1747],{"class":187},"\u002F",[139,1749,1750],{"class":256},"1e6",[139,1752,1753],{"class":187},":.1f",[139,1755,391],{"class":378},[139,1757,1758],{"class":149}," MB file, ",[139,1760,379],{"class":378},[139,1762,1763],{"class":191},"elapsed",[139,1765,1766],{"class":187},":.2f",[139,1768,391],{"class":378},[139,1770,1771],{"class":149},"s, peak ",[139,1773,379],{"class":378},[139,1775,1776],{"class":191},"peak",[139,1778,1747],{"class":187},[139,1780,1750],{"class":256},[139,1782,1753],{"class":187},[139,1784,391],{"class":378},[139,1786,1787],{"class":149}," MB\"",[139,1789,334],{"class":191},[25,1791,34,1796,34,1799,34,1802,34,1804,34,1807,34,1810,34,1815,34,1819,34,1822,34,1827,34,1831,34,1835,34,1840,34,1844],{"viewBox":1792,"role":28,"ariaLabelledBy":1793,"xmlns":32,"style":33},"0 0 760 198",[1794,1795],"bio3-t","bio3-d",[36,1797,1798],{"id":1794},"Memory held during an in-memory build",[40,1800,1801],{"id":1795},"Peak memory is the source data plus the workbook being assembled plus the finished byte string, which is why concurrency rather than file size is usually the limit.",[44,1803],{"x":46,"y":46,"width":47,"height":74,"fill":49},[51,1805,1806],{"x":53,"y":54,"style":55},"Three things alive at the peak",[44,1808],{"x":59,"y":60,"width":1809,"height":935,"rx":886,"fill":64,"stroke":65},"210",[51,1811,1814],{"x":1812,"y":1813,"style":890},"145","84","source data",[51,1816,1818],{"x":1812,"y":1817,"style":895},"108","the DataFrame or rows",[44,1820],{"x":1821,"y":60,"width":1809,"height":935,"rx":886,"fill":90,"stroke":65},"270",[51,1823,1826],{"x":1824,"y":1813,"style":1825},"375","font-size:12.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","workbook under way",[51,1828,1830],{"x":1824,"y":1817,"style":1829},"font-size:11.5px;fill:var(--gold-ink,#7a4e06);text-anchor:middle","xlsxwriter structures",[44,1832],{"x":1833,"y":60,"width":1834,"height":935,"rx":886,"fill":108,"stroke":65},"500","220",[51,1836,1839],{"x":1837,"y":1813,"style":1838},"610","font-size:12.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","finished bytes",[51,1841,1843],{"x":1837,"y":1817,"style":1842},"font-size:11.5px;fill:var(--teal-ink,#0b6157);text-anchor:middle","the compressed file",[51,1845,1847],{"x":53,"y":1846,"style":895},"164","Multiply by concurrent requests — that product, not one file, is the ceiling",[10,1849,1850],{},"If the peak is uncomfortable, the levers in order are: return fewer rows, drop the DataFrame and write rows directly, or move the build to a queue where one worker does it at a time.",[125,1852,1854],{"id":1853},"common-pitfalls-and-gotchas","Common pitfalls and gotchas",[1856,1857,1858,1868,1877,1886,1903],"ul",{},[1859,1860,1861,1867],"li",{},[401,1862,1863,1864,1866],{},"Reading inside the ",[14,1865,287],{}," block."," The zip is not finalised, so the bytes are not a workbook.",[1859,1869,1870,1876],{},[401,1871,1872,1873,23],{},"Skipping ",[14,1874,1875],{},"seek(0)"," Anything that streams the buffer sends nothing.",[1859,1878,1879,1882,1883,1885],{},[401,1880,1881],{},"Reusing one buffer for two workbooks."," Create a new ",[14,1884,891],{}," per file; a rewritten buffer keeps trailing bytes from the previous, larger file.",[1859,1887,1888,1896,1897,1899,1900,23],{},[401,1889,1890,1893,1894,23],{},[14,1891,1892],{},"StringIO"," instead of ",[14,1895,891],{}," An ",[14,1898,410],{}," is binary; a text buffer raises a ",[14,1901,1902],{},"TypeError",[1859,1904,1905,1908],{},[401,1906,1907],{},"Holding buffers in a list."," Every retained buffer is a retained workbook. Release them once sent, especially in a worker that processes many reports per run.",[125,1910,1912],{"id":1911},"performance-and-scale-notes","Performance and scale notes",[10,1914,1915,1916,1919,1920,1923,1924,23],{},"In-memory generation is generally faster than writing to disk, because it skips the filesystem, and it is the only option in an environment with a read-only volume. The cost is memory: peak usage is roughly the size of the finished workbook plus the data it came from, per concurrent build. That is fine for a report of a few thousand rows and a real constraint for a hundred concurrent exports of a large one. If a single workbook is genuinely large, xlsxwriter's ",[14,1917,1918],{},"constant_memory"," mode streams rows out as they are written — but note that it needs a temporary directory, so it conflicts with ",[14,1921,1922],{},"in_memory"," and with a read-only filesystem. The trade-offs are laid out in ",[19,1925,1927],{"href":1926},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fbuilding-excel-reports-with-xlsxwriter\u002Fwrite-a-million-rows-to-excel-with-xlsxwriter-constant-memory\u002F","Write a million rows to Excel with xlsxwriter constant memory",[125,1929,1931],{"id":1930},"conclusion","Conclusion",[10,1933,1934,1936],{},[14,1935,16],{}," turns workbook generation into a pure function: data in, bytes out, nothing on disk. Close the writer before reading, rewind before streaming, and create a fresh buffer per file. From there the same bytes serve an HTTP download, an email attachment or an object-storage upload without change — and reading them back with pandas is a one-line check that the file you produced is really a file.",[125,1938,1940],{"id":1939},"frequently-asked-questions","Frequently asked questions",[10,1942,1943,1946,1947,1949,1950,23],{},[401,1944,1945],{},"Why is my in-memory file empty or corrupt?","\nTwo causes. The writer was still open when the buffer was read — an ",[14,1948,410],{}," is a zip finalised on close — or the buffer was never rewound, so the read started at the end. Close first, then ",[14,1951,1875],{},[10,1953,1954,1957,1958,1960,1961,1963],{},[401,1955,1956],{},"Do I need seek(0) if I use getvalue()?","\nNo. ",[14,1959,418],{}," returns the whole buffer regardless of position. You need ",[14,1962,1875],{}," when handing the buffer itself to something that will read it as a stream.",[10,1965,1966,1969],{},[401,1967,1968],{},"Is in-memory generation slower than writing to disk?","\nNo, it is usually faster, because it avoids the filesystem entirely. The trade-off is memory: the whole workbook exists in RAM, which matters only for very large files or high concurrency.",[10,1971,1972,1975,1976,1979],{},[401,1973,1974],{},"Can openpyxl save to a buffer too?","\nYes. ",[14,1977,1978],{},"wb.save(buffer)"," accepts any file-like object, which is the route to take when you are editing an existing workbook rather than creating a new one.",[10,1981,1982,1985],{},[401,1983,1984],{},"What is xlsxwriter's in_memory option for?","\nIt stops xlsxwriter using temporary files for worksheet data. Set it when the filesystem is read-only — a common container setting — or when temporary files would be a security concern.",[125,1987,1989],{"id":1988},"related","Related",[1856,1991,1992,2001,2006,2013,2020],{},[1859,1993,1994,1997,1998,2000],{},[401,1995,1996],{},"Up:"," ",[19,1999,22],{"href":21}," — where these bytes usually go.",[1859,2002,2003,2005],{},[19,2004,970],{"href":969}," — the buffer as an HTTP response.",[1859,2007,2008,2012],{},[19,2009,2011],{"href":2010},"\u002Fautomating-reporting-workflows\u002Fserving-excel-files-from-python-web-apps\u002Fstream-an-excel-file-from-a-fastapi-endpoint\u002F","Stream an Excel file from a FastAPI endpoint"," — the same, with the async caveats.",[1859,2014,2015,2019],{},[19,2016,2018],{"href":2017},"\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Fattach-multiple-excel-files-to-one-email-in-python\u002F","Attach multiple Excel files to one email in Python"," — several in-memory workbooks on one message.",[1859,2021,2022,2026],{},[19,2023,2025],{"href":2024},"\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fread-an-excel-file-from-a-url-or-bytes-in-python\u002F","Read an Excel file from a URL or bytes in Python"," — the reading counterpart to everything here.",[2028,2029,2030],"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 .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}html pre.shiki code .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}",{"title":135,"searchDepth":184,"depth":184,"links":2032},[2033,2034,2035,2036,2037,2038,2039,2040,2041,2042,2043,2044,2045],{"id":127,"depth":184,"text":128},{"id":168,"depth":184,"text":169},{"id":422,"depth":184,"text":423},{"id":703,"depth":184,"text":704},{"id":959,"depth":184,"text":960},{"id":1178,"depth":184,"text":1179},{"id":1284,"depth":184,"text":1285},{"id":1560,"depth":184,"text":1561},{"id":1853,"depth":184,"text":1854},{"id":1911,"depth":184,"text":1912},{"id":1930,"depth":184,"text":1931},{"id":1939,"depth":184,"text":1940},{"id":1988,"depth":184,"text":1989},"2026-08-27","Create .xlsx entirely in memory with pandas, xlsxwriter or openpyxl: the close-and-seek rules, in_memory mode, sending bytes to S3 or email, and reading them back.","md",[2050,2052,2054,2058,2060],{"q":1945,"a":2051},"Two causes. The writer was still open when the buffer was read — an xlsx is a zip finalised on close — or the buffer was never rewound, so the read started at the end. Close first, then seek(0).",{"q":1956,"a":2053},"No. getvalue() returns the whole buffer regardless of position. You need seek(0) when handing the buffer itself to something that will read it as a stream.",{"q":1968,"a":2055},{"No, it is usually faster, because it avoids the filesystem entirely":2056},{" The trade-off is memory":2057},"the whole workbook exists in RAM, which matters only for very large files or high concurrency.",{"q":1974,"a":2059},"Yes. wb.save(buffer) accepts any file-like object, which is the route to take when you are editing an existing workbook rather than creating a new one.",{"q":1984,"a":2061},"It stops xlsxwriter using temporary files for worksheet data. Set it when the filesystem is read-only — a common container setting — or when temporary files would be a security concern.",{},"\u002Fautomating-reporting-workflows\u002Fserving-excel-files-from-python-web-apps\u002Fbuild-an-excel-workbook-in-memory-with-bytesio",{"title":2065,"description":2066},"Create an Excel File in Memory (BytesIO)","Write xlsx to an io.BytesIO buffer with pandas, xlsxwriter and openpyxl — why close and seek matter, in_memory mode, and how to upload, attach or return the bytes.","build-an-excel-workbook-in-memory-with-bytesio","automating-reporting-workflows\u002Fserving-excel-files-from-python-web-apps\u002Fbuild-an-excel-workbook-in-memory-with-bytesio\u002Findex","how-to","niTujtFOzUj9W7eTfiWE9KMiMajrwroeDqQsGEljROc",[2072,2075],{"title":22,"path":2073,"stem":2074,"children":-1},"\u002Fautomating-reporting-workflows\u002Fserving-excel-files-from-python-web-apps","automating-reporting-workflows\u002Fserving-excel-files-from-python-web-apps\u002Findex",{"title":2076,"path":2077,"stem":2078,"children":-1},"Export a Django Queryset to Excel","\u002Fautomating-reporting-workflows\u002Fserving-excel-files-from-python-web-apps\u002Fexport-a-django-queryset-to-excel","automating-reporting-workflows\u002Fserving-excel-files-from-python-web-apps\u002Fexport-a-django-queryset-to-excel\u002Findex",1788710158831]