[{"data":1,"prerenderedAt":2197},["ShallowReactive",2],{"doc:\u002Fgetting-started-with-python-excel-automation\u002Fchoosing-a-python-excel-library\u002Ftranslate-common-vba-macros-to-python":3,"surround:\u002Fgetting-started-with-python-excel-automation\u002Fchoosing-a-python-excel-library\u002Ftranslate-common-vba-macros-to-python":2166,"ancestors:\u002Fgetting-started-with-python-excel-automation\u002Fchoosing-a-python-excel-library\u002Ftranslate-common-vba-macros-to-python":2174,"children:\u002Fgetting-started-with-python-excel-automation\u002Fchoosing-a-python-excel-library\u002Ftranslate-common-vba-macros-to-python":2179,"related:\u002Fgetting-started-with-python-excel-automation\u002Fchoosing-a-python-excel-library\u002Ftranslate-common-vba-macros-to-python":2180},{"id":4,"title":5,"body":6,"dateModified":2133,"datePublished":2133,"description":2134,"extension":2135,"faq":2136,"keyword":2145,"kind":2146,"meta":2147,"navigation":244,"path":2155,"seo":2156,"slug":2159,"stem":2160,"topics":2161,"type":2164,"__hash__":2165},"docs\u002Fgetting-started-with-python-excel-automation\u002Fchoosing-a-python-excel-library\u002Ftranslate-common-vba-macros-to-python\u002Findex.md","Translate Common VBA Macros to Python",{"type":7,"value":8,"toc":2116},"minimark",[9,24,29,58,76,187,191,194,205,216,341,356,360,380,383,442,457,585,589,592,611,614,683,695,915,919,926,986,993,1160,1175,1179,1199,1202,1278,1282,1322,1336,1464,1468,1477,1599,1606,1610,1616,1641,1759,1775,1903,1907,1910,1937,1945,1949,1962,1966,2017,2021,2052,2056,2062,2068,2074,2080,2084,2112],[10,11,12,13,18,19,23],"p",{},"Most Excel macros are built from the same handful of moves: find where the data ends, loop down the rows, delete some of them, look values up, copy the survivors to another sheet, repeat for every file in a folder, save a dated copy. Once you know what each move becomes in Python, translating a macro is mostly a matter of recognising the patterns and replacing loops with operations on whole columns. This page is a phrasebook: each section shows a typical VBA fragment and its Python equivalent, run against the same small workbook. It belongs to ",[14,15,17],"a",{"href":16},"\u002Fgetting-started-with-python-excel-automation\u002Fchoosing-a-python-excel-library\u002F","Choosing a Python Excel Library","; for the case for and against moving at all, see ",[14,20,22],{"href":21},"\u002Fgetting-started-with-python-excel-automation\u002Fchoosing-a-python-excel-library\u002Fpython-vs-vba-for-excel-automation\u002F","Python vs VBA for Excel Automation",".",[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 pandas openpyxl\n","bash","",[37,38,39],"code",{"__ignoreMap":35},[40,41,44,48,52,55],"span",{"class":42,"line":43},"line",1,[40,45,47],{"class":46},"sMTad","pip",[40,49,51],{"class":50},"srMev"," install",[40,53,54],{"class":50}," pandas",[40,56,57],{"class":50}," openpyxl\n",[10,59,60,63,64,67,68,71,72,75],{},[37,61,62],{},"orders.xlsx"," has an ",[37,65,66],{},"Orders"," sheet (order ID, region, SKU, quantity, status) and a ",[37,69,70],{},"Prices"," sheet mapping SKU to unit price; the ",[37,73,74],{},"monthly\u002F"," folder holds three monthly exports and, as real folders often do, an Excel lock file. The VBA fragments are the ones found in real macros, trimmed to the essential lines.",[77,78,86,87,86,91,86,95,86,103,86,113,86,124,86,131,86,137,86,142,86,146,86,150,86,154,86,159,86,166,86,171,86,175,86,178,86,181,86,184],"svg",{"viewBox":79,"role":80,"ariaLabelledBy":81,"xmlns":84,"style":85},"4 0 752 233","img",[82,83],"tv-shift-t","tv-shift-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  ",[88,89,90],"title",{"id":82},"The main shift when translating",[92,93,94],"desc",{"id":83},"Two panels. VBA works cell by cell in a live Excel session, looping over rows and changing the sheet as it goes. Python reads the sheet into a table, applies operations to whole columns, and writes the result once.",[96,97],"rect",{"x":98,"y":99,"width":100,"height":101,"fill":102},"4","0","752","233","#ffffff",[96,104],{"x":105,"y":106,"width":107,"height":108,"rx":109,"fill":110,"stroke":111,"style":112},"20.0","16.0","351.0","201.0","14","#e7ebef","var(--line,#cdd5e6)","stroke-width:2px",[114,115,117],"g",{"transform":116},"translate(182.5,29.0) scale(1.083)",[118,119],"path",{"d":120,"fill":121,"stroke":122,"style":123},"M6 3h9l4 4v14H6z M15 3v4h4 M9.5 11.5l4.5 6 M14 11.5l-4.5 6","none","var(--text,#172033)","stroke-width:1.66px;stroke-linecap:round;stroke-linejoin:round",[125,126,130],"text",{"x":127,"y":128,"style":129},"195.5","78.0","font-size:14px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","VBA",[42,132],{"x1":133,"y1":134,"x2":135,"y2":134,"stroke":111,"style":136},"36.0","91.0","355.0","stroke-width:1px",[125,138,141],{"x":127,"y":139,"style":140},"111.0","font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","cell by cell",[125,143,145],{"x":127,"y":144,"style":140},"137.0","loops over rows",[125,147,149],{"x":127,"y":148,"style":140},"163.0","changes the sheet in place",[125,151,153],{"x":127,"y":152,"style":140},"189.0","needs Excel open",[96,155],{"x":156,"y":106,"width":107,"height":108,"rx":109,"fill":157,"stroke":158,"style":112},"389.0","#f0f4ff","var(--brand,#5b5cf0)",[114,160,162],{"transform":161},"translate(551.5,29.0) scale(1.083)",[118,163],{"d":164,"fill":121,"stroke":165,"style":123},"M12 3c-4 0-4 1.6-4 3v2h4.2v1H6c-2 0-3 1.4-3 4s1 4 3 4h2v-2.6c0-1.6 1.3-2.9 2.9-2.9h4.2c1.3 0 2.4-1.1 2.4-2.4V6c0-1.6-1.8-3-5.5-3z M10 5.5h.01 M12 21c4 0 4-1.6 4-3v-2h-4.2v-1H18c2 0 3-1.4 3-4s-1-4-3-4h-2v2.6c0 1.6-1.3 2.9-2.9 2.9H8.9c-1.3 0-2.4 1.1-2.4 2.4V18c0 1.6 1.8 3 5.5 3z M14 18.5h.01","var(--brand-strong,#4338ca)",[125,167,170],{"x":168,"y":128,"style":169},"564.5","font-size:14px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Python",[42,172],{"x1":173,"y1":134,"x2":174,"y2":134,"stroke":158,"style":136},"405.0","724.0",[125,176,177],{"x":168,"y":139,"style":140},"whole columns at once",[125,179,180],{"x":168,"y":144,"style":140},"filters instead of loops",[125,182,183],{"x":168,"y":148,"style":140},"writes the result once",[125,185,186],{"x":168,"y":152,"style":140},"runs anywhere",[25,188,190],{"id":189},"_1-find-the-last-row","1. Find the last row",[10,192,193],{},"VBA's most common line finds where the data ends, so loops know when to stop:",[30,195,199],{"className":196,"code":197,"language":198,"meta":35,"style":35},"language-vb shiki shiki-themes github-light github-dark-high-contrast","lastRow = ws.Cells(ws.Rows.Count, \"A\").End(xlUp).Row\n","vb",[37,200,201],{"__ignoreMap":35},[40,202,203],{"class":42,"line":43},[40,204,197],{},[10,206,207,208,211,212,215],{},"In pandas the question disappears: ",[37,209,210],{},"read_excel"," reads exactly the rows that hold data. When you work with openpyxl cells directly, ",[37,213,214],{},"max_row"," is the equivalent, with the caveat that formatted but empty rows count too:",[30,217,221],{"className":218,"code":219,"language":220,"meta":35,"style":35},"language-python shiki shiki-themes github-light github-dark-high-contrast","from openpyxl import load_workbook\n\ndf = pd.read_excel(\"orders.xlsx\", sheet_name=\"Orders\")\nprint(\"pandas rows:\", len(df))\n\nws = load_workbook(\"orders.xlsx\")[\"Orders\"]\nprint(\"openpyxl max_row:\", ws.max_row, \"(including the header)\")\n","python",[37,222,223,239,246,276,297,302,323],{"__ignoreMap":35},[40,224,225,229,233,236],{"class":42,"line":43},[40,226,228],{"class":227},"s-kum","from",[40,230,232],{"class":231},"skGVy"," openpyxl ",[40,234,235],{"class":227},"import",[40,237,238],{"class":231}," load_workbook\n",[40,240,242],{"class":42,"line":241},2,[40,243,245],{"emptyLinePlaceholder":244},true,"\n",[40,247,249,252,255,258,261,264,268,270,273],{"class":42,"line":248},3,[40,250,251],{"class":231},"df ",[40,253,254],{"class":227},"=",[40,256,257],{"class":231}," pd.read_excel(",[40,259,260],{"class":50},"\"orders.xlsx\"",[40,262,263],{"class":231},", ",[40,265,267],{"class":266},"sa561","sheet_name",[40,269,254],{"class":227},[40,271,272],{"class":50},"\"Orders\"",[40,274,275],{"class":231},")\n",[40,277,279,283,286,289,291,294],{"class":42,"line":278},4,[40,280,282],{"class":281},"sP0c6","print",[40,284,285],{"class":231},"(",[40,287,288],{"class":50},"\"pandas rows:\"",[40,290,263],{"class":231},[40,292,293],{"class":281},"len",[40,295,296],{"class":231},"(df))\n",[40,298,300],{"class":42,"line":299},5,[40,301,245],{"emptyLinePlaceholder":244},[40,303,305,308,310,313,315,318,320],{"class":42,"line":304},6,[40,306,307],{"class":231},"ws ",[40,309,254],{"class":227},[40,311,312],{"class":231}," load_workbook(",[40,314,260],{"class":50},[40,316,317],{"class":231},")[",[40,319,272],{"class":50},[40,321,322],{"class":231},"]\n",[40,324,326,328,330,333,336,339],{"class":42,"line":325},7,[40,327,282],{"class":281},[40,329,285],{"class":231},[40,331,332],{"class":50},"\"openpyxl max_row:\"",[40,334,335],{"class":231},", ws.max_row, ",[40,337,338],{"class":50},"\"(including the header)\"",[40,340,275],{"class":231},[10,342,343,344,347,348,350,351,355],{},"To match ",[37,345,346],{},"End(xlUp)"," exactly — the last non-empty cell in one column — walk up from ",[37,349,214],{}," until a value appears. ",[14,352,354],{"href":353},"\u002Fgetting-started-with-python-excel-automation\u002Fusing-openpyxl-for-excel-file-manipulation\u002Ffind-the-last-row-with-data-in-openpyxl\u002F","Find the Last Row with Data in openpyxl"," covers that and the other edge cases.",[25,357,359],{"id":358},"_2-loop-over-rows-and-compute-a-column","2. Loop over rows and compute a column",[30,361,363],{"className":196,"code":362,"language":198,"meta":35,"style":35},"For r = 2 To lastRow\n    ws.Cells(r, 6).Value = ws.Cells(r, 4).Value * 1.2\nNext r\n",[37,364,365,370,375],{"__ignoreMap":35},[40,366,367],{"class":42,"line":43},[40,368,369],{},"For r = 2 To lastRow\n",[40,371,372],{"class":42,"line":241},[40,373,374],{},"    ws.Cells(r, 6).Value = ws.Cells(r, 4).Value * 1.2\n",[40,376,377],{"class":42,"line":248},[40,378,379],{},"Next r\n",[10,381,382],{},"The loop becomes one assignment to a new column. Every row is computed at once:",[30,384,386],{"className":218,"code":385,"language":220,"meta":35,"style":35},"df[\"QtyWithBuffer\"] = df[\"Qty\"] * 1.2\nprint(df[[\"OrderID\", \"Qty\", \"QtyWithBuffer\"]].head(3))\n",[37,387,388,415],{"__ignoreMap":35},[40,389,390,393,396,399,401,404,407,409,412],{"class":42,"line":43},[40,391,392],{"class":231},"df[",[40,394,395],{"class":50},"\"QtyWithBuffer\"",[40,397,398],{"class":231},"] ",[40,400,254],{"class":227},[40,402,403],{"class":231}," df[",[40,405,406],{"class":50},"\"Qty\"",[40,408,398],{"class":231},[40,410,411],{"class":227},"*",[40,413,414],{"class":281}," 1.2\n",[40,416,417,419,422,425,427,429,431,433,436,439],{"class":42,"line":241},[40,418,282],{"class":281},[40,420,421],{"class":231},"(df[[",[40,423,424],{"class":50},"\"OrderID\"",[40,426,263],{"class":231},[40,428,406],{"class":50},[40,430,263],{"class":231},[40,432,395],{"class":50},[40,434,435],{"class":231},"]].head(",[40,437,438],{"class":281},"3",[40,440,441],{"class":231},"))\n",[10,443,444,445,448,449,452,453,456],{},"When the row logic has branches — ",[37,446,447],{},"If ... ElseIf ... Else"," — use ",[37,450,451],{},"numpy.select"," or ",[37,454,455],{},"Series.where"," rather than going back to a loop:",[30,458,460],{"className":218,"code":459,"language":220,"meta":35,"style":35},"import numpy as np\n\ndf[\"Priority\"] = np.select(\n    [df[\"Qty\"] >= 10, df[\"Qty\"] >= 4],\n    [\"High\", \"Normal\"],\n    default=\"Low\",\n)\nprint(df[[\"OrderID\", \"Qty\", \"Priority\"]].to_string(index=False))\n",[37,461,462,475,479,493,523,538,551,555],{"__ignoreMap":35},[40,463,464,466,469,472],{"class":42,"line":43},[40,465,235],{"class":227},[40,467,468],{"class":231}," numpy ",[40,470,471],{"class":227},"as",[40,473,474],{"class":231}," np\n",[40,476,477],{"class":42,"line":241},[40,478,245],{"emptyLinePlaceholder":244},[40,480,481,483,486,488,490],{"class":42,"line":248},[40,482,392],{"class":231},[40,484,485],{"class":50},"\"Priority\"",[40,487,398],{"class":231},[40,489,254],{"class":227},[40,491,492],{"class":231}," np.select(\n",[40,494,495,498,500,502,505,508,511,513,515,517,520],{"class":42,"line":278},[40,496,497],{"class":231},"    [df[",[40,499,406],{"class":50},[40,501,398],{"class":231},[40,503,504],{"class":227},">=",[40,506,507],{"class":281}," 10",[40,509,510],{"class":231},", df[",[40,512,406],{"class":50},[40,514,398],{"class":231},[40,516,504],{"class":227},[40,518,519],{"class":281}," 4",[40,521,522],{"class":231},"],\n",[40,524,525,528,531,533,536],{"class":42,"line":299},[40,526,527],{"class":231},"    [",[40,529,530],{"class":50},"\"High\"",[40,532,263],{"class":231},[40,534,535],{"class":50},"\"Normal\"",[40,537,522],{"class":231},[40,539,540,543,545,548],{"class":42,"line":304},[40,541,542],{"class":266},"    default",[40,544,254],{"class":227},[40,546,547],{"class":50},"\"Low\"",[40,549,550],{"class":231},",\n",[40,552,553],{"class":42,"line":325},[40,554,275],{"class":231},[40,556,558,560,562,564,566,568,570,572,575,578,580,583],{"class":42,"line":557},8,[40,559,282],{"class":281},[40,561,421],{"class":231},[40,563,424],{"class":50},[40,565,263],{"class":231},[40,567,406],{"class":50},[40,569,263],{"class":231},[40,571,485],{"class":50},[40,573,574],{"class":231},"]].to_string(",[40,576,577],{"class":266},"index",[40,579,254],{"class":227},[40,581,582],{"class":281},"False",[40,584,441],{"class":231},[25,586,588],{"id":587},"_3-delete-rows-that-match-a-condition","3. Delete rows that match a condition",[10,590,591],{},"VBA has to loop backwards, because deleting a row shifts everything below it up:",[30,593,595],{"className":196,"code":594,"language":198,"meta":35,"style":35},"For r = lastRow To 2 Step -1\n    If ws.Cells(r, 5).Value = \"Cancelled\" Then ws.Rows(r).Delete\nNext r\n",[37,596,597,602,607],{"__ignoreMap":35},[40,598,599],{"class":42,"line":43},[40,600,601],{},"For r = lastRow To 2 Step -1\n",[40,603,604],{"class":42,"line":241},[40,605,606],{},"    If ws.Cells(r, 5).Value = \"Cancelled\" Then ws.Rows(r).Delete\n",[40,608,609],{"class":42,"line":248},[40,610,379],{},[10,612,613],{},"In pandas you keep the rows you want instead of deleting the ones you don't. There is no direction to get wrong:",[30,615,617],{"className":218,"code":616,"language":220,"meta":35,"style":35},"kept = df[df[\"Status\"] != \"Cancelled\"]\nprint(f\"{len(df)} rows -> {len(kept)} rows\")\n",[37,618,619,642],{"__ignoreMap":35},[40,620,621,624,626,629,632,634,637,640],{"class":42,"line":43},[40,622,623],{"class":231},"kept ",[40,625,254],{"class":227},[40,627,628],{"class":231}," df[df[",[40,630,631],{"class":50},"\"Status\"",[40,633,398],{"class":231},[40,635,636],{"class":227},"!=",[40,638,639],{"class":50}," \"Cancelled\"",[40,641,322],{"class":231},[40,643,644,646,648,651,654,658,660,663,666,669,671,673,676,678,681],{"class":42,"line":241},[40,645,282],{"class":281},[40,647,285],{"class":231},[40,649,650],{"class":227},"f",[40,652,653],{"class":50},"\"",[40,655,657],{"class":656},"sSjpA","{",[40,659,293],{"class":281},[40,661,662],{"class":231},"(df)",[40,664,665],{"class":656},"}",[40,667,668],{"class":50}," rows -> ",[40,670,657],{"class":656},[40,672,293],{"class":281},[40,674,675],{"class":231},"(kept)",[40,677,665],{"class":656},[40,679,680],{"class":50}," rows\"",[40,682,275],{"class":231},[10,684,685,686,689,690,694],{},"If the sheet must be edited in place with openpyxl, to keep its formatting for example, the backwards loop comes back, for the same reason as in VBA: ",[37,687,688],{},"ws.delete_rows()"," shifts the rows below. ",[14,691,693],{"href":692},"\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"," shows the reverse-order rule.",[77,696,86,702,86,705,86,708,86,713,86,719,86,725,86,730,86,735,86,739,86,743,86,746,86,750,86,752,86,756,86,758,86,761,86,764,86,767,86,771,86,775,86,777,86,780,86,783,86,786,86,788,86,791,86,793,86,795,86,798,86,802,86,804,86,807,86,809,86,811,86,814,86,818,86,820,86,823,86,825,86,828,86,831,86,834,86,837,86,840,86,844,86,847,86,849,86,851,86,853,86,855,86,857,86,859,86,861,86,863,86,865,86,867,86,869,86,871,86,873,86,875,86,877,86,879,86,881,86,883,86,889,86,893,86,897,86,902,86,910],{"viewBox":697,"role":80,"ariaLabelledBy":698,"xmlns":84,"style":701},"69 0 621 224",[699,700],"tv-delete-t","tv-delete-d","width:100%;max-width:621px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[88,703,704],{"id":699},"Deleting rows becomes keeping rows",[92,706,707],{"id":700},"Part of the orders table on the left has two cancelled rows. A filter that keeps rows whose status is not Cancelled produces the table on the right keeping the remaining rows in their original order.",[96,709],{"x":710,"y":99,"width":711,"height":712,"fill":102},"69","621","224",[96,714],{"x":715,"y":716,"width":717,"height":718,"rx":99,"fill":110,"stroke":111,"style":136},"85.6","40.0","32.0","28.0",[125,720,724],{"x":721,"y":722,"style":723},"101.6","59.0","font-size:11px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","1",[96,726],{"x":727,"y":716,"width":728,"height":718,"rx":99,"fill":157,"stroke":158,"style":729},"117.6","68.1","stroke-width:1.5px",[125,731,734],{"x":732,"y":722,"style":733},"151.6","font-size:11px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","OrderID",[96,736],{"x":737,"y":716,"width":738,"height":718,"rx":99,"fill":157,"stroke":158,"style":729},"185.6","80.5",[125,740,742],{"x":741,"y":722,"style":733},"225.9","Status",[96,744],{"x":715,"y":745,"width":717,"height":718,"rx":99,"fill":110,"stroke":111,"style":136},"68.0",[125,747,749],{"x":721,"y":748,"style":723},"87.0","2",[96,751],{"x":727,"y":745,"width":728,"height":718,"rx":99,"fill":102,"stroke":111,"style":136},[125,753,755],{"x":732,"y":748,"style":754},"font-size:11px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","5001",[96,757],{"x":737,"y":745,"width":738,"height":718,"rx":99,"fill":102,"stroke":111,"style":136},[125,759,760],{"x":741,"y":748,"style":754},"Shipped",[96,762],{"x":715,"y":763,"width":717,"height":718,"rx":99,"fill":110,"stroke":111,"style":136},"96.0",[125,765,438],{"x":721,"y":766,"style":723},"115.0",[96,768],{"x":727,"y":763,"width":728,"height":718,"rx":99,"fill":769,"stroke":770,"style":729},"#fce9e9","var(--danger,#dc2626)",[125,772,774],{"x":732,"y":766,"style":773},"font-size:11px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","5002",[96,776],{"x":737,"y":763,"width":738,"height":718,"rx":99,"fill":769,"stroke":770,"style":729},[125,778,779],{"x":741,"y":766,"style":773},"Cancelled",[96,781],{"x":715,"y":782,"width":717,"height":718,"rx":99,"fill":110,"stroke":111,"style":136},"124.0",[125,784,98],{"x":721,"y":785,"style":723},"143.0",[96,787],{"x":727,"y":782,"width":728,"height":718,"rx":99,"fill":102,"stroke":111,"style":136},[125,789,790],{"x":732,"y":785,"style":754},"5003",[96,792],{"x":737,"y":782,"width":738,"height":718,"rx":99,"fill":102,"stroke":111,"style":136},[125,794,760],{"x":741,"y":785,"style":754},[96,796],{"x":715,"y":797,"width":717,"height":718,"rx":99,"fill":110,"stroke":111,"style":136},"152.0",[125,799,801],{"x":721,"y":800,"style":723},"171.0","5",[96,803],{"x":727,"y":797,"width":728,"height":718,"rx":99,"fill":769,"stroke":770,"style":729},[125,805,806],{"x":732,"y":800,"style":773},"5006",[96,808],{"x":737,"y":797,"width":738,"height":718,"rx":99,"fill":769,"stroke":770,"style":729},[125,810,779],{"x":741,"y":800,"style":773},[96,812],{"x":715,"y":813,"width":717,"height":718,"rx":99,"fill":110,"stroke":111,"style":136},"180.0",[125,815,817],{"x":721,"y":816,"style":723},"199.0","6",[96,819],{"x":727,"y":813,"width":728,"height":718,"rx":99,"fill":102,"stroke":111,"style":136},[125,821,822],{"x":732,"y":816,"style":754},"5007",[96,824],{"x":737,"y":813,"width":738,"height":718,"rx":99,"fill":102,"stroke":111,"style":136},[125,826,827],{"x":741,"y":816,"style":754},"Open",[96,829],{"x":830,"y":716,"width":717,"height":718,"rx":99,"fill":110,"stroke":111,"style":136},"503.8",[125,832,724],{"x":833,"y":722,"style":723},"519.8",[96,835],{"x":836,"y":716,"width":728,"height":718,"rx":99,"fill":157,"stroke":158,"style":729},"535.8",[125,838,734],{"x":839,"y":722,"style":733},"569.8",[96,841],{"x":842,"y":716,"width":843,"height":718,"rx":99,"fill":157,"stroke":158,"style":729},"603.8","70.6",[125,845,742],{"x":846,"y":722,"style":733},"639.1",[96,848],{"x":830,"y":745,"width":717,"height":718,"rx":99,"fill":110,"stroke":111,"style":136},[125,850,749],{"x":833,"y":748,"style":723},[96,852],{"x":836,"y":745,"width":728,"height":718,"rx":99,"fill":102,"stroke":111,"style":136},[125,854,755],{"x":839,"y":748,"style":754},[96,856],{"x":842,"y":745,"width":843,"height":718,"rx":99,"fill":102,"stroke":111,"style":136},[125,858,760],{"x":846,"y":748,"style":754},[96,860],{"x":830,"y":763,"width":717,"height":718,"rx":99,"fill":110,"stroke":111,"style":136},[125,862,438],{"x":833,"y":766,"style":723},[96,864],{"x":836,"y":763,"width":728,"height":718,"rx":99,"fill":102,"stroke":111,"style":136},[125,866,790],{"x":839,"y":766,"style":754},[96,868],{"x":842,"y":763,"width":843,"height":718,"rx":99,"fill":102,"stroke":111,"style":136},[125,870,760],{"x":846,"y":766,"style":754},[96,872],{"x":830,"y":782,"width":717,"height":718,"rx":99,"fill":110,"stroke":111,"style":136},[125,874,98],{"x":833,"y":785,"style":723},[96,876],{"x":836,"y":782,"width":728,"height":718,"rx":99,"fill":102,"stroke":111,"style":136},[125,878,822],{"x":839,"y":785,"style":754},[96,880],{"x":842,"y":782,"width":843,"height":718,"rx":99,"fill":102,"stroke":111,"style":136},[125,882,827],{"x":846,"y":785,"style":754},[125,884,888],{"x":885,"y":886,"style":887},"175.9","29.0","font-size:12.5px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","before",[125,890,892],{"x":891,"y":886,"style":887},"589.1","after",[42,894],{"x1":895,"y1":763,"x2":896,"y2":763,"stroke":158,"style":112},"276.2","489.8",[898,899],"polygon",{"points":900,"fill":901},"493.8,96.0 484.8,101.0 484.8,91.0","#5b5cf0",[96,903],{"x":904,"y":905,"width":906,"height":907,"rx":908,"fill":102,"stroke":111,"style":909},"305.9","63.0","158.1","22.0","11","stroke-width:1.2px",[125,911,914],{"x":912,"y":128,"style":913},"385.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","Status != Cancelled",[25,916,918],{"id":917},"_4-look-values-up-with-a-dictionary","4. Look values up with a dictionary",[10,920,921,922,925],{},"Macros that look up prices, names or codes usually load a ",[37,923,924],{},"Scripting.Dictionary"," first, then read from it in the row loop:",[30,927,929],{"className":196,"code":928,"language":198,"meta":35,"style":35},"Set prices = CreateObject(\"Scripting.Dictionary\")\nFor r = 2 To pLast\n    prices(wsP.Cells(r, 1).Value) = wsP.Cells(r, 2).Value\nNext r\nFor r = 2 To lastRow\n    If prices.Exists(ws.Cells(r, 3).Value) Then\n        ws.Cells(r, 7).Value = prices(ws.Cells(r, 3).Value)\n    Else\n        ws.Cells(r, 7).Value = \"missing\"\n    End If\nNext r\n",[37,930,931,936,941,946,950,954,959,964,969,975,981],{"__ignoreMap":35},[40,932,933],{"class":42,"line":43},[40,934,935],{},"Set prices = CreateObject(\"Scripting.Dictionary\")\n",[40,937,938],{"class":42,"line":241},[40,939,940],{},"For r = 2 To pLast\n",[40,942,943],{"class":42,"line":248},[40,944,945],{},"    prices(wsP.Cells(r, 1).Value) = wsP.Cells(r, 2).Value\n",[40,947,948],{"class":42,"line":278},[40,949,379],{},[40,951,952],{"class":42,"line":299},[40,953,369],{},[40,955,956],{"class":42,"line":304},[40,957,958],{},"    If prices.Exists(ws.Cells(r, 3).Value) Then\n",[40,960,961],{"class":42,"line":325},[40,962,963],{},"        ws.Cells(r, 7).Value = prices(ws.Cells(r, 3).Value)\n",[40,965,966],{"class":42,"line":557},[40,967,968],{},"    Else\n",[40,970,972],{"class":42,"line":971},9,[40,973,974],{},"        ws.Cells(r, 7).Value = \"missing\"\n",[40,976,978],{"class":42,"line":977},10,[40,979,980],{},"    End If\n",[40,982,984],{"class":42,"line":983},11,[40,985,379],{},[10,987,988,989,992],{},"A Python dictionary does the same thing, and ",[37,990,991],{},"Series.map"," applies it to a whole column. A left merge is the table-shaped equivalent; both leave missing keys empty rather than raising an error:",[30,994,996],{"className":218,"code":995,"language":220,"meta":35,"style":35},"prices = pd.read_excel(\"orders.xlsx\", sheet_name=\"Prices\")\nlookup = dict(zip(prices[\"SKU\"], prices[\"Price\"]))\nkept = kept.assign(Price=kept[\"SKU\"].map(lookup))\nkept[\"Value\"] = kept[\"Qty\"] * kept[\"Price\"]\nprint(kept[[\"OrderID\", \"SKU\", \"Qty\", \"Price\", \"Value\"]].to_string(index=False))\nprint(\"SKUs without a price:\", sorted(kept.loc[kept[\"Price\"].isna(), \"SKU\"].unique()))\n",[37,997,998,1020,1050,1072,1098,1133],{"__ignoreMap":35},[40,999,1000,1003,1005,1007,1009,1011,1013,1015,1018],{"class":42,"line":43},[40,1001,1002],{"class":231},"prices ",[40,1004,254],{"class":227},[40,1006,257],{"class":231},[40,1008,260],{"class":50},[40,1010,263],{"class":231},[40,1012,267],{"class":266},[40,1014,254],{"class":227},[40,1016,1017],{"class":50},"\"Prices\"",[40,1019,275],{"class":231},[40,1021,1022,1025,1027,1030,1032,1035,1038,1041,1044,1047],{"class":42,"line":241},[40,1023,1024],{"class":231},"lookup ",[40,1026,254],{"class":227},[40,1028,1029],{"class":281}," dict",[40,1031,285],{"class":231},[40,1033,1034],{"class":281},"zip",[40,1036,1037],{"class":231},"(prices[",[40,1039,1040],{"class":50},"\"SKU\"",[40,1042,1043],{"class":231},"], prices[",[40,1045,1046],{"class":50},"\"Price\"",[40,1048,1049],{"class":231},"]))\n",[40,1051,1052,1054,1056,1059,1062,1064,1067,1069],{"class":42,"line":248},[40,1053,623],{"class":231},[40,1055,254],{"class":227},[40,1057,1058],{"class":231}," kept.assign(",[40,1060,1061],{"class":266},"Price",[40,1063,254],{"class":227},[40,1065,1066],{"class":231},"kept[",[40,1068,1040],{"class":50},[40,1070,1071],{"class":231},"].map(lookup))\n",[40,1073,1074,1076,1079,1081,1083,1086,1088,1090,1092,1094,1096],{"class":42,"line":278},[40,1075,1066],{"class":231},[40,1077,1078],{"class":50},"\"Value\"",[40,1080,398],{"class":231},[40,1082,254],{"class":227},[40,1084,1085],{"class":231}," kept[",[40,1087,406],{"class":50},[40,1089,398],{"class":231},[40,1091,411],{"class":227},[40,1093,1085],{"class":231},[40,1095,1046],{"class":50},[40,1097,322],{"class":231},[40,1099,1100,1102,1105,1107,1109,1111,1113,1115,1117,1119,1121,1123,1125,1127,1129,1131],{"class":42,"line":299},[40,1101,282],{"class":281},[40,1103,1104],{"class":231},"(kept[[",[40,1106,424],{"class":50},[40,1108,263],{"class":231},[40,1110,1040],{"class":50},[40,1112,263],{"class":231},[40,1114,406],{"class":50},[40,1116,263],{"class":231},[40,1118,1046],{"class":50},[40,1120,263],{"class":231},[40,1122,1078],{"class":50},[40,1124,574],{"class":231},[40,1126,577],{"class":266},[40,1128,254],{"class":227},[40,1130,582],{"class":281},[40,1132,441],{"class":231},[40,1134,1135,1137,1139,1142,1144,1147,1150,1152,1155,1157],{"class":42,"line":304},[40,1136,282],{"class":281},[40,1138,285],{"class":231},[40,1140,1141],{"class":50},"\"SKUs without a price:\"",[40,1143,263],{"class":231},[40,1145,1146],{"class":281},"sorted",[40,1148,1149],{"class":231},"(kept.loc[kept[",[40,1151,1046],{"class":50},[40,1153,1154],{"class":231},"].isna(), ",[40,1156,1040],{"class":50},[40,1158,1159],{"class":231},"].unique()))\n",[10,1161,1162,1165,1166,1169,1170,1174],{},[37,1163,1164],{},"D-40"," has no price, the case the VBA marks ",[37,1167,1168],{},"\"missing\"",". List those keys explicitly rather than letting them turn into silent blanks. ",[14,1171,1173],{"href":1172},"\u002Fadvanced-data-transformation-and-cleaning\u002Fmerging-and-joining-excel-dataframes\u002Fvlookup-equivalent-in-pandas-for-excel-files\u002F","The VLOOKUP Equivalent in pandas for Excel Files"," goes through the merge options.",[25,1176,1178],{"id":1177},"_5-filter-and-copy-to-another-sheet","5. Filter and copy to another sheet",[30,1180,1182],{"className":196,"code":1181,"language":198,"meta":35,"style":35},"ws.Range(\"A1\").AutoFilter Field:=2, Criteria1:=\"North\"\nws.AutoFilter.Range.SpecialCells(xlCellTypeVisible).Copy wsNorth.Range(\"A1\")\nws.AutoFilterMode = False\n",[37,1183,1184,1189,1194],{"__ignoreMap":35},[40,1185,1186],{"class":42,"line":43},[40,1187,1188],{},"ws.Range(\"A1\").AutoFilter Field:=2, Criteria1:=\"North\"\n",[40,1190,1191],{"class":42,"line":241},[40,1192,1193],{},"ws.AutoFilter.Range.SpecialCells(xlCellTypeVisible).Copy wsNorth.Range(\"A1\")\n",[40,1195,1196],{"class":42,"line":248},[40,1197,1198],{},"ws.AutoFilterMode = False\n",[10,1200,1201],{},"Filter the table and write the result as its own sheet. Doing every region in one loop is how the \"one sheet per region\" macro usually ends:",[30,1203,1205],{"className":218,"code":1204,"language":220,"meta":35,"style":35},"with pd.ExcelWriter(\"by_region.xlsx\") as writer:\n    for region, part in kept.groupby(\"Region\"):\n        part.to_excel(writer, sheet_name=region, index=False)\nprint(load_workbook(\"by_region.xlsx\").sheetnames)\n",[37,1206,1207,1226,1246,1266],{"__ignoreMap":35},[40,1208,1209,1212,1215,1218,1221,1223],{"class":42,"line":43},[40,1210,1211],{"class":227},"with",[40,1213,1214],{"class":231}," pd.ExcelWriter(",[40,1216,1217],{"class":50},"\"by_region.xlsx\"",[40,1219,1220],{"class":231},") ",[40,1222,471],{"class":227},[40,1224,1225],{"class":231}," writer:\n",[40,1227,1228,1231,1234,1237,1240,1243],{"class":42,"line":241},[40,1229,1230],{"class":227},"    for",[40,1232,1233],{"class":231}," region, part ",[40,1235,1236],{"class":227},"in",[40,1238,1239],{"class":231}," kept.groupby(",[40,1241,1242],{"class":50},"\"Region\"",[40,1244,1245],{"class":231},"):\n",[40,1247,1248,1251,1253,1255,1258,1260,1262,1264],{"class":42,"line":248},[40,1249,1250],{"class":231},"        part.to_excel(writer, ",[40,1252,267],{"class":266},[40,1254,254],{"class":227},[40,1256,1257],{"class":231},"region, ",[40,1259,577],{"class":266},[40,1261,254],{"class":227},[40,1263,582],{"class":281},[40,1265,275],{"class":231},[40,1267,1268,1270,1273,1275],{"class":42,"line":278},[40,1269,282],{"class":281},[40,1271,1272],{"class":231},"(load_workbook(",[40,1274,1217],{"class":50},[40,1276,1277],{"class":231},").sheetnames)\n",[25,1279,1281],{"id":1280},"_6-loop-over-every-file-in-a-folder","6. Loop over every file in a folder",[30,1283,1285],{"className":196,"code":1284,"language":198,"meta":35,"style":35},"f = Dir(folder & \"\\*.xlsx\")\nDo While f \u003C> \"\"\n    Set wb = Workbooks.Open(folder & \"\\\" & f)\n    ' ... copy the data out ...\n    wb.Close SaveChanges:=False\n    f = Dir()\nLoop\n",[37,1286,1287,1292,1297,1302,1307,1312,1317],{"__ignoreMap":35},[40,1288,1289],{"class":42,"line":43},[40,1290,1291],{},"f = Dir(folder & \"\\*.xlsx\")\n",[40,1293,1294],{"class":42,"line":241},[40,1295,1296],{},"Do While f \u003C> \"\"\n",[40,1298,1299],{"class":42,"line":248},[40,1300,1301],{},"    Set wb = Workbooks.Open(folder & \"\\\" & f)\n",[40,1303,1304],{"class":42,"line":278},[40,1305,1306],{},"    ' ... copy the data out ...\n",[40,1308,1309],{"class":42,"line":299},[40,1310,1311],{},"    wb.Close SaveChanges:=False\n",[40,1313,1314],{"class":42,"line":304},[40,1315,1316],{},"    f = Dir()\n",[40,1318,1319],{"class":42,"line":325},[40,1320,1321],{},"Loop\n",[10,1323,1324,1327,1328,1331,1332,1335],{},[37,1325,1326],{},"pathlib"," replaces ",[37,1329,1330],{},"Dir()",". Skip Excel's ",[37,1333,1334],{},"~$"," lock files, which a VBA macro running inside Excel rarely meets but a script running beside users often does:",[30,1337,1339],{"className":218,"code":1338,"language":220,"meta":35,"style":35},"from pathlib import Path\n\nframes = []\nfor path in sorted(Path(\"monthly\").glob(\"*.xlsx\")):\n    if path.name.startswith(\"~$\"):\n        print(\"skipping lock file\", path.name)\n        continue\n    frames.append(pd.read_excel(path))\ncombined = pd.concat(frames, ignore_index=True)\nprint(combined.groupby(\"Month\").size().to_string())\n",[37,1340,1341,1353,1357,1367,1395,1408,1421,1426,1431,1451],{"__ignoreMap":35},[40,1342,1343,1345,1348,1350],{"class":42,"line":43},[40,1344,228],{"class":227},[40,1346,1347],{"class":231}," pathlib ",[40,1349,235],{"class":227},[40,1351,1352],{"class":231}," Path\n",[40,1354,1355],{"class":42,"line":241},[40,1356,245],{"emptyLinePlaceholder":244},[40,1358,1359,1362,1364],{"class":42,"line":248},[40,1360,1361],{"class":231},"frames ",[40,1363,254],{"class":227},[40,1365,1366],{"class":231}," []\n",[40,1368,1369,1372,1375,1377,1380,1383,1386,1389,1392],{"class":42,"line":278},[40,1370,1371],{"class":227},"for",[40,1373,1374],{"class":231}," path ",[40,1376,1236],{"class":227},[40,1378,1379],{"class":281}," sorted",[40,1381,1382],{"class":231},"(Path(",[40,1384,1385],{"class":50},"\"monthly\"",[40,1387,1388],{"class":231},").glob(",[40,1390,1391],{"class":50},"\"*.xlsx\"",[40,1393,1394],{"class":231},")):\n",[40,1396,1397,1400,1403,1406],{"class":42,"line":299},[40,1398,1399],{"class":227},"    if",[40,1401,1402],{"class":231}," path.name.startswith(",[40,1404,1405],{"class":50},"\"~$\"",[40,1407,1245],{"class":231},[40,1409,1410,1413,1415,1418],{"class":42,"line":304},[40,1411,1412],{"class":281},"        print",[40,1414,285],{"class":231},[40,1416,1417],{"class":50},"\"skipping lock file\"",[40,1419,1420],{"class":231},", path.name)\n",[40,1422,1423],{"class":42,"line":325},[40,1424,1425],{"class":227},"        continue\n",[40,1427,1428],{"class":42,"line":557},[40,1429,1430],{"class":231},"    frames.append(pd.read_excel(path))\n",[40,1432,1433,1436,1438,1441,1444,1446,1449],{"class":42,"line":971},[40,1434,1435],{"class":231},"combined ",[40,1437,254],{"class":227},[40,1439,1440],{"class":231}," pd.concat(frames, ",[40,1442,1443],{"class":266},"ignore_index",[40,1445,254],{"class":227},[40,1447,1448],{"class":281},"True",[40,1450,275],{"class":231},[40,1452,1453,1455,1458,1461],{"class":42,"line":977},[40,1454,282],{"class":281},[40,1456,1457],{"class":231},"(combined.groupby(",[40,1459,1460],{"class":50},"\"Month\"",[40,1462,1463],{"class":231},").size().to_string())\n",[25,1465,1467],{"id":1466},"_7-save-a-copy-with-todays-date","7. Save a copy with today's date",[30,1469,1471],{"className":196,"code":1470,"language":198,"meta":35,"style":35},"wb.SaveCopyAs folder & \"\\Orders_\" & Format(Date, \"yyyy-mm-dd\") & \".xlsx\"\n",[37,1472,1473],{"__ignoreMap":35},[40,1474,1475],{"class":42,"line":43},[40,1476,1470],{},[30,1478,1480],{"className":218,"code":1479,"language":220,"meta":35,"style":35},"from datetime import date\n\nout = Path(\"exports\") \u002F f\"Orders_{date(2026, 9, 29):%Y-%m-%d}.xlsx\"\nout.parent.mkdir(exist_ok=True)\nkept.to_excel(out, index=False)\nprint(out)\n",[37,1481,1482,1494,1498,1565,1579,1592],{"__ignoreMap":35},[40,1483,1484,1486,1489,1491],{"class":42,"line":43},[40,1485,228],{"class":227},[40,1487,1488],{"class":231}," datetime ",[40,1490,235],{"class":227},[40,1492,1493],{"class":231}," date\n",[40,1495,1496],{"class":42,"line":241},[40,1497,245],{"emptyLinePlaceholder":244},[40,1499,1500,1503,1505,1508,1511,1513,1516,1519,1522,1524,1527,1530,1532,1535,1537,1540,1543,1546,1549,1552,1555,1557,1560,1562],{"class":42,"line":248},[40,1501,1502],{"class":231},"out ",[40,1504,254],{"class":227},[40,1506,1507],{"class":231}," Path(",[40,1509,1510],{"class":50},"\"exports\"",[40,1512,1220],{"class":231},[40,1514,1515],{"class":227},"\u002F",[40,1517,1518],{"class":227}," f",[40,1520,1521],{"class":50},"\"Orders_",[40,1523,657],{"class":656},[40,1525,1526],{"class":231},"date(",[40,1528,1529],{"class":281},"2026",[40,1531,263],{"class":231},[40,1533,1534],{"class":281},"9",[40,1536,263],{"class":231},[40,1538,1539],{"class":281},"29",[40,1541,1542],{"class":231},"):",[40,1544,1545],{"class":227},"%",[40,1547,1548],{"class":231},"Y",[40,1550,1551],{"class":227},"-%",[40,1553,1554],{"class":231},"m",[40,1556,1551],{"class":227},[40,1558,1559],{"class":231},"d",[40,1561,665],{"class":656},[40,1563,1564],{"class":50},".xlsx\"\n",[40,1566,1567,1570,1573,1575,1577],{"class":42,"line":278},[40,1568,1569],{"class":231},"out.parent.mkdir(",[40,1571,1572],{"class":266},"exist_ok",[40,1574,254],{"class":227},[40,1576,1448],{"class":281},[40,1578,275],{"class":231},[40,1580,1581,1584,1586,1588,1590],{"class":42,"line":299},[40,1582,1583],{"class":231},"kept.to_excel(out, ",[40,1585,577],{"class":266},[40,1587,254],{"class":227},[40,1589,582],{"class":281},[40,1591,275],{"class":231},[40,1593,1594,1596],{"class":42,"line":304},[40,1595,282],{"class":281},[40,1597,1598],{"class":231},"(out)\n",[10,1600,1601,1602,1605],{},"In a real job use ",[37,1603,1604],{},"date.today()","; the fixed date keeps this example's output stable.",[25,1607,1609],{"id":1608},"_8-error-handling","8. Error handling",[10,1611,1612,1615],{},[37,1613,1614],{},"On Error Resume Next"," hides every error for the rest of the procedure, which is how macros quietly produce half a report. Python's equivalent is narrower: catch the specific error, where it can happen, and say what happened:",[30,1617,1619],{"className":196,"code":1618,"language":198,"meta":35,"style":35},"On Error Resume Next\nSet wb = Workbooks.Open(path)\nIf wb Is Nothing Then MsgBox \"Could not open \" & path\nOn Error GoTo 0\n",[37,1620,1621,1626,1631,1636],{"__ignoreMap":35},[40,1622,1623],{"class":42,"line":43},[40,1624,1625],{},"On Error Resume Next\n",[40,1627,1628],{"class":42,"line":241},[40,1629,1630],{},"Set wb = Workbooks.Open(path)\n",[40,1632,1633],{"class":42,"line":248},[40,1634,1635],{},"If wb Is Nothing Then MsgBox \"Could not open \" & path\n",[40,1637,1638],{"class":42,"line":278},[40,1639,1640],{},"On Error GoTo 0\n",[30,1642,1644],{"className":218,"code":1643,"language":220,"meta":35,"style":35},"for path in [Path(\"monthly\u002Forders_2026-07.xlsx\"), Path(\"monthly\u002Forders_2026-10.xlsx\")]:\n    try:\n        rows = len(pd.read_excel(path))\n    except FileNotFoundError:\n        print(f\"missing: {path.name} - skipped\")\n        continue\n    print(f\"read {path.name}: {rows} rows\")\n",[37,1645,1646,1669,1677,1690,1700,1723,1727],{"__ignoreMap":35},[40,1647,1648,1650,1652,1654,1657,1660,1663,1666],{"class":42,"line":43},[40,1649,1371],{"class":227},[40,1651,1374],{"class":231},[40,1653,1236],{"class":227},[40,1655,1656],{"class":231}," [Path(",[40,1658,1659],{"class":50},"\"monthly\u002Forders_2026-07.xlsx\"",[40,1661,1662],{"class":231},"), Path(",[40,1664,1665],{"class":50},"\"monthly\u002Forders_2026-10.xlsx\"",[40,1667,1668],{"class":231},")]:\n",[40,1670,1671,1674],{"class":42,"line":241},[40,1672,1673],{"class":227},"    try",[40,1675,1676],{"class":231},":\n",[40,1678,1679,1682,1684,1687],{"class":42,"line":248},[40,1680,1681],{"class":231},"        rows ",[40,1683,254],{"class":227},[40,1685,1686],{"class":281}," len",[40,1688,1689],{"class":231},"(pd.read_excel(path))\n",[40,1691,1692,1695,1698],{"class":42,"line":278},[40,1693,1694],{"class":227},"    except",[40,1696,1697],{"class":281}," FileNotFoundError",[40,1699,1676],{"class":231},[40,1701,1702,1704,1706,1708,1711,1713,1716,1718,1721],{"class":42,"line":299},[40,1703,1412],{"class":281},[40,1705,285],{"class":231},[40,1707,650],{"class":227},[40,1709,1710],{"class":50},"\"missing: ",[40,1712,657],{"class":656},[40,1714,1715],{"class":231},"path.name",[40,1717,665],{"class":656},[40,1719,1720],{"class":50}," - skipped\"",[40,1722,275],{"class":231},[40,1724,1725],{"class":42,"line":304},[40,1726,1425],{"class":227},[40,1728,1729,1732,1734,1736,1739,1741,1743,1745,1748,1750,1753,1755,1757],{"class":42,"line":325},[40,1730,1731],{"class":281},"    print",[40,1733,285],{"class":231},[40,1735,650],{"class":227},[40,1737,1738],{"class":50},"\"read ",[40,1740,657],{"class":656},[40,1742,1715],{"class":231},[40,1744,665],{"class":656},[40,1746,1747],{"class":50},": ",[40,1749,657],{"class":656},[40,1751,1752],{"class":231},"rows",[40,1754,665],{"class":656},[40,1756,680],{"class":50},[40,1758,275],{"class":231},[10,1760,1761,1762,1765,1766,1769,1770,1774],{},"Replace ",[37,1763,1764],{},"MsgBox"," with ",[37,1767,1768],{},"logging"," in scheduled jobs, since nobody is there to click OK. ",[14,1771,1773],{"href":1772},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Flog-python-excel-script-output-to-a-file\u002F","Log Python Excel Script Output to a File"," sets that up.",[77,1776,86,1782,86,1785,86,1788,86,1793,86,1797,86,1801,86,1805,86,1808,86,1811,86,1814,86,1818,86,1822,86,1825,86,1828,86,1830,86,1833,86,1836,86,1840,86,1842,86,1845,86,1848,86,1851,86,1853,86,1856,86,1859,86,1863,86,1865,86,1868,86,1871,86,1874,86,1876,86,1879,86,1882,86,1886,86,1888,86,1891,86,1894,86,1898,86,1900],{"viewBox":1777,"role":80,"ariaLabelledBy":1778,"xmlns":84,"style":1781},"222 0 316 284",[1779,1780],"tv-map-t","tv-map-d","width:100%;max-width:316px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[88,1783,1784],{"id":1779},"VBA idioms and their Python counterparts",[92,1786,1787],{"id":1780},"A reference table. End(xlUp) becomes reading the table or max_row. A For loop over rows becomes a column operation. Deleting rows becomes a boolean filter. Scripting.Dictionary becomes a dict with Series.map. AutoFilter and copy becomes groupby and to_excel. Dir becomes pathlib glob. SaveCopyAs becomes to_excel with a dated name. On Error becomes try and except.",[96,1789],{"x":1790,"y":99,"width":1791,"height":1792,"fill":102},"222","316","284",[96,1794],{"x":1795,"y":106,"width":1796,"height":718,"rx":99,"fill":157,"stroke":158,"style":729},"238.1","137.4",[125,1798,130],{"x":1799,"y":1800,"style":733},"306.8","35.0",[96,1802],{"x":1803,"y":106,"width":1804,"height":718,"rx":99,"fill":157,"stroke":158,"style":729},"375.5","146.5",[125,1806,170],{"x":1807,"y":1800,"style":733},"448.7",[96,1809],{"x":1795,"y":1810,"width":1796,"height":718,"rx":99,"fill":102,"stroke":111,"style":136},"44.0",[125,1812,1813],{"x":1799,"y":905,"style":754},"End(xlUp).Row",[96,1815],{"x":1803,"y":1810,"width":1804,"height":718,"rx":99,"fill":1816,"stroke":1817,"style":729},"#d9f4f1","var(--teal,#0f9488)",[125,1819,1821],{"x":1807,"y":905,"style":1820},"font-size:11px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","read_excel \u002F max_row",[96,1823],{"x":1795,"y":1824,"width":1796,"height":718,"rx":99,"fill":102,"stroke":111,"style":136},"72.0",[125,1826,1827],{"x":1799,"y":134,"style":754},"For r = 2 To last",[96,1829],{"x":1803,"y":1824,"width":1804,"height":718,"rx":99,"fill":1816,"stroke":1817,"style":729},[125,1831,1832],{"x":1807,"y":134,"style":1820},"column expression",[96,1834],{"x":1795,"y":1835,"width":1796,"height":718,"rx":99,"fill":102,"stroke":111,"style":136},"100.0",[125,1837,1839],{"x":1799,"y":1838,"style":754},"119.0","Rows(r).Delete",[96,1841],{"x":1803,"y":1835,"width":1804,"height":718,"rx":99,"fill":1816,"stroke":1817,"style":729},[125,1843,1844],{"x":1807,"y":1838,"style":1820},"df[condition]",[96,1846],{"x":1795,"y":1847,"width":1796,"height":718,"rx":99,"fill":102,"stroke":111,"style":136},"128.0",[125,1849,924],{"x":1799,"y":1850,"style":754},"147.0",[96,1852],{"x":1803,"y":1847,"width":1804,"height":718,"rx":99,"fill":1816,"stroke":1817,"style":729},[125,1854,1855],{"x":1807,"y":1850,"style":1820},"dict + Series.map",[96,1857],{"x":1795,"y":1858,"width":1796,"height":718,"rx":99,"fill":102,"stroke":111,"style":136},"156.0",[125,1860,1862],{"x":1799,"y":1861,"style":754},"175.0","AutoFilter + Copy",[96,1864],{"x":1803,"y":1858,"width":1804,"height":718,"rx":99,"fill":1816,"stroke":1817,"style":729},[125,1866,1867],{"x":1807,"y":1861,"style":1820},"groupby + to_excel",[96,1869],{"x":1795,"y":1870,"width":1796,"height":718,"rx":99,"fill":102,"stroke":111,"style":136},"184.0",[125,1872,1330],{"x":1799,"y":1873,"style":754},"203.0",[96,1875],{"x":1803,"y":1870,"width":1804,"height":718,"rx":99,"fill":1816,"stroke":1817,"style":729},[125,1877,1878],{"x":1807,"y":1873,"style":1820},"Path.glob",[96,1880],{"x":1795,"y":1881,"width":1796,"height":718,"rx":99,"fill":102,"stroke":111,"style":136},"212.0",[125,1883,1885],{"x":1799,"y":1884,"style":754},"231.0","SaveCopyAs",[96,1887],{"x":1803,"y":1881,"width":1804,"height":718,"rx":99,"fill":1816,"stroke":1817,"style":729},[125,1889,1890],{"x":1807,"y":1884,"style":1820},"to_excel dated name",[96,1892],{"x":1795,"y":1893,"width":1796,"height":718,"rx":99,"fill":102,"stroke":111,"style":136},"240.0",[125,1895,1897],{"x":1799,"y":1896,"style":754},"259.0","On Error",[96,1899],{"x":1803,"y":1893,"width":1804,"height":718,"rx":99,"fill":1816,"stroke":1817,"style":729},[125,1901,1902],{"x":1807,"y":1896,"style":1820},"try \u002F except",[25,1904,1906],{"id":1905},"what-does-not-translate","What does not translate",[10,1908,1909],{},"A few things macros do have no file-based equivalent, because they depend on Excel running:",[1911,1912,1913,1921,1927],"ul",{},[1914,1915,1916,1920],"li",{},[1917,1918,1919],"strong",{},"Recalculation."," openpyxl writes formulas but does not calculate them. If the macro relies on Excel computing values before reading them back, compute in Python instead, or recalculate with LibreOffice or Excel as a step.",[1914,1922,1923,1926],{},[1917,1924,1925],{},"User interaction."," Input boxes, forms and buttons belong in Excel. Keep those parts in VBA, or replace them with parameters to the script.",[1914,1928,1929,1932,1933,1936],{},[1917,1930,1931],{},"Pivot refreshes and charts copied from templates."," Refreshing a live PivotTable needs Excel; building the summary with ",[37,1934,1935],{},"pivot_table"," and writing a chart with openpyxl is the Python route.",[10,1938,1939,1940,1944],{},"For those, driving Excel from Python is the bridge: ",[14,1941,1943],{"href":1942},"\u002Fgetting-started-with-python-excel-automation\u002Fautomating-excel-with-com-and-pywin32\u002Frun-an-excel-macro-from-python-with-win32com\u002F","Run an Excel Macro from Python with win32com"," lets a script call the remaining VBA while the rest moves over.",[25,1946,1948],{"id":1947},"checking-the-translation","Checking the translation",[10,1950,1951,1952,1956,1957,1961],{},"Run the old macro and the new script on the same input and compare the outputs before switching. The comparison should be on values, not formatting, and should report differing cells rather than just \"different\": ",[14,1953,1955],{"href":1954},"\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fcompare-two-excel-files-for-differences-with-python\u002F","Compare Two Excel Files for Differences with Python"," builds that check. To see what the macros contain before starting, ",[14,1958,1960],{"href":1959},"\u002Fgetting-started-with-python-excel-automation\u002Fautomating-excel-with-com-and-pywin32\u002Fextract-vba-macro-code-from-an-excel-file\u002F","Extract VBA Macro Code from an Excel File"," lists every module in a folder of workbooks.",[25,1963,1965],{"id":1964},"common-pitfalls","Common pitfalls",[1911,1967,1968,1974,1980,1998,2004],{},[1914,1969,1970,1973],{},[1917,1971,1972],{},"Translating line by line."," A literal port keeps the row loops and runs slower than the original; translate the intent of each block.",[1914,1975,1976,1979],{},[1917,1977,1978],{},"Row numbers."," VBA rows start at 1 and include the header; pandas indices start at 0 and do not. Avoid using positions at all where a column value will do.",[1914,1981,1982,1985,1986,1989,1990,1993,1994,1997],{},[1917,1983,1984],{},"Blank versus zero."," VBA's ",[37,1987,1988],{},"Cells(r, c).Value"," is ",[37,1991,1992],{},"Empty"," for a blank cell and compares equal to 0 and \"\"; in pandas a blank is ",[37,1995,1996],{},"NaN"," and equals nothing, including itself.",[1914,1999,2000,2003],{},[1917,2001,2002],{},"Dates."," Macros often rely on Excel's locale to parse dates typed as text; parse them explicitly with a format in Python.",[1914,2005,2006,2009,2010,2012,2013,2016],{},[1917,2007,2008],{},"Silent errors."," Don't reproduce ",[37,2011,1614],{}," with a bare ",[37,2014,2015],{},"except:","; catch the specific error.",[25,2018,2020],{"id":2019},"conclusion","Conclusion",[10,2022,2023,2024,2026,2027,2030,2031,2033,2034,263,2036,2039,2040,1327,2042,2044,2045,2048,2049,2051],{},"Translate macros pattern by pattern: reading the table replaces ",[37,2025,346],{},", column expressions replace row loops, filters replace backward delete loops, ",[37,2028,2029],{},"dict"," and ",[37,2032,991],{}," replace ",[37,2035,924],{},[37,2037,2038],{},"groupby"," replaces AutoFilter and copy, ",[37,2041,1326],{},[37,2043,1330],{},", and specific ",[37,2046,2047],{},"try\u002Fexcept"," blocks replace ",[37,2050,1614],{},". Keep what depends on a running Excel — recalculation, forms, live pivots — in VBA or drive it with win32com, and compare old and new outputs before retiring the macro.",[25,2053,2055],{"id":2054},"frequently-asked-questions","Frequently asked questions",[10,2057,2058,2061],{},[1917,2059,2060],{},"Is there a tool that converts VBA to Python automatically?","\nNot one that produces good Python. Line-by-line converters keep VBA's cell loops and Excel object model. Translating by pattern into pandas and openpyxl gives shorter, faster code that runs without Excel.",[10,2063,2064,2067],{},[1917,2065,2066],{},"How do I find the last row in Python like End(xlUp)?","\nWith pandas, read_excel only returns rows with data, so len(df) is the count. With openpyxl, ws.max_row gives the last used row, which can include formatted but empty rows; walk up from it to find the last value in a column.",[10,2069,2070,2073],{},[1917,2071,2072],{},"What replaces Scripting.Dictionary in Python?","\nA built-in dict. Build it from two columns with dict(zip(keys, values)) and apply it to a column with Series.map; keys that are not found become NaN.",[10,2075,2076,2079],{},[1917,2077,2078],{},"Can Python run the parts of a macro I have not translated yet?","\nYes, on Windows with Excel installed. win32com or xlwings can open the workbook and call a macro by name, so the script and the remaining VBA can run in sequence.",[25,2081,2083],{"id":2082},"related","Related",[1911,2085,2086,2092,2097,2102,2107],{},[1914,2087,2088,2089,2091],{},"Up one level: ",[14,2090,17],{"href":16}," — picking the right tool for each job.",[1914,2093,2094,2096],{},[14,2095,22],{"href":21}," — whether to move at all.",[1914,2098,2099,2101],{},[14,2100,1960],{"href":1959}," — getting the code out before translating it.",[1914,2103,2104,2106],{},[14,2105,1943],{"href":1942}," — calling the VBA that remains.",[1914,2108,2109,2111],{},[14,2110,1173],{"href":1172}," — lookups beyond a dictionary.",[2113,2114,2115],"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 .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}",{"title":35,"searchDepth":241,"depth":241,"links":2117},[2118,2119,2120,2121,2122,2123,2124,2125,2126,2127,2128,2129,2130,2131,2132],{"id":27,"depth":241,"text":28},{"id":189,"depth":241,"text":190},{"id":358,"depth":241,"text":359},{"id":587,"depth":241,"text":588},{"id":917,"depth":241,"text":918},{"id":1177,"depth":241,"text":1178},{"id":1280,"depth":241,"text":1281},{"id":1466,"depth":241,"text":1467},{"id":1608,"depth":241,"text":1609},{"id":1905,"depth":241,"text":1906},{"id":1947,"depth":241,"text":1948},{"id":1964,"depth":241,"text":1965},{"id":2019,"depth":241,"text":2020},{"id":2054,"depth":241,"text":2055},{"id":2082,"depth":241,"text":2083},"2026-09-29","A phrasebook for moving VBA to Python: the eight patterns that make up most Excel macros — finding the last row, looping over rows, deleting rows that match, lookups with a dictionary, filtering and copying, looping over a folder of files, saving a copy with a date, and error handling — each shown in VBA and in pandas or openpyxl.","md",[2137,2139,2141,2143],{"q":2060,"a":2138},"Not one that produces good Python. Line-by-line converters keep VBA's cell loops and Excel object model. Translating by pattern into pandas and openpyxl gives shorter, faster code that runs without Excel.",{"q":2066,"a":2140},"With pandas, read_excel only returns rows with data, so len(df) is the count. With openpyxl, ws.max_row gives the last used row, which can include formatted but empty rows; walk up from it to find the last value in a column.",{"q":2072,"a":2142},"A built-in dict. Build it from two columns with dict(zip(keys, values)) and apply it to a column with Series.map; keys that are not found become NaN.",{"q":2078,"a":2144},"Yes, on Windows with Excel installed. win32com or xlwings can open the workbook and call a macro by name, so the script and the remaining VBA can run in sequence.","translate common vba macros to python",null,{"breadcrumb":2148},[2149,2151,2154],{"name":2150,"item":1515},"Home",{"name":2152,"item":2153},"Getting Started with Python Excel Automation","\u002Fgetting-started-with-python-excel-automation\u002F",{"name":17,"item":16},"\u002Fgetting-started-with-python-excel-automation\u002Fchoosing-a-python-excel-library\u002Ftranslate-common-vba-macros-to-python",{"title":2157,"description":2158},"Translate Common VBA Macros to Python (Side-by-Side Examples)","Convert VBA macros to Python pattern by pattern: last row with End(xlUp), For loops over rows, deleting rows, Scripting.Dictionary lookups, AutoFilter and copy, Dir() folder loops, SaveAs with a date, and On Error, in pandas and openpyxl.","translate-common-vba-macros-to-python","getting-started-with-python-excel-automation\u002Fchoosing-a-python-excel-library\u002Ftranslate-common-vba-macros-to-python\u002Findex",[2162,2163],"macros-and-vba","openpyxl","how-to","qVIAKhxoNb58zpXyUV-MvGQRFtDOQQpW7em6mFi_ACU",[2167,2170],{"title":22,"path":2168,"stem":2169,"children":-1},"\u002Fgetting-started-with-python-excel-automation\u002Fchoosing-a-python-excel-library\u002Fpython-vs-vba-for-excel-automation","getting-started-with-python-excel-automation\u002Fchoosing-a-python-excel-library\u002Fpython-vs-vba-for-excel-automation\u002Findex",{"title":2171,"path":2172,"stem":2173,"children":-1},"When to Use xlwings Instead of openpyxl","\u002Fgetting-started-with-python-excel-automation\u002Fchoosing-a-python-excel-library\u002Fwhen-to-use-xlwings-instead-of-openpyxl","getting-started-with-python-excel-automation\u002Fchoosing-a-python-excel-library\u002Fwhen-to-use-xlwings-instead-of-openpyxl\u002Findex",[2175,2177],{"path":2176,"title":17},"\u002Fgetting-started-with-python-excel-automation\u002Fchoosing-a-python-excel-library",{"path":2178,"title":2152},"\u002Fgetting-started-with-python-excel-automation",[],[2181,2184,2186,2188,2191,2194],{"path":2182,"title":2183,"kind":2164},"\u002Fgetting-started-with-python-excel-automation\u002Fautomating-excel-with-com-and-pywin32\u002Fadd-a-vba-macro-to-a-workbook-with-xlsxwriter","Add a VBA Macro to a Workbook with xlsxwriter",{"path":2185,"title":1960,"kind":2164},"\u002Fgetting-started-with-python-excel-automation\u002Fautomating-excel-with-com-and-pywin32\u002Fextract-vba-macro-code-from-an-excel-file",{"path":2187,"title":1943,"kind":2164},"\u002Fgetting-started-with-python-excel-automation\u002Fautomating-excel-with-com-and-pywin32\u002Frun-an-excel-macro-from-python-with-win32com",{"path":2189,"title":2190,"kind":2164},"\u002Fgetting-started-with-python-excel-automation\u002Fautomating-excel-with-xlwings-basics\u002Fcall-python-from-excel-with-xlwings-udf","Call Python from Excel with an xlwings UDF",{"path":2192,"title":2193,"kind":2164},"\u002Fgetting-started-with-python-excel-automation\u002Fautomating-excel-with-xlwings-basics\u002Fxlwings-run-macro-from-python-example","xlwings: Run a VBA Macro From Python (Example)",{"path":2195,"title":2196,"kind":2164},"\u002Fgetting-started-with-python-excel-automation\u002Fautomating-excel-with-xlwings-basics\u002Frun-python-from-an-excel-button-with-xlwings","Run Python from an Excel Button with xlwings RunPython",1790972058957]