[{"data":1,"prerenderedAt":2587},["ShallowReactive",2],{"doc:\u002Fformatting-and-charting-excel-reports-with-python\u002Fprotecting-and-sharing-excel-workbooks\u002Fadd-comments-and-notes-to-excel-cells-with-python":3,"surround:\u002Fformatting-and-charting-excel-reports-with-python\u002Fprotecting-and-sharing-excel-workbooks\u002Fadd-comments-and-notes-to-excel-cells-with-python":2579},{"id":4,"title":5,"body":6,"dateModified":2556,"datePublished":2556,"description":2557,"extension":2558,"faq":2559,"meta":2570,"navigation":248,"path":2571,"seo":2572,"slug":2575,"stem":2576,"type":2577,"__hash__":2578},"docs\u002Fformatting-and-charting-excel-reports-with-python\u002Fprotecting-and-sharing-excel-workbooks\u002Fadd-comments-and-notes-to-excel-cells-with-python\u002Findex.md","Add Comments and Notes to Excel Cells with Python",{"type":7,"value":8,"toc":2544},"minimark",[9,19,151,156,184,196,200,207,432,435,496,499,509,631,635,638,1159,1170,1174,1177,1289,1510,1513,1669,1672,1676,1679,1869,1872,2021,2025,2149,2153,2232,2235,2243,2289,2292,2413,2416,2427,2431,2440,2444,2450,2459,2473,2493,2499,2503,2540],[10,11,12,13,18],"p",{},"A number in a report always raises the same question eventually: where did this come from, and why is it that? Answering it in a covering email works until the file is forwarded. Answering it in the cell itself travels with the data. openpyxl attaches comments in two lines, and used well they carry the provenance of a figure — the source system, the as-at date, the assumption behind a forecast — exactly where a reader is looking when the question occurs to them. This guide covers writing, sizing, bulk annotation and reading comments back. It completes ",[14,15,17],"a",{"href":16},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fprotecting-and-sharing-excel-workbooks\u002F","Protecting and Sharing Excel Workbooks",".",[20,21,30,31,30,35,30,39,30,46,30,55,30,62,30,66,30,71,30,76,30,79,30,83,30,86,30,90,30,97,30,102,30,108,30,113,30,117,30,121,30,126,30,131,30,136,30,141,30,145],"svg",{"viewBox":22,"role":23,"ariaLabel":24,"ariaLabelledBy":25,"xmlns":28,"style":29},"0 0 800 250","img","A cell with an attached note: the grid cell carries a small marker, and the hover box shows the explanation text and the author name.",[26,27],"cmt-t","cmt-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:800px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[32,33,34],"title",{"id":26},"What an attached note looks like in the sheet",[36,37,38],"desc",{"id":27},"A small grid fragment showing a Unit price column. The cell holding 12.50 carries a marker in its corner, and a connected callout box displays the note text explaining that the price comes from the Q3 price list effective the first of July, together with the author name Reporting. The note travels inside the file rather than in a covering email.",[40,41],"rect",{"x":42,"y":42,"width":43,"height":44,"fill":45},"0","800","250","#ffffff",[40,47],{"x":48,"y":49,"width":50,"height":48,"rx":51,"fill":52,"stroke":53,"style":54},"30","40","150","5","#ebebfd","var(--brand,#5b5cf0)","stroke-width:2px",[56,57,61],"text",{"x":58,"y":59,"style":60},"105","60","font-size:11.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Unit price",[40,63],{"x":48,"y":64,"width":50,"height":48,"rx":51,"fill":45,"stroke":65,"style":54},"76","var(--line,#cdd5e6)",[56,67,70],{"x":58,"y":68,"style":69},"96","font-size:11.5px;fill:var(--text,#172033);text-anchor:middle","12.50",[72,73],"polygon",{"points":74,"fill":75},"180,76 180,86 170,76","#f43f8f",[40,77],{"x":48,"y":78,"width":50,"height":48,"rx":51,"fill":45,"stroke":65},"112",[56,80,82],{"x":58,"y":81,"style":69},"132","11.00",[40,84],{"x":48,"y":85,"width":50,"height":48,"rx":51,"fill":45,"stroke":65},"148",[56,87,89],{"x":58,"y":88,"style":69},"168","13.25",[91,92],"line",{"x1":93,"y1":94,"x2":95,"y2":59,"stroke":96,"style":54},"180","82","238","var(--gold,#b4740a)",[40,98],{"x":95,"y":48,"width":99,"height":78,"rx":100,"fill":101,"stroke":96,"style":54},"330","10","#fdefd8",[56,103,107],{"x":104,"y":105,"style":106},"258","56","font-size:11.5px;font-weight:700;fill:var(--gold-ink,#7a4e06)","Reporting:",[56,109,112],{"x":104,"y":110,"style":111},"80","font-size:11px;fill:var(--text,#172033)","Unit price from the Q3 price list,",[56,114,116],{"x":104,"y":115,"style":111},"100","effective 1 July.",[56,118,120],{"x":104,"y":119,"style":111},"124","Update the Lookups sheet, not this cell.",[40,122],{"x":123,"y":48,"width":93,"height":78,"rx":100,"fill":124,"stroke":125,"style":54},"596","#d9f4f1","var(--teal,#0f9488)",[56,127,130],{"x":128,"y":59,"style":129},"686","font-size:11.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","it travels",[56,132,135],{"x":128,"y":133,"style":134},"84","font-size:11px;fill:var(--text,#172033);text-anchor:middle","with the file",[56,137,140],{"x":128,"y":138,"style":139},"110","font-size:10.5px;fill:var(--muted,#5b6780);text-anchor:middle","a covering email",[56,142,144],{"x":128,"y":143,"style":139},"128","does not",[56,146,150],{"x":147,"y":148,"style":149},"400","212","font-size:11px;fill:var(--muted,#5b6780);text-anchor:middle","the marker in the corner is the only thing the reader sees until they hover",[152,153,155],"h2",{"id":154},"prerequisites","Prerequisites",[157,158,163],"pre",{"className":159,"code":160,"language":161,"meta":162,"style":162},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install openpyxl pandas\n","bash","",[164,165,166],"code",{"__ignoreMap":162},[167,168,170,174,178,181],"span",{"class":91,"line":169},1,[167,171,173],{"class":172},"sMTad","pip",[167,175,177],{"class":176},"srMev"," install",[167,179,180],{"class":176}," openpyxl",[167,182,183],{"class":176}," pandas\n",[10,185,186,187,191,192,195],{},"A distinction worth having straight before you start. Excel now has two annotation mechanisms: ",[188,189,190],"strong",{},"notes"," (the classic yellow hover boxes) and ",[188,193,194],{},"threaded comments"," (the newer reply-based ones). openpyxl writes notes, and Excel displays them under its Notes menu. That is the right choice for generated annotations — a script is making a statement, not opening a conversation — but it does explain why the annotations you write do not appear where a colleague expects to find \"comments\".",[152,197,199],{"id":198},"step-1-attach-a-comment","Step 1 — Attach a comment",[10,201,202,203,206],{},"The ",[164,204,205],{},"Comment"," object takes text and an author, and you assign it to a cell:",[157,208,212],{"className":209,"code":210,"language":211,"meta":162,"style":162},"language-python shiki shiki-themes github-light github-dark-high-contrast","from openpyxl import Workbook\nfrom openpyxl.comments import Comment\n\nwb = Workbook()\nws = wb.active\nws.title = \"Forecast\"\n\nws[\"A1\"] = \"Region\"\nws[\"B1\"] = \"Unit price\"\nws[\"A2\"], ws[\"B2\"] = \"North\", 12.50\n\nnote = Comment(\n    \"Unit price from the Q3 price list, effective 1 July.\\n\"\n    \"Update the Lookups sheet, not this cell.\",\n    \"Reporting\",\n)\nws[\"B2\"].comment = note\n\nwb.save(\"forecast.xlsx\")\n","python",[164,213,214,230,243,250,262,273,284,289,306,321,349,354,365,378,387,395,401,416,421],{"__ignoreMap":162},[167,215,216,220,224,227],{"class":91,"line":169},[167,217,219],{"class":218},"s-kum","from",[167,221,223],{"class":222},"skGVy"," openpyxl ",[167,225,226],{"class":218},"import",[167,228,229],{"class":222}," Workbook\n",[167,231,233,235,238,240],{"class":91,"line":232},2,[167,234,219],{"class":218},[167,236,237],{"class":222}," openpyxl.comments ",[167,239,226],{"class":218},[167,241,242],{"class":222}," Comment\n",[167,244,246],{"class":91,"line":245},3,[167,247,249],{"emptyLinePlaceholder":248},true,"\n",[167,251,253,256,259],{"class":91,"line":252},4,[167,254,255],{"class":222},"wb ",[167,257,258],{"class":218},"=",[167,260,261],{"class":222}," Workbook()\n",[167,263,265,268,270],{"class":91,"line":264},5,[167,266,267],{"class":222},"ws ",[167,269,258],{"class":218},[167,271,272],{"class":222}," wb.active\n",[167,274,276,279,281],{"class":91,"line":275},6,[167,277,278],{"class":222},"ws.title ",[167,280,258],{"class":218},[167,282,283],{"class":176}," \"Forecast\"\n",[167,285,287],{"class":91,"line":286},7,[167,288,249],{"emptyLinePlaceholder":248},[167,290,292,295,298,301,303],{"class":91,"line":291},8,[167,293,294],{"class":222},"ws[",[167,296,297],{"class":176},"\"A1\"",[167,299,300],{"class":222},"] ",[167,302,258],{"class":218},[167,304,305],{"class":176}," \"Region\"\n",[167,307,309,311,314,316,318],{"class":91,"line":308},9,[167,310,294],{"class":222},[167,312,313],{"class":176},"\"B1\"",[167,315,300],{"class":222},[167,317,258],{"class":218},[167,319,320],{"class":176}," \"Unit price\"\n",[167,322,324,326,329,332,335,337,339,342,345],{"class":91,"line":323},10,[167,325,294],{"class":222},[167,327,328],{"class":176},"\"A2\"",[167,330,331],{"class":222},"], ws[",[167,333,334],{"class":176},"\"B2\"",[167,336,300],{"class":222},[167,338,258],{"class":218},[167,340,341],{"class":176}," \"North\"",[167,343,344],{"class":222},", ",[167,346,348],{"class":347},"sP0c6","12.50\n",[167,350,352],{"class":91,"line":351},11,[167,353,249],{"emptyLinePlaceholder":248},[167,355,357,360,362],{"class":91,"line":356},12,[167,358,359],{"class":222},"note ",[167,361,258],{"class":218},[167,363,364],{"class":222}," Comment(\n",[167,366,368,371,375],{"class":91,"line":367},13,[167,369,370],{"class":176},"    \"Unit price from the Q3 price list, effective 1 July.",[167,372,374],{"class":373},"sSjpA","\\n",[167,376,377],{"class":176},"\"\n",[167,379,381,384],{"class":91,"line":380},14,[167,382,383],{"class":176},"    \"Update the Lookups sheet, not this cell.\"",[167,385,386],{"class":222},",\n",[167,388,390,393],{"class":91,"line":389},15,[167,391,392],{"class":176},"    \"Reporting\"",[167,394,386],{"class":222},[167,396,398],{"class":91,"line":397},16,[167,399,400],{"class":222},")\n",[167,402,404,406,408,411,413],{"class":91,"line":403},17,[167,405,294],{"class":222},[167,407,334],{"class":176},[167,409,410],{"class":222},"].comment ",[167,412,258],{"class":218},[167,414,415],{"class":222}," note\n",[167,417,419],{"class":91,"line":418},18,[167,420,249],{"emptyLinePlaceholder":248},[167,422,424,427,430],{"class":91,"line":423},19,[167,425,426],{"class":222},"wb.save(",[167,428,429],{"class":176},"\"forecast.xlsx\"",[167,431,400],{"class":222},[10,433,434],{},"The default box is small and clips anything beyond a short sentence. Size it to the content — the units are pixels:",[157,436,438],{"className":209,"code":437,"language":211,"meta":162,"style":162},"note = Comment(\"A longer explanation that would otherwise be cut off \"\n               \"by the default comment box size.\", \"Reporting\")\nnote.width = 280\nnote.height = 110\nws[\"B2\"].comment = note\n",[164,439,440,452,464,474,484],{"__ignoreMap":162},[167,441,442,444,446,449],{"class":91,"line":169},[167,443,359],{"class":222},[167,445,258],{"class":218},[167,447,448],{"class":222}," Comment(",[167,450,451],{"class":176},"\"A longer explanation that would otherwise be cut off \"\n",[167,453,454,457,459,462],{"class":91,"line":232},[167,455,456],{"class":176},"               \"by the default comment box size.\"",[167,458,344],{"class":222},[167,460,461],{"class":176},"\"Reporting\"",[167,463,400],{"class":222},[167,465,466,469,471],{"class":91,"line":245},[167,467,468],{"class":222},"note.width ",[167,470,258],{"class":218},[167,472,473],{"class":347}," 280\n",[167,475,476,479,481],{"class":91,"line":252},[167,477,478],{"class":222},"note.height ",[167,480,258],{"class":218},[167,482,483],{"class":347}," 110\n",[167,485,486,488,490,492,494],{"class":91,"line":264},[167,487,294],{"class":222},[167,489,334],{"class":176},[167,491,410],{"class":222},[167,493,258],{"class":218},[167,495,415],{"class":222},[10,497,498],{},"A rough rule that works well: allow about 7 pixels of width per character on the longest line, and about 18 pixels of height per line of text plus a little for the author name.",[10,500,501,502,508],{},"One constraint to know: ",[188,503,504,505,507],{},"a ",[164,506,205],{}," object can be assigned to one cell only",". Reusing the same instance across cells silently moves it rather than copying it, so the annotation ends up on the last cell alone. Build a fresh one each time:",[157,510,512],{"className":209,"code":511,"language":211,"meta":162,"style":162},"from openpyxl.comments import Comment\n\n# Wrong — one object, one cell. B2 and B3 will not both end up annotated.\nshared = Comment(\"Provisional\", \"Reporting\")\nws[\"B2\"].comment = shared\nws[\"B3\"].comment = shared        # B2's note is now gone\n\n# Right — a new object per cell.\nfor ref in (\"B2\", \"B3\"):\n    ws[ref].comment = Comment(\"Provisional\", \"Reporting\")\n",[164,513,514,524,528,534,552,565,582,586,591,614],{"__ignoreMap":162},[167,515,516,518,520,522],{"class":91,"line":169},[167,517,219],{"class":218},[167,519,237],{"class":222},[167,521,226],{"class":218},[167,523,242],{"class":222},[167,525,526],{"class":91,"line":232},[167,527,249],{"emptyLinePlaceholder":248},[167,529,530],{"class":91,"line":245},[167,531,533],{"class":532},"s-wDw","# Wrong — one object, one cell. B2 and B3 will not both end up annotated.\n",[167,535,536,539,541,543,546,548,550],{"class":91,"line":252},[167,537,538],{"class":222},"shared ",[167,540,258],{"class":218},[167,542,448],{"class":222},[167,544,545],{"class":176},"\"Provisional\"",[167,547,344],{"class":222},[167,549,461],{"class":176},[167,551,400],{"class":222},[167,553,554,556,558,560,562],{"class":91,"line":264},[167,555,294],{"class":222},[167,557,334],{"class":176},[167,559,410],{"class":222},[167,561,258],{"class":218},[167,563,564],{"class":222}," shared\n",[167,566,567,569,572,574,576,579],{"class":91,"line":275},[167,568,294],{"class":222},[167,570,571],{"class":176},"\"B3\"",[167,573,410],{"class":222},[167,575,258],{"class":218},[167,577,578],{"class":222}," shared        ",[167,580,581],{"class":532},"# B2's note is now gone\n",[167,583,584],{"class":91,"line":286},[167,585,249],{"emptyLinePlaceholder":248},[167,587,588],{"class":91,"line":291},[167,589,590],{"class":532},"# Right — a new object per cell.\n",[167,592,593,596,599,602,605,607,609,611],{"class":91,"line":308},[167,594,595],{"class":218},"for",[167,597,598],{"class":222}," ref ",[167,600,601],{"class":218},"in",[167,603,604],{"class":222}," (",[167,606,334],{"class":176},[167,608,344],{"class":222},[167,610,571],{"class":176},[167,612,613],{"class":222},"):\n",[167,615,616,619,621,623,625,627,629],{"class":91,"line":323},[167,617,618],{"class":222},"    ws[ref].comment ",[167,620,258],{"class":218},[167,622,448],{"class":222},[167,624,545],{"class":176},[167,626,344],{"class":222},[167,628,461],{"class":176},[167,630,400],{"class":222},[152,632,634],{"id":633},"step-2-annotate-in-bulk-from-a-rules-table","Step 2 — Annotate in bulk from a rules table",[10,636,637],{},"Hand-placing comments does not scale past a few. Drive them from a table of rules, so the annotation logic sits in one readable place:",[157,639,641],{"className":209,"code":640,"language":211,"meta":162,"style":162},"from openpyxl import load_workbook\nfrom openpyxl.comments import Comment\n\nAUTHOR = \"Reporting\"\n\n# (header name, predicate, message)\nRULES = [\n    (\"revenue\", lambda v: isinstance(v, (int, float)) and v == 0,\n     \"Zero revenue — check the source extract ran for this region.\"),\n    (\"revenue\", lambda v: isinstance(v, (int, float)) and v > 100_000,\n     \"Above the usual range. Confirm before circulating.\"),\n    (\"status\", lambda v: v == \"provisional\",\n     \"Provisional figure. Restated at month end.\"),\n]\n\ndef annotate(path, dest, sheet=\"Summary\"):\n    wb = load_workbook(path)\n    ws = wb[sheet]\n\n    headers = {str(c.value).strip(): c.column for c in ws[1] if c.value}\n    added = 0\n\n    for name, predicate, message in RULES:\n        col = headers.get(name)\n        if col is None:\n            continue\n        for (cell,) in ws.iter_rows(min_row=2, min_col=col, max_col=col):\n            if cell.comment is None and predicate(cell.value):\n                note = Comment(message, AUTHOR)\n                note.width, note.height = 260, 80\n                cell.comment = note\n                added += 1\n\n    wb.save(dest)\n    return added\n\nprint(f\"added {annotate('report.xlsx', 'report_annotated.xlsx')} notes\")\n",[164,642,643,654,664,668,679,683,688,698,745,753,789,796,817,824,829,833,852,862,872,876,914,925,930,947,958,975,981,1022,1041,1056,1072,1082,1094,1099,1105,1114,1119],{"__ignoreMap":162},[167,644,645,647,649,651],{"class":91,"line":169},[167,646,219],{"class":218},[167,648,223],{"class":222},[167,650,226],{"class":218},[167,652,653],{"class":222}," load_workbook\n",[167,655,656,658,660,662],{"class":91,"line":232},[167,657,219],{"class":218},[167,659,237],{"class":222},[167,661,226],{"class":218},[167,663,242],{"class":222},[167,665,666],{"class":91,"line":245},[167,667,249],{"emptyLinePlaceholder":248},[167,669,670,673,676],{"class":91,"line":252},[167,671,672],{"class":347},"AUTHOR",[167,674,675],{"class":218}," =",[167,677,678],{"class":176}," \"Reporting\"\n",[167,680,681],{"class":91,"line":264},[167,682,249],{"emptyLinePlaceholder":248},[167,684,685],{"class":91,"line":275},[167,686,687],{"class":532},"# (header name, predicate, message)\n",[167,689,690,693,695],{"class":91,"line":286},[167,691,692],{"class":347},"RULES",[167,694,675],{"class":218},[167,696,697],{"class":222}," [\n",[167,699,700,703,706,708,711,714,717,720,723,725,728,731,734,737,740,743],{"class":91,"line":291},[167,701,702],{"class":222},"    (",[167,704,705],{"class":176},"\"revenue\"",[167,707,344],{"class":222},[167,709,710],{"class":218},"lambda",[167,712,713],{"class":222}," v: ",[167,715,716],{"class":347},"isinstance",[167,718,719],{"class":222},"(v, (",[167,721,722],{"class":347},"int",[167,724,344],{"class":222},[167,726,727],{"class":347},"float",[167,729,730],{"class":222},")) ",[167,732,733],{"class":218},"and",[167,735,736],{"class":222}," v ",[167,738,739],{"class":218},"==",[167,741,742],{"class":347}," 0",[167,744,386],{"class":222},[167,746,747,750],{"class":91,"line":308},[167,748,749],{"class":176},"     \"Zero revenue — check the source extract ran for this region.\"",[167,751,752],{"class":222},"),\n",[167,754,755,757,759,761,763,765,767,769,771,773,775,777,779,781,784,787],{"class":91,"line":323},[167,756,702],{"class":222},[167,758,705],{"class":176},[167,760,344],{"class":222},[167,762,710],{"class":218},[167,764,713],{"class":222},[167,766,716],{"class":347},[167,768,719],{"class":222},[167,770,722],{"class":347},[167,772,344],{"class":222},[167,774,727],{"class":347},[167,776,730],{"class":222},[167,778,733],{"class":218},[167,780,736],{"class":222},[167,782,783],{"class":218},">",[167,785,786],{"class":347}," 100_000",[167,788,386],{"class":222},[167,790,791,794],{"class":91,"line":351},[167,792,793],{"class":176},"     \"Above the usual range. Confirm before circulating.\"",[167,795,752],{"class":222},[167,797,798,800,803,805,807,810,812,815],{"class":91,"line":356},[167,799,702],{"class":222},[167,801,802],{"class":176},"\"status\"",[167,804,344],{"class":222},[167,806,710],{"class":218},[167,808,809],{"class":222}," v: v ",[167,811,739],{"class":218},[167,813,814],{"class":176}," \"provisional\"",[167,816,386],{"class":222},[167,818,819,822],{"class":91,"line":367},[167,820,821],{"class":176},"     \"Provisional figure. Restated at month end.\"",[167,823,752],{"class":222},[167,825,826],{"class":91,"line":380},[167,827,828],{"class":222},"]\n",[167,830,831],{"class":91,"line":389},[167,832,249],{"emptyLinePlaceholder":248},[167,834,835,838,842,845,847,850],{"class":91,"line":397},[167,836,837],{"class":218},"def",[167,839,841],{"class":840},"s_Opv"," annotate",[167,843,844],{"class":222},"(path, dest, sheet",[167,846,258],{"class":218},[167,848,849],{"class":176},"\"Summary\"",[167,851,613],{"class":222},[167,853,854,857,859],{"class":91,"line":403},[167,855,856],{"class":222},"    wb ",[167,858,258],{"class":218},[167,860,861],{"class":222}," load_workbook(path)\n",[167,863,864,867,869],{"class":91,"line":418},[167,865,866],{"class":222},"    ws ",[167,868,258],{"class":218},[167,870,871],{"class":222}," wb[sheet]\n",[167,873,874],{"class":91,"line":423},[167,875,249],{"emptyLinePlaceholder":248},[167,877,879,882,884,887,890,893,895,898,900,903,906,908,911],{"class":91,"line":878},20,[167,880,881],{"class":222},"    headers ",[167,883,258],{"class":218},[167,885,886],{"class":222}," {",[167,888,889],{"class":347},"str",[167,891,892],{"class":222},"(c.value).strip(): c.column ",[167,894,595],{"class":218},[167,896,897],{"class":222}," c ",[167,899,601],{"class":218},[167,901,902],{"class":222}," ws[",[167,904,905],{"class":347},"1",[167,907,300],{"class":222},[167,909,910],{"class":218},"if",[167,912,913],{"class":222}," c.value}\n",[167,915,917,920,922],{"class":91,"line":916},21,[167,918,919],{"class":222},"    added ",[167,921,258],{"class":218},[167,923,924],{"class":347}," 0\n",[167,926,928],{"class":91,"line":927},22,[167,929,249],{"emptyLinePlaceholder":248},[167,931,933,936,939,941,944],{"class":91,"line":932},23,[167,934,935],{"class":218},"    for",[167,937,938],{"class":222}," name, predicate, message ",[167,940,601],{"class":218},[167,942,943],{"class":347}," RULES",[167,945,946],{"class":222},":\n",[167,948,950,953,955],{"class":91,"line":949},24,[167,951,952],{"class":222},"        col ",[167,954,258],{"class":218},[167,956,957],{"class":222}," headers.get(name)\n",[167,959,961,964,967,970,973],{"class":91,"line":960},25,[167,962,963],{"class":218},"        if",[167,965,966],{"class":222}," col ",[167,968,969],{"class":218},"is",[167,971,972],{"class":347}," None",[167,974,946],{"class":222},[167,976,978],{"class":91,"line":977},26,[167,979,980],{"class":218},"            continue\n",[167,982,984,987,990,992,995,999,1001,1004,1006,1009,1011,1014,1017,1019],{"class":91,"line":983},27,[167,985,986],{"class":218},"        for",[167,988,989],{"class":222}," (cell,) ",[167,991,601],{"class":218},[167,993,994],{"class":222}," ws.iter_rows(",[167,996,998],{"class":997},"sa561","min_row",[167,1000,258],{"class":218},[167,1002,1003],{"class":347},"2",[167,1005,344],{"class":222},[167,1007,1008],{"class":997},"min_col",[167,1010,258],{"class":218},[167,1012,1013],{"class":222},"col, ",[167,1015,1016],{"class":997},"max_col",[167,1018,258],{"class":218},[167,1020,1021],{"class":222},"col):\n",[167,1023,1025,1028,1031,1033,1035,1038],{"class":91,"line":1024},28,[167,1026,1027],{"class":218},"            if",[167,1029,1030],{"class":222}," cell.comment ",[167,1032,969],{"class":218},[167,1034,972],{"class":347},[167,1036,1037],{"class":218}," and",[167,1039,1040],{"class":222}," predicate(cell.value):\n",[167,1042,1044,1047,1049,1052,1054],{"class":91,"line":1043},29,[167,1045,1046],{"class":222},"                note ",[167,1048,258],{"class":218},[167,1050,1051],{"class":222}," Comment(message, ",[167,1053,672],{"class":347},[167,1055,400],{"class":222},[167,1057,1059,1062,1064,1067,1069],{"class":91,"line":1058},30,[167,1060,1061],{"class":222},"                note.width, note.height ",[167,1063,258],{"class":218},[167,1065,1066],{"class":347}," 260",[167,1068,344],{"class":222},[167,1070,1071],{"class":347},"80\n",[167,1073,1075,1078,1080],{"class":91,"line":1074},31,[167,1076,1077],{"class":222},"                cell.comment ",[167,1079,258],{"class":218},[167,1081,415],{"class":222},[167,1083,1085,1088,1091],{"class":91,"line":1084},32,[167,1086,1087],{"class":222},"                added ",[167,1089,1090],{"class":218},"+=",[167,1092,1093],{"class":347}," 1\n",[167,1095,1097],{"class":91,"line":1096},33,[167,1098,249],{"emptyLinePlaceholder":248},[167,1100,1102],{"class":91,"line":1101},34,[167,1103,1104],{"class":222},"    wb.save(dest)\n",[167,1106,1108,1111],{"class":91,"line":1107},35,[167,1109,1110],{"class":218},"    return",[167,1112,1113],{"class":222}," added\n",[167,1115,1117],{"class":91,"line":1116},36,[167,1118,249],{"emptyLinePlaceholder":248},[167,1120,1122,1125,1128,1131,1134,1137,1140,1143,1145,1148,1151,1154,1157],{"class":91,"line":1121},37,[167,1123,1124],{"class":347},"print",[167,1126,1127],{"class":222},"(",[167,1129,1130],{"class":218},"f",[167,1132,1133],{"class":176},"\"added ",[167,1135,1136],{"class":373},"{",[167,1138,1139],{"class":222},"annotate(",[167,1141,1142],{"class":176},"'report.xlsx'",[167,1144,344],{"class":222},[167,1146,1147],{"class":176},"'report_annotated.xlsx'",[167,1149,1150],{"class":222},")",[167,1152,1153],{"class":373},"}",[167,1155,1156],{"class":176}," notes\"",[167,1158,400],{"class":222},[10,1160,202,1161,1164,1165,1169],{},[164,1162,1163],{},"cell.comment is None"," guard makes the function idempotent — running it twice does not stack duplicate notes, which matters when a job retries. This pairs naturally with the visual flagging in ",[14,1166,1168],{"href":1167},"\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fhighlight-invalid-cells-in-excel-with-python\u002F","highlighting invalid cells in Excel with Python",": the fill catches the eye, the comment explains why.",[152,1171,1173],{"id":1172},"step-3-read-comments-back","Step 3 — Read comments back",[10,1175,1176],{},"Comments are readable, which makes them useful as a lightweight audit trail — you can extract every annotation from an incoming workbook and act on it:",[20,1178,30,1184,30,1187,30,1190,30,1194,30,1199,30,1206,30,1210,30,1213,30,1217,30,1220,30,1224,30,1239,30,1243,30,1249,30,1253,30,1257,30,1262,30,1266,30,1272,30,1277,30,1282,30,1285],{"viewBox":1179,"role":23,"ariaLabel":1180,"ariaLabelledBy":1181,"xmlns":28,"style":29},"0 -1 800 189","Extracting comments from a workbook into a table: each annotated cell yields a row with the sheet, coordinate, cell value, author and note text.",[1182,1183],"read-t","read-d",[32,1185,1186],{"id":1182},"Turning scattered cell notes into one reviewable table",[36,1188,1189],{"id":1183},"Three annotated cells scattered across two sheets on the left feed an extraction step that walks every cell and collects the ones whose comment attribute is not None. The result on the right is a flat table with one row per note, carrying the sheet name, the cell coordinate, the cell's value, the author and the note text — a form that can be sorted, filtered and circulated.",[40,1191],{"x":42,"y":1192,"width":43,"height":1193,"fill":45},"-1","189",[56,1195,1198],{"x":138,"y":1196,"style":1197},"26","font-size:11.5px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","notes scattered in cells",[40,1200],{"x":1201,"y":1202,"width":1203,"height":1204,"rx":1205,"fill":101,"stroke":96,"style":54},"16","42","188","34","7",[56,1207,1209],{"x":138,"y":1208,"style":134},"64","Summary!C4",[40,1211],{"x":1201,"y":1212,"width":1203,"height":1204,"rx":1205,"fill":101,"stroke":96,"style":54},"86",[56,1214,1216],{"x":138,"y":1215,"style":134},"108","Summary!C9",[40,1218],{"x":1201,"y":1219,"width":1203,"height":1204,"rx":1205,"fill":101,"stroke":96,"style":54},"130",[56,1221,1223],{"x":138,"y":1222,"style":134},"152","Detail!F22",[1225,1226,1228,1229,1228,1233,1228,1236,30],"g",{"stroke":96,"style":54,"fill":1227},"none","\n    ",[1230,1231],"path",{"d":1232},"M204 59 H 240 V 103",[1230,1234],{"d":1235},"M204 103 H 246",[1230,1237],{"d":1238},"M204 147 H 240 V 103",[72,1240],{"points":1241,"fill":1242},"254,103 242,97 242,109","#b4740a",[40,1244],{"x":1245,"y":1246,"width":1247,"height":59,"rx":1248,"fill":52,"stroke":53,"style":54},"262","74","176","12",[56,1250,1252],{"x":1251,"y":115,"style":60},"350","walk every cell",[56,1254,1256],{"x":1251,"y":1255,"style":139},"120","keep comment is not None",[91,1258],{"x1":1259,"y1":1260,"x2":1261,"y2":1260,"stroke":53,"style":54},"438","104","472",[72,1263],{"points":1264,"fill":1265},"480,104 468,98 468,110","#5b5cf0",[40,1267],{"x":1268,"y":1269,"width":1270,"height":1271,"rx":1248,"fill":124,"stroke":125,"style":54},"488","36","296","136",[56,1273,1276],{"x":1274,"y":1275,"style":129},"636","62","one flat table of notes",[56,1278,1281],{"x":1274,"y":1279,"style":1280},"90","font-size:10.5px;fill:var(--text,#172033);text-anchor:middle","sheet · cell · value",[56,1283,1284],{"x":1274,"y":78,"style":1280},"author · text",[56,1286,1288],{"x":1274,"y":1287,"style":139},"142","sortable, filterable, circulatable",[157,1290,1292],{"className":209,"code":1291,"language":211,"meta":162,"style":162},"import pandas as pd\nfrom openpyxl import load_workbook\n\ndef extract_comments(path):\n    \"\"\"Collect every cell note in a workbook into a DataFrame.\"\"\"\n    wb = load_workbook(path)\n    rows = []\n    for ws in wb.worksheets:\n        for row in ws.iter_rows():\n            for cell in row:\n                if cell.comment is not None:\n                    rows.append({\n                        \"sheet\": ws.title,\n                        \"cell\": cell.coordinate,\n                        \"value\": cell.value,\n                        \"author\": cell.comment.author,\n                        \"note\": cell.comment.text,\n                    })\n    return pd.DataFrame(rows)\n\nnotes = extract_comments(\"report_annotated.xlsx\")\nprint(notes)\nnotes.to_excel(\"notes_log.xlsx\", index=False)\n",[164,1293,1294,1307,1317,1321,1331,1336,1344,1354,1366,1378,1391,1407,1412,1420,1428,1436,1444,1452,1457,1464,1468,1483,1490],{"__ignoreMap":162},[167,1295,1296,1298,1301,1304],{"class":91,"line":169},[167,1297,226],{"class":218},[167,1299,1300],{"class":222}," pandas ",[167,1302,1303],{"class":218},"as",[167,1305,1306],{"class":222}," pd\n",[167,1308,1309,1311,1313,1315],{"class":91,"line":232},[167,1310,219],{"class":218},[167,1312,223],{"class":222},[167,1314,226],{"class":218},[167,1316,653],{"class":222},[167,1318,1319],{"class":91,"line":245},[167,1320,249],{"emptyLinePlaceholder":248},[167,1322,1323,1325,1328],{"class":91,"line":252},[167,1324,837],{"class":218},[167,1326,1327],{"class":840}," extract_comments",[167,1329,1330],{"class":222},"(path):\n",[167,1332,1333],{"class":91,"line":264},[167,1334,1335],{"class":176},"    \"\"\"Collect every cell note in a workbook into a DataFrame.\"\"\"\n",[167,1337,1338,1340,1342],{"class":91,"line":275},[167,1339,856],{"class":222},[167,1341,258],{"class":218},[167,1343,861],{"class":222},[167,1345,1346,1349,1351],{"class":91,"line":286},[167,1347,1348],{"class":222},"    rows ",[167,1350,258],{"class":218},[167,1352,1353],{"class":222}," []\n",[167,1355,1356,1358,1361,1363],{"class":91,"line":291},[167,1357,935],{"class":218},[167,1359,1360],{"class":222}," ws ",[167,1362,601],{"class":218},[167,1364,1365],{"class":222}," wb.worksheets:\n",[167,1367,1368,1370,1373,1375],{"class":91,"line":308},[167,1369,986],{"class":218},[167,1371,1372],{"class":222}," row ",[167,1374,601],{"class":218},[167,1376,1377],{"class":222}," ws.iter_rows():\n",[167,1379,1380,1383,1386,1388],{"class":91,"line":323},[167,1381,1382],{"class":218},"            for",[167,1384,1385],{"class":222}," cell ",[167,1387,601],{"class":218},[167,1389,1390],{"class":222}," row:\n",[167,1392,1393,1396,1398,1400,1403,1405],{"class":91,"line":351},[167,1394,1395],{"class":218},"                if",[167,1397,1030],{"class":222},[167,1399,969],{"class":218},[167,1401,1402],{"class":218}," not",[167,1404,972],{"class":347},[167,1406,946],{"class":222},[167,1408,1409],{"class":91,"line":356},[167,1410,1411],{"class":222},"                    rows.append({\n",[167,1413,1414,1417],{"class":91,"line":367},[167,1415,1416],{"class":176},"                        \"sheet\"",[167,1418,1419],{"class":222},": ws.title,\n",[167,1421,1422,1425],{"class":91,"line":380},[167,1423,1424],{"class":176},"                        \"cell\"",[167,1426,1427],{"class":222},": cell.coordinate,\n",[167,1429,1430,1433],{"class":91,"line":389},[167,1431,1432],{"class":176},"                        \"value\"",[167,1434,1435],{"class":222},": cell.value,\n",[167,1437,1438,1441],{"class":91,"line":397},[167,1439,1440],{"class":176},"                        \"author\"",[167,1442,1443],{"class":222},": cell.comment.author,\n",[167,1445,1446,1449],{"class":91,"line":403},[167,1447,1448],{"class":176},"                        \"note\"",[167,1450,1451],{"class":222},": cell.comment.text,\n",[167,1453,1454],{"class":91,"line":418},[167,1455,1456],{"class":222},"                    })\n",[167,1458,1459,1461],{"class":91,"line":423},[167,1460,1110],{"class":218},[167,1462,1463],{"class":222}," pd.DataFrame(rows)\n",[167,1465,1466],{"class":91,"line":878},[167,1467,249],{"emptyLinePlaceholder":248},[167,1469,1470,1473,1475,1478,1481],{"class":91,"line":916},[167,1471,1472],{"class":222},"notes ",[167,1474,258],{"class":218},[167,1476,1477],{"class":222}," extract_comments(",[167,1479,1480],{"class":176},"\"report_annotated.xlsx\"",[167,1482,400],{"class":222},[167,1484,1485,1487],{"class":91,"line":927},[167,1486,1124],{"class":347},[167,1488,1489],{"class":222},"(notes)\n",[167,1491,1492,1495,1498,1500,1503,1505,1508],{"class":91,"line":932},[167,1493,1494],{"class":222},"notes.to_excel(",[167,1496,1497],{"class":176},"\"notes_log.xlsx\"",[167,1499,344],{"class":222},[167,1501,1502],{"class":997},"index",[167,1504,258],{"class":218},[167,1506,1507],{"class":347},"False",[167,1509,400],{"class":222},[10,1511,1512],{},"Removing them is the same walk with an assignment. Useful when a workbook carrying internal working notes is about to be sent outside:",[157,1514,1516],{"className":209,"code":1515,"language":211,"meta":162,"style":162},"from openpyxl import load_workbook\n\ndef strip_comments(path, dest):\n    wb = load_workbook(path)\n    removed = 0\n    for ws in wb.worksheets:\n        for row in ws.iter_rows():\n            for cell in row:\n                if cell.comment is not None:\n                    cell.comment = None\n                    removed += 1\n    wb.save(dest)\n    return removed\n\nprint(f\"removed {strip_comments('internal.xlsx', 'external.xlsx')} notes\")\n",[164,1517,1518,1528,1532,1542,1550,1559,1569,1579,1589,1603,1613,1622,1626,1633,1637],{"__ignoreMap":162},[167,1519,1520,1522,1524,1526],{"class":91,"line":169},[167,1521,219],{"class":218},[167,1523,223],{"class":222},[167,1525,226],{"class":218},[167,1527,653],{"class":222},[167,1529,1530],{"class":91,"line":232},[167,1531,249],{"emptyLinePlaceholder":248},[167,1533,1534,1536,1539],{"class":91,"line":245},[167,1535,837],{"class":218},[167,1537,1538],{"class":840}," strip_comments",[167,1540,1541],{"class":222},"(path, dest):\n",[167,1543,1544,1546,1548],{"class":91,"line":252},[167,1545,856],{"class":222},[167,1547,258],{"class":218},[167,1549,861],{"class":222},[167,1551,1552,1555,1557],{"class":91,"line":264},[167,1553,1554],{"class":222},"    removed ",[167,1556,258],{"class":218},[167,1558,924],{"class":347},[167,1560,1561,1563,1565,1567],{"class":91,"line":275},[167,1562,935],{"class":218},[167,1564,1360],{"class":222},[167,1566,601],{"class":218},[167,1568,1365],{"class":222},[167,1570,1571,1573,1575,1577],{"class":91,"line":286},[167,1572,986],{"class":218},[167,1574,1372],{"class":222},[167,1576,601],{"class":218},[167,1578,1377],{"class":222},[167,1580,1581,1583,1585,1587],{"class":91,"line":291},[167,1582,1382],{"class":218},[167,1584,1385],{"class":222},[167,1586,601],{"class":218},[167,1588,1390],{"class":222},[167,1590,1591,1593,1595,1597,1599,1601],{"class":91,"line":308},[167,1592,1395],{"class":218},[167,1594,1030],{"class":222},[167,1596,969],{"class":218},[167,1598,1402],{"class":218},[167,1600,972],{"class":347},[167,1602,946],{"class":222},[167,1604,1605,1608,1610],{"class":91,"line":323},[167,1606,1607],{"class":222},"                    cell.comment ",[167,1609,258],{"class":218},[167,1611,1612],{"class":347}," None\n",[167,1614,1615,1618,1620],{"class":91,"line":351},[167,1616,1617],{"class":222},"                    removed ",[167,1619,1090],{"class":218},[167,1621,1093],{"class":347},[167,1623,1624],{"class":91,"line":356},[167,1625,1104],{"class":222},[167,1627,1628,1630],{"class":91,"line":367},[167,1629,1110],{"class":218},[167,1631,1632],{"class":222}," removed\n",[167,1634,1635],{"class":91,"line":380},[167,1636,249],{"emptyLinePlaceholder":248},[167,1638,1639,1641,1643,1645,1648,1650,1653,1656,1658,1661,1663,1665,1667],{"class":91,"line":389},[167,1640,1124],{"class":347},[167,1642,1127],{"class":222},[167,1644,1130],{"class":218},[167,1646,1647],{"class":176},"\"removed ",[167,1649,1136],{"class":373},[167,1651,1652],{"class":222},"strip_comments(",[167,1654,1655],{"class":176},"'internal.xlsx'",[167,1657,344],{"class":222},[167,1659,1660],{"class":176},"'external.xlsx'",[167,1662,1150],{"class":222},[167,1664,1153],{"class":373},[167,1666,1156],{"class":176},[167,1668,400],{"class":222},[10,1670,1671],{},"That is a genuinely important step before external distribution. Working notes written for colleagues read very differently to a customer, and nothing in the file warns you they are there.",[152,1673,1675],{"id":1674},"step-4-record-provenance-at-the-file-level","Step 4 — Record provenance at the file level",[10,1677,1678],{},"Some context belongs to the whole workbook rather than a cell. Document properties carry it, and they survive forwarding in a way a covering email does not:",[157,1680,1682],{"className":209,"code":1681,"language":211,"meta":162,"style":162},"from datetime import datetime\nfrom openpyxl import load_workbook\n\nwb = load_workbook(\"report.xlsx\")\nprops = wb.properties\nprops.title = \"Regional revenue forecast\"\nprops.subject = \"Monthly reporting pack\"\nprops.creator = \"Reporting automation\"\nprops.description = (\n    \"Generated from the sales warehouse, snapshot 2026-08-15 06:00 UTC. \"\n    \"Provisional until month-end close.\"\n)\nprops.keywords = \"revenue; forecast; monthly\"\nprops.created = datetime(2026, 8, 15, 6, 0)\nprops.modified = datetime(2026, 8, 15, 6, 0)\nwb.save(\"report.xlsx\")\n",[164,1683,1684,1696,1706,1710,1724,1734,1744,1754,1764,1774,1779,1784,1788,1798,1832,1861],{"__ignoreMap":162},[167,1685,1686,1688,1691,1693],{"class":91,"line":169},[167,1687,219],{"class":218},[167,1689,1690],{"class":222}," datetime ",[167,1692,226],{"class":218},[167,1694,1695],{"class":222}," datetime\n",[167,1697,1698,1700,1702,1704],{"class":91,"line":232},[167,1699,219],{"class":218},[167,1701,223],{"class":222},[167,1703,226],{"class":218},[167,1705,653],{"class":222},[167,1707,1708],{"class":91,"line":245},[167,1709,249],{"emptyLinePlaceholder":248},[167,1711,1712,1714,1716,1719,1722],{"class":91,"line":252},[167,1713,255],{"class":222},[167,1715,258],{"class":218},[167,1717,1718],{"class":222}," load_workbook(",[167,1720,1721],{"class":176},"\"report.xlsx\"",[167,1723,400],{"class":222},[167,1725,1726,1729,1731],{"class":91,"line":264},[167,1727,1728],{"class":222},"props ",[167,1730,258],{"class":218},[167,1732,1733],{"class":222}," wb.properties\n",[167,1735,1736,1739,1741],{"class":91,"line":275},[167,1737,1738],{"class":222},"props.title ",[167,1740,258],{"class":218},[167,1742,1743],{"class":176}," \"Regional revenue forecast\"\n",[167,1745,1746,1749,1751],{"class":91,"line":286},[167,1747,1748],{"class":222},"props.subject ",[167,1750,258],{"class":218},[167,1752,1753],{"class":176}," \"Monthly reporting pack\"\n",[167,1755,1756,1759,1761],{"class":91,"line":291},[167,1757,1758],{"class":222},"props.creator ",[167,1760,258],{"class":218},[167,1762,1763],{"class":176}," \"Reporting automation\"\n",[167,1765,1766,1769,1771],{"class":91,"line":308},[167,1767,1768],{"class":222},"props.description ",[167,1770,258],{"class":218},[167,1772,1773],{"class":222}," (\n",[167,1775,1776],{"class":91,"line":323},[167,1777,1778],{"class":176},"    \"Generated from the sales warehouse, snapshot 2026-08-15 06:00 UTC. \"\n",[167,1780,1781],{"class":91,"line":351},[167,1782,1783],{"class":176},"    \"Provisional until month-end close.\"\n",[167,1785,1786],{"class":91,"line":356},[167,1787,400],{"class":222},[167,1789,1790,1793,1795],{"class":91,"line":367},[167,1791,1792],{"class":222},"props.keywords ",[167,1794,258],{"class":218},[167,1796,1797],{"class":176}," \"revenue; forecast; monthly\"\n",[167,1799,1800,1803,1805,1808,1811,1813,1816,1818,1821,1823,1826,1828,1830],{"class":91,"line":380},[167,1801,1802],{"class":222},"props.created ",[167,1804,258],{"class":218},[167,1806,1807],{"class":222}," datetime(",[167,1809,1810],{"class":347},"2026",[167,1812,344],{"class":222},[167,1814,1815],{"class":347},"8",[167,1817,344],{"class":222},[167,1819,1820],{"class":347},"15",[167,1822,344],{"class":222},[167,1824,1825],{"class":347},"6",[167,1827,344],{"class":222},[167,1829,42],{"class":347},[167,1831,400],{"class":222},[167,1833,1834,1837,1839,1841,1843,1845,1847,1849,1851,1853,1855,1857,1859],{"class":91,"line":389},[167,1835,1836],{"class":222},"props.modified ",[167,1838,258],{"class":218},[167,1840,1807],{"class":222},[167,1842,1810],{"class":347},[167,1844,344],{"class":222},[167,1846,1815],{"class":347},[167,1848,344],{"class":222},[167,1850,1820],{"class":347},[167,1852,344],{"class":222},[167,1854,1825],{"class":347},[167,1856,344],{"class":222},[167,1858,42],{"class":347},[167,1860,400],{"class":222},[167,1862,1863,1865,1867],{"class":91,"line":397},[167,1864,426],{"class":222},[167,1866,1721],{"class":176},[167,1868,400],{"class":222},[10,1870,1871],{},"A visible header row on the sheet itself does the same job for readers who never open the properties dialog — which is most of them:",[157,1873,1875],{"className":209,"code":1874,"language":211,"meta":162,"style":162},"from openpyxl import load_workbook\nfrom openpyxl.styles import Font\n\nwb = load_workbook(\"report.xlsx\")\nws = wb[\"Summary\"]\nws.insert_rows(1)\nws[\"A1\"] = (\"Generated 2026-08-15 06:00 UTC from the sales warehouse \"\n            \"· provisional until month-end close\")\nws[\"A1\"].font = Font(italic=True, color=\"5B6780\", size=9)\nws.merge_cells(\"A1:F1\")\nwb.save(\"report.xlsx\")\n",[164,1876,1877,1887,1899,1903,1915,1928,1937,1952,1959,2003,2013],{"__ignoreMap":162},[167,1878,1879,1881,1883,1885],{"class":91,"line":169},[167,1880,219],{"class":218},[167,1882,223],{"class":222},[167,1884,226],{"class":218},[167,1886,653],{"class":222},[167,1888,1889,1891,1894,1896],{"class":91,"line":232},[167,1890,219],{"class":218},[167,1892,1893],{"class":222}," openpyxl.styles ",[167,1895,226],{"class":218},[167,1897,1898],{"class":222}," Font\n",[167,1900,1901],{"class":91,"line":245},[167,1902,249],{"emptyLinePlaceholder":248},[167,1904,1905,1907,1909,1911,1913],{"class":91,"line":252},[167,1906,255],{"class":222},[167,1908,258],{"class":218},[167,1910,1718],{"class":222},[167,1912,1721],{"class":176},[167,1914,400],{"class":222},[167,1916,1917,1919,1921,1924,1926],{"class":91,"line":264},[167,1918,267],{"class":222},[167,1920,258],{"class":218},[167,1922,1923],{"class":222}," wb[",[167,1925,849],{"class":176},[167,1927,828],{"class":222},[167,1929,1930,1933,1935],{"class":91,"line":275},[167,1931,1932],{"class":222},"ws.insert_rows(",[167,1934,905],{"class":347},[167,1936,400],{"class":222},[167,1938,1939,1941,1943,1945,1947,1949],{"class":91,"line":286},[167,1940,294],{"class":222},[167,1942,297],{"class":176},[167,1944,300],{"class":222},[167,1946,258],{"class":218},[167,1948,604],{"class":222},[167,1950,1951],{"class":176},"\"Generated 2026-08-15 06:00 UTC from the sales warehouse \"\n",[167,1953,1954,1957],{"class":91,"line":291},[167,1955,1956],{"class":176},"            \"· provisional until month-end close\"",[167,1958,400],{"class":222},[167,1960,1961,1963,1965,1968,1970,1973,1976,1978,1981,1983,1986,1988,1991,1993,1996,1998,2001],{"class":91,"line":308},[167,1962,294],{"class":222},[167,1964,297],{"class":176},[167,1966,1967],{"class":222},"].font ",[167,1969,258],{"class":218},[167,1971,1972],{"class":222}," Font(",[167,1974,1975],{"class":997},"italic",[167,1977,258],{"class":218},[167,1979,1980],{"class":347},"True",[167,1982,344],{"class":222},[167,1984,1985],{"class":997},"color",[167,1987,258],{"class":218},[167,1989,1990],{"class":176},"\"5B6780\"",[167,1992,344],{"class":222},[167,1994,1995],{"class":997},"size",[167,1997,258],{"class":218},[167,1999,2000],{"class":347},"9",[167,2002,400],{"class":222},[167,2004,2005,2008,2011],{"class":91,"line":323},[167,2006,2007],{"class":222},"ws.merge_cells(",[167,2009,2010],{"class":176},"\"A1:F1\"",[167,2012,400],{"class":222},[167,2014,2015,2017,2019],{"class":91,"line":351},[167,2016,426],{"class":222},[167,2018,1721],{"class":176},[167,2020,400],{"class":222},[152,2022,2024],{"id":2023},"common-pitfalls-and-fixes","Common pitfalls and fixes",[2026,2027,2028,2044],"table",{},[2029,2030,2031],"thead",{},[2032,2033,2034,2038,2041],"tr",{},[2035,2036,2037],"th",{},"Symptom",[2035,2039,2040],{},"Cause",[2035,2042,2043],{},"Fix",[2045,2046,2047,2065,2084,2098,2109,2123,2134],"tbody",{},[2032,2048,2049,2053,2059],{},[2050,2051,2052],"td",{},"Only the last cell has a note",[2050,2054,2055,2056,2058],{},"One ",[164,2057,205],{}," object reused",[2050,2060,2061,2062,2064],{},"Construct a new ",[164,2063,205],{}," per cell.",[2032,2066,2067,2070,2073],{},[2050,2068,2069],{},"Comment text is cut off",[2050,2071,2072],{},"Default box too small",[2050,2074,2075,2076,2079,2080,2083],{},"Set ",[164,2077,2078],{},"width"," and ",[164,2081,2082],{},"height"," on the object.",[2032,2085,2086,2089,2095],{},[2050,2087,2088],{},"Comments gone after the job runs",[2050,2090,2091,2094],{},[164,2092,2093],{},"to_excel"," replaced the sheet",[2050,2096,2097],{},"Annotate as the final step.",[2032,2099,2100,2103,2106],{},[2050,2101,2102],{},"Notes not under Excel's Comments menu",[2050,2104,2105],{},"openpyxl writes classic notes",[2050,2107,2108],{},"Look under Notes; this is expected.",[2032,2110,2111,2114,2117],{},[2050,2112,2113],{},"Duplicate notes after a retry",[2050,2115,2116],{},"No idempotency guard",[2050,2118,2119,2120,18],{},"Skip cells where ",[164,2121,2122],{},"cell.comment is not None",[2032,2124,2125,2128,2131],{},[2050,2126,2127],{},"Comments not in the printed output",[2050,2129,2130],{},"Excel does not print notes by default",[2050,2132,2133],{},"Set the sheet's print options, or use a visible column.",[2032,2135,2136,2139,2142],{},[2050,2137,2138],{},"Internal notes reached a client",[2050,2140,2141],{},"Never stripped before sending",[2050,2143,2144,2145,2148],{},"Run a ",[164,2146,2147],{},"strip_comments"," step before external delivery.",[152,2150,2152],{"id":2151},"performance-and-scale-notes","Performance and scale notes",[20,2154,30,2160,30,2163,30,2166,30,2169,30,2175,30,2181,30,2185,30,2189,30,2193,30,2196,30,2201,30,2205,30,2208,30,2213,30,2216,30,2219,30,2222,30,2225,30,2228],{"viewBox":2155,"role":23,"ariaLabel":2156,"ariaLabelledBy":2157,"xmlns":28,"style":29},"0 0 800 230","Choosing between a comment and a notes column: comments suit a handful of exceptional cells, a real column suits every row, and both scale very differently.",[2158,2159],"notecol-t","notecol-d",[32,2161,2162],{"id":2158},"Comment or notes column, decided by how many rows need one",[36,2164,2165],{"id":2159},"Two panels. A comment is one drawing object per annotated cell, is invisible until hovered, and cannot be filtered, sorted or exported — which suits a handful of exceptional cells. A notes column costs nothing per row, is visible at a glance, and filters, sorts and exports like any other column — which suits the case where most rows have something to say. The crossover in practice is a few hundred annotations.",[40,2167],{"x":42,"y":42,"width":43,"height":2168,"fill":45},"230",[40,2170],{"x":2171,"y":2172,"width":2173,"height":2174,"rx":2171,"fill":101,"stroke":96,"style":54},"14","20","368","196",[56,2176,2180],{"x":2177,"y":2178,"style":2179},"198","48","font-size:12.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","a cell comment",[56,2182,2184],{"x":2177,"y":2183,"style":134},"78","one drawing object per note",[56,2186,2188],{"x":2177,"y":2187,"style":134},"102","invisible until hovered",[56,2190,2192],{"x":2177,"y":2191,"style":134},"126","cannot be filtered or sorted",[56,2194,2195],{"x":2177,"y":50,"style":134},"never reaches a CSV export",[40,2197],{"x":2198,"y":2199,"width":2200,"height":1204,"rx":1815,"fill":45,"stroke":96},"46","166","304",[56,2202,2204],{"x":2177,"y":1203,"style":2203},"font-size:11px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","right for a handful of exceptions",[40,2206],{"x":2207,"y":2172,"width":2173,"height":2174,"rx":2171,"fill":124,"stroke":125,"style":54},"418",[56,2209,2212],{"x":2210,"y":2178,"style":2211},"602","font-size:12.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","a notes column",[56,2214,2215],{"x":2210,"y":2183,"style":134},"no per-row object at all",[56,2217,2218],{"x":2210,"y":2187,"style":134},"visible at a glance",[56,2220,2221],{"x":2210,"y":2191,"style":134},"filters and sorts like any column",[56,2223,2224],{"x":2210,"y":50,"style":134},"survives every export",[40,2226],{"x":2227,"y":2199,"width":2200,"height":1204,"rx":1815,"fill":45,"stroke":125},"450",[56,2229,2231],{"x":2210,"y":1203,"style":2230},"font-size:11px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","right when most rows have something to say",[10,2233,2234],{},"Every comment is a separate drawing object in the file, with its own XML and anchor. The cost is small individually and adds up quickly: a few hundred notes is unremarkable, several thousand noticeably inflates the file and slows Excel's rendering, and tens of thousands makes a workbook uncomfortable to open.",[10,2236,2237,2238,2242],{},"The scaling rule is therefore about ",[2239,2240,2241],"em",{},"choosing the right mechanism",", not optimising the wrong one:",[2026,2244,2245,2255],{},[2029,2246,2247],{},[2032,2248,2249,2252],{},[2035,2250,2251],{},"Situation",[2035,2253,2254],{},"Use",[2045,2256,2257,2265,2273,2281],{},[2032,2258,2259,2262],{},[2050,2260,2261],{},"A handful of exceptional cells need explaining",[2050,2263,2264],{},"Comments",[2032,2266,2267,2270],{},[2050,2268,2269],{},"Every row has something to say",[2050,2271,2272],{},"A real notes column",[2032,2274,2275,2278],{},[2050,2276,2277],{},"A whole column shares one caveat",[2050,2279,2280],{},"A header note or a comment on the header cell",[2032,2282,2283,2286],{},[2050,2284,2285],{},"The caveat applies to the file",[2050,2287,2288],{},"Document properties plus a header row",[10,2290,2291],{},"A notes column is not a fallback — it is better in most respects. It can be filtered, sorted, exported to CSV, and read by pandas, none of which a comment can:",[157,2293,2295],{"className":209,"code":2294,"language":211,"meta":162,"style":162},"import numpy as np\nimport pandas as pd\n\ndf[\"note\"] = np.select(\n    [df[\"revenue\"] == 0, df[\"revenue\"] > 100_000],\n    [\"zero — check the extract\", \"above the usual range\"],\n    default=\"\",\n)\ndf.to_excel(\"report.xlsx\", index=False)\n",[164,2296,2297,2309,2319,2323,2338,2365,2380,2392,2396],{"__ignoreMap":162},[167,2298,2299,2301,2304,2306],{"class":91,"line":169},[167,2300,226],{"class":218},[167,2302,2303],{"class":222}," numpy ",[167,2305,1303],{"class":218},[167,2307,2308],{"class":222}," np\n",[167,2310,2311,2313,2315,2317],{"class":91,"line":232},[167,2312,226],{"class":218},[167,2314,1300],{"class":222},[167,2316,1303],{"class":218},[167,2318,1306],{"class":222},[167,2320,2321],{"class":91,"line":245},[167,2322,249],{"emptyLinePlaceholder":248},[167,2324,2325,2328,2331,2333,2335],{"class":91,"line":252},[167,2326,2327],{"class":222},"df[",[167,2329,2330],{"class":176},"\"note\"",[167,2332,300],{"class":222},[167,2334,258],{"class":218},[167,2336,2337],{"class":222}," np.select(\n",[167,2339,2340,2343,2345,2347,2349,2351,2354,2356,2358,2360,2362],{"class":91,"line":264},[167,2341,2342],{"class":222},"    [df[",[167,2344,705],{"class":176},[167,2346,300],{"class":222},[167,2348,739],{"class":218},[167,2350,742],{"class":347},[167,2352,2353],{"class":222},", df[",[167,2355,705],{"class":176},[167,2357,300],{"class":222},[167,2359,783],{"class":218},[167,2361,786],{"class":347},[167,2363,2364],{"class":222},"],\n",[167,2366,2367,2370,2373,2375,2378],{"class":91,"line":275},[167,2368,2369],{"class":222},"    [",[167,2371,2372],{"class":176},"\"zero — check the extract\"",[167,2374,344],{"class":222},[167,2376,2377],{"class":176},"\"above the usual range\"",[167,2379,2364],{"class":222},[167,2381,2382,2385,2387,2390],{"class":91,"line":286},[167,2383,2384],{"class":997},"    default",[167,2386,258],{"class":218},[167,2388,2389],{"class":176},"\"\"",[167,2391,386],{"class":222},[167,2393,2394],{"class":91,"line":291},[167,2395,400],{"class":222},[167,2397,2398,2401,2403,2405,2407,2409,2411],{"class":91,"line":308},[167,2399,2400],{"class":222},"df.to_excel(",[167,2402,1721],{"class":176},[167,2404,344],{"class":222},[167,2406,1502],{"class":997},[167,2408,258],{"class":218},[167,2410,1507],{"class":347},[167,2412,400],{"class":222},[10,2414,2415],{},"That is vectorised and costs nothing regardless of row count, where annotating the same rows with comments would create one drawing object each.",[10,2417,2418,2419,2422,2423,2426],{},"Two more notes for large jobs. Comments cannot be written in openpyxl's ",[164,2420,2421],{},"write_only"," mode, so a streaming write followed by an annotation pass means re-opening the file in normal mode — which defeats the memory saving. And the cell walk in ",[164,2424,2425],{},"extract_comments"," above visits every cell in the used range; on a large workbook, restrict it to the sheets and ranges you care about rather than scanning the lot.",[152,2428,2430],{"id":2429},"conclusion","Conclusion",[10,2432,2433,2434,2436,2437,2439],{},"Comments put the answer to \"why is this number what it is\" next to the number itself, where the question actually gets asked. Build a fresh ",[164,2435,205],{}," per cell, size the box to the text, and guard on ",[164,2438,1163],{}," so retries do not stack duplicates. Read them back when you need an audit trail, and strip them before a workbook goes outside the organisation. When every row needs an explanation, use a real column instead — it filters, sorts and exports, and it costs nothing at any scale.",[152,2441,2443],{"id":2442},"frequently-asked-questions","Frequently asked questions",[10,2445,2446,2449],{},[188,2447,2448],{},"Do openpyxl comments show up as modern threaded comments in Excel?","\nNo. openpyxl writes classic notes — the yellow hover boxes attached to a cell. Excel shows them under Notes rather than Comments. They are the right tool for machine-generated annotations, which are statements rather than conversations.",[10,2451,2452,2455,2456,2458],{},[188,2453,2454],{},"Why did my comments disappear after the script ran again?","\nSomething rewrote the sheet. pandas ",[164,2457,2093],{}," replaces a sheet wholesale, discarding comments along with protection and column widths. Annotate as the final step, or edit cells through openpyxl rather than replacing the sheet.",[10,2460,2461,2464,2465,2079,2467,2469,2470,2472],{},[188,2462,2463],{},"Can I control the size of a comment box?","\nYes — set ",[164,2466,2078],{},[164,2468,2082],{}," on the ",[164,2471,205],{}," object in pixels before assigning it. The default box is small and clips longer text, so size it to the content.",[10,2474,2475,2478,2479,2482,2483,2486,2487,2079,2489,2492],{},[188,2476,2477],{},"How do I read comments that are already in a workbook?","\nIterate the cells and check ",[164,2480,2481],{},"cell.comment",", which is ",[164,2484,2485],{},"None"," where there is no note. Its ",[164,2488,56],{},[164,2490,2491],{},"author"," attributes hold the content, so you can audit or migrate annotations.",[10,2494,2495,2498],{},[188,2496,2497],{},"Should I use comments or a notes column?","\nComments for occasional explanations that would clutter the grid; a real column when every row has something to say, because a column can be filtered, sorted and exported while a comment cannot.",[152,2500,2502],{"id":2501},"related","Related",[2504,2505,2506,2513,2519,2526,2533],"ul",{},[2507,2508,2509,2510,2512],"li",{},"Up to the parent: ",[14,2511,17],{"href":16}," — comments alongside the other sharing controls.",[2507,2514,2515,2518],{},[14,2516,2517],{"href":1167},"Highlight Invalid Cells in Excel with Python"," — the visual flag a comment explains.",[2507,2520,2521,2525],{},[14,2522,2524],{"href":2523},"\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"," — stopping the edit the comment warns against.",[2507,2527,2528,2532],{},[14,2529,2531],{"href":2530},"\u002Fautomating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards\u002Fadd-summary-sheet-to-excel-report-python\u002F","Add a Summary Sheet to an Excel Report"," — where file-level provenance usually belongs.",[2507,2534,2535,2539],{},[14,2536,2538],{"href":2537},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fvalidate-an-excel-report-before-sending-it\u002F","Validate an Excel Report Before Sending It"," — the gate that should also strip internal notes.",[2541,2542,2543],"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 .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}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}",{"title":162,"searchDepth":232,"depth":232,"links":2545},[2546,2547,2548,2549,2550,2551,2552,2553,2554,2555],{"id":154,"depth":232,"text":155},{"id":198,"depth":232,"text":199},{"id":633,"depth":232,"text":634},{"id":1172,"depth":232,"text":1173},{"id":1674,"depth":232,"text":1675},{"id":2023,"depth":232,"text":2024},{"id":2151,"depth":232,"text":2152},{"id":2429,"depth":232,"text":2430},{"id":2442,"depth":232,"text":2443},{"id":2501,"depth":232,"text":2502},"2026-08-15","Annotate generated Excel reports from Python: attach openpyxl comments, size them, read existing notes back, and use document properties to record where the numbers came from.","md",[2560,2562,2564,2566,2568],{"q":2448,"a":2561},"No. openpyxl writes classic notes — the yellow hover boxes attached to a cell. Excel shows them under Notes rather than Comments. They are the right tool for machine-generated annotations, which are statements rather than conversations.",{"q":2454,"a":2563},"Something rewrote the sheet. pandas to_excel replaces a sheet wholesale, discarding comments along with protection and column widths. Annotate as the final step, or edit cells through openpyxl rather than replacing the sheet.",{"q":2463,"a":2565},"Yes — set width and height on the Comment object in pixels before assigning it. The default box is small and clips longer text, so size it to the content.",{"q":2477,"a":2567},"Iterate the cells and check cell.comment, which is None where there is no note. Its text and author attributes hold the content, so you can audit or migrate annotations.",{"q":2497,"a":2569},"Comments for occasional explanations that would clutter the grid; a real column when every row has something to say, because a column can be filtered, sorted and exported while a comment cannot.",{},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fprotecting-and-sharing-excel-workbooks\u002Fadd-comments-and-notes-to-excel-cells-with-python",{"title":2573,"description":2574},"Add Comments to Excel Cells with Python (openpyxl)","Attach and read cell comments with openpyxl — the Comment object, sizing, bulk annotation from a rules table, reading notes back, and workbook-level document properties.","add-comments-and-notes-to-excel-cells-with-python","formatting-and-charting-excel-reports-with-python\u002Fprotecting-and-sharing-excel-workbooks\u002Fadd-comments-and-notes-to-excel-cells-with-python\u002Findex","how-to","-GbmeHbLautqSNf8IBSN3YDfOgDOYJwVV0Xg7nKDhOw",[2580,2583],{"title":17,"path":2581,"stem":2582,"children":-1},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fprotecting-and-sharing-excel-workbooks","formatting-and-charting-excel-reports-with-python\u002Fprotecting-and-sharing-excel-workbooks\u002Findex",{"title":2584,"path":2585,"stem":2586,"children":-1},"Hide Sheets, Rows and Columns with openpyxl","\u002Fformatting-and-charting-excel-reports-with-python\u002Fprotecting-and-sharing-excel-workbooks\u002Fhide-sheets-rows-and-columns-with-openpyxl","formatting-and-charting-excel-reports-with-python\u002Fprotecting-and-sharing-excel-workbooks\u002Fhide-sheets-rows-and-columns-with-openpyxl\u002Findex",1786800027185]