[{"data":1,"prerenderedAt":1984},["ShallowReactive",2],{"doc:\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-mergedcell-value-is-read-only-in-openpyxl":3,"surround:\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-mergedcell-value-is-read-only-in-openpyxl":1948,"ancestors:\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-mergedcell-value-is-read-only-in-openpyxl":1957,"children:\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-mergedcell-value-is-read-only-in-openpyxl":1962,"related:\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-mergedcell-value-is-read-only-in-openpyxl":1963},{"id":4,"title":5,"body":6,"dateModified":1914,"datePublished":1914,"description":1915,"extension":1916,"faq":1917,"keyword":1926,"kind":1927,"meta":1928,"navigation":290,"path":1936,"seo":1937,"slug":1940,"stem":1941,"topics":1942,"type":1946,"__hash__":1947},"docs\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-mergedcell-value-is-read-only-in-openpyxl\u002Findex.md","Fix \"MergedCell object attribute value is read-only\" in openpyxl",{"type":7,"value":8,"toc":1901},"minimark",[9,13,24,42,222,227,249,386,390,404,505,511,528,532,535,654,657,661,668,737,745,748,885,889,892,1085,1109,1283,1294,1529,1535,1539,1542,1548,1559,1737,1748,1752,1791,1795,1805,1809,1830,1834,1840,1846,1852,1864,1868,1897],[10,11,12],"p",{},"You write a value into a worksheet and openpyxl stops you:",[14,15,21],"pre",{"className":16,"code":18,"language":19,"meta":20},[17],"language-text","AttributeError: 'MergedCell' object attribute 'value' is read-only\n","text","",[22,23,18],"code",{"__ignoreMap":20},[10,25,26,27,30,31,35,36,41],{},"The cell you addressed is part of a merged range, and it is not the range's top-left corner. In Excel's model a merged range has exactly one value, stored in that corner; every other cell in the range is covered. openpyxl represents the covered cells as ",[22,28,29],{},"MergedCell"," objects, which have a position but no writable value. The fix is simple once you know which range covers the cell — and the interesting part of this error is the case where you did not ",[32,33,34],"em",{},"think"," any range covered it. This page is part of ",[37,38,40],"a",{"href":39},"\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002F","Troubleshooting Common Python Excel Errors",".",[43,44,52,53,52,57,52,61,52,69,52,75,52,84,52,88,52,94,52,98,52,102,52,106,52,110,52,114,52,119,52,125,52,130,52,133,52,137,52,139,52,143,52,145,52,148,52,150,52,153,52,156,52,160,52,162,52,165,52,167,52,170,52,172,52,175,52,183,52,188,52,196,52,207,52,213,52,218],"svg",{"viewBox":45,"role":46,"ariaLabelledBy":47,"xmlns":50,"style":51},"139 0 481 174","img",[48,49],"mc-anatomy-t","mc-anatomy-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:481px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[54,55,56],"title",{"id":48},"Which cells of a merged range can hold a value",[58,59,60],"desc",{"id":49},"A worksheet with A1:C1 merged into one title cell. Only A1, the top-left cell, is a normal Cell that stores the title. B1 and C1 are MergedCell placeholders; assigning a value to either raises the read-only error.",[62,63],"rect",{"x":64,"y":65,"width":66,"height":67,"fill":68},"139","0","481","174","#ffffff",[19,70,74],{"x":71,"y":72,"style":73},"380.0","31.0","font-size:14px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","ws.merge_cells(\"A1:C1\") — one value, three positions",[62,76],{"x":77,"y":78,"width":79,"height":80,"rx":65,"fill":81,"stroke":82,"style":83},"155.8","48.0","32.0","24.0","#e7ebef","var(--line,#cdd5e6)","stroke-width:1px",[62,85],{"x":86,"y":78,"width":87,"height":80,"rx":65,"fill":81,"stroke":82,"style":83},"187.8","66.7",[19,89,93],{"x":90,"y":91,"style":92},"221.2","65.0","font-size:11px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","A",[62,95],{"x":96,"y":78,"width":97,"height":80,"rx":65,"fill":81,"stroke":82,"style":83},"254.5","46.4",[19,99,101],{"x":100,"y":91,"style":92},"277.8","B",[62,103],{"x":104,"y":78,"width":105,"height":80,"rx":65,"fill":81,"stroke":82,"style":83},"301.0","48.8",[19,107,109],{"x":108,"y":91,"style":92},"325.4","C",[62,111],{"x":77,"y":112,"width":79,"height":113,"rx":65,"fill":81,"stroke":82,"style":83},"72.0","28.0",[19,115,118],{"x":116,"y":117,"style":92},"171.8","91.0","1",[62,120],{"x":86,"y":112,"width":121,"height":113,"rx":65,"fill":122,"stroke":123,"style":124},"162.0","#d9f4f1","var(--teal,#0f9488)","stroke-width:1.5px",[19,126,129],{"x":127,"y":117,"style":128},"268.8","font-size:12px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Quarterly sales",[62,131],{"x":77,"y":132,"width":79,"height":113,"rx":65,"fill":81,"stroke":82,"style":83},"100.0",[19,134,136],{"x":116,"y":135,"style":92},"119.0","2",[62,138],{"x":86,"y":132,"width":87,"height":113,"rx":65,"fill":68,"stroke":82,"style":83},[19,140,142],{"x":90,"y":135,"style":141},"font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","Region",[62,144],{"x":96,"y":132,"width":97,"height":113,"rx":65,"fill":68,"stroke":82,"style":83},[19,146,147],{"x":100,"y":135,"style":141},"Q1",[62,149],{"x":104,"y":132,"width":105,"height":113,"rx":65,"fill":68,"stroke":82,"style":83},[19,151,152],{"x":108,"y":135,"style":141},"Q2",[62,154],{"x":77,"y":155,"width":79,"height":113,"rx":65,"fill":81,"stroke":82,"style":83},"128.0",[19,157,159],{"x":116,"y":158,"style":92},"147.0","3",[62,161],{"x":86,"y":155,"width":87,"height":113,"rx":65,"fill":68,"stroke":82,"style":83},[19,163,164],{"x":90,"y":158,"style":141},"North",[62,166],{"x":96,"y":155,"width":97,"height":113,"rx":65,"fill":68,"stroke":82,"style":83},[19,168,169],{"x":100,"y":158,"style":141},"410",[62,171],{"x":104,"y":155,"width":105,"height":113,"rx":65,"fill":68,"stroke":82,"style":83},[19,173,174],{"x":108,"y":158,"style":141},"380",[176,177],"line",{"x1":178,"y1":179,"x2":180,"y2":179,"stroke":181,"style":182},"357.8","103.2","389.8","var(--brand,#5b5cf0)","stroke-width:2px",[184,185],"polygon",{"points":186,"fill":187},"393.8,103.2 384.8,108.2 384.8,98.2","#5b5cf0",[62,189],{"x":190,"y":78,"width":191,"height":192,"rx":193,"fill":194,"stroke":195,"style":182},"399.8","204.4","110.5","12","#fee8f2","var(--accent,#f43f8f)",[197,198,200],"g",{"transform":199},"translate(490.0,61.0) scale(1.000)",[201,202],"path",{"d":203,"fill":204,"stroke":205,"style":206},"M12 3.5l9.5 17h-19z M12 10v5 M12 17.8v.4","none","var(--accent-ink,#be185d)","stroke-width:1.80px;stroke-linecap:round;stroke-linejoin:round",[19,208,212],{"x":209,"y":210,"style":211},"502.0","107.5","font-size:13.5px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","B1 and C1 are MergedCell",[19,214,217],{"x":209,"y":215,"style":216},"127.0","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","no writable value —",[19,219,221],{"x":209,"y":220,"style":216},"144.5","write to A1 instead",[223,224,226],"h2",{"id":225},"prerequisites","Prerequisites",[14,228,232],{"className":229,"code":230,"language":231,"meta":20,"style":20},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install openpyxl\n","bash",[22,233,234],{"__ignoreMap":20},[235,236,238,242,246],"span",{"class":176,"line":237},1,[235,239,241],{"class":240},"sMTad","pip",[235,243,245],{"class":244},"srMev"," install",[235,247,248],{"class":244}," openpyxl\n",[14,250,254],{"className":251,"code":252,"language":253,"meta":20,"style":20},"language-python shiki shiki-themes github-light github-dark-high-contrast","from openpyxl import Workbook, load_workbook\nfrom openpyxl.utils import get_column_letter\n\nwb = Workbook()\nws = wb.active\nws.merge_cells(\"A1:C1\")\nws[\"A1\"] = \"Quarterly sales\"\nws.append([\"Region\", \"Q1\", \"Q2\"])\nws.append([\"North\", 410, 380])\n","python",[22,255,256,272,285,292,304,315,327,344,367],{"__ignoreMap":20},[235,257,258,262,266,269],{"class":176,"line":237},[235,259,261],{"class":260},"s-kum","from",[235,263,265],{"class":264},"skGVy"," openpyxl ",[235,267,268],{"class":260},"import",[235,270,271],{"class":264}," Workbook, load_workbook\n",[235,273,275,277,280,282],{"class":176,"line":274},2,[235,276,261],{"class":260},[235,278,279],{"class":264}," openpyxl.utils ",[235,281,268],{"class":260},[235,283,284],{"class":264}," get_column_letter\n",[235,286,288],{"class":176,"line":287},3,[235,289,291],{"emptyLinePlaceholder":290},true,"\n",[235,293,295,298,301],{"class":176,"line":294},4,[235,296,297],{"class":264},"wb ",[235,299,300],{"class":260},"=",[235,302,303],{"class":264}," Workbook()\n",[235,305,307,310,312],{"class":176,"line":306},5,[235,308,309],{"class":264},"ws ",[235,311,300],{"class":260},[235,313,314],{"class":264}," wb.active\n",[235,316,318,321,324],{"class":176,"line":317},6,[235,319,320],{"class":264},"ws.merge_cells(",[235,322,323],{"class":244},"\"A1:C1\"",[235,325,326],{"class":264},")\n",[235,328,330,333,336,339,341],{"class":176,"line":329},7,[235,331,332],{"class":264},"ws[",[235,334,335],{"class":244},"\"A1\"",[235,337,338],{"class":264},"] ",[235,340,300],{"class":260},[235,342,343],{"class":244}," \"Quarterly sales\"\n",[235,345,347,350,353,356,359,361,364],{"class":176,"line":346},8,[235,348,349],{"class":264},"ws.append([",[235,351,352],{"class":244},"\"Region\"",[235,354,355],{"class":264},", ",[235,357,358],{"class":244},"\"Q1\"",[235,360,355],{"class":264},[235,362,363],{"class":244},"\"Q2\"",[235,365,366],{"class":264},"])\n",[235,368,370,372,375,377,380,382,384],{"class":176,"line":369},9,[235,371,349],{"class":264},[235,373,374],{"class":244},"\"North\"",[235,376,355],{"class":264},[235,378,169],{"class":379},"sP0c6",[235,381,355],{"class":264},[235,383,174],{"class":379},[235,385,366],{"class":264},[223,387,389],{"id":388},"step-1-confirm-the-cell-is-a-placeholder","Step 1 — Confirm the cell is a placeholder",[10,391,392,393,395,396,399,400,403],{},"Check the cell's type before anything else. Every cell in a merged range except the corner is a ",[22,394,29],{},", and writing to it raises the error whether you use ",[22,397,398],{},"ws[\"B1\"] = ..."," or ",[22,401,402],{},"ws.cell(row=1, column=2, value=...)",":",[14,405,407],{"className":251,"code":406,"language":253,"meta":20,"style":20},"print(type(ws[\"A1\"]).__name__, type(ws[\"B1\"]).__name__)\n# Cell MergedCell\n\ntry:\n    ws[\"B1\"] = \"overwrite\"\nexcept AttributeError as exc:\n    print(exc)\n# 'MergedCell' object attribute 'value' is read-only\n",[22,408,409,446,452,456,464,478,492,500],{"__ignoreMap":20},[235,410,411,414,417,420,423,425,428,431,433,435,437,440,442,444],{"class":176,"line":237},[235,412,413],{"class":379},"print",[235,415,416],{"class":264},"(",[235,418,419],{"class":379},"type",[235,421,422],{"class":264},"(ws[",[235,424,335],{"class":244},[235,426,427],{"class":264},"]).",[235,429,430],{"class":379},"__name__",[235,432,355],{"class":264},[235,434,419],{"class":379},[235,436,422],{"class":264},[235,438,439],{"class":244},"\"B1\"",[235,441,427],{"class":264},[235,443,430],{"class":379},[235,445,326],{"class":264},[235,447,448],{"class":176,"line":274},[235,449,451],{"class":450},"s-wDw","# Cell MergedCell\n",[235,453,454],{"class":176,"line":287},[235,455,291],{"emptyLinePlaceholder":290},[235,457,458,461],{"class":176,"line":294},[235,459,460],{"class":260},"try",[235,462,463],{"class":264},":\n",[235,465,466,469,471,473,475],{"class":176,"line":306},[235,467,468],{"class":264},"    ws[",[235,470,439],{"class":244},[235,472,338],{"class":264},[235,474,300],{"class":260},[235,476,477],{"class":244}," \"overwrite\"\n",[235,479,480,483,486,489],{"class":176,"line":317},[235,481,482],{"class":260},"except",[235,484,485],{"class":379}," AttributeError",[235,487,488],{"class":260}," as",[235,490,491],{"class":264}," exc:\n",[235,493,494,497],{"class":176,"line":329},[235,495,496],{"class":379},"    print",[235,498,499],{"class":264},"(exc)\n",[235,501,502],{"class":176,"line":346},[235,503,504],{"class":450},"# 'MergedCell' object attribute 'value' is read-only\n",[10,506,507,510],{},[22,508,509],{},"ws.merged_cells.ranges"," lists every merge on the sheet. When a workbook comes from someone else, printing it is the fastest way to see what you are dealing with:",[14,512,514],{"className":251,"code":513,"language":253,"meta":20,"style":20},"print(ws.merged_cells.ranges)\n# {\u003CMergedCellRange A1:C1>}\n",[22,515,516,523],{"__ignoreMap":20},[235,517,518,520],{"class":176,"line":237},[235,519,413],{"class":379},[235,521,522],{"class":264},"(ws.merged_cells.ranges)\n",[235,524,525],{"class":176,"line":274},[235,526,527],{"class":450},"# {\u003CMergedCellRange A1:C1>}\n",[223,529,531],{"id":530},"step-2-write-to-the-ranges-top-left-cell","Step 2 — Write to the range's top-left cell",[10,533,534],{},"If the intent is \"put this value in the merged block\", write to the corner. A small helper finds the range that contains any coordinate and returns the cell that actually stores the value, so calling code does not need to know where the merges are:",[14,536,538],{"className":251,"code":537,"language":253,"meta":20,"style":20},"def owner(ws, coord):\n    \"\"\"Return the cell that stores the value for coord (itself, or its merge's corner).\"\"\"\n    for rng in ws.merged_cells.ranges:\n        if coord in rng:\n            return ws.cell(row=rng.min_row, column=rng.min_col)\n    return ws[coord]\n\nowner(ws, \"C1\").value = \"Quarterly sales — revised\"\nprint(ws[\"A1\"].value)\n# Quarterly sales — revised\n",[22,539,540,552,557,571,584,609,617,621,637,648],{"__ignoreMap":20},[235,541,542,545,549],{"class":176,"line":237},[235,543,544],{"class":260},"def",[235,546,548],{"class":547},"s_Opv"," owner",[235,550,551],{"class":264},"(ws, coord):\n",[235,553,554],{"class":176,"line":274},[235,555,556],{"class":244},"    \"\"\"Return the cell that stores the value for coord (itself, or its merge's corner).\"\"\"\n",[235,558,559,562,565,568],{"class":176,"line":287},[235,560,561],{"class":260},"    for",[235,563,564],{"class":264}," rng ",[235,566,567],{"class":260},"in",[235,569,570],{"class":264}," ws.merged_cells.ranges:\n",[235,572,573,576,579,581],{"class":176,"line":294},[235,574,575],{"class":260},"        if",[235,577,578],{"class":264}," coord ",[235,580,567],{"class":260},[235,582,583],{"class":264}," rng:\n",[235,585,586,589,592,596,598,601,604,606],{"class":176,"line":306},[235,587,588],{"class":260},"            return",[235,590,591],{"class":264}," ws.cell(",[235,593,595],{"class":594},"sa561","row",[235,597,300],{"class":260},[235,599,600],{"class":264},"rng.min_row, ",[235,602,603],{"class":594},"column",[235,605,300],{"class":260},[235,607,608],{"class":264},"rng.min_col)\n",[235,610,611,614],{"class":176,"line":317},[235,612,613],{"class":260},"    return",[235,615,616],{"class":264}," ws[coord]\n",[235,618,619],{"class":176,"line":329},[235,620,291],{"emptyLinePlaceholder":290},[235,622,623,626,629,632,634],{"class":176,"line":346},[235,624,625],{"class":264},"owner(ws, ",[235,627,628],{"class":244},"\"C1\"",[235,630,631],{"class":264},").value ",[235,633,300],{"class":260},[235,635,636],{"class":244}," \"Quarterly sales — revised\"\n",[235,638,639,641,643,645],{"class":176,"line":369},[235,640,413],{"class":379},[235,642,422],{"class":264},[235,644,335],{"class":244},[235,646,647],{"class":264},"].value)\n",[235,649,651],{"class":176,"line":650},10,[235,652,653],{"class":450},"# Quarterly sales — revised\n",[10,655,656],{},"This is the right behaviour for templates, where someone merged a title or an address block and your script should fill it without caring about the merge. It is the wrong behaviour when the merge itself is a mistake, which is Step 3.",[223,658,660],{"id":659},"step-3-unmerge-when-each-cell-should-hold-its-own-value","Step 3 — Unmerge when each cell should hold its own value",[10,662,663,664,667],{},"If the script genuinely needs separate values in B1 and C1, the merge has to go. ",[22,665,666],{},"unmerge_cells"," turns the placeholders back into ordinary cells. The corner keeps its value; the others start empty:",[14,669,671],{"className":251,"code":670,"language":253,"meta":20,"style":20},"ws.unmerge_cells(\"A1:C1\")\nws[\"B1\"] = \"draft\"\nprint(ws[\"A1\"].value, \"|\", ws[\"B1\"].value, \"|\", type(ws[\"C1\"]).__name__)\n# Quarterly sales — revised | draft | Cell\n",[22,672,673,682,695,732],{"__ignoreMap":20},[235,674,675,678,680],{"class":176,"line":237},[235,676,677],{"class":264},"ws.unmerge_cells(",[235,679,323],{"class":244},[235,681,326],{"class":264},[235,683,684,686,688,690,692],{"class":176,"line":274},[235,685,332],{"class":264},[235,687,439],{"class":244},[235,689,338],{"class":264},[235,691,300],{"class":260},[235,693,694],{"class":244}," \"draft\"\n",[235,696,697,699,701,703,706,709,712,714,716,718,720,722,724,726,728,730],{"class":176,"line":287},[235,698,413],{"class":379},[235,700,422],{"class":264},[235,702,335],{"class":244},[235,704,705],{"class":264},"].value, ",[235,707,708],{"class":244},"\"|\"",[235,710,711],{"class":264},", ws[",[235,713,439],{"class":244},[235,715,705],{"class":264},[235,717,708],{"class":244},[235,719,355],{"class":264},[235,721,419],{"class":379},[235,723,422],{"class":264},[235,725,628],{"class":244},[235,727,427],{"class":264},[235,729,430],{"class":379},[235,731,326],{"class":264},[235,733,734],{"class":176,"line":294},[235,735,736],{"class":450},"# Quarterly sales — revised | draft | Cell\n",[10,738,739,740,744],{},"For sheets where merged blocks stand for \"this value applies to every cell below\", unmerging and filling the value into each cell is a data-cleaning step in its own right — ",[37,741,743],{"href":742},"\u002Fgetting-started-with-python-excel-automation\u002Fusing-openpyxl-for-excel-file-manipulation\u002Funmerge-cells-and-fill-values-with-openpyxl\u002F","Unmerge Cells and Fill the Values with openpyxl"," covers it.",[10,746,747],{},"Choosing between Step 2 and Step 3 is really a question about what the merge means. A merged title or a merged address block is presentation: the sheet shows one value across several columns, and your script should respect that by writing to the corner. A merge over cells that should each hold data — a header that someone merged across two quarters, a \"Region\" label merged down five rows — is structure that fights the data, and unmerging is the honest fix. When the workbook is a template owned by someone else, ask before unmerging; when it is output your own script produces, simply stop creating that merge.",[43,749,52,755,52,758,52,761,52,766,52,773,52,781,52,787,52,791,52,797,52,803,52,807,52,812,52,816,52,821,52,825,52,831,52,836,52,843,52,848,52,852,52,856,52,861,52,864,52,869,52,872,52,877,52,880],{"viewBox":750,"role":46,"ariaLabelledBy":751,"xmlns":50,"style":754},"4 0 752 233",[752,753],"mc-choose-t","mc-choose-d","width:100%;max-width:752px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[54,756,757],{"id":752},"Write to the corner or unmerge",[58,759,760],{"id":753},"Two panels. Write to the top-left cell when the merge is presentation, such as a title, an address block or a template label, and one value should cover the block. Unmerge when each covered cell needs its own value, such as merged headers over data columns or labels merged down rows, and the cells must be written separately.",[62,762],{"x":763,"y":65,"width":764,"height":765,"fill":68},"4","752","233",[62,767],{"x":768,"y":769,"width":770,"height":771,"rx":772,"fill":122,"stroke":123,"style":182},"20.0","16.0","351.0","201.0","14",[197,774,776],{"transform":775},"translate(182.5,29.0) scale(1.083)",[201,777],{"d":778,"fill":204,"stroke":779,"style":780},"M5 12.5l4.5 4.5L19 7","var(--teal-ink,#0b6157)","stroke-width:1.66px;stroke-linecap:round;stroke-linejoin:round",[19,782,786],{"x":783,"y":784,"style":785},"195.5","78.0","font-size:14px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Write to the top-left cell",[176,788],{"x1":789,"y1":117,"x2":790,"y2":117,"stroke":123,"style":83},"36.0","355.0",[19,792,796],{"x":793,"y":794,"style":795},"60.0","111.0","font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:start","merged title or banner",[197,798,800],{"transform":799},"translate(38.5,99.5) scale(0.625)",[201,801],{"d":778,"fill":204,"stroke":123,"style":802},"stroke-width:2.88px;stroke-linecap:round;stroke-linejoin:round",[19,804,806],{"x":793,"y":805,"style":795},"137.0","address or signature block",[197,808,810],{"transform":809},"translate(38.5,125.5) scale(0.625)",[201,811],{"d":778,"fill":204,"stroke":123,"style":802},[19,813,815],{"x":793,"y":814,"style":795},"163.0","template label you fill",[197,817,819],{"transform":818},"translate(38.5,151.5) scale(0.625)",[201,820],{"d":778,"fill":204,"stroke":123,"style":802},[19,822,824],{"x":793,"y":823,"style":795},"189.0","values for each covered cell",[197,826,828],{"transform":827},"translate(38.5,177.5) scale(0.625)",[201,829],{"d":830,"fill":204,"stroke":195,"style":802},"M6 6l12 12 M18 6L6 18",[62,832],{"x":833,"y":769,"width":770,"height":771,"rx":772,"fill":834,"stroke":835,"style":182},"389.0","#fdefd8","var(--gold,#b4740a)",[197,837,839],{"transform":838},"translate(551.5,29.0) scale(1.083)",[201,840],{"d":841,"fill":204,"stroke":842,"style":780},"M4 12h6 M10 12l6-6h4 M10 12l6 6h4 M17 3l3 3-3 3 M17 15l3 3-3 3","var(--gold-ink,#7a4e06)",[19,844,847],{"x":845,"y":784,"style":846},"564.5","font-size:14px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Unmerge the range",[176,849],{"x1":850,"y1":117,"x2":851,"y2":117,"stroke":835,"style":83},"405.0","724.0",[19,853,855],{"x":854,"y":794,"style":795},"429.0","header merged over data columns",[197,857,859],{"transform":858},"translate(407.5,99.5) scale(0.625)",[201,860],{"d":778,"fill":204,"stroke":123,"style":802},[19,862,863],{"x":854,"y":805,"style":795},"label merged down many rows",[197,865,867],{"transform":866},"translate(407.5,125.5) scale(0.625)",[201,868],{"d":778,"fill":204,"stroke":123,"style":802},[19,870,871],{"x":854,"y":814,"style":795},"output your own script creates",[197,873,875],{"transform":874},"translate(407.5,151.5) scale(0.625)",[201,876],{"d":778,"fill":204,"stroke":123,"style":802},[19,878,879],{"x":854,"y":823,"style":795},"somebody else's template, unasked",[197,881,883],{"transform":882},"translate(407.5,177.5) scale(0.625)",[201,884],{"d":830,"fill":204,"stroke":195,"style":802},[223,886,888],{"id":887},"step-4-the-hidden-cause-inserting-or-deleting-rows","Step 4 — The hidden cause: inserting or deleting rows",[10,890,891],{},"Here is the version of the error that confuses people. The merge is on the title row, and the script writes to a data row — yet it still fails:",[14,893,895],{"className":251,"code":894,"language":253,"meta":20,"style":20},"wb = Workbook()\nws = wb.active\nws.merge_cells(\"A1:C1\")\nws[\"A1\"] = \"Quarterly sales\"\nws.append([\"Region\", \"Q1\", \"Q2\"])\n\nws.insert_rows(1)                     # make room for a banner line\n\nprint(ws.merged_cells.ranges)         # still A1:C1\nprint({c: type(ws[c]).__name__ for c in (\"A1\", \"B1\", \"A2\", \"B2\", \"C2\")})\n# {'A1': 'Cell', 'B1': 'Cell', 'A2': 'Cell', 'B2': 'MergedCell', 'C2': 'MergedCell'}\n\ntry:\n    ws[\"B2\"] = \"Q1\"\nexcept AttributeError as exc:\n    print(\"B2:\", exc)\n",[22,896,897,905,913,921,933,949,953,966,970,980,1029,1035,1040,1047,1061,1072],{"__ignoreMap":20},[235,898,899,901,903],{"class":176,"line":237},[235,900,297],{"class":264},[235,902,300],{"class":260},[235,904,303],{"class":264},[235,906,907,909,911],{"class":176,"line":274},[235,908,309],{"class":264},[235,910,300],{"class":260},[235,912,314],{"class":264},[235,914,915,917,919],{"class":176,"line":287},[235,916,320],{"class":264},[235,918,323],{"class":244},[235,920,326],{"class":264},[235,922,923,925,927,929,931],{"class":176,"line":294},[235,924,332],{"class":264},[235,926,335],{"class":244},[235,928,338],{"class":264},[235,930,300],{"class":260},[235,932,343],{"class":244},[235,934,935,937,939,941,943,945,947],{"class":176,"line":306},[235,936,349],{"class":264},[235,938,352],{"class":244},[235,940,355],{"class":264},[235,942,358],{"class":244},[235,944,355],{"class":264},[235,946,363],{"class":244},[235,948,366],{"class":264},[235,950,951],{"class":176,"line":317},[235,952,291],{"emptyLinePlaceholder":290},[235,954,955,958,960,963],{"class":176,"line":329},[235,956,957],{"class":264},"ws.insert_rows(",[235,959,118],{"class":379},[235,961,962],{"class":264},")                     ",[235,964,965],{"class":450},"# make room for a banner line\n",[235,967,968],{"class":176,"line":346},[235,969,291],{"emptyLinePlaceholder":290},[235,971,972,974,977],{"class":176,"line":369},[235,973,413],{"class":379},[235,975,976],{"class":264},"(ws.merged_cells.ranges)         ",[235,978,979],{"class":450},"# still A1:C1\n",[235,981,982,984,987,989,992,994,997,1000,1002,1005,1007,1009,1011,1013,1016,1018,1021,1023,1026],{"class":176,"line":650},[235,983,413],{"class":379},[235,985,986],{"class":264},"({c: ",[235,988,419],{"class":379},[235,990,991],{"class":264},"(ws[c]).",[235,993,430],{"class":379},[235,995,996],{"class":260}," for",[235,998,999],{"class":264}," c ",[235,1001,567],{"class":260},[235,1003,1004],{"class":264}," (",[235,1006,335],{"class":244},[235,1008,355],{"class":264},[235,1010,439],{"class":244},[235,1012,355],{"class":264},[235,1014,1015],{"class":244},"\"A2\"",[235,1017,355],{"class":264},[235,1019,1020],{"class":244},"\"B2\"",[235,1022,355],{"class":264},[235,1024,1025],{"class":244},"\"C2\"",[235,1027,1028],{"class":264},")})\n",[235,1030,1032],{"class":176,"line":1031},11,[235,1033,1034],{"class":450},"# {'A1': 'Cell', 'B1': 'Cell', 'A2': 'Cell', 'B2': 'MergedCell', 'C2': 'MergedCell'}\n",[235,1036,1038],{"class":176,"line":1037},12,[235,1039,291],{"emptyLinePlaceholder":290},[235,1041,1043,1045],{"class":176,"line":1042},13,[235,1044,460],{"class":260},[235,1046,463],{"class":264},[235,1048,1050,1052,1054,1056,1058],{"class":176,"line":1049},14,[235,1051,468],{"class":264},[235,1053,1020],{"class":244},[235,1055,338],{"class":264},[235,1057,300],{"class":260},[235,1059,1060],{"class":244}," \"Q1\"\n",[235,1062,1064,1066,1068,1070],{"class":176,"line":1063},15,[235,1065,482],{"class":260},[235,1067,485],{"class":379},[235,1069,488],{"class":260},[235,1071,491],{"class":264},[235,1073,1075,1077,1079,1082],{"class":176,"line":1074},16,[235,1076,496],{"class":379},[235,1078,416],{"class":264},[235,1080,1081],{"class":244},"\"B2:\"",[235,1083,1084],{"class":264},", exc)\n",[10,1086,1087,1090,1091,1094,1095,1097,1098,355,1101,1104,1105,1108],{},[22,1088,1089],{},"insert_rows"," moved every cell down one row, ",[32,1092,1093],{},"including the placeholders",", so B2 and C2 are now ",[22,1096,29],{}," objects. But it did not move the merged range, which still says A1:C1. The sheet is now inconsistent: the placeholders sit on row 2, where the title went, while the merge covers row 1. Save it and the file records a merge over the empty banner row with the title left unmerged underneath it. ",[22,1099,1100],{},"delete_rows",[22,1102,1103],{},"insert_cols"," and ",[22,1106,1107],{},"delete_cols"," behave the same way.",[43,1110,52,1116,52,1119,52,1122,52,1127,52,1131,52,1134,52,1138,52,1142,52,1145,52,1148,52,1151,52,1154,52,1158,52,1161,52,1164,52,1167,52,1169,52,1171,52,1173,52,1175,52,1177,52,1179,52,1181,52,1184,52,1188,52,1191,52,1195,52,1198,52,1201,52,1204,52,1206,52,1209,52,1211,52,1213,52,1215,52,1217,52,1219,52,1221,52,1223,52,1225,52,1229,52,1231,52,1233,52,1236,52,1239,52,1241,52,1243,52,1245,52,1247,52,1249,52,1251,52,1257,52,1261,52,1266,52,1269,52,1277],{"viewBox":1111,"role":46,"ariaLabelledBy":1112,"xmlns":50,"style":1115},"15 0 730 164",[1113,1114],"mc-shift-t","mc-shift-d","width:100%;max-width:730px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[54,1117,1118],{"id":1113},"What insert_rows does to a merged title",[58,1120,1121],{"id":1114},"Before: the title in A1 is merged across A1:C1. After insert_rows(1), the cells moved down so the title and its MergedCell placeholders are on row 2, but the merged range still covers A1:C1, leaving row 2 with read-only placeholders in B2 and C2.",[62,1123],{"x":1124,"y":65,"width":1125,"height":1126,"fill":68},"15","730","164",[62,1128],{"x":1129,"y":1130,"width":79,"height":80,"rx":65,"fill":81,"stroke":82,"style":83},"31.3","40.0",[62,1132],{"x":1133,"y":1130,"width":87,"height":80,"rx":65,"fill":81,"stroke":82,"style":83},"63.3",[19,1135,93],{"x":1136,"y":1137,"style":92},"96.7","57.0",[62,1139],{"x":1140,"y":1130,"width":1141,"height":80,"rx":65,"fill":81,"stroke":82,"style":83},"130.1","44.0",[19,1143,101],{"x":1144,"y":1137,"style":92},"152.1",[62,1146],{"x":1147,"y":1130,"width":1141,"height":80,"rx":65,"fill":81,"stroke":82,"style":83},"174.1",[19,1149,109],{"x":1150,"y":1137,"style":92},"196.1",[62,1152],{"x":1129,"y":1153,"width":79,"height":113,"rx":65,"fill":81,"stroke":82,"style":83},"64.0",[19,1155,118],{"x":1156,"y":1157,"style":92},"47.3","83.0",[62,1159],{"x":1133,"y":1153,"width":1160,"height":113,"rx":65,"fill":122,"stroke":123,"style":124},"154.7",[19,1162,129],{"x":1163,"y":1157,"style":128},"140.7",[62,1165],{"x":1129,"y":1166,"width":79,"height":113,"rx":65,"fill":81,"stroke":82,"style":83},"92.0",[19,1168,136],{"x":1156,"y":794,"style":92},[62,1170],{"x":1133,"y":1166,"width":87,"height":113,"rx":65,"fill":68,"stroke":82,"style":83},[19,1172,142],{"x":1136,"y":794,"style":141},[62,1174],{"x":1140,"y":1166,"width":1141,"height":113,"rx":65,"fill":68,"stroke":82,"style":83},[19,1176,147],{"x":1144,"y":794,"style":141},[62,1178],{"x":1147,"y":1166,"width":1141,"height":113,"rx":65,"fill":68,"stroke":82,"style":83},[19,1180,152],{"x":1150,"y":794,"style":141},[62,1182],{"x":1183,"y":1130,"width":79,"height":80,"rx":65,"fill":81,"stroke":82,"style":83},"410.3",[62,1185],{"x":1186,"y":1130,"width":1187,"height":80,"rx":65,"fill":81,"stroke":82,"style":83},"442.3","117.7",[19,1189,93],{"x":1190,"y":1137,"style":92},"501.1",[62,1192],{"x":1193,"y":1130,"width":1194,"height":80,"rx":65,"fill":81,"stroke":82,"style":83},"559.9","84.4",[19,1196,101],{"x":1197,"y":1137,"style":92},"602.1",[62,1199],{"x":1200,"y":1130,"width":1194,"height":80,"rx":65,"fill":81,"stroke":82,"style":83},"644.3",[19,1202,109],{"x":1203,"y":1137,"style":92},"686.5",[62,1205],{"x":1183,"y":1153,"width":79,"height":113,"rx":65,"fill":81,"stroke":82,"style":83},[19,1207,118],{"x":1208,"y":1157,"style":92},"426.3",[62,1210],{"x":1186,"y":1153,"width":1187,"height":113,"rx":65,"fill":834,"stroke":835,"style":124},[62,1212],{"x":1193,"y":1153,"width":1194,"height":113,"rx":65,"fill":834,"stroke":835,"style":124},[62,1214],{"x":1200,"y":1153,"width":1194,"height":113,"rx":65,"fill":834,"stroke":835,"style":124},[62,1216],{"x":1183,"y":1166,"width":79,"height":113,"rx":65,"fill":81,"stroke":82,"style":83},[19,1218,136],{"x":1208,"y":794,"style":92},[62,1220],{"x":1186,"y":1166,"width":1187,"height":113,"rx":65,"fill":68,"stroke":82,"style":83},[19,1222,129],{"x":1190,"y":794,"style":141},[62,1224],{"x":1193,"y":1166,"width":1194,"height":113,"rx":65,"fill":194,"stroke":195,"style":124},[19,1226,1228],{"x":1197,"y":794,"style":1227},"font-size:12px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","read-only",[62,1230],{"x":1200,"y":1166,"width":1194,"height":113,"rx":65,"fill":194,"stroke":195,"style":124},[19,1232,1228],{"x":1203,"y":794,"style":1227},[62,1234],{"x":1183,"y":1235,"width":79,"height":113,"rx":65,"fill":81,"stroke":82,"style":83},"120.0",[19,1237,159],{"x":1208,"y":1238,"style":92},"139.0",[62,1240],{"x":1186,"y":1235,"width":1187,"height":113,"rx":65,"fill":68,"stroke":82,"style":83},[19,1242,142],{"x":1190,"y":1238,"style":141},[62,1244],{"x":1193,"y":1235,"width":1194,"height":113,"rx":65,"fill":68,"stroke":82,"style":83},[19,1246,147],{"x":1197,"y":1238,"style":141},[62,1248],{"x":1200,"y":1235,"width":1194,"height":113,"rx":65,"fill":68,"stroke":82,"style":83},[19,1250,152],{"x":1203,"y":1238,"style":141},[19,1252,1256],{"x":1253,"y":1254,"style":1255},"124.7","29.0","font-size:12.5px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","before — merge A1:C1",[19,1258,1260],{"x":1259,"y":1254,"style":1255},"569.5","after — merge still A1:C1",[176,1262],{"x1":1263,"y1":1264,"x2":1265,"y2":1264,"stroke":181,"style":182},"228.1","80.0","396.3",[184,1267],{"points":1268,"fill":187},"400.3,80.0 391.3,85.0 391.3,75.0",[62,1270],{"x":1271,"y":1272,"width":1273,"height":1274,"rx":1275,"fill":68,"stroke":82,"style":1276},"253.3","47.0","121.8","22.0","11","stroke-width:1.2px",[19,1278,1282],{"x":1279,"y":1280,"style":1281},"314.2","62.0","font-size:11.5px;font-weight:400;fill:var(--brand-strong,#4338ca);text-anchor:middle;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace","insert_rows(1)",[10,1284,1285,1286,1289,1290,1293],{},"The fix is to move the merged ranges yourself, straight after the insert or delete. Each ",[22,1287,1288],{},"MergedCellRange"," has a ",[22,1291,1292],{},"shift"," method; ranges at or below the insertion point move by the number of rows inserted:",[14,1295,1297],{"className":251,"code":1296,"language":253,"meta":20,"style":20},"def insert_rows_keeping_merges(ws, idx, amount=1):\n    moving = [r for r in ws.merged_cells.ranges if r.min_row >= idx]\n    for rng in moving:\n        ws.unmerge_cells(str(rng))\n    ws.insert_rows(idx, amount)\n    for rng in moving:\n        rng.shift(row_shift=amount)\n        ws.merge_cells(str(rng))\n\nwb = Workbook()\nws = wb.active\nws.merge_cells(\"A1:C1\")\nws[\"A1\"] = \"Quarterly sales\"\nws.append([\"Region\", \"Q1\", \"Q2\"])\n\ninsert_rows_keeping_merges(ws, 1)\nprint(ws.merged_cells.ranges, ws[\"A2\"].value)\nws[\"A1\"] = \"Banner line\"          # works: row 1 is ordinary cells now\nprint(type(ws[\"B2\"]).__name__)    # MergedCell — correctly, it is inside A2:C2\n",[22,1298,1299,1316,1349,1360,1371,1376,1386,1399,1408,1412,1420,1428,1436,1448,1464,1468,1477,1489,1506],{"__ignoreMap":20},[235,1300,1301,1303,1306,1309,1311,1313],{"class":176,"line":237},[235,1302,544],{"class":260},[235,1304,1305],{"class":547}," insert_rows_keeping_merges",[235,1307,1308],{"class":264},"(ws, idx, amount",[235,1310,300],{"class":260},[235,1312,118],{"class":379},[235,1314,1315],{"class":264},"):\n",[235,1317,1318,1321,1323,1326,1329,1332,1334,1337,1340,1343,1346],{"class":176,"line":274},[235,1319,1320],{"class":264},"    moving ",[235,1322,300],{"class":260},[235,1324,1325],{"class":264}," [r ",[235,1327,1328],{"class":260},"for",[235,1330,1331],{"class":264}," r ",[235,1333,567],{"class":260},[235,1335,1336],{"class":264}," ws.merged_cells.ranges ",[235,1338,1339],{"class":260},"if",[235,1341,1342],{"class":264}," r.min_row ",[235,1344,1345],{"class":260},">=",[235,1347,1348],{"class":264}," idx]\n",[235,1350,1351,1353,1355,1357],{"class":176,"line":287},[235,1352,561],{"class":260},[235,1354,564],{"class":264},[235,1356,567],{"class":260},[235,1358,1359],{"class":264}," moving:\n",[235,1361,1362,1365,1368],{"class":176,"line":294},[235,1363,1364],{"class":264},"        ws.unmerge_cells(",[235,1366,1367],{"class":379},"str",[235,1369,1370],{"class":264},"(rng))\n",[235,1372,1373],{"class":176,"line":306},[235,1374,1375],{"class":264},"    ws.insert_rows(idx, amount)\n",[235,1377,1378,1380,1382,1384],{"class":176,"line":317},[235,1379,561],{"class":260},[235,1381,564],{"class":264},[235,1383,567],{"class":260},[235,1385,1359],{"class":264},[235,1387,1388,1391,1394,1396],{"class":176,"line":329},[235,1389,1390],{"class":264},"        rng.shift(",[235,1392,1393],{"class":594},"row_shift",[235,1395,300],{"class":260},[235,1397,1398],{"class":264},"amount)\n",[235,1400,1401,1404,1406],{"class":176,"line":346},[235,1402,1403],{"class":264},"        ws.merge_cells(",[235,1405,1367],{"class":379},[235,1407,1370],{"class":264},[235,1409,1410],{"class":176,"line":369},[235,1411,291],{"emptyLinePlaceholder":290},[235,1413,1414,1416,1418],{"class":176,"line":650},[235,1415,297],{"class":264},[235,1417,300],{"class":260},[235,1419,303],{"class":264},[235,1421,1422,1424,1426],{"class":176,"line":1031},[235,1423,309],{"class":264},[235,1425,300],{"class":260},[235,1427,314],{"class":264},[235,1429,1430,1432,1434],{"class":176,"line":1037},[235,1431,320],{"class":264},[235,1433,323],{"class":244},[235,1435,326],{"class":264},[235,1437,1438,1440,1442,1444,1446],{"class":176,"line":1042},[235,1439,332],{"class":264},[235,1441,335],{"class":244},[235,1443,338],{"class":264},[235,1445,300],{"class":260},[235,1447,343],{"class":244},[235,1449,1450,1452,1454,1456,1458,1460,1462],{"class":176,"line":1049},[235,1451,349],{"class":264},[235,1453,352],{"class":244},[235,1455,355],{"class":264},[235,1457,358],{"class":244},[235,1459,355],{"class":264},[235,1461,363],{"class":244},[235,1463,366],{"class":264},[235,1465,1466],{"class":176,"line":1063},[235,1467,291],{"emptyLinePlaceholder":290},[235,1469,1470,1473,1475],{"class":176,"line":1074},[235,1471,1472],{"class":264},"insert_rows_keeping_merges(ws, ",[235,1474,118],{"class":379},[235,1476,326],{"class":264},[235,1478,1480,1482,1485,1487],{"class":176,"line":1479},17,[235,1481,413],{"class":379},[235,1483,1484],{"class":264},"(ws.merged_cells.ranges, ws[",[235,1486,1015],{"class":244},[235,1488,647],{"class":264},[235,1490,1492,1494,1496,1498,1500,1503],{"class":176,"line":1491},18,[235,1493,332],{"class":264},[235,1495,335],{"class":244},[235,1497,338],{"class":264},[235,1499,300],{"class":260},[235,1501,1502],{"class":244}," \"Banner line\"",[235,1504,1505],{"class":450},"          # works: row 1 is ordinary cells now\n",[235,1507,1509,1511,1513,1515,1517,1519,1521,1523,1526],{"class":176,"line":1508},19,[235,1510,413],{"class":379},[235,1512,416],{"class":264},[235,1514,419],{"class":379},[235,1516,422],{"class":264},[235,1518,1020],{"class":244},[235,1520,427],{"class":264},[235,1522,430],{"class":379},[235,1524,1525],{"class":264},")    ",[235,1527,1528],{"class":450},"# MergedCell — correctly, it is inside A2:C2\n",[10,1530,1531,1532,1534],{},"Unmerging before the insert matters: it turns the placeholders back into normal cells, so ",[22,1533,1089],{}," moves ordinary cells and the helper re-creates the merge at the new position. Ranges that straddle the insertion point are rare in reports; if yours has them, decide per case whether they should grow or move.",[223,1536,1538],{"id":1537},"the-column_letter-variant","The column_letter variant",[10,1540,1541],{},"A closely related error appears in column-width code:",[14,1543,1546],{"className":1544,"code":1545,"language":19,"meta":20},[17],"AttributeError: 'MergedCell' object has no attribute 'column_letter'\n",[22,1547,1545],{"__ignoreMap":20},[10,1549,1550,1551,1554,1555,1558],{},"The classic auto-width loop reads the letter from the first cell of each column, and when row 1 has a merged title, the first cell of column B is a placeholder with no ",[22,1552,1553],{},"column_letter",". Use the column ",[32,1556,1557],{},"number",", which both cell types have, and convert it:",[14,1560,1562],{"className":251,"code":1561,"language":253,"meta":20,"style":20},"wb = Workbook()\nws = wb.active\nws.merge_cells(\"A1:C1\")\nws[\"A1\"] = \"Quarterly sales\"\nws.append([\"Region\", \"Quarter one\", \"Quarter two\"])\n\nfor col in ws.iter_cols(min_row=2):\n    letter = get_column_letter(col[0].column)\n    width = max(len(str(c.value)) for c in col if c.value is not None)\n    ws.column_dimensions[letter].width = width + 2\nprint({k: v.width for k, v in ws.column_dimensions.items()})\n",[22,1563,1564,1572,1580,1588,1600,1618,1622,1643,1658,1704,1720],{"__ignoreMap":20},[235,1565,1566,1568,1570],{"class":176,"line":237},[235,1567,297],{"class":264},[235,1569,300],{"class":260},[235,1571,303],{"class":264},[235,1573,1574,1576,1578],{"class":176,"line":274},[235,1575,309],{"class":264},[235,1577,300],{"class":260},[235,1579,314],{"class":264},[235,1581,1582,1584,1586],{"class":176,"line":287},[235,1583,320],{"class":264},[235,1585,323],{"class":244},[235,1587,326],{"class":264},[235,1589,1590,1592,1594,1596,1598],{"class":176,"line":294},[235,1591,332],{"class":264},[235,1593,335],{"class":244},[235,1595,338],{"class":264},[235,1597,300],{"class":260},[235,1599,343],{"class":244},[235,1601,1602,1604,1606,1608,1611,1613,1616],{"class":176,"line":306},[235,1603,349],{"class":264},[235,1605,352],{"class":244},[235,1607,355],{"class":264},[235,1609,1610],{"class":244},"\"Quarter one\"",[235,1612,355],{"class":264},[235,1614,1615],{"class":244},"\"Quarter two\"",[235,1617,366],{"class":264},[235,1619,1620],{"class":176,"line":317},[235,1621,291],{"emptyLinePlaceholder":290},[235,1623,1624,1626,1629,1631,1634,1637,1639,1641],{"class":176,"line":329},[235,1625,1328],{"class":260},[235,1627,1628],{"class":264}," col ",[235,1630,567],{"class":260},[235,1632,1633],{"class":264}," ws.iter_cols(",[235,1635,1636],{"class":594},"min_row",[235,1638,300],{"class":260},[235,1640,136],{"class":379},[235,1642,1315],{"class":264},[235,1644,1645,1648,1650,1653,1655],{"class":176,"line":346},[235,1646,1647],{"class":264},"    letter ",[235,1649,300],{"class":260},[235,1651,1652],{"class":264}," get_column_letter(col[",[235,1654,65],{"class":379},[235,1656,1657],{"class":264},"].column)\n",[235,1659,1660,1663,1665,1668,1670,1673,1675,1677,1680,1682,1684,1686,1688,1690,1693,1696,1699,1702],{"class":176,"line":369},[235,1661,1662],{"class":264},"    width ",[235,1664,300],{"class":260},[235,1666,1667],{"class":379}," max",[235,1669,416],{"class":264},[235,1671,1672],{"class":379},"len",[235,1674,416],{"class":264},[235,1676,1367],{"class":379},[235,1678,1679],{"class":264},"(c.value)) ",[235,1681,1328],{"class":260},[235,1683,999],{"class":264},[235,1685,567],{"class":260},[235,1687,1628],{"class":264},[235,1689,1339],{"class":260},[235,1691,1692],{"class":264}," c.value ",[235,1694,1695],{"class":260},"is",[235,1697,1698],{"class":260}," not",[235,1700,1701],{"class":379}," None",[235,1703,326],{"class":264},[235,1705,1706,1709,1711,1714,1717],{"class":176,"line":650},[235,1707,1708],{"class":264},"    ws.column_dimensions[letter].width ",[235,1710,300],{"class":260},[235,1712,1713],{"class":264}," width ",[235,1715,1716],{"class":260},"+",[235,1718,1719],{"class":379}," 2\n",[235,1721,1722,1724,1727,1729,1732,1734],{"class":176,"line":1031},[235,1723,413],{"class":379},[235,1725,1726],{"class":264},"({k: v.width ",[235,1728,1328],{"class":260},[235,1730,1731],{"class":264}," k, v ",[235,1733,567],{"class":260},[235,1735,1736],{"class":264}," ws.column_dimensions.items()})\n",[10,1738,1739,1740,1743,1744,41],{},"Starting the loop at ",[22,1741,1742],{},"min_row=2"," also stops a long merged title from making column A enormous — its length belongs to the whole merged block, not to one column. The full recipe is in ",[37,1745,1747],{"href":1746},"\u002Fgetting-started-with-python-excel-automation\u002Fwriting-dataframes-to-excel-with-pandas\u002Fauto-fit-column-widths-when-writing-with-pandas\u002F","Auto-Fit Column Widths When Writing with pandas",[223,1749,1751],{"id":1750},"common-pitfalls","Common pitfalls",[1753,1754,1755,1763,1769,1781],"ul",{},[1756,1757,1758,1762],"li",{},[1759,1760,1761],"strong",{},"Catching the error and skipping the cell."," The value you meant to write silently disappears. Decide whether it belongs in the corner (Step 2) or in its own unmerged cell (Step 3).",[1756,1764,1765,1768],{},[1759,1766,1767],{},"Merging before writing data."," Build the grid first, merge last. Merging early turns later writes into placeholders.",[1756,1770,1771,1774,1775,1777,1778,1780],{},[1759,1772,1773],{},"Structural edits on templates with merges."," Any ",[22,1776,1089],{},"\u002F",[22,1779,1100],{}," call on a sheet with merged ranges below the edit point needs the shift from Step 4.",[1756,1782,1783,1786,1787,1790],{},[1759,1784,1785],{},"Assuming read-only mode helps."," Workbooks opened with ",[22,1788,1789],{},"read_only=True"," cannot be written at all; the error you get there is different, and the answer is to open the file normally.",[223,1792,1794],{"id":1793},"performance-notes","Performance notes",[10,1796,1797,1798,1800,1801,1804],{},"Scanning ",[22,1799,509],{}," is cheap — even heavily formatted reports have tens of merges, not thousands. If a helper like ",[22,1802,1803],{},"owner()"," is called for every cell of a large sheet, build a dictionary from coordinate to corner once and look cells up in it, rather than scanning every range for every cell.",[223,1806,1808],{"id":1807},"conclusion","Conclusion",[10,1810,1811,1814,1815,399,1817,1819,1820,1822,1823,1826,1827,41],{},[22,1812,1813],{},"'MergedCell' object attribute 'value' is read-only"," means you wrote to a covered cell of a merged range. Write to the corner when the value belongs to the whole block, unmerge when cells need their own values, and after ",[22,1816,1089],{},[22,1818,1100],{}," shift the merged ranges yourself, because openpyxl moves the placeholders but not the merges. The ",[22,1821,1553],{}," variant has the same cause and is fixed by working from ",[22,1824,1825],{},"cell.column"," with ",[22,1828,1829],{},"get_column_letter",[223,1831,1833],{"id":1832},"frequently-asked-questions","Frequently asked questions",[10,1835,1836,1839],{},[1759,1837,1838],{},"Why can I write to A1 but not B1 in a merged range A1:C1?","\nOnly the top-left cell of a merged range holds a value. openpyxl represents every other cell in the range as a MergedCell placeholder, and placeholders have no writable value. Write to A1.",[10,1841,1842,1845],{},[1759,1843,1844],{},"Why do I get the error on a row that has no merged cells in Excel?","\nMost often because insert_rows or delete_rows ran earlier in the script. They move the MergedCell placeholders along with the other cells, but they do not move the merged ranges, so ordinary data cells end up as read-only placeholders. Shift the ranges yourself after inserting or deleting.",[10,1847,1848,1851],{},[1759,1849,1850],{},"Does unmerging lose data?","\nNo. After a merge only the top-left cell has a value, so unmerging leaves that value where it is and turns the other cells back into empty, writable cells.",[10,1853,1854,1857,1858,1860,1861,1863],{},[1759,1855,1856],{},"What about 'MergedCell' object has no attribute 'column_letter'?","\nSame cause, different attribute. Code that loops over ws.columns and reads col",[235,1859,65],{},".column_letter fails when the first cell of a column is a placeholder. Use get_column_letter from openpyxl.utils on col",[235,1862,65],{},".column instead, which works for both cell types.",[223,1865,1867],{"id":1866},"related","Related",[1753,1869,1870,1876,1883,1890],{},[1756,1871,1872,1873,1875],{},"Up one level: ",[37,1874,40],{"href":39}," — other openpyxl errors and their causes.",[1756,1877,1878,1882],{},[37,1879,1881],{"href":1880},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fstyling-excel-cells-with-openpyxl\u002Fmerge-cells-and-centre-a-report-title-with-openpyxl\u002F","Merge Cells and Centre a Report Title with openpyxl"," — creating merges the right way round.",[1756,1884,1885,1889],{},[37,1886,1888],{"href":1887},"\u002Fgetting-started-with-python-excel-automation\u002Fusing-openpyxl-for-excel-file-manipulation\u002Finsert-and-delete-rows-and-columns-with-openpyxl\u002F","Insert and Delete Rows and Columns with openpyxl"," — what else structural edits do and do not move.",[1756,1891,1892,1896],{},[37,1893,1895],{"href":1894},"\u002Fgetting-started-with-python-excel-automation\u002Freading-excel-files-with-pandas\u002Fhandle-merged-cells-when-reading-excel-with-pandas\u002F","Handle Merged Cells When Reading Excel with pandas"," — the reading side of merges.",[1898,1899,1900],"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-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":20,"searchDepth":274,"depth":274,"links":1902},[1903,1904,1905,1906,1907,1908,1909,1910,1911,1912,1913],{"id":225,"depth":274,"text":226},{"id":388,"depth":274,"text":389},{"id":530,"depth":274,"text":531},{"id":659,"depth":274,"text":660},{"id":887,"depth":274,"text":888},{"id":1537,"depth":274,"text":1538},{"id":1750,"depth":274,"text":1751},{"id":1793,"depth":274,"text":1794},{"id":1807,"depth":274,"text":1808},{"id":1832,"depth":274,"text":1833},{"id":1866,"depth":274,"text":1867},"2026-09-25","openpyxl raises this AttributeError when code writes to a cell that sits inside a merged range. Find the merge, write to its top-left cell, and watch for the less obvious cause — insert_rows and delete_rows move cells but leave merged ranges behind.","md",[1918,1920,1922,1924],{"q":1838,"a":1919},"Only the top-left cell of a merged range holds a value. openpyxl represents every other cell in the range as a MergedCell placeholder, and placeholders have no writable value. Write to A1.",{"q":1844,"a":1921},"Most often because insert_rows or delete_rows ran earlier in the script. They move the MergedCell placeholders along with the other cells, but they do not move the merged ranges, so ordinary data cells end up as read-only placeholders. Shift the ranges yourself after inserting or deleting.",{"q":1850,"a":1923},"No. After a merge only the top-left cell has a value, so unmerging leaves that value where it is and turns the other cells back into empty, writable cells.",{"q":1856,"a":1925},"Same cause, different attribute. Code that loops over ws.columns and reads col[0].column_letter fails when the first cell of a column is a placeholder. Use get_column_letter from openpyxl.utils on col[0].column instead, which works for both cell types.","fix mergedcell object attribute value is read-only in openpyxl",null,{"breadcrumb":1929},[1930,1932,1935],{"name":1931,"item":1777},"Home",{"name":1933,"item":1934},"Getting Started with Python Excel Automation","\u002Fgetting-started-with-python-excel-automation\u002F",{"name":40,"item":39},"\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-mergedcell-value-is-read-only-in-openpyxl",{"title":1938,"description":1939},"Fix openpyxl AttributeError: 'MergedCell' object attribute 'value' is read-only","Why openpyxl refuses to write to a MergedCell, how to find the merged range that owns a cell, and how insert_rows and delete_rows leave merges misaligned.","fix-mergedcell-value-is-read-only-in-openpyxl","getting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-mergedcell-value-is-read-only-in-openpyxl\u002Findex",[1943,1944,1945],"troubleshooting","openpyxl","messy-sheets","how-to","K5B_IDnLDIWF-W6n6OGQX6y__ptVS9r9RDxYPxTpf3Q",[1949,1953],{"title":1950,"path":1951,"stem":1952,"children":-1},"Fix Long ID Numbers Losing Digits or Turning into 1.23E+18 in Excel","\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-long-id-numbers-losing-digits-in-excel","getting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-long-id-numbers-losing-digits-in-excel\u002Findex",{"title":1954,"path":1955,"stem":1956,"children":-1},"Fix \"Missing optional dependency 'openpyxl'\" in pandas","\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-missing-optional-dependency-openpyxl","getting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-missing-optional-dependency-openpyxl\u002Findex",[1958,1960],{"path":1959,"title":1933},"\u002Fgetting-started-with-python-excel-automation",{"path":1961,"title":40},"\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors",[],[1964,1968,1971,1974,1977,1980],{"path":1965,"title":1966,"kind":1967},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fhandle-permission-denied-when-writing-excel-in-python","Handle Permission Denied When Writing Excel in Python","fix",{"path":1969,"title":1970,"kind":1946},"\u002Fgetting-started-with-python-excel-automation\u002Fusing-openpyxl-for-excel-file-manipulation\u002Fwhat-openpyxl-loses-when-saving-an-existing-workbook","What openpyxl Loses When You Save an Existing Workbook",{"path":1972,"title":1973,"kind":1967},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Ffix-excel-serial-numbers-showing-instead-of-dates","Fix Excel Serial Numbers Showing Instead of Dates",{"path":1975,"title":1976,"kind":1946},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Fhandle-daylight-saving-time-in-excel-timestamps","Handle Daylight Saving Time in Excel Timestamps",{"path":1978,"title":1979,"kind":1946},"\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002Fhandle-microsoft-graph-throttling-for-excel","Handle Microsoft Graph Throttling in Excel Automation",{"path":1981,"title":1982,"kind":1983},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation","Error Handling and Logging in Excel Automation","guide",1790972065950]