[{"data":1,"prerenderedAt":2089},["ShallowReactive",2],{"doc:\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fconvert-excel-sheets-to-yaml-or-toml-config":3,"surround:\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fconvert-excel-sheets-to-yaml-or-toml-config":2053,"ancestors:\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fconvert-excel-sheets-to-yaml-or-toml-config":2062,"children:\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fconvert-excel-sheets-to-yaml-or-toml-config":2067,"related:\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fconvert-excel-sheets-to-yaml-or-toml-config":2068},{"id":4,"title":5,"body":6,"dateModified":948,"datePublished":948,"description":2021,"extension":2022,"faq":2023,"keyword":2032,"kind":2033,"meta":2034,"navigation":125,"path":2043,"seo":2044,"slug":2047,"stem":2048,"topics":2049,"type":2051,"__hash__":2052},"docs\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fconvert-excel-sheets-to-yaml-or-toml-config\u002Findex.md","Convert Excel Sheets to YAML or TOML Config",{"type":7,"value":8,"toc":2009},"minimark",[9,24,29,60,79,83,701,738,1003,1007,1140,1193,1197,1208,1406,1432,1523,1527,1530,1691,1709,1868,1872,1879,1883,1932,1936,1946,1950,1956,1962,1968,1974,1978,2005],[10,11,12,13,17,18,23],"p",{},"Many automation projects keep their configuration in a workbook because that is where the people who own it are comfortable: thresholds, report recipients, region-to-manager mappings, product categories. The programs that use it, though, are happier with a text config file — YAML for a deployment or a scheduler, TOML for a Python tool's ",[14,15,16],"code",{},"pyproject","-style settings — which can be versioned, diffed and reviewed in a pull request. A small converter bridges the two: the business edits the workbook, the converter writes the config file, and the job reads the config file. Doing that well means getting the types right — Excel stores every number as a float and every date as a datetime — handling empty cells, which TOML cannot express, and checking that the generated file reads back to exactly the data in the workbook. This page builds that converter for both formats. It belongs to ",[19,20,22],"a",{"href":21},"\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002F","Handling Excel File Formats and Conversions",".",[25,26,28],"h2",{"id":27},"prerequisites","Prerequisites",[30,31,36],"pre",{"className":32,"code":33,"language":34,"meta":35,"style":35},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install openpyxl pyyaml tomli-w\n","bash","",[14,37,38],{"__ignoreMap":35},[39,40,43,47,51,54,57],"span",{"class":41,"line":42},"line",1,[39,44,46],{"class":45},"sMTad","pip",[39,48,50],{"class":49},"srMev"," install",[39,52,53],{"class":49}," openpyxl",[39,55,56],{"class":49}," pyyaml",[39,58,59],{"class":49}," tomli-w\n",[10,61,62,65,66,69,70,74,75,78],{},[14,63,64],{},"tomllib",", used to read TOML back, is in the standard library from Python 3.11; ",[14,67,68],{},"tomli-w"," writes TOML. The workbook has a ",[71,72,73],"em",{},"Settings"," sheet of dotted keys and values with a notes column, and a ",[71,76,77],{},"Recipients"," table — the two shapes most configuration takes.",[25,80,82],{"id":81},"step-1-read-the-sheets-with-the-right-types","Step 1 — Read the sheets with the right types",[30,84,88],{"className":85,"code":86,"language":87,"meta":35,"style":35},"language-python shiki shiki-themes github-light github-dark-high-contrast","import datetime as dt\nfrom openpyxl import load_workbook\n\nTRUE_WORDS, FALSE_WORDS = {\"yes\", \"true\", \"y\", \"1\"}, {\"no\", \"false\", \"n\", \"0\"}\n\ndef clean(value):\n    if isinstance(value, float) and value.is_integer():\n        return int(value)                                     # 50000.0 -> 50000\n    if isinstance(value, dt.datetime):\n        return value.date() if value.time() == dt.time() else value\n    if isinstance(value, str):\n        text = value.strip()\n        if text.lower() in TRUE_WORDS:\n            return True\n        if text.lower() in FALSE_WORDS:\n            return False\n        return text\n    return value\n\ndef nest(pairs):\n    root = {}\n    for key, value in pairs:\n        *parents, leaf = key.split(\".\")\n        node = root\n        for p in parents:\n            node = node.setdefault(p, {})\n        node[leaf] = value\n    return root\n\nwb = load_workbook(\"config.xlsx\", data_only=True)\nsettings = nest((k, clean(v)) for k, v, *_ in wb[\"Settings\"].iter_rows(min_row=2, values_only=True) if k)\nrows = list(wb[\"Recipients\"].iter_rows(values_only=True))\nrecipients = [{h.lower(): clean(v) for h, v in zip(rows[0], r)} for r in rows[1:]]\nconfig = {**settings, \"recipients\": recipients}\nprint(config[\"report\"], \"|\", config[\"smtp\"])\n","python",[14,89,90,106,120,127,188,193,206,230,246,256,282,297,309,327,336,350,358,366,374,379,390,401,415,435,446,460,471,481,488,493,520,578,606,652,674],{"__ignoreMap":35},[39,91,92,96,100,103],{"class":41,"line":42},[39,93,95],{"class":94},"s-kum","import",[39,97,99],{"class":98},"skGVy"," datetime ",[39,101,102],{"class":94},"as",[39,104,105],{"class":98}," dt\n",[39,107,109,112,115,117],{"class":41,"line":108},2,[39,110,111],{"class":94},"from",[39,113,114],{"class":98}," openpyxl ",[39,116,95],{"class":94},[39,118,119],{"class":98}," load_workbook\n",[39,121,123],{"class":41,"line":122},3,[39,124,126],{"emptyLinePlaceholder":125},true,"\n",[39,128,130,134,137,140,143,146,149,151,154,156,159,161,164,167,170,172,175,177,180,182,185],{"class":41,"line":129},4,[39,131,133],{"class":132},"sP0c6","TRUE_WORDS",[39,135,136],{"class":98},", ",[39,138,139],{"class":132},"FALSE_WORDS",[39,141,142],{"class":94}," =",[39,144,145],{"class":98}," {",[39,147,148],{"class":49},"\"yes\"",[39,150,136],{"class":98},[39,152,153],{"class":49},"\"true\"",[39,155,136],{"class":98},[39,157,158],{"class":49},"\"y\"",[39,160,136],{"class":98},[39,162,163],{"class":49},"\"1\"",[39,165,166],{"class":98},"}, {",[39,168,169],{"class":49},"\"no\"",[39,171,136],{"class":98},[39,173,174],{"class":49},"\"false\"",[39,176,136],{"class":98},[39,178,179],{"class":49},"\"n\"",[39,181,136],{"class":98},[39,183,184],{"class":49},"\"0\"",[39,186,187],{"class":98},"}\n",[39,189,191],{"class":41,"line":190},5,[39,192,126],{"emptyLinePlaceholder":125},[39,194,196,199,203],{"class":41,"line":195},6,[39,197,198],{"class":94},"def",[39,200,202],{"class":201},"s_Opv"," clean",[39,204,205],{"class":98},"(value):\n",[39,207,209,212,215,218,221,224,227],{"class":41,"line":208},7,[39,210,211],{"class":94},"    if",[39,213,214],{"class":132}," isinstance",[39,216,217],{"class":98},"(value, ",[39,219,220],{"class":132},"float",[39,222,223],{"class":98},") ",[39,225,226],{"class":94},"and",[39,228,229],{"class":98}," value.is_integer():\n",[39,231,233,236,239,242],{"class":41,"line":232},8,[39,234,235],{"class":94},"        return",[39,237,238],{"class":132}," int",[39,240,241],{"class":98},"(value)                                     ",[39,243,245],{"class":244},"s-wDw","# 50000.0 -> 50000\n",[39,247,249,251,253],{"class":41,"line":248},9,[39,250,211],{"class":94},[39,252,214],{"class":132},[39,254,255],{"class":98},"(value, dt.datetime):\n",[39,257,259,261,264,267,270,273,276,279],{"class":41,"line":258},10,[39,260,235],{"class":94},[39,262,263],{"class":98}," value.date() ",[39,265,266],{"class":94},"if",[39,268,269],{"class":98}," value.time() ",[39,271,272],{"class":94},"==",[39,274,275],{"class":98}," dt.time() ",[39,277,278],{"class":94},"else",[39,280,281],{"class":98}," value\n",[39,283,285,287,289,291,294],{"class":41,"line":284},11,[39,286,211],{"class":94},[39,288,214],{"class":132},[39,290,217],{"class":98},[39,292,293],{"class":132},"str",[39,295,296],{"class":98},"):\n",[39,298,300,303,306],{"class":41,"line":299},12,[39,301,302],{"class":98},"        text ",[39,304,305],{"class":94},"=",[39,307,308],{"class":98}," value.strip()\n",[39,310,312,315,318,321,324],{"class":41,"line":311},13,[39,313,314],{"class":94},"        if",[39,316,317],{"class":98}," text.lower() ",[39,319,320],{"class":94},"in",[39,322,323],{"class":132}," TRUE_WORDS",[39,325,326],{"class":98},":\n",[39,328,330,333],{"class":41,"line":329},14,[39,331,332],{"class":94},"            return",[39,334,335],{"class":132}," True\n",[39,337,339,341,343,345,348],{"class":41,"line":338},15,[39,340,314],{"class":94},[39,342,317],{"class":98},[39,344,320],{"class":94},[39,346,347],{"class":132}," FALSE_WORDS",[39,349,326],{"class":98},[39,351,353,355],{"class":41,"line":352},16,[39,354,332],{"class":94},[39,356,357],{"class":132}," False\n",[39,359,361,363],{"class":41,"line":360},17,[39,362,235],{"class":94},[39,364,365],{"class":98}," text\n",[39,367,369,372],{"class":41,"line":368},18,[39,370,371],{"class":94},"    return",[39,373,281],{"class":98},[39,375,377],{"class":41,"line":376},19,[39,378,126],{"emptyLinePlaceholder":125},[39,380,382,384,387],{"class":41,"line":381},20,[39,383,198],{"class":94},[39,385,386],{"class":201}," nest",[39,388,389],{"class":98},"(pairs):\n",[39,391,393,396,398],{"class":41,"line":392},21,[39,394,395],{"class":98},"    root ",[39,397,305],{"class":94},[39,399,400],{"class":98}," {}\n",[39,402,404,407,410,412],{"class":41,"line":403},22,[39,405,406],{"class":94},"    for",[39,408,409],{"class":98}," key, value ",[39,411,320],{"class":94},[39,413,414],{"class":98}," pairs:\n",[39,416,418,421,424,426,429,432],{"class":41,"line":417},23,[39,419,420],{"class":94},"        *",[39,422,423],{"class":98},"parents, leaf ",[39,425,305],{"class":94},[39,427,428],{"class":98}," key.split(",[39,430,431],{"class":49},"\".\"",[39,433,434],{"class":98},")\n",[39,436,438,441,443],{"class":41,"line":437},24,[39,439,440],{"class":98},"        node ",[39,442,305],{"class":94},[39,444,445],{"class":98}," root\n",[39,447,449,452,455,457],{"class":41,"line":448},25,[39,450,451],{"class":94},"        for",[39,453,454],{"class":98}," p ",[39,456,320],{"class":94},[39,458,459],{"class":98}," parents:\n",[39,461,463,466,468],{"class":41,"line":462},26,[39,464,465],{"class":98},"            node ",[39,467,305],{"class":94},[39,469,470],{"class":98}," node.setdefault(p, {})\n",[39,472,474,477,479],{"class":41,"line":473},27,[39,475,476],{"class":98},"        node[leaf] ",[39,478,305],{"class":94},[39,480,281],{"class":98},[39,482,484,486],{"class":41,"line":483},28,[39,485,371],{"class":94},[39,487,445],{"class":98},[39,489,491],{"class":41,"line":490},29,[39,492,126],{"emptyLinePlaceholder":125},[39,494,496,499,501,504,507,509,513,515,518],{"class":41,"line":495},30,[39,497,498],{"class":98},"wb ",[39,500,305],{"class":94},[39,502,503],{"class":98}," load_workbook(",[39,505,506],{"class":49},"\"config.xlsx\"",[39,508,136],{"class":98},[39,510,512],{"class":511},"sa561","data_only",[39,514,305],{"class":94},[39,516,517],{"class":132},"True",[39,519,434],{"class":98},[39,521,523,526,528,531,534,537,540,543,545,548,551,554,557,559,562,564,567,569,571,573,575],{"class":41,"line":522},31,[39,524,525],{"class":98},"settings ",[39,527,305],{"class":94},[39,529,530],{"class":98}," nest((k, clean(v)) ",[39,532,533],{"class":94},"for",[39,535,536],{"class":98}," k, v, ",[39,538,539],{"class":94},"*",[39,541,542],{"class":98},"_ ",[39,544,320],{"class":94},[39,546,547],{"class":98}," wb[",[39,549,550],{"class":49},"\"Settings\"",[39,552,553],{"class":98},"].iter_rows(",[39,555,556],{"class":511},"min_row",[39,558,305],{"class":94},[39,560,561],{"class":132},"2",[39,563,136],{"class":98},[39,565,566],{"class":511},"values_only",[39,568,305],{"class":94},[39,570,517],{"class":132},[39,572,223],{"class":98},[39,574,266],{"class":94},[39,576,577],{"class":98}," k)\n",[39,579,581,584,586,589,592,595,597,599,601,603],{"class":41,"line":580},32,[39,582,583],{"class":98},"rows ",[39,585,305],{"class":94},[39,587,588],{"class":132}," list",[39,590,591],{"class":98},"(wb[",[39,593,594],{"class":49},"\"Recipients\"",[39,596,553],{"class":98},[39,598,566],{"class":511},[39,600,305],{"class":94},[39,602,517],{"class":132},[39,604,605],{"class":98},"))\n",[39,607,609,612,614,617,619,622,624,627,630,633,636,638,641,643,646,649],{"class":41,"line":608},33,[39,610,611],{"class":98},"recipients ",[39,613,305],{"class":94},[39,615,616],{"class":98}," [{h.lower(): clean(v) ",[39,618,533],{"class":94},[39,620,621],{"class":98}," h, v ",[39,623,320],{"class":94},[39,625,626],{"class":132}," zip",[39,628,629],{"class":98},"(rows[",[39,631,632],{"class":132},"0",[39,634,635],{"class":98},"], r)} ",[39,637,533],{"class":94},[39,639,640],{"class":98}," r ",[39,642,320],{"class":94},[39,644,645],{"class":98}," rows[",[39,647,648],{"class":132},"1",[39,650,651],{"class":98},":]]\n",[39,653,655,658,660,662,665,668,671],{"class":41,"line":654},34,[39,656,657],{"class":98},"config ",[39,659,305],{"class":94},[39,661,145],{"class":98},[39,663,664],{"class":94},"**",[39,666,667],{"class":98},"settings, ",[39,669,670],{"class":49},"\"recipients\"",[39,672,673],{"class":98},": recipients}\n",[39,675,677,680,683,686,689,692,695,698],{"class":41,"line":676},35,[39,678,679],{"class":132},"print",[39,681,682],{"class":98},"(config[",[39,684,685],{"class":49},"\"report\"",[39,687,688],{"class":98},"], ",[39,690,691],{"class":49},"\"|\"",[39,693,694],{"class":98},", config[",[39,696,697],{"class":49},"\"smtp\"",[39,699,700],{"class":98},"])\n",[10,702,703,704,707,708,711,712,715,716,719,720,722,723,726,727,136,730,733,734,737],{},"Three type problems are fixed on the way in. Whole numbers come out of Excel as floats in some workflows — a value typed into a cell formatted as currency, or a column that once held decimals — so ",[14,705,706],{},"50000.0"," is turned back into ",[14,709,710],{},"50000",". Dates come out as datetimes at midnight, so a pure date is reduced to a ",[14,713,714],{},"date",". And \"Yes\", \"No\", \"TRUE\" typed as text become real booleans; a real Excel boolean, like the ",[71,717,718],{},"Active"," column, already arrives as ",[14,721,517],{}," or ",[14,724,725],{},"False",". The dotted keys — ",[14,728,729],{},"report.title",[14,731,732],{},"smtp.port"," — are turned into nested sections, which is how both YAML and TOML structure configuration. ",[14,735,736],{},"data_only=True"," reads formula results, so a setting computed by a formula in the workbook converts as its value.",[739,740,748,749,748,753,748,757,748,764,748,773,748,777,748,784,748,788,748,792,748,796,748,800,748,802,748,806,748,808,748,811,748,814,748,817,748,819,748,823,748,825,748,827,748,830,748,834,748,836,748,839,748,841,748,844,748,847,748,850,748,852,748,855,748,857,748,860,748,863,748,867,748,869,748,872,748,874,748,877,748,880,748,884,748,887,748,891,748,894,748,896,748,899,748,901,748,904,748,906,748,908,748,910,748,912,748,914,748,917,748,919,748,921,748,923,748,925,748,927,748,930,748,932,748,935,748,937,748,939,748,941,748,944,748,946,748,949,748,951,748,953,748,955,748,958,748,963,748,967,748,973,748,977,748,984,748,989,748,997],"svg",{"viewBox":741,"role":742,"ariaLabelledBy":743,"xmlns":746,"style":747},"4 0 752 220","img",[744,745],"ym-types-t","ym-types-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:752px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[750,751,752],"title",{"id":744},"From Excel cells to config values",[754,755,756],"desc",{"id":745},"The Settings sheet's dotted keys become nested sections. A value of 50000.0 becomes the integer 50000, the text Yes becomes true, a datetime at midnight becomes a date, and an empty cell becomes null in YAML but is left out in TOML.",[758,759],"rect",{"x":760,"y":632,"width":761,"height":762,"fill":763},"4","752","220","#ffffff",[758,765],{"x":766,"y":767,"width":768,"height":769,"rx":632,"fill":770,"stroke":771,"style":772},"20.0","40.0","32.0","24.0","#e7ebef","var(--line,#cdd5e6)","stroke-width:1px",[758,774],{"x":775,"y":767,"width":776,"height":769,"rx":632,"fill":770,"stroke":771,"style":772},"52.0","113.8",[778,779,783],"text",{"x":780,"y":781,"style":782},"108.9","57.0","font-size:11px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","A",[758,785],{"x":786,"y":767,"width":787,"height":769,"rx":632,"fill":770,"stroke":771,"style":772},"165.8","123.4",[778,789,791],{"x":790,"y":781,"style":782},"227.5","B",[758,793],{"x":766,"y":794,"width":768,"height":795,"rx":632,"fill":770,"stroke":771,"style":772},"64.0","28.0",[778,797,648],{"x":798,"y":799,"style":782},"36.0","83.0",[758,801],{"x":775,"y":794,"width":776,"height":795,"rx":632,"fill":763,"stroke":771,"style":772},[778,803,805],{"x":780,"y":799,"style":804},"font-size:12px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","Key",[758,807],{"x":786,"y":794,"width":787,"height":795,"rx":632,"fill":763,"stroke":771,"style":772},[778,809,810],{"x":790,"y":799,"style":804},"Value",[758,812],{"x":766,"y":813,"width":768,"height":795,"rx":632,"fill":770,"stroke":771,"style":772},"92.0",[778,815,561],{"x":798,"y":816,"style":782},"111.0",[758,818],{"x":775,"y":813,"width":776,"height":795,"rx":632,"fill":763,"stroke":771,"style":772},[778,820,822],{"x":780,"y":816,"style":821},"font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","report.max_rows",[758,824],{"x":786,"y":813,"width":787,"height":795,"rx":632,"fill":763,"stroke":771,"style":772},[778,826,706],{"x":790,"y":816,"style":821},[758,828],{"x":766,"y":829,"width":768,"height":795,"rx":632,"fill":770,"stroke":771,"style":772},"120.0",[778,831,833],{"x":798,"y":832,"style":782},"139.0","3",[758,835],{"x":775,"y":829,"width":776,"height":795,"rx":632,"fill":763,"stroke":771,"style":772},[778,837,838],{"x":780,"y":832,"style":821},"report.send",[758,840],{"x":786,"y":829,"width":787,"height":795,"rx":632,"fill":763,"stroke":771,"style":772},[778,842,843],{"x":790,"y":832,"style":821},"Yes",[758,845],{"x":766,"y":846,"width":768,"height":795,"rx":632,"fill":770,"stroke":771,"style":772},"148.0",[778,848,760],{"x":798,"y":849,"style":782},"167.0",[758,851],{"x":775,"y":846,"width":776,"height":795,"rx":632,"fill":763,"stroke":771,"style":772},[778,853,854],{"x":780,"y":849,"style":821},"report.cutoff",[758,856],{"x":786,"y":846,"width":787,"height":795,"rx":632,"fill":763,"stroke":771,"style":772},[778,858,859],{"x":790,"y":849,"style":821},"2026-10-02 00:00",[758,861],{"x":766,"y":862,"width":768,"height":795,"rx":632,"fill":770,"stroke":771,"style":772},"176.0",[778,864,866],{"x":798,"y":865,"style":782},"195.0","5",[758,868],{"x":775,"y":862,"width":776,"height":795,"rx":632,"fill":763,"stroke":771,"style":772},[778,870,871],{"x":780,"y":865,"style":821},"smtp.reply_to",[758,873],{"x":786,"y":862,"width":787,"height":795,"rx":632,"fill":763,"stroke":771,"style":772},[778,875,876],{"x":790,"y":865,"style":821},"(empty)",[758,878],{"x":879,"y":767,"width":768,"height":769,"rx":632,"fill":770,"stroke":771,"style":772},"499.6",[758,881],{"x":882,"y":767,"width":883,"height":769,"rx":632,"fill":770,"stroke":771,"style":772},"531.6","116.2",[778,885,783],{"x":886,"y":781,"style":782},"589.7",[758,888],{"x":889,"y":767,"width":890,"height":769,"rx":632,"fill":770,"stroke":771,"style":772},"647.8","92.2",[778,892,791],{"x":893,"y":781,"style":782},"693.9",[758,895],{"x":879,"y":794,"width":768,"height":795,"rx":632,"fill":770,"stroke":771,"style":772},[778,897,648],{"x":898,"y":799,"style":782},"515.6",[758,900],{"x":882,"y":794,"width":883,"height":795,"rx":632,"fill":763,"stroke":771,"style":772},[778,902,903],{"x":886,"y":799,"style":804},"Section",[758,905],{"x":889,"y":794,"width":890,"height":795,"rx":632,"fill":763,"stroke":771,"style":772},[778,907,810],{"x":893,"y":799,"style":804},[758,909],{"x":879,"y":813,"width":768,"height":795,"rx":632,"fill":770,"stroke":771,"style":772},[778,911,561],{"x":898,"y":816,"style":782},[758,913],{"x":882,"y":813,"width":883,"height":795,"rx":632,"fill":763,"stroke":771,"style":772},[778,915,916],{"x":886,"y":816,"style":821},"report: max_rows",[758,918],{"x":889,"y":813,"width":890,"height":795,"rx":632,"fill":763,"stroke":771,"style":772},[778,920,710],{"x":893,"y":816,"style":821},[758,922],{"x":879,"y":829,"width":768,"height":795,"rx":632,"fill":770,"stroke":771,"style":772},[778,924,833],{"x":898,"y":832,"style":782},[758,926],{"x":882,"y":829,"width":883,"height":795,"rx":632,"fill":763,"stroke":771,"style":772},[778,928,929],{"x":886,"y":832,"style":821},"report: send",[758,931],{"x":889,"y":829,"width":890,"height":795,"rx":632,"fill":763,"stroke":771,"style":772},[778,933,934],{"x":893,"y":832,"style":821},"true",[758,936],{"x":879,"y":846,"width":768,"height":795,"rx":632,"fill":770,"stroke":771,"style":772},[778,938,760],{"x":898,"y":849,"style":782},[758,940],{"x":882,"y":846,"width":883,"height":795,"rx":632,"fill":763,"stroke":771,"style":772},[778,942,943],{"x":886,"y":849,"style":821},"report: cutoff",[758,945],{"x":889,"y":846,"width":890,"height":795,"rx":632,"fill":763,"stroke":771,"style":772},[778,947,948],{"x":893,"y":849,"style":821},"2026-10-02",[758,950],{"x":879,"y":862,"width":768,"height":795,"rx":632,"fill":770,"stroke":771,"style":772},[778,952,866],{"x":898,"y":865,"style":782},[758,954],{"x":882,"y":862,"width":883,"height":795,"rx":632,"fill":763,"stroke":771,"style":772},[778,956,957],{"x":886,"y":865,"style":821},"smtp: reply_to",[758,959],{"x":889,"y":862,"width":890,"height":795,"rx":632,"fill":960,"stroke":961,"style":962},"#fdefd8","var(--gold,#b4740a)","stroke-width:1.5px",[778,964,966],{"x":893,"y":865,"style":965},"font-size:12px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","null \u002F omitted",[778,968,972],{"x":969,"y":970,"style":971},"154.6","29.0","font-size:12.5px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","before",[778,974,976],{"x":975,"y":970,"style":971},"619.8","after",[41,978],{"x1":979,"y1":980,"x2":981,"y2":980,"stroke":982,"style":983},"299.2","122.0","485.6","var(--brand,#5b5cf0)","stroke-width:2px",[985,986],"polygon",{"points":987,"fill":988},"489.6,122.0 480.6,127.0 480.6,117.0","#5b5cf0",[758,990],{"x":991,"y":992,"width":993,"height":994,"rx":995,"fill":763,"stroke":771,"style":996},"326.2","89.0","136.3","22.0","11","stroke-width:1.2px",[778,998,1002],{"x":999,"y":1000,"style":1001},"394.4","104.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","clean() + nest()",[25,1004,1006],{"id":1005},"step-2-write-yaml","Step 2 — Write YAML",[30,1008,1010],{"className":85,"code":1009,"language":87,"meta":35,"style":35},"import yaml\n\nwith open(\"config.yaml\", \"w\", encoding=\"utf-8\") as f:\n    f.write(\"# Generated from config.xlsx — edit the workbook, not this file\\n\")\n    yaml.safe_dump(config, f, sort_keys=False, allow_unicode=True, default_flow_style=False)\nfrom pathlib import Path\nprint(Path(\"config.yaml\").read_text(encoding=\"utf-8\"))\n",[14,1011,1012,1019,1023,1059,1076,1108,1120],{"__ignoreMap":35},[39,1013,1014,1016],{"class":41,"line":42},[39,1015,95],{"class":94},[39,1017,1018],{"class":98}," yaml\n",[39,1020,1021],{"class":41,"line":108},[39,1022,126],{"emptyLinePlaceholder":125},[39,1024,1025,1028,1031,1034,1037,1039,1042,1044,1047,1049,1052,1054,1056],{"class":41,"line":122},[39,1026,1027],{"class":94},"with",[39,1029,1030],{"class":132}," open",[39,1032,1033],{"class":98},"(",[39,1035,1036],{"class":49},"\"config.yaml\"",[39,1038,136],{"class":98},[39,1040,1041],{"class":49},"\"w\"",[39,1043,136],{"class":98},[39,1045,1046],{"class":511},"encoding",[39,1048,305],{"class":94},[39,1050,1051],{"class":49},"\"utf-8\"",[39,1053,223],{"class":98},[39,1055,102],{"class":94},[39,1057,1058],{"class":98}," f:\n",[39,1060,1061,1064,1067,1071,1074],{"class":41,"line":129},[39,1062,1063],{"class":98},"    f.write(",[39,1065,1066],{"class":49},"\"# Generated from config.xlsx — edit the workbook, not this file",[39,1068,1070],{"class":1069},"sSjpA","\\n",[39,1072,1073],{"class":49},"\"",[39,1075,434],{"class":98},[39,1077,1078,1081,1084,1086,1088,1090,1093,1095,1097,1099,1102,1104,1106],{"class":41,"line":190},[39,1079,1080],{"class":98},"    yaml.safe_dump(config, f, ",[39,1082,1083],{"class":511},"sort_keys",[39,1085,305],{"class":94},[39,1087,725],{"class":132},[39,1089,136],{"class":98},[39,1091,1092],{"class":511},"allow_unicode",[39,1094,305],{"class":94},[39,1096,517],{"class":132},[39,1098,136],{"class":98},[39,1100,1101],{"class":511},"default_flow_style",[39,1103,305],{"class":94},[39,1105,725],{"class":132},[39,1107,434],{"class":98},[39,1109,1110,1112,1115,1117],{"class":41,"line":195},[39,1111,111],{"class":94},[39,1113,1114],{"class":98}," pathlib ",[39,1116,95],{"class":94},[39,1118,1119],{"class":98}," Path\n",[39,1121,1122,1124,1127,1129,1132,1134,1136,1138],{"class":41,"line":208},[39,1123,679],{"class":132},[39,1125,1126],{"class":98},"(Path(",[39,1128,1036],{"class":49},[39,1130,1131],{"class":98},").read_text(",[39,1133,1046],{"class":511},[39,1135,305],{"class":94},[39,1137,1051],{"class":49},[39,1139,605],{"class":98},[10,1141,1142,1145,1146,1149,1150,1153,1154,1157,1158,1161,1162,136,1165,136,1168,1171,1172,1175,1176,1179,1180,1182,1183,136,1186,1189,1190,1192],{},[14,1143,1144],{},"safe_dump"," writes only standard YAML types — no Python-specific tags — so any YAML reader can load the file. ",[14,1147,1148],{},"sort_keys=False"," keeps the order of the workbook, which makes the file easier to compare with it; ",[14,1151,1152],{},"allow_unicode=True"," writes names such as \"Café Señor\" as they are rather than as escape sequences. Dates are written as plain ISO dates, which YAML loaders read back as dates, and the empty ",[14,1155,1156],{},"reply_to"," becomes ",[14,1159,1160],{},"null",". The comment at the top tells anyone who opens the file where to make changes. Two YAML details are worth knowing when the values come from a spreadsheet. Unquoted words such as ",[14,1163,1164],{},"yes",[14,1166,1167],{},"no",[14,1169,1170],{},"on"," and ",[14,1173,1174],{},"off"," are read as booleans by older YAML 1.1 loaders, and codes such as ",[14,1177,1178],{},"0123"," can be read as numbers; ",[14,1181,1144],{}," quotes strings that would be misread, — ",[14,1184,1185],{},"'yes'",[14,1187,1188],{},"'0123'"," — which is one more reason to generate the file rather than write it by hand. And the order of keys in the output follows the workbook only because ",[14,1191,1148],{}," is set — without it, keys are sorted alphabetically and the file no longer mirrors the sheet.",[25,1194,1196],{"id":1195},"step-3-write-toml-which-has-no-null","Step 3 — Write TOML, which has no null",[10,1198,1199,1200,1203,1204,1207],{},"TOML is stricter: it has no null value at all, so an empty cell cannot be written as \"nothing\" — ",[14,1201,1202],{},"tomli_w"," raises ",[14,1205,1206],{},"TypeError: Object of type 'NoneType' is not TOML serializable",". Drop missing values before writing, and make sure the reading program treats a missing key as \"not set\":",[30,1209,1211],{"className":85,"code":1210,"language":87,"meta":35,"style":35},"import tomli_w\n\ndef drop_none(obj):\n    if isinstance(obj, dict):\n        return {k: drop_none(v) for k, v in obj.items() if v is not None}\n    if isinstance(obj, list):\n        return [drop_none(v) for v in obj]\n    return obj\n\ntry:\n    tomli_w.dumps(config)\nexcept TypeError as e:\n    print(\"with None:\", e)\ntoml_text = \"# Generated from config.xlsx — edit the workbook, not this file\\n\" + tomli_w.dumps(drop_none(config))\nPath(\"config.toml\").write_text(toml_text, encoding=\"utf-8\")\nprint(toml_text)\n",[14,1212,1213,1220,1224,1234,1248,1281,1294,1310,1317,1321,1328,1333,1347,1360,1380,1399],{"__ignoreMap":35},[39,1214,1215,1217],{"class":41,"line":42},[39,1216,95],{"class":94},[39,1218,1219],{"class":98}," tomli_w\n",[39,1221,1222],{"class":41,"line":108},[39,1223,126],{"emptyLinePlaceholder":125},[39,1225,1226,1228,1231],{"class":41,"line":122},[39,1227,198],{"class":94},[39,1229,1230],{"class":201}," drop_none",[39,1232,1233],{"class":98},"(obj):\n",[39,1235,1236,1238,1240,1243,1246],{"class":41,"line":129},[39,1237,211],{"class":94},[39,1239,214],{"class":132},[39,1241,1242],{"class":98},"(obj, ",[39,1244,1245],{"class":132},"dict",[39,1247,296],{"class":98},[39,1249,1250,1252,1255,1257,1260,1262,1265,1267,1270,1273,1276,1279],{"class":41,"line":190},[39,1251,235],{"class":94},[39,1253,1254],{"class":98}," {k: drop_none(v) ",[39,1256,533],{"class":94},[39,1258,1259],{"class":98}," k, v ",[39,1261,320],{"class":94},[39,1263,1264],{"class":98}," obj.items() ",[39,1266,266],{"class":94},[39,1268,1269],{"class":98}," v ",[39,1271,1272],{"class":94},"is",[39,1274,1275],{"class":94}," not",[39,1277,1278],{"class":132}," None",[39,1280,187],{"class":98},[39,1282,1283,1285,1287,1289,1292],{"class":41,"line":195},[39,1284,211],{"class":94},[39,1286,214],{"class":132},[39,1288,1242],{"class":98},[39,1290,1291],{"class":132},"list",[39,1293,296],{"class":98},[39,1295,1296,1298,1301,1303,1305,1307],{"class":41,"line":208},[39,1297,235],{"class":94},[39,1299,1300],{"class":98}," [drop_none(v) ",[39,1302,533],{"class":94},[39,1304,1269],{"class":98},[39,1306,320],{"class":94},[39,1308,1309],{"class":98}," obj]\n",[39,1311,1312,1314],{"class":41,"line":232},[39,1313,371],{"class":94},[39,1315,1316],{"class":98}," obj\n",[39,1318,1319],{"class":41,"line":248},[39,1320,126],{"emptyLinePlaceholder":125},[39,1322,1323,1326],{"class":41,"line":258},[39,1324,1325],{"class":94},"try",[39,1327,326],{"class":98},[39,1329,1330],{"class":41,"line":284},[39,1331,1332],{"class":98},"    tomli_w.dumps(config)\n",[39,1334,1335,1338,1341,1344],{"class":41,"line":299},[39,1336,1337],{"class":94},"except",[39,1339,1340],{"class":132}," TypeError",[39,1342,1343],{"class":94}," as",[39,1345,1346],{"class":98}," e:\n",[39,1348,1349,1352,1354,1357],{"class":41,"line":311},[39,1350,1351],{"class":132},"    print",[39,1353,1033],{"class":98},[39,1355,1356],{"class":49},"\"with None:\"",[39,1358,1359],{"class":98},", e)\n",[39,1361,1362,1365,1367,1370,1372,1374,1377],{"class":41,"line":329},[39,1363,1364],{"class":98},"toml_text ",[39,1366,305],{"class":94},[39,1368,1369],{"class":49}," \"# Generated from config.xlsx — edit the workbook, not this file",[39,1371,1070],{"class":1069},[39,1373,1073],{"class":49},[39,1375,1376],{"class":94}," +",[39,1378,1379],{"class":98}," tomli_w.dumps(drop_none(config))\n",[39,1381,1382,1385,1388,1391,1393,1395,1397],{"class":41,"line":338},[39,1383,1384],{"class":98},"Path(",[39,1386,1387],{"class":49},"\"config.toml\"",[39,1389,1390],{"class":98},").write_text(toml_text, ",[39,1392,1046],{"class":511},[39,1394,305],{"class":94},[39,1396,1051],{"class":49},[39,1398,434],{"class":98},[39,1400,1401,1403],{"class":41,"line":352},[39,1402,679],{"class":132},[39,1404,1405],{"class":98},"(toml_text)\n",[10,1407,1408,1409,136,1412,1171,1415,1418,1419,1422,1423,1425,1426,1429,1430,23],{},"Nested dictionaries become ",[14,1410,1411],{},"[report]",[14,1413,1414],{},"[thresholds]",[14,1416,1417],{},"[smtp]"," tables, and the list of recipient dictionaries is written as an array of inline tables, one line per person. It comes first in the file because TOML requires plain keys to appear before any ",[14,1420,1421],{},"[section]"," header; that is a rule of the format, and the reading program sees the same structure either way. Chloe's empty region and the empty ",[14,1424,1156],{}," are simply absent. TOML dates are a native type too, so ",[14,1427,1428],{},"cutoff = 2026-10-02"," is written without quotes and read back as a ",[14,1431,714],{},[739,1433,748,1438,748,1441,748,1444,748,1447,748,1454,748,1465,748,1471,748,1475,748,1478,748,1482,748,1486,748,1490,748,1495,748,1502,748,1507,748,1511,748,1514,748,1517,748,1520],{"viewBox":1434,"role":742,"ariaLabelledBy":1435,"xmlns":746,"style":747},"4 0 752 233",[1436,1437],"ym-formats-t","ym-formats-d",[750,1439,1440],{"id":1436},"YAML or TOML for the generated config",[754,1442,1443],{"id":1437},"YAML supports null, so empty cells can be written as null, and it is common for deployment and scheduler configuration. TOML has no null, so empty cells must be left out, and it suits Python tool settings and is read by the standard library's tomllib.",[758,1445],{"x":760,"y":632,"width":761,"height":1446,"fill":763},"233",[758,1448],{"x":766,"y":1449,"width":1450,"height":1451,"rx":1452,"fill":1453,"stroke":982,"style":983},"16.0","351.0","201.0","14","#f0f4ff",[1455,1456,1458],"g",{"transform":1457},"translate(182.5,29.0) scale(1.083)",[1459,1460],"path",{"d":1461,"fill":1462,"stroke":1463,"style":1464},"M6 3h9l4 4v14H6z M15 3v4h4","none","var(--brand-strong,#4338ca)","stroke-width:1.66px;stroke-linecap:round;stroke-linejoin:round",[778,1466,1470],{"x":1467,"y":1468,"style":1469},"195.5","78.0","font-size:14px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","YAML",[41,1472],{"x1":798,"y1":1473,"x2":1474,"y2":1473,"stroke":982,"style":772},"91.0","355.0",[778,1476,1477],{"x":1467,"y":816,"style":821},"null for empty cells",[778,1479,1481],{"x":1467,"y":1480,"style":821},"137.0","lists and nesting by indentation",[778,1483,1485],{"x":1467,"y":1484,"style":821},"163.0","common for deployments",[778,1487,1489],{"x":1467,"y":1488,"style":821},"189.0","safe_dump \u002F safe_load",[758,1491],{"x":1492,"y":1449,"width":1450,"height":1451,"rx":1452,"fill":1493,"stroke":1494,"style":983},"389.0","#d9f4f1","var(--teal,#0f9488)",[1455,1496,1498],{"transform":1497},"translate(551.5,29.0) scale(1.083)",[1459,1499],{"d":1500,"fill":1462,"stroke":1501,"style":1464},"M12 9a3 3 0 1 0 .01 0z M12 2.5v3 M12 18.5v3 M2.5 12h3 M18.5 12h3 M5.3 5.3l2.1 2.1 M16.6 16.6l2.1 2.1 M5.3 18.7l2.1-2.1 M16.6 7.4l2.1-2.1","var(--teal-ink,#0b6157)",[778,1503,1506],{"x":1504,"y":1468,"style":1505},"564.5","font-size:14px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","TOML",[41,1508],{"x1":1509,"y1":1473,"x2":1510,"y2":1473,"stroke":1494,"style":772},"405.0","724.0",[778,1512,1513],{"x":1504,"y":816,"style":821},"no null — omit empty keys",[778,1515,1516],{"x":1504,"y":1480,"style":821},"[sections] and [[arrays]]",[778,1518,1519],{"x":1504,"y":1484,"style":821},"stdlib tomllib to read",[778,1521,1522],{"x":1504,"y":1488,"style":821},"tomli_w to write",[25,1524,1526],{"id":1525},"step-4-prove-the-round-trip","Step 4 — Prove the round trip",[10,1528,1529],{},"A generated config file is only trustworthy if reading it back gives exactly the workbook's data:",[30,1531,1533],{"className":85,"code":1532,"language":87,"meta":35,"style":35},"import tomllib\n\nfrom_yaml = yaml.safe_load(Path(\"config.yaml\").read_text(encoding=\"utf-8\"))\nfrom_toml = tomllib.loads(Path(\"config.toml\").read_text(encoding=\"utf-8\"))\nprint(\"YAML round trip equal:\", from_yaml == config)\nprint(\"TOML round trip equal:\", from_toml == drop_none(config))\nprint(\"types:\", type(from_toml[\"report\"][\"cutoff\"]).__name__, type(from_toml[\"smtp\"][\"port\"]).__name__, type(from_toml[\"report\"][\"send\"]).__name__)\n",[14,1534,1535,1542,1546,1568,1590,1607,1624],{"__ignoreMap":35},[39,1536,1537,1539],{"class":41,"line":42},[39,1538,95],{"class":94},[39,1540,1541],{"class":98}," tomllib\n",[39,1543,1544],{"class":41,"line":108},[39,1545,126],{"emptyLinePlaceholder":125},[39,1547,1548,1551,1553,1556,1558,1560,1562,1564,1566],{"class":41,"line":122},[39,1549,1550],{"class":98},"from_yaml ",[39,1552,305],{"class":94},[39,1554,1555],{"class":98}," yaml.safe_load(Path(",[39,1557,1036],{"class":49},[39,1559,1131],{"class":98},[39,1561,1046],{"class":511},[39,1563,305],{"class":94},[39,1565,1051],{"class":49},[39,1567,605],{"class":98},[39,1569,1570,1573,1575,1578,1580,1582,1584,1586,1588],{"class":41,"line":129},[39,1571,1572],{"class":98},"from_toml ",[39,1574,305],{"class":94},[39,1576,1577],{"class":98}," tomllib.loads(Path(",[39,1579,1387],{"class":49},[39,1581,1131],{"class":98},[39,1583,1046],{"class":511},[39,1585,305],{"class":94},[39,1587,1051],{"class":49},[39,1589,605],{"class":98},[39,1591,1592,1594,1596,1599,1602,1604],{"class":41,"line":190},[39,1593,679],{"class":132},[39,1595,1033],{"class":98},[39,1597,1598],{"class":49},"\"YAML round trip equal:\"",[39,1600,1601],{"class":98},", from_yaml ",[39,1603,272],{"class":94},[39,1605,1606],{"class":98}," config)\n",[39,1608,1609,1611,1613,1616,1619,1621],{"class":41,"line":195},[39,1610,679],{"class":132},[39,1612,1033],{"class":98},[39,1614,1615],{"class":49},"\"TOML round trip equal:\"",[39,1617,1618],{"class":98},", from_toml ",[39,1620,272],{"class":94},[39,1622,1623],{"class":98}," drop_none(config))\n",[39,1625,1626,1628,1630,1633,1635,1638,1641,1643,1646,1649,1652,1655,1657,1659,1661,1663,1665,1668,1670,1672,1674,1676,1678,1680,1682,1685,1687,1689],{"class":41,"line":208},[39,1627,679],{"class":132},[39,1629,1033],{"class":98},[39,1631,1632],{"class":49},"\"types:\"",[39,1634,136],{"class":98},[39,1636,1637],{"class":132},"type",[39,1639,1640],{"class":98},"(from_toml[",[39,1642,685],{"class":49},[39,1644,1645],{"class":98},"][",[39,1647,1648],{"class":49},"\"cutoff\"",[39,1650,1651],{"class":98},"]).",[39,1653,1654],{"class":132},"__name__",[39,1656,136],{"class":98},[39,1658,1637],{"class":132},[39,1660,1640],{"class":98},[39,1662,697],{"class":49},[39,1664,1645],{"class":98},[39,1666,1667],{"class":49},"\"port\"",[39,1669,1651],{"class":98},[39,1671,1654],{"class":132},[39,1673,136],{"class":98},[39,1675,1637],{"class":132},[39,1677,1640],{"class":98},[39,1679,685],{"class":49},[39,1681,1645],{"class":98},[39,1683,1684],{"class":49},"\"send\"",[39,1686,1651],{"class":98},[39,1688,1654],{"class":132},[39,1690,434],{"class":98},[10,1692,1693,1694,1696,1697,1700,1701,1704,1705,23],{},"Both files load back to the same structure and types that were written: the cut-off date is a ",[14,1695,714],{},", the port an ",[14,1698,1699],{},"int"," and the send flag a ",[14,1702,1703],{},"bool"," — not strings, which is what a hand-maintained copy often ends up with. Run this check every time the converter runs, and fail the conversion if it does not hold. Loading such a config into a report script, with defaults and validation, is covered in ",[19,1706,1708],{"href":1707},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fkeep-excel-report-settings-in-a-config-file\u002F","Keep Excel Report Settings in a Config File",[739,1710,748,1715,748,1718,748,1721,748,1724,748,1729,748,1737,748,1743,748,1748,748,1752,748,1757,748,1760,748,1763,748,1769,748,1774,748,1778,748,1781,748,1785,748,1788,748,1791,748,1796,748,1801,748,1804,748,1807,748,1811,748,1814,748,1817,748,1824,748,1829,748,1832,748,1835,748,1839,748,1842,748,1845,748,1851,748,1856,748,1860,748,1864],{"viewBox":1711,"role":742,"ariaLabelledBy":1712,"xmlns":746,"style":747},"4 0 752 160",[1713,1714],"ym-pipeline-t","ym-pipeline-d",[750,1716,1717],{"id":1713},"From workbook to checked config file",[754,1719,1720],{"id":1714},"The business edits the workbook. The converter reads the sheets, fixes the types, nests the dotted keys, writes YAML and TOML with a header saying the workbook is the source, loads the files back and fails if they differ, and only then is the config committed and used by the job.",[758,1722],{"x":760,"y":632,"width":761,"height":1723,"fill":763},"160",[758,1725],{"x":766,"y":1449,"width":1726,"height":1727,"rx":1728,"fill":770,"stroke":771,"style":983},"115.2","128.0","12",[1455,1730,1732],{"transform":1731},"translate(65.6,37.8) scale(1.000)",[1459,1733],{"d":1734,"fill":1462,"stroke":1735,"style":1736},"M6 3h9l4 4v14H6z M15 3v4h4 M9.5 11.5l4.5 6 M14 11.5l-4.5 6","var(--text,#172033)","stroke-width:1.80px;stroke-linecap:round;stroke-linejoin:round",[778,1738,1742],{"x":1739,"y":1740,"style":1741},"77.6","84.2","font-size:13.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","Workbook",[778,1744,1747],{"x":1739,"y":1745,"style":1746},"103.8","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","edited by the",[778,1749,1751],{"x":1739,"y":1750,"style":1746},"121.2","owners",[41,1753],{"x1":1754,"y1":1755,"x2":1756,"y2":1755,"stroke":982,"style":983},"140.2","80.0","162.2",[985,1758],{"points":1759,"fill":988},"166.2,80.0 157.2,85.0 157.2,75.0",[758,1761],{"x":1762,"y":1449,"width":1726,"height":1727,"rx":1728,"fill":1453,"stroke":982,"style":983},"171.2",[1455,1764,1766],{"transform":1765},"translate(216.8,37.8) scale(1.000)",[1459,1767],{"d":1768,"fill":1462,"stroke":1463,"style":1736},"M3 5h18l-7 8v6l-4 2v-8z",[778,1770,1773],{"x":1771,"y":1740,"style":1772},"228.8","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Read and",[778,1775,1777],{"x":1771,"y":1776,"style":1772},"101.8","clean",[778,1779,1780],{"x":1771,"y":1750,"style":1746},"types, nesting",[41,1782],{"x1":1783,"y1":1755,"x2":1784,"y2":1755,"stroke":982,"style":983},"291.4","313.4",[985,1786],{"points":1787,"fill":988},"317.4,80.0 308.4,85.0 308.4,75.0",[758,1789],{"x":1790,"y":1449,"width":1726,"height":1727,"rx":1728,"fill":1493,"stroke":1494,"style":983},"322.4",[1455,1792,1794],{"transform":1793},"translate(368.0,37.8) scale(1.000)",[1459,1795],{"d":1461,"fill":1462,"stroke":1501,"style":1736},[778,1797,1800],{"x":1798,"y":1740,"style":1799},"380.0","font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Write",[778,1802,1803],{"x":1798,"y":1745,"style":1746},"YAML \u002F TOML +",[778,1805,1806],{"x":1798,"y":1750,"style":1746},"header",[41,1808],{"x1":1809,"y1":1755,"x2":1810,"y2":1755,"stroke":982,"style":983},"442.6","464.6",[985,1812],{"points":1813,"fill":988},"468.6,80.0 459.6,85.0 459.6,75.0",[758,1815],{"x":1816,"y":1449,"width":1726,"height":1727,"rx":1728,"fill":960,"stroke":961,"style":983},"473.6",[1455,1818,1820],{"transform":1819},"translate(519.2,37.8) scale(1.000)",[1459,1821],{"d":1822,"fill":1462,"stroke":1823,"style":1736},"M5 12.5l4.5 4.5L19 7","var(--gold-ink,#7a4e06)",[778,1825,1828],{"x":1826,"y":1740,"style":1827},"531.2","font-size:13.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Round-trip",[778,1830,1831],{"x":1826,"y":1776,"style":1827},"check",[778,1833,1834],{"x":1826,"y":1750,"style":1746},"fail if different",[41,1836],{"x1":1837,"y1":1755,"x2":1838,"y2":1755,"stroke":982,"style":983},"593.8","615.8",[985,1840],{"points":1841,"fill":988},"619.8,80.0 610.8,85.0 610.8,75.0",[758,1843],{"x":1844,"y":1449,"width":1726,"height":1727,"rx":1728,"fill":770,"stroke":771,"style":983},"624.8",[1455,1846,1848],{"transform":1847},"translate(670.4,29.0) scale(1.000)",[1459,1849],{"d":1850,"fill":1462,"stroke":1735,"style":1736},"M9 8l-4 4 4 4 M15 8l4 4-4 4",[778,1852,1855],{"x":1853,"y":1854,"style":1741},"682.4","75.5","Commit and",[778,1857,1859],{"x":1853,"y":1858,"style":1741},"93.0","use",[778,1861,1863],{"x":1853,"y":1862,"style":1746},"112.5","reviewed as a",[778,1865,1867],{"x":1853,"y":1866,"style":1746},"130.0","diff",[25,1869,1871],{"id":1870},"where-the-converter-fits","Where the converter fits",[10,1873,1874,1875,1878],{},"Run the converter as part of the change process rather than by hand. A common arrangement is a small script in the repository that regenerates the config file from the workbook and fails if the round trip does not match; the person who changes the workbook runs it, and the resulting diff — \"margin_alert: 0.15 → 0.12\" — is what gets reviewed and committed. That gives the business their spreadsheet, the engineers a reviewable text change, and everyone a history of who changed which setting and when. Validating the ",[71,1876,1877],{},"values"," as well as the types — a port between 1 and 65535, a threshold between 0 and 1, every recipient with an email address — belongs in the same script, so a typo in the workbook stops at conversion instead of surfacing in a failed overnight job.",[25,1880,1882],{"id":1881},"common-pitfalls","Common pitfalls",[1884,1885,1886,1900,1906,1915,1926],"ul",{},[1887,1888,1889,1893,1894,1897,1898,23],"li",{},[1890,1891,1892],"strong",{},"Floats where integers belong."," Convert whole-number floats such as ",[14,1895,1896],{},"587.0"," back to ",[14,1899,1699],{},[1887,1901,1902,1905],{},[1890,1903,1904],{},"\"Yes\" and \"No\" left as text."," Map them to booleans explicitly.",[1887,1907,1908,1914],{},[1890,1909,1910,1913],{},[14,1911,1912],{},"None"," in TOML."," TOML has no null; omit missing keys.",[1887,1916,1917,1925],{},[1890,1918,1919,1922,1923,23],{},[14,1920,1921],{},"yaml.dump"," instead of ",[14,1924,1144],{}," It can write Python-specific tags other tools cannot read.",[1887,1927,1928,1931],{},[1890,1929,1930],{},"Editing the generated file."," Say in a header comment that the workbook is the source, and regenerate rather than patch.",[25,1933,1935],{"id":1934},"conclusion","Conclusion",[10,1937,1938,1939,1942,1943,1945],{},"To turn a configuration workbook into YAML or TOML, read the sheets with openpyxl, fix Excel's types on the way — whole-number floats to integers, midnight datetimes to dates, yes\u002Fno text to booleans — and build nested sections from dotted keys and lists of dictionaries from tables. Write YAML with ",[14,1940,1941],{},"yaml.safe_dump",", write TOML with ",[14,1944,1202],{}," after dropping empty values, and load both back to prove they match the workbook before the file is used.",[25,1947,1949],{"id":1948},"frequently-asked-questions","Frequently asked questions",[10,1951,1952,1955],{},[1890,1953,1954],{},"How do I convert an Excel sheet to YAML in Python?","\nRead the rows with openpyxl or pandas into dictionaries and lists, fix the value types, and write them with yaml.safe_dump with sort_keys=False.",[10,1957,1958,1961],{},[1890,1959,1960],{},"How do I convert Excel to TOML?","\nBuild the same nested dictionary and write it with tomli_w.dumps, leaving out empty cells because TOML has no null value.",[10,1963,1964,1967],{},[1890,1965,1966],{},"Why does tomli_w fail with None?","\nTOML cannot represent null; remove keys whose value is None before writing, and treat missing keys as unset when reading.",[10,1969,1970,1973],{},[1890,1971,1972],{},"Why are my integers written as 587.0?","\nExcel stores numbers as floats; convert floats with no fractional part to int before writing the config.",[25,1975,1977],{"id":1976},"related","Related",[1884,1979,1980,1986,1993,1998],{},[1887,1981,1982,1983,1985],{},"Up one level: ",[19,1984,22],{"href":21}," — converting between formats.",[1887,1987,1988,1992],{},[19,1989,1991],{"href":1990},"\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fconvert-an-excel-sheet-to-json-with-python\u002F","Convert an Excel Sheet to JSON with Python"," — the JSON equivalent.",[1887,1994,1995,1997],{},[19,1996,1708],{"href":1707}," — using the config.",[1887,1999,2000,2004],{},[19,2001,2003],{"href":2002},"\u002Fgetting-started-with-python-excel-automation\u002Freading-excel-files-with-pandas\u002Fread-boolean-columns-from-excel-with-pandas\u002F","Read Boolean Columns from Excel with pandas"," — more on yes\u002Fno values.",[2006,2007,2008],"style",{},"html pre.shiki code .sMTad, html code.shiki .sMTad{--shiki-default:#6F42C1;--shiki-dark:#FFB757}html pre.shiki code .srMev, html code.shiki .srMev{--shiki-default:#032F62;--shiki-dark:#ADDCFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s-kum, html code.shiki .s-kum{--shiki-default:#D73A49;--shiki-dark:#FF9492}html pre.shiki code .skGVy, html code.shiki .skGVy{--shiki-default:#24292E;--shiki-dark:#F0F3F6}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}html pre.shiki code .s-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}",{"title":35,"searchDepth":108,"depth":108,"links":2010},[2011,2012,2013,2014,2015,2016,2017,2018,2019,2020],{"id":27,"depth":108,"text":28},{"id":81,"depth":108,"text":82},{"id":1005,"depth":108,"text":1006},{"id":1195,"depth":108,"text":1196},{"id":1525,"depth":108,"text":1526},{"id":1870,"depth":108,"text":1871},{"id":1881,"depth":108,"text":1882},{"id":1934,"depth":108,"text":1935},{"id":1948,"depth":108,"text":1949},{"id":1976,"depth":108,"text":1977},"Turn settings and lookup tables maintained in Excel into YAML or TOML configuration files with Python: a key-value settings sheet and table sheets read with openpyxl, Excel's floats, dates and booleans converted to the right types, missing values handled for TOML (which has no null), nested sections from dotted keys, and a round trip that proves the file says what the workbook says.","md",[2024,2026,2028,2030],{"q":1954,"a":2025},"Read the rows with openpyxl or pandas into dictionaries and lists, fix the value types, and write them with yaml.safe_dump with sort_keys=False.",{"q":1960,"a":2027},"Build the same nested dictionary and write it with tomli_w.dumps, leaving out empty cells because TOML has no null value.",{"q":1966,"a":2029},"TOML cannot represent null; remove keys whose value is None before writing, and treat missing keys as unset when reading.",{"q":1972,"a":2031},"Excel stores numbers as floats; convert floats with no fractional part to int before writing the config.","convert excel sheets to yaml or toml config",null,{"breadcrumb":2035},[2036,2039,2042],{"name":2037,"item":2038},"Home","\u002F",{"name":2040,"item":2041},"Getting Started with Python Excel Automation","\u002Fgetting-started-with-python-excel-automation\u002F",{"name":22,"item":21},"\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fconvert-excel-sheets-to-yaml-or-toml-config",{"title":2045,"description":2046},"Convert Excel Sheets to YAML or TOML Config with Python","Generate YAML and TOML config from Excel: settings sheets to nested dicts, tables to lists, type fixes for floats, dates and booleans, TOML's missing null, PyYAML safe_dump, tomli_w and tomllib round-trip checks.","convert-excel-sheets-to-yaml-or-toml-config","getting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fconvert-excel-sheets-to-yaml-or-toml-config\u002Findex",[2050],"file-formats","how-to","SUNeYiQJc1_L07OW0KJjxAab7Z0jqbTuW1OZ9sDSGb8",[2054,2058],{"title":2055,"path":2056,"stem":2057,"children":-1},"Convert CSV Files to Excel with Python","\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fconvert-csv-files-to-excel-with-python","getting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fconvert-csv-files-to-excel-with-python\u002Findex",{"title":2059,"path":2060,"stem":2061,"children":-1},"Convert Fixed-Width Text Files to Excel with pandas","\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fconvert-fixed-width-text-files-to-excel-with-pandas","getting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fconvert-fixed-width-text-files-to-excel-with-pandas\u002Findex",[2063,2065],{"path":2064,"title":22},"\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions",{"path":2066,"title":2040},"\u002Fgetting-started-with-python-excel-automation",[],[2069,2072,2075,2079,2082,2086],{"path":2070,"title":2071,"kind":2051},"\u002Fadvanced-data-transformation-and-cleaning\u002Freading-excel-with-polars-and-arrow\u002Fconvert-excel-files-to-parquet-with-python","Convert Excel Files to Parquet with Python",{"path":2073,"title":2074,"kind":2051},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-large-excel-files-in-python\u002Fconvert-excel-to-csv-with-python","Convert Excel to CSV with Python",{"path":2076,"title":2077,"kind":2078},"\u002Fgetting-started-with-python-excel-automation\u002Fchoosing-a-python-excel-library\u002Fexcel-vs-csv-vs-parquet-for-python-data-pipelines","Excel vs CSV vs Parquet for Python Data Pipelines","compare",{"path":2080,"title":2081,"kind":2078},"\u002Fgetting-started-with-python-excel-automation\u002Fchoosing-a-python-excel-library\u002Fpick-an-excel-engine-for-xlsx-xlsm-xls-xlsb-and-ods","Pick an Excel Engine for .xlsx, .xlsm, .xls, .xlsb and .ods",{"path":2083,"title":2084,"kind":2085},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Ffix-garbled-characters-in-excel-data","Fix Garbled Characters Like \"CafÃ©\" and \"â€™\" in Excel Data","fix",{"path":2087,"title":2088,"kind":2051},"\u002Fgetting-started-with-python-excel-automation\u002Fautomating-excel-with-com-and-pywin32\u002Fextract-vba-macro-code-from-an-excel-file","Extract VBA Macro Code from an Excel File",1790972059137]