[{"data":1,"prerenderedAt":2635},["ShallowReactive",2],{"doc:\u002Fformatting-and-charting-excel-reports-with-python\u002Fprotecting-and-sharing-excel-workbooks\u002Fpassword-protect-an-excel-file-with-python":3,"surround:\u002Fformatting-and-charting-excel-reports-with-python\u002Fprotecting-and-sharing-excel-workbooks\u002Fpassword-protect-an-excel-file-with-python":2627},{"id":4,"title":5,"body":6,"dateModified":2603,"datePublished":2603,"description":2604,"extension":2605,"faq":2606,"meta":2618,"navigation":223,"path":2619,"seo":2620,"slug":2623,"stem":2624,"type":2625,"__hash__":2626},"docs\u002Fformatting-and-charting-excel-reports-with-python\u002Fprotecting-and-sharing-excel-workbooks\u002Fpassword-protect-an-excel-file-with-python\u002Findex.md","Password Protect an Excel File with Python",{"type":7,"value":8,"toc":2591},"minimark",[9,24,144,149,179,182,203,272,275,279,286,611,614,700,703,707,710,879,882,886,889,1048,1060,1063,1197,1208,1212,1215,1340,1799,1805,1809,1942,1946,2049,2052,2055,2288,2296,2453,2465,2469,2482,2486,2496,2505,2523,2540,2546,2550,2587],[10,11,12,13,17,18,23],"p",{},"Sheet protection stops a reader typing in the wrong cell. It does not stop them reading anything — every value in a \"protected\" workbook is plainly visible, and any library will hand them over. When the contents genuinely should not be seen, you need file-level encryption: the workbook wrapped in an encrypted container that cannot be opened at all without the password. Neither openpyxl nor xlsxwriter can write one, so this guide uses ",[14,15,16],"code",{},"msoffcrypto-tool",", covering both directions and the operational details that decide whether the protection is real. It is the security layer of ",[19,20,22],"a",{"href":21},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fprotecting-and-sharing-excel-workbooks\u002F","Protecting and Sharing Excel Workbooks",".",[25,26,35,36,35,40,35,44,35,52,35,62,35,69,35,74,35,79,35,85,35,90,35,98,35,104,35,109,35,113,35,117,35,121,35,127,35,132,35,135,35,138],"svg",{"viewBox":27,"role":28,"ariaLabel":29,"ariaLabelledBy":30,"xmlns":33,"style":34},"-2 38 802 177","img","Encryption pipeline: pandas writes a plain workbook into memory, msoffcrypto wraps those bytes in an encrypted OLE2 container, and the file on disk cannot be read without the password.",[31,32],"enc-t","enc-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:802px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[37,38,39],"title",{"id":31},"Where encryption happens in the write path",[41,42,43],"desc",{"id":32},"A DataFrame is written by pandas into an in-memory buffer as an ordinary xlsx zip. That buffer is passed to msoffcrypto, which derives a key from the password and wraps the whole zip in an encrypted OLE2 container. The file that lands on disk starts with the OLE2 signature rather than PK, and no spreadsheet library can read it until the password is supplied.",[45,46],"rect",{"x":47,"y":48,"width":49,"height":50,"fill":51},"-2","38","802","177","#ffffff",[45,53],{"x":54,"y":55,"width":56,"height":57,"rx":58,"fill":59,"stroke":60,"style":61},"14","80","150","70","13","#ebebfd","var(--brand,#5b5cf0)","stroke-width:2px",[63,64,68],"text",{"x":65,"y":66,"style":67},"89","64","font-size:11px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","in memory",[63,70,73],{"x":65,"y":71,"style":72},"110","font-size:12px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","plain .xlsx",[63,75,78],{"x":65,"y":76,"style":77},"130","font-size:10.5px;fill:var(--muted,#5b6780);text-anchor:middle","a zip, starts \"PK\"",[80,81],"line",{"x1":82,"y1":83,"x2":84,"y2":83,"stroke":60,"style":61},"164","115","200",[86,87],"polygon",{"points":88,"fill":89},"208,115 196,109 196,121","#5b5cf0",[45,91],{"x":92,"y":93,"width":94,"height":95,"rx":58,"fill":96,"stroke":97,"style":61},"216","66","220","98","#fdefd8","var(--gold,#b4740a)",[63,99,103],{"x":100,"y":101,"style":102},"326","94","font-size:12px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","msoffcrypto.encrypt",[63,105,108],{"x":100,"y":106,"style":107},"120","font-size:10.5px;fill:var(--text,#172033);text-anchor:middle","key derived from the password",[63,110,112],{"x":100,"y":111,"style":107},"140","AES over the whole archive",[80,114],{"x1":115,"y1":83,"x2":116,"y2":83,"stroke":97,"style":61},"436","472",[86,118],{"points":119,"fill":120},"480,115 468,109 468,121","#b4740a",[45,122],{"x":123,"y":93,"width":124,"height":95,"rx":58,"fill":125,"stroke":126,"style":61},"488","296","#d9f4f1","var(--teal,#0f9488)",[63,128,131],{"x":129,"y":101,"style":130},"636","font-size:12px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","encrypted file on disk",[63,133,134],{"x":129,"y":106,"style":107},"an OLE2 container, starts D0 CF 11 E0",[63,136,137],{"x":129,"y":111,"style":107},"unreadable without the password",[63,139,143],{"x":140,"y":141,"style":142},"400","196","font-size:11px;fill:var(--muted,#5b6780);text-anchor:middle","the plain bytes never touch the filesystem — the buffer is discarded when the function returns",[145,146,148],"h2",{"id":147},"prerequisites","Prerequisites",[150,151,156],"pre",{"className":152,"code":153,"language":154,"meta":155,"style":155},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install pandas xlsxwriter msoffcrypto-tool\n","bash","",[14,157,158],{"__ignoreMap":155},[159,160,162,166,170,173,176],"span",{"class":80,"line":161},1,[159,163,165],{"class":164},"sMTad","pip",[159,167,169],{"class":168},"srMev"," install",[159,171,172],{"class":168}," pandas",[159,174,175],{"class":168}," xlsxwriter",[159,177,178],{"class":168}," msoffcrypto-tool\n",[10,180,181],{},"And a password that does not live in your code. Read it from the environment:",[150,183,185],{"className":152,"code":184,"language":154,"meta":155,"style":155},"export REPORT_PASSWORD='...'\n",[14,186,187],{"__ignoreMap":155},[159,188,189,193,197,200],{"class":80,"line":161},[159,190,192],{"class":191},"s-kum","export",[159,194,196],{"class":195},"skGVy"," REPORT_PASSWORD",[159,198,199],{"class":191},"=",[159,201,202],{"class":168},"'...'\n",[150,204,208],{"className":205,"code":206,"language":207,"meta":155,"style":155},"language-python shiki shiki-themes github-light github-dark-high-contrast","import os\n\npassword = os.environ.get(\"REPORT_PASSWORD\")\nif not password:\n    raise RuntimeError(\"REPORT_PASSWORD is not set\")\n","python",[14,209,210,218,225,242,254],{"__ignoreMap":155},[159,211,212,215],{"class":80,"line":161},[159,213,214],{"class":191},"import",[159,216,217],{"class":195}," os\n",[159,219,221],{"class":80,"line":220},2,[159,222,224],{"emptyLinePlaceholder":223},true,"\n",[159,226,228,231,233,236,239],{"class":80,"line":227},3,[159,229,230],{"class":195},"password ",[159,232,199],{"class":191},[159,234,235],{"class":195}," os.environ.get(",[159,237,238],{"class":168},"\"REPORT_PASSWORD\"",[159,240,241],{"class":195},")\n",[159,243,245,248,251],{"class":80,"line":244},4,[159,246,247],{"class":191},"if",[159,249,250],{"class":191}," not",[159,252,253],{"class":195}," password:\n",[159,255,257,260,264,267,270],{"class":80,"line":256},5,[159,258,259],{"class":191},"    raise",[159,261,263],{"class":262},"sP0c6"," RuntimeError",[159,265,266],{"class":195},"(",[159,268,269],{"class":168},"\"REPORT_PASSWORD is not set\"",[159,271,241],{"class":195},[10,273,274],{},"Failing loudly at start-up beats discovering the variable was missing after the job wrote an unencrypted payroll file.",[145,276,278],{"id":277},"step-1-encrypt-a-workbook-you-generate","Step 1 — Encrypt a workbook you generate",[10,280,281,282,285],{},"Build the workbook in memory, then hand the bytes to ",[14,283,284],{},"msoffcrypto",". The unencrypted version never reaches the filesystem, so there is no window in which a plain copy exists on disk:",[150,287,289],{"className":205,"code":288,"language":207,"meta":155,"style":155},"import io\nimport os\nimport msoffcrypto\nimport pandas as pd\n\ndef write_encrypted(df, path, password, sheet_name=\"Summary\"):\n    \"\"\"Write a DataFrame to a password-encrypted .xlsx.\"\"\"\n    if not password:\n        raise ValueError(\"refusing to write with an empty password\")\n\n    plain = io.BytesIO()\n    with pd.ExcelWriter(plain, engine=\"xlsxwriter\") as writer:\n        df.to_excel(writer, sheet_name=sheet_name, index=False)\n    plain.seek(0)\n\n    office = msoffcrypto.OfficeFile(plain)\n    with open(path, \"wb\") as out:\n        office.encrypt(password, out)\n    return path\n\npayroll = pd.DataFrame({\n    \"employee\": [\"A. Chen\", \"B. Ortiz\", \"C. Novak\"],\n    \"salary\": [72000, 68500, 81250],\n})\nwrite_encrypted(payroll, \"payroll.xlsx\", os.environ[\"REPORT_PASSWORD\"])\nprint(\"wrote encrypted payroll.xlsx\")\n",[14,290,291,298,304,311,324,328,349,355,365,381,386,397,423,447,458,463,474,495,501,510,515,526,552,575,581,598],{"__ignoreMap":155},[159,292,293,295],{"class":80,"line":161},[159,294,214],{"class":191},[159,296,297],{"class":195}," io\n",[159,299,300,302],{"class":80,"line":220},[159,301,214],{"class":191},[159,303,217],{"class":195},[159,305,306,308],{"class":80,"line":227},[159,307,214],{"class":191},[159,309,310],{"class":195}," msoffcrypto\n",[159,312,313,315,318,321],{"class":80,"line":244},[159,314,214],{"class":191},[159,316,317],{"class":195}," pandas ",[159,319,320],{"class":191},"as",[159,322,323],{"class":195}," pd\n",[159,325,326],{"class":80,"line":256},[159,327,224],{"emptyLinePlaceholder":223},[159,329,331,334,338,341,343,346],{"class":80,"line":330},6,[159,332,333],{"class":191},"def",[159,335,337],{"class":336},"s_Opv"," write_encrypted",[159,339,340],{"class":195},"(df, path, password, sheet_name",[159,342,199],{"class":191},[159,344,345],{"class":168},"\"Summary\"",[159,347,348],{"class":195},"):\n",[159,350,352],{"class":80,"line":351},7,[159,353,354],{"class":168},"    \"\"\"Write a DataFrame to a password-encrypted .xlsx.\"\"\"\n",[159,356,358,361,363],{"class":80,"line":357},8,[159,359,360],{"class":191},"    if",[159,362,250],{"class":191},[159,364,253],{"class":195},[159,366,368,371,374,376,379],{"class":80,"line":367},9,[159,369,370],{"class":191},"        raise",[159,372,373],{"class":262}," ValueError",[159,375,266],{"class":195},[159,377,378],{"class":168},"\"refusing to write with an empty password\"",[159,380,241],{"class":195},[159,382,384],{"class":80,"line":383},10,[159,385,224],{"emptyLinePlaceholder":223},[159,387,389,392,394],{"class":80,"line":388},11,[159,390,391],{"class":195},"    plain ",[159,393,199],{"class":191},[159,395,396],{"class":195}," io.BytesIO()\n",[159,398,400,403,406,410,412,415,418,420],{"class":80,"line":399},12,[159,401,402],{"class":191},"    with",[159,404,405],{"class":195}," pd.ExcelWriter(plain, ",[159,407,409],{"class":408},"sa561","engine",[159,411,199],{"class":191},[159,413,414],{"class":168},"\"xlsxwriter\"",[159,416,417],{"class":195},") ",[159,419,320],{"class":191},[159,421,422],{"class":195}," writer:\n",[159,424,426,429,432,434,437,440,442,445],{"class":80,"line":425},13,[159,427,428],{"class":195},"        df.to_excel(writer, ",[159,430,431],{"class":408},"sheet_name",[159,433,199],{"class":191},[159,435,436],{"class":195},"sheet_name, ",[159,438,439],{"class":408},"index",[159,441,199],{"class":191},[159,443,444],{"class":262},"False",[159,446,241],{"class":195},[159,448,450,453,456],{"class":80,"line":449},14,[159,451,452],{"class":195},"    plain.seek(",[159,454,455],{"class":262},"0",[159,457,241],{"class":195},[159,459,461],{"class":80,"line":460},15,[159,462,224],{"emptyLinePlaceholder":223},[159,464,466,469,471],{"class":80,"line":465},16,[159,467,468],{"class":195},"    office ",[159,470,199],{"class":191},[159,472,473],{"class":195}," msoffcrypto.OfficeFile(plain)\n",[159,475,477,479,482,485,488,490,492],{"class":80,"line":476},17,[159,478,402],{"class":191},[159,480,481],{"class":262}," open",[159,483,484],{"class":195},"(path, ",[159,486,487],{"class":168},"\"wb\"",[159,489,417],{"class":195},[159,491,320],{"class":191},[159,493,494],{"class":195}," out:\n",[159,496,498],{"class":80,"line":497},18,[159,499,500],{"class":195},"        office.encrypt(password, out)\n",[159,502,504,507],{"class":80,"line":503},19,[159,505,506],{"class":191},"    return",[159,508,509],{"class":195}," path\n",[159,511,513],{"class":80,"line":512},20,[159,514,224],{"emptyLinePlaceholder":223},[159,516,518,521,523],{"class":80,"line":517},21,[159,519,520],{"class":195},"payroll ",[159,522,199],{"class":191},[159,524,525],{"class":195}," pd.DataFrame({\n",[159,527,529,532,535,538,541,544,546,549],{"class":80,"line":528},22,[159,530,531],{"class":168},"    \"employee\"",[159,533,534],{"class":195},": [",[159,536,537],{"class":168},"\"A. Chen\"",[159,539,540],{"class":195},", ",[159,542,543],{"class":168},"\"B. Ortiz\"",[159,545,540],{"class":195},[159,547,548],{"class":168},"\"C. Novak\"",[159,550,551],{"class":195},"],\n",[159,553,555,558,560,563,565,568,570,573],{"class":80,"line":554},23,[159,556,557],{"class":168},"    \"salary\"",[159,559,534],{"class":195},[159,561,562],{"class":262},"72000",[159,564,540],{"class":195},[159,566,567],{"class":262},"68500",[159,569,540],{"class":195},[159,571,572],{"class":262},"81250",[159,574,551],{"class":195},[159,576,578],{"class":80,"line":577},24,[159,579,580],{"class":195},"})\n",[159,582,584,587,590,593,595],{"class":80,"line":583},25,[159,585,586],{"class":195},"write_encrypted(payroll, ",[159,588,589],{"class":168},"\"payroll.xlsx\"",[159,591,592],{"class":195},", os.environ[",[159,594,238],{"class":168},[159,596,597],{"class":195},"])\n",[159,599,601,604,606,609],{"class":80,"line":600},26,[159,602,603],{"class":262},"print",[159,605,266],{"class":195},[159,607,608],{"class":168},"\"wrote encrypted payroll.xlsx\"",[159,610,241],{"class":195},[10,612,613],{},"Confirm it worked rather than assuming. An encrypted workbook is an OLE2 container, not a zip, so the first four bytes tell you immediately:",[150,615,617],{"className":205,"code":616,"language":207,"meta":155,"style":155},"with open(\"payroll.xlsx\", \"rb\") as fh:\n    head = fh.read(4)\n\nassert head == b\"\\xd0\\xcf\\x11\\xe0\", \"file is NOT encrypted — it is still a plain zip\"\nprint(\"confirmed encrypted\")\n",[14,618,619,642,657,661,689],{"__ignoreMap":155},[159,620,621,624,626,628,630,632,635,637,639],{"class":80,"line":161},[159,622,623],{"class":191},"with",[159,625,481],{"class":262},[159,627,266],{"class":195},[159,629,589],{"class":168},[159,631,540],{"class":195},[159,633,634],{"class":168},"\"rb\"",[159,636,417],{"class":195},[159,638,320],{"class":191},[159,640,641],{"class":195}," fh:\n",[159,643,644,647,649,652,655],{"class":80,"line":220},[159,645,646],{"class":195},"    head ",[159,648,199],{"class":191},[159,650,651],{"class":195}," fh.read(",[159,653,654],{"class":262},"4",[159,656,241],{"class":195},[159,658,659],{"class":80,"line":227},[159,660,224],{"emptyLinePlaceholder":223},[159,662,663,666,669,672,675,678,682,684,686],{"class":80,"line":244},[159,664,665],{"class":191},"assert",[159,667,668],{"class":195}," head ",[159,670,671],{"class":191},"==",[159,673,674],{"class":191}," b",[159,676,677],{"class":168},"\"",[159,679,681],{"class":680},"sSjpA","\\xd0\\xcf\\x11\\xe0",[159,683,677],{"class":168},[159,685,540],{"class":195},[159,687,688],{"class":168},"\"file is NOT encrypted — it is still a plain zip\"\n",[159,690,691,693,695,698],{"class":80,"line":256},[159,692,603],{"class":262},[159,694,266],{"class":195},[159,696,697],{"class":168},"\"confirmed encrypted\"",[159,699,241],{"class":195},[10,701,702],{},"That assertion is worth keeping in the job. An encryption step that silently no-ops is the worst possible failure, because everything downstream looks normal.",[145,704,706],{"id":705},"step-2-encrypt-an-existing-file","Step 2 — Encrypt an existing file",[10,708,709],{},"When the workbook already exists — produced by an earlier stage, or by a template fill — encrypt it in place from disk:",[150,711,713],{"className":205,"code":712,"language":207,"meta":155,"style":155},"import io\nimport msoffcrypto\nfrom pathlib import Path\n\ndef encrypt_file(src, dest, password):\n    \"\"\"Encrypt an existing .xlsx, leaving the source untouched.\"\"\"\n    src, dest = Path(src), Path(dest)\n    data = io.BytesIO(src.read_bytes())\n\n    office = msoffcrypto.OfficeFile(data)\n    if office.is_encrypted():\n        raise ValueError(f\"{src.name} is already encrypted\")\n\n    with dest.open(\"wb\") as out:\n        office.encrypt(password, out)\n    return dest\n\nencrypt_file(\"report.xlsx\", \"report_secure.xlsx\", os.environ[\"REPORT_PASSWORD\"])\n",[14,714,715,721,727,740,744,754,759,769,779,783,792,799,826,830,845,849,856,860],{"__ignoreMap":155},[159,716,717,719],{"class":80,"line":161},[159,718,214],{"class":191},[159,720,297],{"class":195},[159,722,723,725],{"class":80,"line":220},[159,724,214],{"class":191},[159,726,310],{"class":195},[159,728,729,732,735,737],{"class":80,"line":227},[159,730,731],{"class":191},"from",[159,733,734],{"class":195}," pathlib ",[159,736,214],{"class":191},[159,738,739],{"class":195}," Path\n",[159,741,742],{"class":80,"line":244},[159,743,224],{"emptyLinePlaceholder":223},[159,745,746,748,751],{"class":80,"line":256},[159,747,333],{"class":191},[159,749,750],{"class":336}," encrypt_file",[159,752,753],{"class":195},"(src, dest, password):\n",[159,755,756],{"class":80,"line":330},[159,757,758],{"class":168},"    \"\"\"Encrypt an existing .xlsx, leaving the source untouched.\"\"\"\n",[159,760,761,764,766],{"class":80,"line":351},[159,762,763],{"class":195},"    src, dest ",[159,765,199],{"class":191},[159,767,768],{"class":195}," Path(src), Path(dest)\n",[159,770,771,774,776],{"class":80,"line":357},[159,772,773],{"class":195},"    data ",[159,775,199],{"class":191},[159,777,778],{"class":195}," io.BytesIO(src.read_bytes())\n",[159,780,781],{"class":80,"line":367},[159,782,224],{"emptyLinePlaceholder":223},[159,784,785,787,789],{"class":80,"line":383},[159,786,468],{"class":195},[159,788,199],{"class":191},[159,790,791],{"class":195}," msoffcrypto.OfficeFile(data)\n",[159,793,794,796],{"class":80,"line":388},[159,795,360],{"class":191},[159,797,798],{"class":195}," office.is_encrypted():\n",[159,800,801,803,805,807,810,812,815,818,821,824],{"class":80,"line":399},[159,802,370],{"class":191},[159,804,373],{"class":262},[159,806,266],{"class":195},[159,808,809],{"class":191},"f",[159,811,677],{"class":168},[159,813,814],{"class":680},"{",[159,816,817],{"class":195},"src.name",[159,819,820],{"class":680},"}",[159,822,823],{"class":168}," is already encrypted\"",[159,825,241],{"class":195},[159,827,828],{"class":80,"line":425},[159,829,224],{"emptyLinePlaceholder":223},[159,831,832,834,837,839,841,843],{"class":80,"line":449},[159,833,402],{"class":191},[159,835,836],{"class":195}," dest.open(",[159,838,487],{"class":168},[159,840,417],{"class":195},[159,842,320],{"class":191},[159,844,494],{"class":195},[159,846,847],{"class":80,"line":460},[159,848,500],{"class":195},[159,850,851,853],{"class":80,"line":465},[159,852,506],{"class":191},[159,854,855],{"class":195}," dest\n",[159,857,858],{"class":80,"line":476},[159,859,224],{"emptyLinePlaceholder":223},[159,861,862,865,868,870,873,875,877],{"class":80,"line":497},[159,863,864],{"class":195},"encrypt_file(",[159,866,867],{"class":168},"\"report.xlsx\"",[159,869,540],{"class":195},[159,871,872],{"class":168},"\"report_secure.xlsx\"",[159,874,592],{"class":195},[159,876,238],{"class":168},[159,878,597],{"class":195},[10,880,881],{},"Write to a new path rather than over the source. If encryption fails halfway, an in-place write leaves you with a truncated file and no original — and for a monthly report that is often the only copy.",[145,883,885],{"id":884},"step-3-read-an-encrypted-file-back","Step 3 — Read an encrypted file back",[10,887,888],{},"The reverse direction matters just as much: banks, payroll bureaux and insurers routinely send encrypted workbooks, and an ingest job has to open them.",[150,890,892],{"className":205,"code":891,"language":207,"meta":155,"style":155},"import io\nimport msoffcrypto\nimport pandas as pd\n\ndef read_encrypted(path, password, **read_kwargs):\n    \"\"\"Decrypt an .xlsx in memory and read it with pandas.\"\"\"\n    decrypted = io.BytesIO()\n    with open(path, \"rb\") as fh:\n        office = msoffcrypto.OfficeFile(fh)\n        office.load_key(password=password)\n        office.decrypt(decrypted)\n\n    decrypted.seek(0)\n    return pd.read_excel(decrypted, **read_kwargs)\n\ndf = read_encrypted(\"payroll.xlsx\", os.environ[\"REPORT_PASSWORD\"])\nprint(df.head())\n",[14,893,894,900,906,916,920,936,941,950,966,976,989,994,998,1007,1019,1023,1041],{"__ignoreMap":155},[159,895,896,898],{"class":80,"line":161},[159,897,214],{"class":191},[159,899,297],{"class":195},[159,901,902,904],{"class":80,"line":220},[159,903,214],{"class":191},[159,905,310],{"class":195},[159,907,908,910,912,914],{"class":80,"line":227},[159,909,214],{"class":191},[159,911,317],{"class":195},[159,913,320],{"class":191},[159,915,323],{"class":195},[159,917,918],{"class":80,"line":244},[159,919,224],{"emptyLinePlaceholder":223},[159,921,922,924,927,930,933],{"class":80,"line":256},[159,923,333],{"class":191},[159,925,926],{"class":336}," read_encrypted",[159,928,929],{"class":195},"(path, password, ",[159,931,932],{"class":191},"**",[159,934,935],{"class":195},"read_kwargs):\n",[159,937,938],{"class":80,"line":330},[159,939,940],{"class":168},"    \"\"\"Decrypt an .xlsx in memory and read it with pandas.\"\"\"\n",[159,942,943,946,948],{"class":80,"line":351},[159,944,945],{"class":195},"    decrypted ",[159,947,199],{"class":191},[159,949,396],{"class":195},[159,951,952,954,956,958,960,962,964],{"class":80,"line":357},[159,953,402],{"class":191},[159,955,481],{"class":262},[159,957,484],{"class":195},[159,959,634],{"class":168},[159,961,417],{"class":195},[159,963,320],{"class":191},[159,965,641],{"class":195},[159,967,968,971,973],{"class":80,"line":367},[159,969,970],{"class":195},"        office ",[159,972,199],{"class":191},[159,974,975],{"class":195}," msoffcrypto.OfficeFile(fh)\n",[159,977,978,981,984,986],{"class":80,"line":383},[159,979,980],{"class":195},"        office.load_key(",[159,982,983],{"class":408},"password",[159,985,199],{"class":191},[159,987,988],{"class":195},"password)\n",[159,990,991],{"class":80,"line":388},[159,992,993],{"class":195},"        office.decrypt(decrypted)\n",[159,995,996],{"class":80,"line":399},[159,997,224],{"emptyLinePlaceholder":223},[159,999,1000,1003,1005],{"class":80,"line":425},[159,1001,1002],{"class":195},"    decrypted.seek(",[159,1004,455],{"class":262},[159,1006,241],{"class":195},[159,1008,1009,1011,1014,1016],{"class":80,"line":449},[159,1010,506],{"class":191},[159,1012,1013],{"class":195}," pd.read_excel(decrypted, ",[159,1015,932],{"class":191},[159,1017,1018],{"class":195},"read_kwargs)\n",[159,1020,1021],{"class":80,"line":460},[159,1022,224],{"emptyLinePlaceholder":223},[159,1024,1025,1028,1030,1033,1035,1037,1039],{"class":80,"line":465},[159,1026,1027],{"class":195},"df ",[159,1029,199],{"class":191},[159,1031,1032],{"class":195}," read_encrypted(",[159,1034,589],{"class":168},[159,1036,592],{"class":195},[159,1038,238],{"class":168},[159,1040,597],{"class":195},[159,1042,1043,1045],{"class":80,"line":476},[159,1044,603],{"class":262},[159,1046,1047],{"class":195},"(df.head())\n",[10,1049,1050,1051,1054,1055,1059],{},"The decrypted bytes stay in a ",[14,1052,1053],{},"BytesIO",", so no plaintext copy is written to disk for another process to find. That in-memory handoff is the same technique described in the ",[19,1056,1058],{"href":1057},"\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002F","formats topic"," for reading workbooks from HTTP responses.",[10,1061,1062],{},"A wrong password raises, and the message is not always obvious. Catch it and say something useful:",[150,1064,1066],{"className":205,"code":1065,"language":207,"meta":155,"style":155},"import msoffcrypto\n\ndef read_encrypted_safely(path, password):\n    try:\n        return read_encrypted(path, password)\n    except msoffcrypto.exceptions.InvalidKeyError:\n        raise RuntimeError(\n            f\"Wrong password for {path}. Check REPORT_PASSWORD, and note that \"\n            \"some providers rotate it monthly.\"\n        ) from None\n    except msoffcrypto.exceptions.FileFormatError:\n        raise RuntimeError(\n            f\"{path} is not an encrypted Office file — it may already be plain \"\n            \"or may be a different format entirely.\"\n        ) from None\n",[14,1067,1068,1074,1078,1088,1096,1104,1112,1121,1139,1144,1154,1161,1169,1184,1189],{"__ignoreMap":155},[159,1069,1070,1072],{"class":80,"line":161},[159,1071,214],{"class":191},[159,1073,310],{"class":195},[159,1075,1076],{"class":80,"line":220},[159,1077,224],{"emptyLinePlaceholder":223},[159,1079,1080,1082,1085],{"class":80,"line":227},[159,1081,333],{"class":191},[159,1083,1084],{"class":336}," read_encrypted_safely",[159,1086,1087],{"class":195},"(path, password):\n",[159,1089,1090,1093],{"class":80,"line":244},[159,1091,1092],{"class":191},"    try",[159,1094,1095],{"class":195},":\n",[159,1097,1098,1101],{"class":80,"line":256},[159,1099,1100],{"class":191},"        return",[159,1102,1103],{"class":195}," read_encrypted(path, password)\n",[159,1105,1106,1109],{"class":80,"line":330},[159,1107,1108],{"class":191},"    except",[159,1110,1111],{"class":195}," msoffcrypto.exceptions.InvalidKeyError:\n",[159,1113,1114,1116,1118],{"class":80,"line":351},[159,1115,370],{"class":191},[159,1117,263],{"class":262},[159,1119,1120],{"class":195},"(\n",[159,1122,1123,1126,1129,1131,1134,1136],{"class":80,"line":357},[159,1124,1125],{"class":191},"            f",[159,1127,1128],{"class":168},"\"Wrong password for ",[159,1130,814],{"class":680},[159,1132,1133],{"class":195},"path",[159,1135,820],{"class":680},[159,1137,1138],{"class":168},". Check REPORT_PASSWORD, and note that \"\n",[159,1140,1141],{"class":80,"line":367},[159,1142,1143],{"class":168},"            \"some providers rotate it monthly.\"\n",[159,1145,1146,1149,1151],{"class":80,"line":383},[159,1147,1148],{"class":195},"        ) ",[159,1150,731],{"class":191},[159,1152,1153],{"class":262}," None\n",[159,1155,1156,1158],{"class":80,"line":388},[159,1157,1108],{"class":191},[159,1159,1160],{"class":195}," msoffcrypto.exceptions.FileFormatError:\n",[159,1162,1163,1165,1167],{"class":80,"line":399},[159,1164,370],{"class":191},[159,1166,263],{"class":262},[159,1168,1120],{"class":195},[159,1170,1171,1173,1175,1177,1179,1181],{"class":80,"line":425},[159,1172,1125],{"class":191},[159,1174,677],{"class":168},[159,1176,814],{"class":680},[159,1178,1133],{"class":195},[159,1180,820],{"class":680},[159,1182,1183],{"class":168}," is not an encrypted Office file — it may already be plain \"\n",[159,1185,1186],{"class":80,"line":449},[159,1187,1188],{"class":168},"            \"or may be a different format entirely.\"\n",[159,1190,1191,1193,1195],{"class":80,"line":460},[159,1192,1148],{"class":195},[159,1194,731],{"class":191},[159,1196,1153],{"class":262},[10,1198,1199,1200,1203,1204,23],{},"Never retry on ",[14,1201,1202],{},"InvalidKeyError",". A wrong password will be wrong on the second attempt too, and a retry loop only wastes time — the same reasoning applied to authentication failures in ",[19,1205,1207],{"href":1206},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fretry-a-failed-excel-report-job-in-python\u002F","retrying a failed Excel report job",[145,1209,1211],{"id":1210},"step-4-handle-a-mixed-inbox","Step 4 — Handle a mixed inbox",[10,1213,1214],{},"In practice a folder of incoming files contains both encrypted and plain workbooks, and you cannot tell from the name. Detect and branch:",[25,1216,35,1223,35,1226,35,1229,35,1233,35,1240,35,1245,35,1250,35,1254,35,1259,35,1262,35,1266,35,1271,35,1274,35,1288,35,1299,35,1305,35,1311,35,1315,35,1318,35,1323,35,1327,35,1332,35,1336],{"viewBox":1217,"role":28,"ariaLabel":1218,"ariaLabelledBy":1219,"xmlns":33,"style":1222},"0 0 800 236","Ingest branch for a mixed inbox: the leading bytes decide whether a file is decrypted first or read directly, with a third branch for files whose password is unknown.",[1220,1221],"inbox-t","inbox-d","width:100%;max-width:800px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[37,1224,1225],{"id":1220},"Branching an inbox on whether each file is encrypted",[41,1227,1228],{"id":1221},"Each incoming file is tested by its leading bytes. Files starting with PK are plain zips and go straight to the reader. Files starting with D0 CF 11 E0 are encrypted containers and are decrypted in memory first. A file whose password fails is routed to a quarantine folder with a clear message rather than aborting the whole run.",[45,1230],{"x":455,"y":455,"width":1231,"height":1232,"fill":51},"800","236",[45,1234],{"x":54,"y":1235,"width":1236,"height":93,"rx":1237,"fill":1238,"stroke":1239,"style":61},"88","146","12","#f0f2f5","var(--line,#cdd5e6)",[63,1241,1244],{"x":1242,"y":1243,"style":67},"87","72","inbox\u002F",[63,1246,1249],{"x":1242,"y":1247,"style":1248},"116","font-size:12px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","*.xlsx",[63,1251,1253],{"x":1242,"y":1252,"style":77},"136","mixed sources",[80,1255],{"x1":1256,"y1":1257,"x2":1258,"y2":1257,"stroke":1239,"style":61},"160","121","192",[86,1260],{"points":1261,"fill":89},"200,121 188,115 188,127",[45,1263],{"x":1264,"y":1235,"width":1265,"height":93,"rx":1237,"fill":59,"stroke":60,"style":61},"208","168",[63,1267,1270],{"x":1268,"y":1247,"style":1269},"292","font-size:11.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","read 4 bytes",[63,1272,1273],{"x":1268,"y":1252,"style":77},"PK or D0 CF 11 E0?",[1275,1276,1278,1279,1278,1282,1278,1285,35],"g",{"stroke":60,"style":61,"fill":1277},"none","\n    ",[1133,1280],{"d":1281},"M376 108 H 420 V 44 H 456",[1133,1283],{"d":1284},"M376 121 H 456",[1133,1286],{"d":1287},"M376 134 H 420 V 196 H 456",[1275,1289,1278,1290,1278,1293,1278,1296,35],{"fill":89},[86,1291],{"points":1292},"464,44 452,38 452,50",[86,1294],{"points":1295},"464,121 452,115 452,127",[86,1297],{"points":1298},"464,196 452,190 452,202",[45,1300],{"x":116,"y":1301,"width":1302,"height":1303,"rx":1304,"fill":125,"stroke":126,"style":61},"20","312","48","11",[63,1306,1310],{"x":1307,"y":1308,"style":1309},"628","40","font-size:11.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","PK → plain, read directly",[63,1312,1314],{"x":1307,"y":1313,"style":77},"58","pd.read_excel(path)",[45,1316],{"x":116,"y":1317,"width":1302,"height":1303,"rx":1304,"fill":96,"stroke":97,"style":61},"97",[63,1319,1322],{"x":1307,"y":1320,"style":1321},"117","font-size:11.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","OLE2 → decrypt in memory",[63,1324,1326],{"x":1307,"y":1325,"style":77},"135","load_key, decrypt, then read",[45,1328],{"x":116,"y":1329,"width":1302,"height":1303,"rx":1304,"fill":1330,"stroke":1331,"style":61},"172","#fee8f2","var(--accent,#f43f8f)",[63,1333,1335],{"x":1307,"y":1258,"style":1334},"font-size:11.5px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","password fails → quarantine",[63,1337,1339],{"x":1307,"y":1338,"style":77},"210","the batch keeps going",[150,1341,1343],{"className":205,"code":1342,"language":207,"meta":155,"style":155},"import io\nimport os\nimport shutil\nfrom pathlib import Path\nimport msoffcrypto\nimport pandas as pd\n\nOLE2 = b\"\\xd0\\xcf\\x11\\xe0\"\n\ndef read_maybe_encrypted(path, password=None):\n    \"\"\"Read an .xlsx whether or not it is encrypted.\"\"\"\n    path = Path(path)\n    head = path.open(\"rb\").read(4)\n\n    if head != OLE2:\n        return pd.read_excel(path)          # ordinary zip-based workbook\n\n    if not password:\n        raise RuntimeError(f\"{path.name} is encrypted but no password was given\")\n\n    buffer = io.BytesIO()\n    with path.open(\"rb\") as fh:\n        office = msoffcrypto.OfficeFile(fh)\n        office.load_key(password=password)\n        office.decrypt(buffer)\n    buffer.seek(0)\n    return pd.read_excel(buffer)\n\nquarantine = Path(\"quarantine\")\nquarantine.mkdir(exist_ok=True)\n\nfor src in sorted(Path(\"inbox\").glob(\"*.xlsx\")):\n    try:\n        frame = read_maybe_encrypted(src, os.environ.get(\"REPORT_PASSWORD\"))\n        print(f\"{src.name:\u003C32} {frame.shape[0]:>6} rows\")\n    except Exception as exc:\n        shutil.move(str(src), quarantine \u002F src.name)\n        print(f\"{src.name:\u003C32} quarantined: {exc}\")\n",[14,1344,1345,1351,1357,1364,1374,1380,1390,1394,1411,1415,1432,1437,1447,1465,1469,1483,1494,1498,1506,1530,1534,1543,1557,1565,1575,1580,1589,1597,1602,1618,1634,1639,1669,1676,1692,1734,1748,1766],{"__ignoreMap":155},[159,1346,1347,1349],{"class":80,"line":161},[159,1348,214],{"class":191},[159,1350,297],{"class":195},[159,1352,1353,1355],{"class":80,"line":220},[159,1354,214],{"class":191},[159,1356,217],{"class":195},[159,1358,1359,1361],{"class":80,"line":227},[159,1360,214],{"class":191},[159,1362,1363],{"class":195}," shutil\n",[159,1365,1366,1368,1370,1372],{"class":80,"line":244},[159,1367,731],{"class":191},[159,1369,734],{"class":195},[159,1371,214],{"class":191},[159,1373,739],{"class":195},[159,1375,1376,1378],{"class":80,"line":256},[159,1377,214],{"class":191},[159,1379,310],{"class":195},[159,1381,1382,1384,1386,1388],{"class":80,"line":330},[159,1383,214],{"class":191},[159,1385,317],{"class":195},[159,1387,320],{"class":191},[159,1389,323],{"class":195},[159,1391,1392],{"class":80,"line":351},[159,1393,224],{"emptyLinePlaceholder":223},[159,1395,1396,1399,1402,1404,1406,1408],{"class":80,"line":357},[159,1397,1398],{"class":262},"OLE2",[159,1400,1401],{"class":191}," =",[159,1403,674],{"class":191},[159,1405,677],{"class":168},[159,1407,681],{"class":680},[159,1409,1410],{"class":168},"\"\n",[159,1412,1413],{"class":80,"line":367},[159,1414,224],{"emptyLinePlaceholder":223},[159,1416,1417,1419,1422,1425,1427,1430],{"class":80,"line":383},[159,1418,333],{"class":191},[159,1420,1421],{"class":336}," read_maybe_encrypted",[159,1423,1424],{"class":195},"(path, password",[159,1426,199],{"class":191},[159,1428,1429],{"class":262},"None",[159,1431,348],{"class":195},[159,1433,1434],{"class":80,"line":388},[159,1435,1436],{"class":168},"    \"\"\"Read an .xlsx whether or not it is encrypted.\"\"\"\n",[159,1438,1439,1442,1444],{"class":80,"line":399},[159,1440,1441],{"class":195},"    path ",[159,1443,199],{"class":191},[159,1445,1446],{"class":195}," Path(path)\n",[159,1448,1449,1451,1453,1456,1458,1461,1463],{"class":80,"line":425},[159,1450,646],{"class":195},[159,1452,199],{"class":191},[159,1454,1455],{"class":195}," path.open(",[159,1457,634],{"class":168},[159,1459,1460],{"class":195},").read(",[159,1462,654],{"class":262},[159,1464,241],{"class":195},[159,1466,1467],{"class":80,"line":449},[159,1468,224],{"emptyLinePlaceholder":223},[159,1470,1471,1473,1475,1478,1481],{"class":80,"line":460},[159,1472,360],{"class":191},[159,1474,668],{"class":195},[159,1476,1477],{"class":191},"!=",[159,1479,1480],{"class":262}," OLE2",[159,1482,1095],{"class":195},[159,1484,1485,1487,1490],{"class":80,"line":465},[159,1486,1100],{"class":191},[159,1488,1489],{"class":195}," pd.read_excel(path)          ",[159,1491,1493],{"class":1492},"s-wDw","# ordinary zip-based workbook\n",[159,1495,1496],{"class":80,"line":476},[159,1497,224],{"emptyLinePlaceholder":223},[159,1499,1500,1502,1504],{"class":80,"line":497},[159,1501,360],{"class":191},[159,1503,250],{"class":191},[159,1505,253],{"class":195},[159,1507,1508,1510,1512,1514,1516,1518,1520,1523,1525,1528],{"class":80,"line":503},[159,1509,370],{"class":191},[159,1511,263],{"class":262},[159,1513,266],{"class":195},[159,1515,809],{"class":191},[159,1517,677],{"class":168},[159,1519,814],{"class":680},[159,1521,1522],{"class":195},"path.name",[159,1524,820],{"class":680},[159,1526,1527],{"class":168}," is encrypted but no password was given\"",[159,1529,241],{"class":195},[159,1531,1532],{"class":80,"line":512},[159,1533,224],{"emptyLinePlaceholder":223},[159,1535,1536,1539,1541],{"class":80,"line":517},[159,1537,1538],{"class":195},"    buffer ",[159,1540,199],{"class":191},[159,1542,396],{"class":195},[159,1544,1545,1547,1549,1551,1553,1555],{"class":80,"line":528},[159,1546,402],{"class":191},[159,1548,1455],{"class":195},[159,1550,634],{"class":168},[159,1552,417],{"class":195},[159,1554,320],{"class":191},[159,1556,641],{"class":195},[159,1558,1559,1561,1563],{"class":80,"line":554},[159,1560,970],{"class":195},[159,1562,199],{"class":191},[159,1564,975],{"class":195},[159,1566,1567,1569,1571,1573],{"class":80,"line":577},[159,1568,980],{"class":195},[159,1570,983],{"class":408},[159,1572,199],{"class":191},[159,1574,988],{"class":195},[159,1576,1577],{"class":80,"line":583},[159,1578,1579],{"class":195},"        office.decrypt(buffer)\n",[159,1581,1582,1585,1587],{"class":80,"line":600},[159,1583,1584],{"class":195},"    buffer.seek(",[159,1586,455],{"class":262},[159,1588,241],{"class":195},[159,1590,1592,1594],{"class":80,"line":1591},27,[159,1593,506],{"class":191},[159,1595,1596],{"class":195}," pd.read_excel(buffer)\n",[159,1598,1600],{"class":80,"line":1599},28,[159,1601,224],{"emptyLinePlaceholder":223},[159,1603,1605,1608,1610,1613,1616],{"class":80,"line":1604},29,[159,1606,1607],{"class":195},"quarantine ",[159,1609,199],{"class":191},[159,1611,1612],{"class":195}," Path(",[159,1614,1615],{"class":168},"\"quarantine\"",[159,1617,241],{"class":195},[159,1619,1621,1624,1627,1629,1632],{"class":80,"line":1620},30,[159,1622,1623],{"class":195},"quarantine.mkdir(",[159,1625,1626],{"class":408},"exist_ok",[159,1628,199],{"class":191},[159,1630,1631],{"class":262},"True",[159,1633,241],{"class":195},[159,1635,1637],{"class":80,"line":1636},31,[159,1638,224],{"emptyLinePlaceholder":223},[159,1640,1642,1645,1648,1651,1654,1657,1660,1663,1666],{"class":80,"line":1641},32,[159,1643,1644],{"class":191},"for",[159,1646,1647],{"class":195}," src ",[159,1649,1650],{"class":191},"in",[159,1652,1653],{"class":262}," sorted",[159,1655,1656],{"class":195},"(Path(",[159,1658,1659],{"class":168},"\"inbox\"",[159,1661,1662],{"class":195},").glob(",[159,1664,1665],{"class":168},"\"*.xlsx\"",[159,1667,1668],{"class":195},")):\n",[159,1670,1672,1674],{"class":80,"line":1671},33,[159,1673,1092],{"class":191},[159,1675,1095],{"class":195},[159,1677,1679,1682,1684,1687,1689],{"class":80,"line":1678},34,[159,1680,1681],{"class":195},"        frame ",[159,1683,199],{"class":191},[159,1685,1686],{"class":195}," read_maybe_encrypted(src, os.environ.get(",[159,1688,238],{"class":168},[159,1690,1691],{"class":195},"))\n",[159,1693,1695,1698,1700,1702,1704,1706,1708,1711,1713,1716,1719,1721,1724,1727,1729,1732],{"class":80,"line":1694},35,[159,1696,1697],{"class":262},"        print",[159,1699,266],{"class":195},[159,1701,809],{"class":191},[159,1703,677],{"class":168},[159,1705,814],{"class":680},[159,1707,817],{"class":195},[159,1709,1710],{"class":191},":\u003C32",[159,1712,820],{"class":680},[159,1714,1715],{"class":680}," {",[159,1717,1718],{"class":195},"frame.shape[",[159,1720,455],{"class":262},[159,1722,1723],{"class":195},"]",[159,1725,1726],{"class":191},":>6",[159,1728,820],{"class":680},[159,1730,1731],{"class":168}," rows\"",[159,1733,241],{"class":195},[159,1735,1737,1739,1742,1745],{"class":80,"line":1736},36,[159,1738,1108],{"class":191},[159,1740,1741],{"class":262}," Exception",[159,1743,1744],{"class":191}," as",[159,1746,1747],{"class":195}," exc:\n",[159,1749,1751,1754,1757,1760,1763],{"class":80,"line":1750},37,[159,1752,1753],{"class":195},"        shutil.move(",[159,1755,1756],{"class":262},"str",[159,1758,1759],{"class":195},"(src), quarantine ",[159,1761,1762],{"class":191},"\u002F",[159,1764,1765],{"class":195}," src.name)\n",[159,1767,1769,1771,1773,1775,1777,1779,1781,1783,1785,1788,1790,1793,1795,1797],{"class":80,"line":1768},38,[159,1770,1697],{"class":262},[159,1772,266],{"class":195},[159,1774,809],{"class":191},[159,1776,677],{"class":168},[159,1778,814],{"class":680},[159,1780,817],{"class":195},[159,1782,1710],{"class":191},[159,1784,820],{"class":680},[159,1786,1787],{"class":168}," quarantined: ",[159,1789,814],{"class":680},[159,1791,1792],{"class":195},"exc",[159,1794,820],{"class":680},[159,1796,677],{"class":168},[159,1798,241],{"class":195},[10,1800,1801,1802,23],{},"Quarantining rather than raising keeps one bad file from costing you the other forty. The sniffing technique generalises to every format, as described in ",[19,1803,1804],{"href":1057},"handling Excel file formats and conversions",[145,1806,1808],{"id":1807},"common-pitfalls-and-fixes","Common pitfalls and fixes",[1810,1811,1812,1828],"table",{},[1813,1814,1815],"thead",{},[1816,1817,1818,1822,1825],"tr",{},[1819,1820,1821],"th",{},"Symptom",[1819,1823,1824],{},"Cause",[1819,1826,1827],{},"Fix",[1829,1830,1831,1853,1865,1878,1892,1906,1917,1928],"tbody",{},[1816,1832,1833,1844,1847],{},[1834,1835,1836,1839,1840,1843],"td",{},[14,1837,1838],{},"AttributeError"," on openpyxl's ",[14,1841,1842],{},"save"," with a password",[1834,1845,1846],{},"openpyxl cannot encrypt",[1834,1848,1849,1850,1852],{},"Use ",[14,1851,16],{}," as shown here.",[1816,1854,1855,1859,1862],{},[1834,1856,1857],{},[14,1858,1202],{},[1834,1860,1861],{},"Wrong password",[1834,1863,1864],{},"Check the environment variable; do not retry.",[1816,1866,1867,1872,1875],{},[1834,1868,1869],{},[14,1870,1871],{},"FileFormatError",[1834,1873,1874],{},"File is not an encrypted Office document",[1834,1876,1877],{},"Sniff the leading bytes and branch.",[1816,1879,1880,1883,1886],{},[1834,1881,1882],{},"Encrypted file opens without a prompt",[1834,1884,1885],{},"The encrypt step silently no-opped",[1834,1887,1888,1889,23],{},"Assert the output starts with ",[14,1890,1891],{},"D0 CF 11 E0",[1816,1893,1894,1900,1903],{},[1834,1895,1896,1899],{},[14,1897,1898],{},"pd.read_excel"," fails on the encrypted file",[1834,1901,1902],{},"Reading before decrypting",[1834,1904,1905],{},"Decrypt to a buffer, then read the buffer.",[1816,1907,1908,1911,1914],{},[1834,1909,1910],{},"Password visible in logs",[1834,1912,1913],{},"Password interpolated into a message",[1834,1915,1916],{},"Never format the secret into log output.",[1816,1918,1919,1922,1925],{},[1834,1920,1921],{},"Recipients cannot open it",[1834,1923,1924],{},"Password sent in the same email",[1834,1926,1927],{},"Send the password by a different channel.",[1816,1929,1930,1933,1936],{},[1834,1931,1932],{},"Plain copy left on disk",[1834,1934,1935],{},"Encrypting a file written to disk first",[1834,1937,1938,1939,1941],{},"Build in a ",[14,1940,1053],{}," and encrypt from memory.",[145,1943,1945],{"id":1944},"performance-and-scale-notes","Performance and scale notes",[25,1947,35,1953,35,1956,35,1959,35,1961,35,1966,35,1972,35,1976,35,1980,35,1985,35,1991,35,1995,35,1998,35,2004,35,2006,35,2009,35,2041,35,2046],{"viewBox":1948,"role":28,"ariaLabel":1949,"ariaLabelledBy":1950,"xmlns":33,"style":1222},"0 0 800 220","Cost profile of encryption: a large fixed key-derivation cost per file plus a small cost proportional to data size, so many small files cost far more in total than one large one.",[1951,1952],"kdf-t","kdf-d",[37,1954,1955],{"id":1951},"Encryption cost is mostly a fixed price per file",[41,1957,1958],{"id":1952},"Two bars comparing total time. Encrypting one twenty-megabyte report pays the key derivation cost once, with a modest data segment on top. Encrypting twenty one-megabyte reports pays that same derivation cost twenty times, so the total is dominated by repeated key derivation even though the data volume is identical. The practical consequence is that a fan-out job should parallelise across files rather than trying to make each one faster.",[45,1960],{"x":455,"y":455,"width":1231,"height":94,"fill":51},[45,1962],{"x":1963,"y":1964,"width":54,"height":54,"rx":1965,"fill":89},"238","18","3",[63,1967,1971],{"x":1968,"y":1969,"style":1970},"260","29","font-size:11px;fill:var(--muted,#5b6780)","key derivation (fixed per file)",[45,1973],{"x":1974,"y":1964,"width":54,"height":54,"rx":1965,"fill":1975},"500","var(--teal,#14b8a6)",[63,1977,1979],{"x":1978,"y":1969,"style":1970},"522","encrypting the bytes",[45,1981],{"x":54,"y":1982,"width":111,"height":1983,"rx":1984,"fill":1238,"stroke":1239},"56","30","6",[63,1986,1990],{"x":1987,"y":1988,"style":1989},"84","76","font-size:10.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","one 20 MB file",[45,1992],{"x":82,"y":1313,"width":1993,"height":1994,"rx":654,"fill":89},"86","28",[45,1996],{"x":1997,"y":1313,"width":57,"height":1994,"rx":654,"fill":1975},"250",[63,1999,2003],{"x":2000,"y":2001,"style":2002},"336","77","font-size:10.5px;fill:var(--muted,#5b6780)","derivation paid once",[45,2005],{"x":54,"y":106,"width":111,"height":1983,"rx":1984,"fill":1238,"stroke":1239},[63,2007,2008],{"x":1987,"y":111,"style":1989},"20 × 1 MB files",[1275,2010,1278,2011,1278,2014,1278,2017,1278,2020,1278,2023,1278,2026,1278,2029,1278,2032,1278,2035,1278,2038,35],{},[45,2012],{"x":82,"y":2013,"width":1993,"height":1994,"rx":654,"fill":89},"122",[45,2015],{"x":1997,"y":2013,"width":654,"height":1994,"rx":2016,"fill":1975},"1",[45,2018],{"x":2019,"y":2013,"width":1993,"height":1994,"rx":654,"fill":89},"256",[45,2021],{"x":2022,"y":2013,"width":654,"height":1994,"rx":2016,"fill":1975},"342",[45,2024],{"x":2025,"y":2013,"width":1993,"height":1994,"rx":654,"fill":89},"348",[45,2027],{"x":2028,"y":2013,"width":654,"height":1994,"rx":2016,"fill":1975},"434",[45,2030],{"x":2031,"y":2013,"width":1993,"height":1994,"rx":654,"fill":89},"440",[45,2033],{"x":2034,"y":2013,"width":654,"height":1994,"rx":2016,"fill":1975},"526",[45,2036],{"x":2037,"y":2013,"width":1993,"height":1994,"rx":654,"fill":89},"532",[45,2039],{"x":2040,"y":2013,"width":654,"height":1994,"rx":2016,"fill":1975},"618",[63,2042,2045],{"x":129,"y":2043,"style":2044},"141","font-size:10.5px;font-weight:700;fill:var(--accent-ink,#be185d)","… and 15 more",[63,2047,2048],{"x":140,"y":1258,"style":142},"same data volume, far more total work — parallelise across files, not within one",[10,2050,2051],{},"Encryption cost is dominated by key derivation, not by the data. The agile scheme runs many hashing rounds by design — making a guessed password expensive to test — and that fixed cost lands on every call, so encrypting fifty small reports costs roughly fifty times as much as encrypting one, regardless of how small they are.",[10,2053,2054],{},"For a fan-out job producing one file per recipient, that is worth measuring:",[150,2056,2058],{"className":205,"code":2057,"language":207,"meta":155,"style":155},"import io, os, time\nimport msoffcrypto\nimport pandas as pd\n\ndf = pd.DataFrame({\"region\": [\"North\"], \"revenue\": [159.92]})\npassword = os.environ[\"REPORT_PASSWORD\"]\n\nstart = time.perf_counter()\nfor i in range(10):\n    buf = io.BytesIO()\n    with pd.ExcelWriter(buf, engine=\"xlsxwriter\") as writer:\n        df.to_excel(writer, index=False)\n    buf.seek(0)\n    with open(f\"out_{i}.xlsx\", \"wb\") as fh:\n        msoffcrypto.OfficeFile(buf).encrypt(password, fh)\nprint(f\"{(time.perf_counter() - start) \u002F 10:.2f}s per file\")\n",[14,2059,2060,2067,2073,2083,2087,2118,2132,2136,2146,2165,2174,2193,2205,2214,2247,2252],{"__ignoreMap":155},[159,2061,2062,2064],{"class":80,"line":161},[159,2063,214],{"class":191},[159,2065,2066],{"class":195}," io, os, time\n",[159,2068,2069,2071],{"class":80,"line":220},[159,2070,214],{"class":191},[159,2072,310],{"class":195},[159,2074,2075,2077,2079,2081],{"class":80,"line":227},[159,2076,214],{"class":191},[159,2078,317],{"class":195},[159,2080,320],{"class":191},[159,2082,323],{"class":195},[159,2084,2085],{"class":80,"line":244},[159,2086,224],{"emptyLinePlaceholder":223},[159,2088,2089,2091,2093,2096,2099,2101,2104,2107,2110,2112,2115],{"class":80,"line":256},[159,2090,1027],{"class":195},[159,2092,199],{"class":191},[159,2094,2095],{"class":195}," pd.DataFrame({",[159,2097,2098],{"class":168},"\"region\"",[159,2100,534],{"class":195},[159,2102,2103],{"class":168},"\"North\"",[159,2105,2106],{"class":195},"], ",[159,2108,2109],{"class":168},"\"revenue\"",[159,2111,534],{"class":195},[159,2113,2114],{"class":262},"159.92",[159,2116,2117],{"class":195},"]})\n",[159,2119,2120,2122,2124,2127,2129],{"class":80,"line":330},[159,2121,230],{"class":195},[159,2123,199],{"class":191},[159,2125,2126],{"class":195}," os.environ[",[159,2128,238],{"class":168},[159,2130,2131],{"class":195},"]\n",[159,2133,2134],{"class":80,"line":351},[159,2135,224],{"emptyLinePlaceholder":223},[159,2137,2138,2141,2143],{"class":80,"line":357},[159,2139,2140],{"class":195},"start ",[159,2142,199],{"class":191},[159,2144,2145],{"class":195}," time.perf_counter()\n",[159,2147,2148,2150,2153,2155,2158,2160,2163],{"class":80,"line":367},[159,2149,1644],{"class":191},[159,2151,2152],{"class":195}," i ",[159,2154,1650],{"class":191},[159,2156,2157],{"class":262}," range",[159,2159,266],{"class":195},[159,2161,2162],{"class":262},"10",[159,2164,348],{"class":195},[159,2166,2167,2170,2172],{"class":80,"line":383},[159,2168,2169],{"class":195},"    buf ",[159,2171,199],{"class":191},[159,2173,396],{"class":195},[159,2175,2176,2178,2181,2183,2185,2187,2189,2191],{"class":80,"line":388},[159,2177,402],{"class":191},[159,2179,2180],{"class":195}," pd.ExcelWriter(buf, ",[159,2182,409],{"class":408},[159,2184,199],{"class":191},[159,2186,414],{"class":168},[159,2188,417],{"class":195},[159,2190,320],{"class":191},[159,2192,422],{"class":195},[159,2194,2195,2197,2199,2201,2203],{"class":80,"line":399},[159,2196,428],{"class":195},[159,2198,439],{"class":408},[159,2200,199],{"class":191},[159,2202,444],{"class":262},[159,2204,241],{"class":195},[159,2206,2207,2210,2212],{"class":80,"line":425},[159,2208,2209],{"class":195},"    buf.seek(",[159,2211,455],{"class":262},[159,2213,241],{"class":195},[159,2215,2216,2218,2220,2222,2224,2227,2229,2232,2234,2237,2239,2241,2243,2245],{"class":80,"line":449},[159,2217,402],{"class":191},[159,2219,481],{"class":262},[159,2221,266],{"class":195},[159,2223,809],{"class":191},[159,2225,2226],{"class":168},"\"out_",[159,2228,814],{"class":680},[159,2230,2231],{"class":195},"i",[159,2233,820],{"class":680},[159,2235,2236],{"class":168},".xlsx\"",[159,2238,540],{"class":195},[159,2240,487],{"class":168},[159,2242,417],{"class":195},[159,2244,320],{"class":191},[159,2246,641],{"class":195},[159,2248,2249],{"class":80,"line":460},[159,2250,2251],{"class":195},"        msoffcrypto.OfficeFile(buf).encrypt(password, fh)\n",[159,2253,2254,2256,2258,2260,2262,2264,2267,2270,2273,2275,2278,2281,2283,2286],{"class":80,"line":465},[159,2255,603],{"class":262},[159,2257,266],{"class":195},[159,2259,809],{"class":191},[159,2261,677],{"class":168},[159,2263,814],{"class":680},[159,2265,2266],{"class":195},"(time.perf_counter() ",[159,2268,2269],{"class":191},"-",[159,2271,2272],{"class":195}," start) ",[159,2274,1762],{"class":191},[159,2276,2277],{"class":262}," 10",[159,2279,2280],{"class":191},":.2f",[159,2282,820],{"class":680},[159,2284,2285],{"class":168},"s per file\"",[159,2287,241],{"class":195},[10,2289,2290,2291,2295],{},"Because each file is independent, the work parallelises cleanly across processes — the same shape as the per-region loop in ",[19,2292,2294],{"href":2293},"\u002Fautomating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Fgenerate-one-excel-report-per-region-in-a-loop\u002F","generating one Excel report per region",":",[150,2297,2299],{"className":205,"code":2298,"language":207,"meta":155,"style":155},"from concurrent.futures import ProcessPoolExecutor\n\ndef build_one(region):\n    frame = load_region(region)                     # your data step\n    return write_encrypted(frame, f\"{region}.xlsx\", os.environ[\"REPORT_PASSWORD\"])\n\nif __name__ == \"__main__\":\n    with ProcessPoolExecutor(max_workers=4) as pool:\n        for path in pool.map(build_one, [\"north\", \"south\", \"west\", \"east\"]):\n            print(\"wrote\", path)\n",[14,2300,2301,2313,2317,2327,2340,2366,2370,2385,2406,2440],{"__ignoreMap":155},[159,2302,2303,2305,2308,2310],{"class":80,"line":161},[159,2304,731],{"class":191},[159,2306,2307],{"class":195}," concurrent.futures ",[159,2309,214],{"class":191},[159,2311,2312],{"class":195}," ProcessPoolExecutor\n",[159,2314,2315],{"class":80,"line":220},[159,2316,224],{"emptyLinePlaceholder":223},[159,2318,2319,2321,2324],{"class":80,"line":227},[159,2320,333],{"class":191},[159,2322,2323],{"class":336}," build_one",[159,2325,2326],{"class":195},"(region):\n",[159,2328,2329,2332,2334,2337],{"class":80,"line":244},[159,2330,2331],{"class":195},"    frame ",[159,2333,199],{"class":191},[159,2335,2336],{"class":195}," load_region(region)                     ",[159,2338,2339],{"class":1492},"# your data step\n",[159,2341,2342,2344,2347,2349,2351,2353,2356,2358,2360,2362,2364],{"class":80,"line":256},[159,2343,506],{"class":191},[159,2345,2346],{"class":195}," write_encrypted(frame, ",[159,2348,809],{"class":191},[159,2350,677],{"class":168},[159,2352,814],{"class":680},[159,2354,2355],{"class":195},"region",[159,2357,820],{"class":680},[159,2359,2236],{"class":168},[159,2361,592],{"class":195},[159,2363,238],{"class":168},[159,2365,597],{"class":195},[159,2367,2368],{"class":80,"line":330},[159,2369,224],{"emptyLinePlaceholder":223},[159,2371,2372,2374,2377,2380,2383],{"class":80,"line":351},[159,2373,247],{"class":191},[159,2375,2376],{"class":262}," __name__",[159,2378,2379],{"class":191}," ==",[159,2381,2382],{"class":168}," \"__main__\"",[159,2384,1095],{"class":195},[159,2386,2387,2389,2392,2395,2397,2399,2401,2403],{"class":80,"line":357},[159,2388,402],{"class":191},[159,2390,2391],{"class":195}," ProcessPoolExecutor(",[159,2393,2394],{"class":408},"max_workers",[159,2396,199],{"class":191},[159,2398,654],{"class":262},[159,2400,417],{"class":195},[159,2402,320],{"class":191},[159,2404,2405],{"class":195}," pool:\n",[159,2407,2408,2411,2414,2416,2419,2422,2424,2427,2429,2432,2434,2437],{"class":80,"line":367},[159,2409,2410],{"class":191},"        for",[159,2412,2413],{"class":195}," path ",[159,2415,1650],{"class":191},[159,2417,2418],{"class":195}," pool.map(build_one, [",[159,2420,2421],{"class":168},"\"north\"",[159,2423,540],{"class":195},[159,2425,2426],{"class":168},"\"south\"",[159,2428,540],{"class":195},[159,2430,2431],{"class":168},"\"west\"",[159,2433,540],{"class":195},[159,2435,2436],{"class":168},"\"east\"",[159,2438,2439],{"class":195},"]):\n",[159,2441,2442,2445,2447,2450],{"class":80,"line":383},[159,2443,2444],{"class":262},"            print",[159,2446,266],{"class":195},[159,2448,2449],{"class":168},"\"wrote\"",[159,2451,2452],{"class":195},", path)\n",[10,2454,2455,2456,2459,2460,2464],{},"Two memory notes. Encryption holds the whole workbook in memory twice — once plain, once encrypted — so a 200 MB report needs headroom for both. And decryption cannot stream: ",[14,2457,2458],{},"office.decrypt"," writes the entire plaintext to the target buffer before pandas reads a byte, so the streaming techniques in ",[19,2461,2463],{"href":2462},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-large-excel-files-in-python\u002Fread-large-excel-file-in-chunks-with-pandas\u002F","reading large Excel files in chunks"," apply only after the decryption completes. For very large encrypted inputs, decrypt to a temporary file on a disk you control rather than into RAM.",[145,2466,2468],{"id":2467},"conclusion","Conclusion",[10,2470,2471,2472,2475,2476,2478,2479,2481],{},"Real password protection for an ",[14,2473,2474],{},".xlsx"," means encrypting the container, and that is a job for ",[14,2477,16],{}," rather than openpyxl. Build the workbook in a ",[14,2480,1053],{},", encrypt straight from memory so no plaintext copy ever lands on disk, and assert that the output really starts with the OLE2 signature. On the way in, sniff the leading bytes to tell encrypted files from plain ones, quarantine what will not open, and never retry a wrong password. Then keep the secret in the environment, and send it to recipients by a channel other than the one carrying the file.",[145,2483,2485],{"id":2484},"frequently-asked-questions","Frequently asked questions",[10,2487,2488,2492,2493,2495],{},[2489,2490,2491],"strong",{},"Can openpyxl set a file open password?","\nNo. openpyxl writes sheet and workbook protection, which only restricts editing once the file is open. Encrypting the container needs ",[14,2494,16],{},", or a headless LibreOffice save with a password.",[10,2497,2498,2504],{},[2489,2499,2500,2501,2503],{},"What encryption does an ",[14,2502,2474],{}," actually use?","\nModern Office files use the agile encryption scheme built on AES with a key derived from the password by repeated SHA-512 hashing. It is genuine encryption, so the strength rests almost entirely on how good the password is.",[10,2506,2507,2510,2512,2513,2516,2517,2519,2520,23],{},[2489,2508,2509],{},"How do I tell whether a file is encrypted before opening it?",[14,2511,284],{},"'s ",[14,2514,2515],{},"is_encrypted"," method answers it, and the cheap structural check is that an encrypted workbook is an OLE2 container rather than a zip — its first bytes are ",[14,2518,1891],{}," rather than ",[14,2521,2522],{},"PK",[10,2524,2525,2528,2529,2532,2533,2536,2537,2539],{},[2489,2526,2527],{},"Can I remove the password from a file I have the password for?","\nYes. Decrypt it to a buffer with ",[14,2530,2531],{},"load_key"," and ",[14,2534,2535],{},"decrypt",", then write those bytes out as a normal ",[14,2538,2474],{},". That is the standard first step when ingesting encrypted files from a provider.",[10,2541,2542,2545],{},[2489,2543,2544],{},"Where should the password live in a scheduled job?","\nIn an environment variable injected by the scheduler, or in a secrets manager the job reads at start-up. Never in the script, and never in a config file committed to the repository.",[145,2547,2549],{"id":2548},"related","Related",[2551,2552,2553,2560,2567,2573,2580],"ul",{},[2554,2555,2556,2557,2559],"li",{},"Up to the parent: ",[19,2558,22],{"href":21}," — how encryption compares with the other protection layers.",[2554,2561,2562,2566],{},[19,2563,2565],{"href":2564},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fprotecting-and-sharing-excel-workbooks\u002Flock-cells-and-protect-a-sheet-with-openpyxl\u002F","Lock Cells and Protect a Sheet with openpyxl"," — the interface guard that encryption complements.",[2554,2568,2569,2572],{},[19,2570,2571],{"href":1057},"Handling Excel File Formats and Conversions"," — the byte-sniffing technique used to detect encrypted files.",[2554,2574,2575,2579],{},[19,2576,2578],{"href":2577},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fkeep-excel-report-settings-in-a-config-file\u002F","Keep Excel Report Settings in a Config File"," — configuration without secrets in it.",[2554,2581,2582,2586],{},[19,2583,2585],{"href":2584},"\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002F","Emailing Excel Reports with smtplib"," — delivering the encrypted file to its recipients.",[2588,2589,2590],"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 .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}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":155,"searchDepth":220,"depth":220,"links":2592},[2593,2594,2595,2596,2597,2598,2599,2600,2601,2602],{"id":147,"depth":220,"text":148},{"id":277,"depth":220,"text":278},{"id":705,"depth":220,"text":706},{"id":884,"depth":220,"text":885},{"id":1210,"depth":220,"text":1211},{"id":1807,"depth":220,"text":1808},{"id":1944,"depth":220,"text":1945},{"id":2467,"depth":220,"text":2468},{"id":2484,"depth":220,"text":2485},{"id":2548,"depth":220,"text":2549},"2026-08-15","Encrypt an .xlsx so it cannot be opened without a password, using msoffcrypto-tool — write, read back, handle wrong passwords, and keep the secret out of your source code.","md",[2607,2609,2612,2614,2616],{"q":2491,"a":2608},"No. openpyxl writes sheet and workbook protection, which only restricts editing once the file is open. Encrypting the container needs msoffcrypto-tool, or a headless LibreOffice save with a password.",{"q":2610,"a":2611},"What encryption does an .xlsx actually use?","Modern Office files use the agile encryption scheme built on AES with a key derived from the password by repeated SHA-512 hashing. It is genuine encryption, so the strength rests almost entirely on how good the password is.",{"q":2509,"a":2613},"msoffcrypto's is_encrypted method answers it, and the cheap structural check is that an encrypted workbook is an OLE2 container rather than a zip — its first bytes are D0 CF 11 E0 rather than PK.",{"q":2527,"a":2615},"Yes. Decrypt it to a buffer with load_key and decrypt, then write those bytes out as a normal .xlsx. That is the standard first step when ingesting encrypted files from a provider.",{"q":2544,"a":2617},"In an environment variable injected by the scheduler, or in a secrets manager the job reads at start-up. Never in the script, and never in a config file committed to the repository.",{},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fprotecting-and-sharing-excel-workbooks\u002Fpassword-protect-an-excel-file-with-python",{"title":2621,"description":2622},"Password Protect an Excel File in Python (Encryption)","Real file-level encryption for .xlsx from Python with msoffcrypto-tool: encrypt a generated report, decrypt an incoming one, and manage the password safely in a scheduled job.","password-protect-an-excel-file-with-python","formatting-and-charting-excel-reports-with-python\u002Fprotecting-and-sharing-excel-workbooks\u002Fpassword-protect-an-excel-file-with-python\u002Findex","how-to","xHKxOQZem3C1Lx3v81sGyHGhBp_gcaiZ155HxKe94vI",[2628,2631],{"title":2565,"path":2629,"stem":2630,"children":-1},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fprotecting-and-sharing-excel-workbooks\u002Flock-cells-and-protect-a-sheet-with-openpyxl","formatting-and-charting-excel-reports-with-python\u002Fprotecting-and-sharing-excel-workbooks\u002Flock-cells-and-protect-a-sheet-with-openpyxl\u002Findex",{"title":2632,"path":2633,"stem":2634,"children":-1},"Styling Excel Cells with openpyxl","\u002Fformatting-and-charting-excel-reports-with-python\u002Fstyling-excel-cells-with-openpyxl","formatting-and-charting-excel-reports-with-python\u002Fstyling-excel-cells-with-openpyxl\u002Findex",1786800027202]