[{"data":1,"prerenderedAt":2547},["ShallowReactive",2],{"doc:\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fcreate-calendar-events-from-an-excel-sheet":3,"surround:\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fcreate-calendar-events-from-an-excel-sheet":2511,"ancestors:\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fcreate-calendar-events-from-an-excel-sheet":2520,"children:\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fcreate-calendar-events-from-an-excel-sheet":2525,"related:\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fcreate-calendar-events-from-an-excel-sheet":2526},{"id":4,"title":5,"body":6,"dateModified":2478,"datePublished":2478,"description":2479,"extension":2480,"faq":2481,"keyword":2490,"kind":2491,"meta":2492,"navigation":133,"path":2500,"seo":2501,"slug":2504,"stem":2505,"topics":2506,"type":2509,"__hash__":2510},"docs\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fcreate-calendar-events-from-an-excel-sheet\u002Findex.md","Create Calendar Events from an Excel Sheet with Python",{"type":7,"value":8,"toc":2464},"minimark",[9,31,36,67,77,81,92,417,431,676,680,683,841,855,859,1264,1267,1313,1631,1635,1638,1827,1841,1845,1848,2038,2050,2162,2166,2173,2327,2334,2338,2382,2386,2389,2393,2399,2403,2409,2415,2421,2427,2431,2460],[10,11,12,13,17,18,21,22,24,25,30],"p",{},"Plenty of schedules live in spreadsheets: a training plan, a project's milestone list, an on-call rota, a list of contract renewal dates. People then retype the rows into their calendars, one by one, and miss the change made to row 14 last week. Calendar programs all import the same standard format, iCalendar (",[14,15,16],"code",{},".ics","), so a script can turn the sheet into a file that Outlook, Google Calendar and Apple Calendar load in one go — and, done properly, re-import later to apply changes rather than adding duplicates. This page shows the conversion with pandas and the ",[14,19,20],{},"icalendar"," package, including the details that trip up hand-written ",[14,23,16],{}," files: all-day end dates, time zones and stable identifiers. It belongs to ",[26,27,29],"a",{"href":28},"\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002F","Handling Excel File Formats and Conversions",".",[32,33,35],"h2",{"id":34},"prerequisites","Prerequisites",[37,38,43],"pre",{"className":39,"code":40,"language":41,"meta":42,"style":42},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install pandas openpyxl icalendar\n","bash","",[14,44,45],{"__ignoreMap":42},[46,47,50,54,58,61,64],"span",{"class":48,"line":49},"line",1,[46,51,53],{"class":52},"sMTad","pip",[46,55,57],{"class":56},"srMev"," install",[46,59,60],{"class":56}," pandas",[46,62,63],{"class":56}," openpyxl",[46,65,66],{"class":56}," icalendar\n",[10,68,69,70,72,73,76],{},"The examples use ",[14,71,20],{}," 7.3. ",[14,74,75],{},"schedule.xlsx"," has one row per event with an ID, title, date, start and end times, location, owner and notes. Two rows have no times — they are all-day events — and the last row is an empty leftover that must be skipped.",[32,78,80],{"id":79},"step-1-read-the-sheet-and-build-start-and-end-timestamps","Step 1 — Read the sheet and build start and end timestamps",[10,82,83,84,87,88,91],{},"Excel stores dates and times separately when they are typed into separate columns, and openpyxl returns them as ",[14,85,86],{},"datetime"," and ",[14,89,90],{},"time"," objects. Combine them into one start and one end per row:",[37,93,97],{"className":94,"code":95,"language":96,"meta":42,"style":42},"language-python shiki shiki-themes github-light github-dark-high-contrast","import datetime as dt\nimport pandas as pd\n\ndf = pd.read_excel(\"schedule.xlsx\", sheet_name=\"Schedule\")\ndf = df.dropna(subset=[\"Event\", \"Date\"]).fillna({\"Location\": \"\", \"Notes\": \"\"})\ndf[\"Date\"] = pd.to_datetime(df[\"Date\"]).dt.date\ndf[\"AllDay\"] = df[\"Start\"].isna()\n\ndef combine(day, t):\n    return None if pd.isna(t) else dt.datetime.combine(day, t)\n\ndf[\"StartAt\"] = [combine(d, t) for d, t in zip(df[\"Date\"], df[\"Start\"])]\ndf[\"EndAt\"] = [combine(d, t) for d, t in zip(df[\"Date\"], df[\"End\"])]\nprint(df[[\"ID\", \"Event\", \"AllDay\", \"StartAt\", \"EndAt\"]].to_string(index=False))\n","python",[14,98,99,115,128,135,165,215,236,257,262,275,297,302,342,375],{"__ignoreMap":42},[46,100,101,105,109,112],{"class":48,"line":49},[46,102,104],{"class":103},"s-kum","import",[46,106,108],{"class":107},"skGVy"," datetime ",[46,110,111],{"class":103},"as",[46,113,114],{"class":107}," dt\n",[46,116,118,120,123,125],{"class":48,"line":117},2,[46,119,104],{"class":103},[46,121,122],{"class":107}," pandas ",[46,124,111],{"class":103},[46,126,127],{"class":107}," pd\n",[46,129,131],{"class":48,"line":130},3,[46,132,134],{"emptyLinePlaceholder":133},true,"\n",[46,136,138,141,144,147,150,153,157,159,162],{"class":48,"line":137},4,[46,139,140],{"class":107},"df ",[46,142,143],{"class":103},"=",[46,145,146],{"class":107}," pd.read_excel(",[46,148,149],{"class":56},"\"schedule.xlsx\"",[46,151,152],{"class":107},", ",[46,154,156],{"class":155},"sa561","sheet_name",[46,158,143],{"class":103},[46,160,161],{"class":56},"\"Schedule\"",[46,163,164],{"class":107},")\n",[46,166,168,170,172,175,178,180,183,186,188,191,194,197,200,203,205,208,210,212],{"class":48,"line":167},5,[46,169,140],{"class":107},[46,171,143],{"class":103},[46,173,174],{"class":107}," df.dropna(",[46,176,177],{"class":155},"subset",[46,179,143],{"class":103},[46,181,182],{"class":107},"[",[46,184,185],{"class":56},"\"Event\"",[46,187,152],{"class":107},[46,189,190],{"class":56},"\"Date\"",[46,192,193],{"class":107},"]).fillna({",[46,195,196],{"class":56},"\"Location\"",[46,198,199],{"class":107},": ",[46,201,202],{"class":56},"\"\"",[46,204,152],{"class":107},[46,206,207],{"class":56},"\"Notes\"",[46,209,199],{"class":107},[46,211,202],{"class":56},[46,213,214],{"class":107},"})\n",[46,216,218,221,223,226,228,231,233],{"class":48,"line":217},6,[46,219,220],{"class":107},"df[",[46,222,190],{"class":56},[46,224,225],{"class":107},"] ",[46,227,143],{"class":103},[46,229,230],{"class":107}," pd.to_datetime(df[",[46,232,190],{"class":56},[46,234,235],{"class":107},"]).dt.date\n",[46,237,239,241,244,246,248,251,254],{"class":48,"line":238},7,[46,240,220],{"class":107},[46,242,243],{"class":56},"\"AllDay\"",[46,245,225],{"class":107},[46,247,143],{"class":103},[46,249,250],{"class":107}," df[",[46,252,253],{"class":56},"\"Start\"",[46,255,256],{"class":107},"].isna()\n",[46,258,260],{"class":48,"line":259},8,[46,261,134],{"emptyLinePlaceholder":133},[46,263,265,268,272],{"class":48,"line":264},9,[46,266,267],{"class":103},"def",[46,269,271],{"class":270},"s_Opv"," combine",[46,273,274],{"class":107},"(day, t):\n",[46,276,278,281,285,288,291,294],{"class":48,"line":277},10,[46,279,280],{"class":103},"    return",[46,282,284],{"class":283},"sP0c6"," None",[46,286,287],{"class":103}," if",[46,289,290],{"class":107}," pd.isna(t) ",[46,292,293],{"class":103},"else",[46,295,296],{"class":107}," dt.datetime.combine(day, t)\n",[46,298,300],{"class":48,"line":299},11,[46,301,134],{"emptyLinePlaceholder":133},[46,303,305,307,310,312,314,317,320,323,326,329,332,334,337,339],{"class":48,"line":304},12,[46,306,220],{"class":107},[46,308,309],{"class":56},"\"StartAt\"",[46,311,225],{"class":107},[46,313,143],{"class":103},[46,315,316],{"class":107}," [combine(d, t) ",[46,318,319],{"class":103},"for",[46,321,322],{"class":107}," d, t ",[46,324,325],{"class":103},"in",[46,327,328],{"class":283}," zip",[46,330,331],{"class":107},"(df[",[46,333,190],{"class":56},[46,335,336],{"class":107},"], df[",[46,338,253],{"class":56},[46,340,341],{"class":107},"])]\n",[46,343,345,347,350,352,354,356,358,360,362,364,366,368,370,373],{"class":48,"line":344},13,[46,346,220],{"class":107},[46,348,349],{"class":56},"\"EndAt\"",[46,351,225],{"class":107},[46,353,143],{"class":103},[46,355,316],{"class":107},[46,357,319],{"class":103},[46,359,322],{"class":107},[46,361,325],{"class":103},[46,363,328],{"class":283},[46,365,331],{"class":107},[46,367,190],{"class":56},[46,369,336],{"class":107},[46,371,372],{"class":56},"\"End\"",[46,374,341],{"class":107},[46,376,378,381,384,387,389,391,393,395,397,399,401,403,406,409,411,414],{"class":48,"line":377},14,[46,379,380],{"class":283},"print",[46,382,383],{"class":107},"(df[[",[46,385,386],{"class":56},"\"ID\"",[46,388,152],{"class":107},[46,390,185],{"class":56},[46,392,152],{"class":107},[46,394,243],{"class":56},[46,396,152],{"class":107},[46,398,309],{"class":56},[46,400,152],{"class":107},[46,402,349],{"class":56},[46,404,405],{"class":107},"]].to_string(",[46,407,408],{"class":155},"index",[46,410,143],{"class":103},[46,412,413],{"class":283},"False",[46,415,416],{"class":107},"))\n",[10,418,419,420,422,423,426,427,430],{},"Dropping rows without an event name or date removes the leftover row. The time columns come back as Python ",[14,421,90],{}," objects because the cells hold time values; if your sheet stores times as text such as ",[14,424,425],{},"\"09:30\"",", parse them first with ",[14,428,429],{},"pd.to_datetime(df[\"Start\"], format=\"%H:%M\").dt.time",". Rows without a start time are flagged as all-day.",[432,433,441,442,441,446,441,450,441,458,441,467,441,471,441,478,441,482,441,486,441,490,441,494,441,498,441,502,441,506,441,511,441,516,441,520,441,522,441,525,441,527,441,530,441,532,441,535,441,537,441,541,441,543,441,547,441,549,441,552,441,554,441,557,441,559,441,562,441,565,441,569,441,571,441,574,441,576,441,579,441,581,441,583,441,585,441,587,441,590,441,594,441,596,441,599,441,601,441,604,441,608,441,610,441,614,441,620,441,625,441,630,441,633,441,637,441,640,441,645,441,650,441,653,441,658,441,663,441,671],"svg",{"viewBox":434,"role":435,"ariaLabelledBy":436,"xmlns":439,"style":440},"59 0 641 192","img",[437,438],"ics-rows-t","ics-rows-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:641px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[443,444,445],"title",{"id":437},"Sheet rows become timed and all-day events",[447,448,449],"desc",{"id":438},"On the left the Excel schedule with date, start and end columns, two rows without times. On the right the calendar entries — two timed training sessions on Mondays from 09:30 to 12:00 and a month-end close shown as an all-day event.",[451,452],"rect",{"x":453,"y":454,"width":455,"height":456,"fill":457},"59","0","641","192","#ffffff",[451,459],{"x":460,"y":461,"width":462,"height":463,"rx":454,"fill":464,"stroke":465,"style":466},"76.0","40.0","32.0","24.0","#e7ebef","var(--line,#cdd5e6)","stroke-width:1px",[451,468],{"x":469,"y":461,"width":470,"height":463,"rx":454,"fill":464,"stroke":465,"style":466},"108.0","115.9",[472,473,477],"text",{"x":474,"y":475,"style":476},"165.9","57.0","font-size:11px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","A",[451,479],{"x":480,"y":461,"width":481,"height":463,"rx":454,"fill":464,"stroke":465,"style":466},"223.9","60.2",[472,483,485],{"x":484,"y":475,"style":476},"254.0","B",[451,487],{"x":488,"y":461,"width":489,"height":463,"rx":454,"fill":464,"stroke":465,"style":466},"284.1","56.8",[472,491,493],{"x":492,"y":475,"style":476},"312.5","C",[451,495],{"x":496,"y":461,"width":497,"height":463,"rx":454,"fill":464,"stroke":465,"style":466},"340.9","54.2",[472,499,501],{"x":500,"y":475,"style":476},"368.0","D",[451,503],{"x":460,"y":504,"width":462,"height":505,"rx":454,"fill":464,"stroke":465,"style":466},"64.0","28.0",[472,507,510],{"x":508,"y":509,"style":476},"92.0","83.0","1",[451,512],{"x":469,"y":504,"width":470,"height":505,"rx":454,"fill":513,"stroke":514,"style":515},"#f0f4ff","var(--brand,#5b5cf0)","stroke-width:1.5px",[472,517,519],{"x":474,"y":509,"style":518},"font-size:10.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Event",[451,521],{"x":480,"y":504,"width":481,"height":505,"rx":454,"fill":513,"stroke":514,"style":515},[472,523,524],{"x":484,"y":509,"style":518},"Date",[451,526],{"x":488,"y":504,"width":489,"height":505,"rx":454,"fill":513,"stroke":514,"style":515},[472,528,529],{"x":492,"y":509,"style":518},"Start",[451,531],{"x":496,"y":504,"width":497,"height":505,"rx":454,"fill":513,"stroke":514,"style":515},[472,533,534],{"x":500,"y":509,"style":518},"End",[451,536],{"x":460,"y":508,"width":462,"height":505,"rx":454,"fill":464,"stroke":465,"style":466},[472,538,540],{"x":508,"y":539,"style":476},"111.0","2",[451,542],{"x":469,"y":508,"width":470,"height":505,"rx":454,"fill":457,"stroke":465,"style":466},[472,544,546],{"x":474,"y":539,"style":545},"font-size:10.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","Training 1",[451,548],{"x":480,"y":508,"width":481,"height":505,"rx":454,"fill":457,"stroke":465,"style":466},[472,550,551],{"x":484,"y":539,"style":545},"12 Oct",[451,553],{"x":488,"y":508,"width":489,"height":505,"rx":454,"fill":457,"stroke":465,"style":466},[472,555,556],{"x":492,"y":539,"style":545},"09:30",[451,558],{"x":496,"y":508,"width":497,"height":505,"rx":454,"fill":457,"stroke":465,"style":466},[472,560,561],{"x":500,"y":539,"style":545},"12:00",[451,563],{"x":460,"y":564,"width":462,"height":505,"rx":454,"fill":464,"stroke":465,"style":466},"120.0",[472,566,568],{"x":508,"y":567,"style":476},"139.0","3",[451,570],{"x":469,"y":564,"width":470,"height":505,"rx":454,"fill":457,"stroke":465,"style":466},[472,572,573],{"x":474,"y":567,"style":545},"Training 2",[451,575],{"x":480,"y":564,"width":481,"height":505,"rx":454,"fill":457,"stroke":465,"style":466},[472,577,578],{"x":484,"y":567,"style":545},"19 Oct",[451,580],{"x":488,"y":564,"width":489,"height":505,"rx":454,"fill":457,"stroke":465,"style":466},[472,582,556],{"x":492,"y":567,"style":545},[451,584],{"x":496,"y":564,"width":497,"height":505,"rx":454,"fill":457,"stroke":465,"style":466},[472,586,561],{"x":500,"y":567,"style":545},[451,588],{"x":460,"y":589,"width":462,"height":505,"rx":454,"fill":464,"stroke":465,"style":466},"148.0",[472,591,593],{"x":508,"y":592,"style":476},"167.0","4",[451,595],{"x":469,"y":589,"width":470,"height":505,"rx":454,"fill":457,"stroke":465,"style":466},[472,597,598],{"x":474,"y":592,"style":545},"Month-end close",[451,600],{"x":480,"y":589,"width":481,"height":505,"rx":454,"fill":457,"stroke":465,"style":466},[472,602,603],{"x":484,"y":592,"style":545},"30 Oct",[451,605],{"x":488,"y":589,"width":489,"height":505,"rx":454,"fill":606,"stroke":607,"style":515},"#fdefd8","var(--gold,#b4740a)",[451,609],{"x":496,"y":589,"width":497,"height":505,"rx":454,"fill":606,"stroke":607,"style":515},[451,611],{"x":612,"y":461,"width":613,"height":505,"rx":454,"fill":457,"stroke":465,"style":466},"528.8","155.2",[472,615,619],{"x":616,"y":617,"style":618},"606.4","59.0","font-size:10.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","Calendar entry",[451,621],{"x":612,"y":622,"width":613,"height":505,"rx":454,"fill":623,"stroke":624,"style":515},"68.0","#d9f4f1","var(--teal,#0f9488)",[472,626,629],{"x":616,"y":627,"style":628},"87.0","font-size:10.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Mon 12 Oct 09:30–12:00",[451,631],{"x":612,"y":632,"width":613,"height":505,"rx":454,"fill":623,"stroke":624,"style":515},"96.0",[472,634,636],{"x":616,"y":635,"style":628},"115.0","Mon 19 Oct 09:30–12:00",[451,638],{"x":612,"y":639,"width":613,"height":505,"rx":454,"fill":606,"stroke":607,"style":515},"124.0",[472,641,644],{"x":616,"y":642,"style":643},"143.0","font-size:10.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Fri 30 Oct (all day)",[472,646,75],{"x":647,"y":648,"style":649},"235.6","29.0","font-size:12.5px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle",[472,651,652],{"x":616,"y":648,"style":649},"calendar",[48,654],{"x1":655,"y1":632,"x2":656,"y2":632,"stroke":514,"style":657},"405.1","514.8","stroke-width:2px",[659,660],"polygon",{"points":661,"fill":662},"518.8,96.0 509.8,101.0 509.8,91.0","#5b5cf0",[451,664],{"x":665,"y":666,"width":667,"height":668,"rx":669,"fill":457,"stroke":465,"style":670},"424.5","63.0","74.9","22.0","11","stroke-width:1.2px",[472,672,20],{"x":673,"y":674,"style":675},"462.0","78.0","font-size:11.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle",[32,677,679],{"id":678},"step-2-attach-the-time-zone","Step 2 — Attach the time zone",[10,681,682],{},"A timestamp without a zone is ambiguous: \"09:30\" means one thing in London and another for a colleague in New York who imports the same file. Attach the zone the schedule was written in, and let each calendar app convert it for its user:",[37,684,686],{"className":94,"code":685,"language":96,"meta":42,"style":42},"from zoneinfo import ZoneInfo\n\nTZ = ZoneInfo(\"Europe\u002FLondon\")\ndf[\"StartAt\"] = [s.replace(tzinfo=TZ) if pd.notna(s) else None for s in df[\"StartAt\"]]\ndf[\"EndAt\"] = [e.replace(tzinfo=TZ) if pd.notna(e) else None for e in df[\"EndAt\"]]\nprint(df.loc[df[\"ID\"] == \"MS-02\", \"StartAt\"].iloc[0].isoformat())\n",[14,687,688,701,705,721,769,812],{"__ignoreMap":42},[46,689,690,693,696,698],{"class":48,"line":49},[46,691,692],{"class":103},"from",[46,694,695],{"class":107}," zoneinfo ",[46,697,104],{"class":103},[46,699,700],{"class":107}," ZoneInfo\n",[46,702,703],{"class":48,"line":117},[46,704,134],{"emptyLinePlaceholder":133},[46,706,707,710,713,716,719],{"class":48,"line":130},[46,708,709],{"class":283},"TZ",[46,711,712],{"class":103}," =",[46,714,715],{"class":107}," ZoneInfo(",[46,717,718],{"class":56},"\"Europe\u002FLondon\"",[46,720,164],{"class":107},[46,722,723,725,727,729,731,734,737,739,741,744,747,750,752,754,757,760,762,764,766],{"class":48,"line":137},[46,724,220],{"class":107},[46,726,309],{"class":56},[46,728,225],{"class":107},[46,730,143],{"class":103},[46,732,733],{"class":107}," [s.replace(",[46,735,736],{"class":155},"tzinfo",[46,738,143],{"class":103},[46,740,709],{"class":283},[46,742,743],{"class":107},") ",[46,745,746],{"class":103},"if",[46,748,749],{"class":107}," pd.notna(s) ",[46,751,293],{"class":103},[46,753,284],{"class":283},[46,755,756],{"class":103}," for",[46,758,759],{"class":107}," s ",[46,761,325],{"class":103},[46,763,250],{"class":107},[46,765,309],{"class":56},[46,767,768],{"class":107},"]]\n",[46,770,771,773,775,777,779,782,784,786,788,790,792,795,797,799,801,804,806,808,810],{"class":48,"line":167},[46,772,220],{"class":107},[46,774,349],{"class":56},[46,776,225],{"class":107},[46,778,143],{"class":103},[46,780,781],{"class":107}," [e.replace(",[46,783,736],{"class":155},[46,785,143],{"class":103},[46,787,709],{"class":283},[46,789,743],{"class":107},[46,791,746],{"class":103},[46,793,794],{"class":107}," pd.notna(e) ",[46,796,293],{"class":103},[46,798,284],{"class":283},[46,800,756],{"class":103},[46,802,803],{"class":107}," e ",[46,805,325],{"class":103},[46,807,250],{"class":107},[46,809,349],{"class":56},[46,811,768],{"class":107},[46,813,814,816,819,821,823,826,829,831,833,836,838],{"class":48,"line":217},[46,815,380],{"class":283},[46,817,818],{"class":107},"(df.loc[df[",[46,820,386],{"class":56},[46,822,225],{"class":107},[46,824,825],{"class":103},"==",[46,827,828],{"class":56}," \"MS-02\"",[46,830,152],{"class":107},[46,832,309],{"class":56},[46,834,835],{"class":107},"].iloc[",[46,837,454],{"class":283},[46,839,840],{"class":107},"].isoformat())\n",[10,842,843,844,847,848,851,852,854],{},"The budget workshop on 3 November starts at ",[14,845,846],{},"14:00+00:00"," (GMT), while the October sessions are at ",[14,849,850],{},"+01:00"," (BST): the zone database knows that the clocks change on 25 October 2026, so a single zone name gives the right offset on both sides of the switch. Hard-coding an offset such as ",[14,853,850],{}," instead would put every November event an hour out.",[32,856,858],{"id":857},"step-3-write-the-events","Step 3 — Write the events",[37,860,862],{"className":94,"code":861,"language":96,"meta":42,"style":42},"import hashlib\nfrom icalendar import Calendar, Event\n\ndef stable_uid(row_id):\n    return hashlib.sha1(f\"schedule.xlsx\u002F{row_id}\".encode()).hexdigest()[:16] + \"@reports.example.com\"\n\ncal = Calendar()\ncal.add(\"prodid\", \"-\u002F\u002FExample Reports\u002F\u002FSchedule export\u002F\u002FEN\")\ncal.add(\"version\", \"2.0\")\nstamp = dt.datetime.now(dt.timezone.utc)\nfor row in df.itertuples(index=False):\n    ev = Event()\n    ev.add(\"uid\", stable_uid(row.ID))\n    ev.add(\"dtstamp\", stamp)\n    ev.add(\"summary\", row.Event)\n    if row.AllDay:\n        days = 2 if row.ID == \"HD-01\" else 1\n        ev.add(\"dtstart\", row.Date)\n        ev.add(\"dtend\", row.Date + dt.timedelta(days=days))\n    else:\n        ev.add(\"dtstart\", row.StartAt)\n        ev.add(\"dtend\", row.EndAt)\n    if row.Location:\n        ev.add(\"location\", row.Location)\n    if row.Notes:\n        ev.add(\"description\", row.Notes)\n    cal.add_component(ev)\ncal.add_missing_timezones()\nwith open(\"schedule.ics\", \"wb\") as fh:\n    fh.write(cal.to_ical())\nprint(cal.to_ical().decode().count(\"BEGIN:VEVENT\"), \"events written\")\n",[14,863,864,871,883,887,897,937,941,951,966,980,990,1011,1021,1037,1047,1058,1067,1097,1109,1133,1142,1152,1162,1170,1181,1189,1200,1206,1212,1239,1245],{"__ignoreMap":42},[46,865,866,868],{"class":48,"line":49},[46,867,104],{"class":103},[46,869,870],{"class":107}," hashlib\n",[46,872,873,875,878,880],{"class":48,"line":117},[46,874,692],{"class":103},[46,876,877],{"class":107}," icalendar ",[46,879,104],{"class":103},[46,881,882],{"class":107}," Calendar, Event\n",[46,884,885],{"class":48,"line":130},[46,886,134],{"emptyLinePlaceholder":133},[46,888,889,891,894],{"class":48,"line":137},[46,890,267],{"class":103},[46,892,893],{"class":270}," stable_uid",[46,895,896],{"class":107},"(row_id):\n",[46,898,899,901,904,907,910,914,917,920,923,926,929,931,934],{"class":48,"line":167},[46,900,280],{"class":103},[46,902,903],{"class":107}," hashlib.sha1(",[46,905,906],{"class":103},"f",[46,908,909],{"class":56},"\"schedule.xlsx\u002F",[46,911,913],{"class":912},"sSjpA","{",[46,915,916],{"class":107},"row_id",[46,918,919],{"class":912},"}",[46,921,922],{"class":56},"\"",[46,924,925],{"class":107},".encode()).hexdigest()[:",[46,927,928],{"class":283},"16",[46,930,225],{"class":107},[46,932,933],{"class":103},"+",[46,935,936],{"class":56}," \"@reports.example.com\"\n",[46,938,939],{"class":48,"line":217},[46,940,134],{"emptyLinePlaceholder":133},[46,942,943,946,948],{"class":48,"line":238},[46,944,945],{"class":107},"cal ",[46,947,143],{"class":103},[46,949,950],{"class":107}," Calendar()\n",[46,952,953,956,959,961,964],{"class":48,"line":259},[46,954,955],{"class":107},"cal.add(",[46,957,958],{"class":56},"\"prodid\"",[46,960,152],{"class":107},[46,962,963],{"class":56},"\"-\u002F\u002FExample Reports\u002F\u002FSchedule export\u002F\u002FEN\"",[46,965,164],{"class":107},[46,967,968,970,973,975,978],{"class":48,"line":264},[46,969,955],{"class":107},[46,971,972],{"class":56},"\"version\"",[46,974,152],{"class":107},[46,976,977],{"class":56},"\"2.0\"",[46,979,164],{"class":107},[46,981,982,985,987],{"class":48,"line":277},[46,983,984],{"class":107},"stamp ",[46,986,143],{"class":103},[46,988,989],{"class":107}," dt.datetime.now(dt.timezone.utc)\n",[46,991,992,994,997,999,1002,1004,1006,1008],{"class":48,"line":299},[46,993,319],{"class":103},[46,995,996],{"class":107}," row ",[46,998,325],{"class":103},[46,1000,1001],{"class":107}," df.itertuples(",[46,1003,408],{"class":155},[46,1005,143],{"class":103},[46,1007,413],{"class":283},[46,1009,1010],{"class":107},"):\n",[46,1012,1013,1016,1018],{"class":48,"line":304},[46,1014,1015],{"class":107},"    ev ",[46,1017,143],{"class":103},[46,1019,1020],{"class":107}," Event()\n",[46,1022,1023,1026,1029,1032,1035],{"class":48,"line":344},[46,1024,1025],{"class":107},"    ev.add(",[46,1027,1028],{"class":56},"\"uid\"",[46,1030,1031],{"class":107},", stable_uid(row.",[46,1033,1034],{"class":283},"ID",[46,1036,416],{"class":107},[46,1038,1039,1041,1044],{"class":48,"line":377},[46,1040,1025],{"class":107},[46,1042,1043],{"class":56},"\"dtstamp\"",[46,1045,1046],{"class":107},", stamp)\n",[46,1048,1050,1052,1055],{"class":48,"line":1049},15,[46,1051,1025],{"class":107},[46,1053,1054],{"class":56},"\"summary\"",[46,1056,1057],{"class":107},", row.Event)\n",[46,1059,1061,1064],{"class":48,"line":1060},16,[46,1062,1063],{"class":103},"    if",[46,1065,1066],{"class":107}," row.AllDay:\n",[46,1068,1070,1073,1075,1078,1080,1083,1085,1088,1091,1094],{"class":48,"line":1069},17,[46,1071,1072],{"class":107},"        days ",[46,1074,143],{"class":103},[46,1076,1077],{"class":283}," 2",[46,1079,287],{"class":103},[46,1081,1082],{"class":107}," row.",[46,1084,1034],{"class":283},[46,1086,1087],{"class":103}," ==",[46,1089,1090],{"class":56}," \"HD-01\"",[46,1092,1093],{"class":103}," else",[46,1095,1096],{"class":283}," 1\n",[46,1098,1100,1103,1106],{"class":48,"line":1099},18,[46,1101,1102],{"class":107},"        ev.add(",[46,1104,1105],{"class":56},"\"dtstart\"",[46,1107,1108],{"class":107},", row.Date)\n",[46,1110,1112,1114,1117,1120,1122,1125,1128,1130],{"class":48,"line":1111},19,[46,1113,1102],{"class":107},[46,1115,1116],{"class":56},"\"dtend\"",[46,1118,1119],{"class":107},", row.Date ",[46,1121,933],{"class":103},[46,1123,1124],{"class":107}," dt.timedelta(",[46,1126,1127],{"class":155},"days",[46,1129,143],{"class":103},[46,1131,1132],{"class":107},"days))\n",[46,1134,1136,1139],{"class":48,"line":1135},20,[46,1137,1138],{"class":103},"    else",[46,1140,1141],{"class":107},":\n",[46,1143,1145,1147,1149],{"class":48,"line":1144},21,[46,1146,1102],{"class":107},[46,1148,1105],{"class":56},[46,1150,1151],{"class":107},", row.StartAt)\n",[46,1153,1155,1157,1159],{"class":48,"line":1154},22,[46,1156,1102],{"class":107},[46,1158,1116],{"class":56},[46,1160,1161],{"class":107},", row.EndAt)\n",[46,1163,1165,1167],{"class":48,"line":1164},23,[46,1166,1063],{"class":103},[46,1168,1169],{"class":107}," row.Location:\n",[46,1171,1173,1175,1178],{"class":48,"line":1172},24,[46,1174,1102],{"class":107},[46,1176,1177],{"class":56},"\"location\"",[46,1179,1180],{"class":107},", row.Location)\n",[46,1182,1184,1186],{"class":48,"line":1183},25,[46,1185,1063],{"class":103},[46,1187,1188],{"class":107}," row.Notes:\n",[46,1190,1192,1194,1197],{"class":48,"line":1191},26,[46,1193,1102],{"class":107},[46,1195,1196],{"class":56},"\"description\"",[46,1198,1199],{"class":107},", row.Notes)\n",[46,1201,1203],{"class":48,"line":1202},27,[46,1204,1205],{"class":107},"    cal.add_component(ev)\n",[46,1207,1209],{"class":48,"line":1208},28,[46,1210,1211],{"class":107},"cal.add_missing_timezones()\n",[46,1213,1215,1218,1221,1224,1227,1229,1232,1234,1236],{"class":48,"line":1214},29,[46,1216,1217],{"class":103},"with",[46,1219,1220],{"class":283}," open",[46,1222,1223],{"class":107},"(",[46,1225,1226],{"class":56},"\"schedule.ics\"",[46,1228,152],{"class":107},[46,1230,1231],{"class":56},"\"wb\"",[46,1233,743],{"class":107},[46,1235,111],{"class":103},[46,1237,1238],{"class":107}," fh:\n",[46,1240,1242],{"class":48,"line":1241},30,[46,1243,1244],{"class":107},"    fh.write(cal.to_ical())\n",[46,1246,1248,1250,1253,1256,1259,1262],{"class":48,"line":1247},31,[46,1249,380],{"class":283},[46,1251,1252],{"class":107},"(cal.to_ical().decode().count(",[46,1254,1255],{"class":56},"\"BEGIN:VEVENT\"",[46,1257,1258],{"class":107},"), ",[46,1260,1261],{"class":56},"\"events written\"",[46,1263,164],{"class":107},[10,1265,1266],{},"Three details in this loop decide whether the file imports cleanly:",[1268,1269,1270,1285,1295],"ul",{},[1271,1272,1273,1277,1278,87,1281,1284],"li",{},[1274,1275,1276],"strong",{},"All-day end dates are exclusive."," An all-day event on 30 October has ",[14,1279,1280],{},"DTSTART;VALUE=DATE:20261030",[14,1282,1283],{},"DTEND;VALUE=DATE:20261031",". Setting the end to the same day makes some apps show nothing at all. The two-day office closure ends on the 26th to cover the 24th and 25th; in a real sheet, take the number of days from an end-date column.",[1271,1286,1287,1290,1291,1294],{},[1274,1288,1289],{},"Stable UIDs."," The ",[14,1292,1293],{},"UID"," is derived from the row's ID, not generated randomly, so exporting again after an edit produces the same identifier. Calendar apps treat an event with a known UID as an update to the existing entry rather than a new one.",[1271,1296,1297,1300,1301,1304,1305,1308,1309,1312],{},[1274,1298,1299],{},"Time zone definitions."," ",[14,1302,1303],{},"add_missing_timezones()"," adds a ",[14,1306,1307],{},"VTIMEZONE"," block for Europe\u002FLondon, which the standard requires whenever an event uses ",[14,1310,1311],{},"TZID",". Without it Google Calendar usually guesses correctly and stricter clients may not.",[432,1314,441,1320,441,1323,441,1326,441,1330,441,1338,441,1343,441,1358,441,1360,441,1375,441,1378,441,1387,441,1390,441,1433,441,1436,441,1474,441,1478,441,1482,441,1484,441,1514,441,1517,441,1527,441,1531,441,1535,441,1537,441,1570,441,1574,441,1583,441,1589,441,1592,441,1595,441,1599,441,1602,441,1605,441,1607,441,1612,441,1616,441,1619,441,1622,441,1624,441,1628],{"viewBox":1315,"role":435,"ariaLabelledBy":1316,"xmlns":439,"style":1319},"4 0 752 290",[1317,1318],"ics-event-t","ics-event-d","width:100%;max-width:752px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[443,1321,1322],{"id":1317},"One exported event in the .ics file",[447,1324,1325],{"id":1318},"The text of the budget workshop event. The summary, start and end with the Europe\u002FLondon time zone, a stable UID derived from the row ID, and a description whose comma is escaped with a backslash.",[451,1327],{"x":593,"y":454,"width":1328,"height":1329,"fill":457},"752","290",[451,1331],{"x":1332,"y":1333,"width":1334,"height":1335,"rx":1336,"fill":1337,"stroke":465,"style":515},"20.0","16.0","720.0","192.0","12","#f0f2f5",[472,1339,510],{"x":1340,"y":1341,"style":1342},"60.0","47.0","font-size:11px;font-weight:400;fill:var(--muted,#5b6780);text-anchor:middle;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace",[472,1344,1347,1352,1355],{"x":1345,"y":1341,"style":1346},"82.0","font-size:12.5px;text-anchor:start;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace",[1348,1349,1351],"tspan",{"style":1350},"fill:var(--text,#172033);font-weight:400","BEGIN",[1348,1353,1354],{"style":1350},":",[1348,1356,1357],{"style":1350},"VEVENT",[472,1359,540],{"x":1340,"y":622,"style":1342},[472,1361,1362,1365,1367,1370,1372],{"x":1345,"y":622,"style":1346},[1348,1363,1364],{"style":1350},"SUMMARY",[1348,1366,1354],{"style":1350},[1348,1368,1369],{"style":1350},"Budget",[1348,1371,1300],{"style":1350},[1348,1373,1374],{"style":1350},"workshop",[472,1376,568],{"x":1340,"y":1377,"style":1342},"89.0",[1379,1380],"circle",{"cx":1381,"cy":1382,"r":1383,"fill":1384,"stroke":1385,"style":1386},"36.0","84.5","11.0","#4338ca","none","stroke-width:0px",[472,1388,510],{"x":1381,"y":1377,"style":1389},"font-size:12px;font-weight:700;fill:#ffffff;text-anchor:middle",[472,1391,1392,1395,1398,1400,1402,1405,1408,1411,1413,1415,1417,1419,1422,1424,1426,1428,1430],{"x":1345,"y":1377,"style":1346},[1348,1393,1394],{"style":1350},"DTSTART",[1348,1396,1397],{"style":1350},";",[1348,1399,1311],{"style":1350},[1348,1401,143],{"style":1350},[1348,1403,1404],{"style":1350},"Europe",[1348,1406,1407],{"style":1350},"\u002F",[1348,1409,1410],{"style":1350},"London",[1348,1412,1354],{"style":1350},[1348,1414,540],{"style":1350},[1348,1416,454],{"style":1350},[1348,1418,540],{"style":1350},[1348,1420,1421],{"style":1350},"6",[1348,1423,510],{"style":1350},[1348,1425,510],{"style":1350},[1348,1427,454],{"style":1350},[1348,1429,568],{"style":1350},[1348,1431,1432],{"style":1350},"T140000",[472,1434,593],{"x":1340,"y":1435,"style":1342},"110.0",[472,1437,1438,1441,1443,1445,1447,1449,1451,1453,1455,1457,1459,1461,1463,1465,1467,1469,1471],{"x":1345,"y":1435,"style":1346},[1348,1439,1440],{"style":1350},"DTEND",[1348,1442,1397],{"style":1350},[1348,1444,1311],{"style":1350},[1348,1446,143],{"style":1350},[1348,1448,1404],{"style":1350},[1348,1450,1407],{"style":1350},[1348,1452,1410],{"style":1350},[1348,1454,1354],{"style":1350},[1348,1456,540],{"style":1350},[1348,1458,454],{"style":1350},[1348,1460,540],{"style":1350},[1348,1462,1421],{"style":1350},[1348,1464,510],{"style":1350},[1348,1466,510],{"style":1350},[1348,1468,454],{"style":1350},[1348,1470,568],{"style":1350},[1348,1472,1473],{"style":1350},"T163000",[472,1475,1477],{"x":1340,"y":1476,"style":1342},"131.0","5",[1379,1479],{"cx":1381,"cy":1480,"r":1383,"fill":1481,"stroke":1385,"style":1386},"126.5","#0b6157",[472,1483,540],{"x":1381,"y":1476,"style":1389},[472,1485,1486,1488,1490,1492,1495,1498,1501,1504,1506,1509,1511],{"x":1345,"y":1476,"style":1346},[1348,1487,1293],{"style":1350},[1348,1489,1354],{"style":1350},[1348,1491,593],{"style":1350},[1348,1493,1494],{"style":1350},"f1c",[1348,1496,1497],{"style":1350},"…",[1348,1499,1500],{"style":1350},"@",[1348,1502,1503],{"style":1350},"reports",[1348,1505,30],{"style":1350},[1348,1507,1508],{"style":1350},"example",[1348,1510,30],{"style":1350},[1348,1512,1513],{"style":1350},"com",[472,1515,1421],{"x":1340,"y":1516,"style":1342},"152.0",[472,1518,1519,1522,1524],{"x":1345,"y":1516,"style":1346},[1348,1520,1521],{"style":1350},"LOCATION",[1348,1523,1354],{"style":1350},[1348,1525,1526],{"style":1350},"Teams",[472,1528,1530],{"x":1340,"y":1529,"style":1342},"173.0","7",[1379,1532],{"cx":1381,"cy":1533,"r":1383,"fill":1534,"stroke":1385,"style":1386},"168.5","#8a5808",[472,1536,568],{"x":1381,"y":1529,"style":1389},[472,1538,1539,1542,1544,1547,1549,1551,1554,1556,1559,1562,1565,1567],{"x":1345,"y":1529,"style":1346},[1348,1540,1541],{"style":1350},"DESCRIPTION",[1348,1543,1354],{"style":1350},[1348,1545,1546],{"style":1350},"Agenda",[1348,1548,1354],{"style":1350},[1348,1550,1300],{"style":1350},[1348,1552,1553],{"style":1350},"Q1",[1348,1555,1300],{"style":1350},[1348,1557,1558],{"style":1350},"targets",[1348,1560,1561],{"style":1350},"\\",[1348,1563,1564],{"style":1350},",",[1348,1566,1300],{"style":1350},[1348,1568,1569],{"style":1350},"headcount",[472,1571,1573],{"x":1340,"y":1572,"style":1342},"194.0","8",[472,1575,1576,1579,1581],{"x":1345,"y":1572,"style":1346},[1348,1577,1578],{"style":1350},"END",[1348,1580,1354],{"style":1350},[1348,1582,1357],{"style":1350},[451,1584],{"x":1332,"y":1585,"width":1586,"height":1587,"rx":1588,"fill":513,"stroke":514,"style":515},"222.0","230.7","52.0","10",[1379,1590],{"cx":461,"cy":1591,"r":1383,"fill":1384,"stroke":1385,"style":1386},"248.0",[472,1593,510],{"x":461,"y":1594,"style":1389},"252.5",[472,1596,1598],{"x":1340,"y":1594,"style":1597},"font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:start","zone name, not a fixed offset",[451,1600],{"x":1601,"y":1585,"width":1586,"height":1587,"rx":1588,"fill":623,"stroke":624,"style":515},"264.7",[1379,1603],{"cx":1604,"cy":1591,"r":1383,"fill":1481,"stroke":1385,"style":1386},"284.7",[472,1606,540],{"x":1604,"y":1594,"style":1389},[472,1608,1611],{"x":1609,"y":1610,"style":1597},"304.7","244.0","same UID on every export =",[472,1613,1615],{"x":1609,"y":1614,"style":1597},"261.0","updates",[451,1617],{"x":1618,"y":1585,"width":1586,"height":1587,"rx":1588,"fill":606,"stroke":607,"style":515},"509.3",[1379,1620],{"cx":1621,"cy":1591,"r":1383,"fill":1534,"stroke":1385,"style":1386},"529.3",[472,1623,568],{"x":1621,"y":1594,"style":1389},[472,1625,1627],{"x":1626,"y":1610,"style":1597},"549.3","commas and semicolons are",[472,1629,1630],{"x":1626,"y":1614,"style":1597},"escaped",[32,1632,1634],{"id":1633},"step-4-read-the-file-back-to-check-it","Step 4 — Read the file back to check it",[10,1636,1637],{},"Parsing the output with the same library catches mistakes before anyone imports it:",[37,1639,1641],{"className":94,"code":1640,"language":96,"meta":42,"style":42},"from pathlib import Path\n\ncheck = Calendar.from_ical(Path(\"schedule.ics\").read_bytes())\nfor ev in check.walk(\"VEVENT\"):\n    start = ev.decoded(\"dtstart\")\n    kind = \"all day\" if not isinstance(start, dt.datetime) else start.strftime(\"%H:%M %Z\")\n    print(f\"{str(ev['summary']):35} {start:%d %b} {kind:10} {str(ev.get('description', ''))[:30]}\")\n",[14,1642,1643,1655,1659,1674,1691,1705,1736],{"__ignoreMap":42},[46,1644,1645,1647,1650,1652],{"class":48,"line":49},[46,1646,692],{"class":103},[46,1648,1649],{"class":107}," pathlib ",[46,1651,104],{"class":103},[46,1653,1654],{"class":107}," Path\n",[46,1656,1657],{"class":48,"line":117},[46,1658,134],{"emptyLinePlaceholder":133},[46,1660,1661,1664,1666,1669,1671],{"class":48,"line":130},[46,1662,1663],{"class":107},"check ",[46,1665,143],{"class":103},[46,1667,1668],{"class":107}," Calendar.from_ical(Path(",[46,1670,1226],{"class":56},[46,1672,1673],{"class":107},").read_bytes())\n",[46,1675,1676,1678,1681,1683,1686,1689],{"class":48,"line":137},[46,1677,319],{"class":103},[46,1679,1680],{"class":107}," ev ",[46,1682,325],{"class":103},[46,1684,1685],{"class":107}," check.walk(",[46,1687,1688],{"class":56},"\"VEVENT\"",[46,1690,1010],{"class":107},[46,1692,1693,1696,1698,1701,1703],{"class":48,"line":167},[46,1694,1695],{"class":107},"    start ",[46,1697,143],{"class":103},[46,1699,1700],{"class":107}," ev.decoded(",[46,1702,1105],{"class":56},[46,1704,164],{"class":107},[46,1706,1707,1710,1712,1715,1717,1720,1723,1726,1728,1731,1734],{"class":48,"line":217},[46,1708,1709],{"class":107},"    kind ",[46,1711,143],{"class":103},[46,1713,1714],{"class":56}," \"all day\"",[46,1716,287],{"class":103},[46,1718,1719],{"class":103}," not",[46,1721,1722],{"class":283}," isinstance",[46,1724,1725],{"class":107},"(start, dt.datetime) ",[46,1727,293],{"class":103},[46,1729,1730],{"class":107}," start.strftime(",[46,1732,1733],{"class":56},"\"%H:%M %Z\"",[46,1735,164],{"class":107},[46,1737,1738,1741,1743,1745,1747,1749,1752,1755,1758,1761,1764,1766,1769,1772,1774,1777,1780,1782,1785,1787,1789,1792,1795,1797,1799,1801,1804,1807,1809,1812,1815,1818,1821,1823,1825],{"class":48,"line":238},[46,1739,1740],{"class":283},"    print",[46,1742,1223],{"class":107},[46,1744,906],{"class":103},[46,1746,922],{"class":56},[46,1748,913],{"class":912},[46,1750,1751],{"class":283},"str",[46,1753,1754],{"class":107},"(ev[",[46,1756,1757],{"class":56},"'summary'",[46,1759,1760],{"class":107},"])",[46,1762,1763],{"class":103},":35",[46,1765,919],{"class":912},[46,1767,1768],{"class":912}," {",[46,1770,1771],{"class":107},"start",[46,1773,1354],{"class":103},[46,1775,1776],{"class":107},"%",[46,1778,1779],{"class":103},"d ",[46,1781,1776],{"class":107},[46,1783,1784],{"class":103},"b",[46,1786,919],{"class":912},[46,1788,1768],{"class":912},[46,1790,1791],{"class":107},"kind",[46,1793,1794],{"class":103},":10",[46,1796,919],{"class":912},[46,1798,1768],{"class":912},[46,1800,1751],{"class":283},[46,1802,1803],{"class":107},"(ev.get(",[46,1805,1806],{"class":56},"'description'",[46,1808,152],{"class":107},[46,1810,1811],{"class":56},"''",[46,1813,1814],{"class":107},"))[:",[46,1816,1817],{"class":283},"30",[46,1819,1820],{"class":107},"]",[46,1822,919],{"class":912},[46,1824,922],{"class":56},[46,1826,164],{"class":107},[10,1828,1829,1830,1833,1834,1837,1838,1840],{},"Every event comes back with its original text — ",[14,1831,1832],{},"Bring a laptop; Python 3.12+"," included, even though the file stores it as ",[14,1835,1836],{},"Bring a laptop\\; Python 3.12+",". The library escapes commas, semicolons and backslashes and folds long lines at 75 octets, the two rules hand-written ",[14,1839,16],{}," files most often break. Writing the file with f-strings is tempting; it works until the first note with a comma in it.",[32,1842,1844],{"id":1843},"step-5-one-file-per-person","Step 5 — One file per person",[10,1846,1847],{},"Sending each owner only their own events is a group-by away:",[37,1849,1851],{"className":94,"code":1850,"language":96,"meta":42,"style":42},"out = Path(\"calendars\")\nout.mkdir(exist_ok=True)\nfor owner, group in df.groupby(\"Owner\"):\n    personal = Calendar()\n    personal.add(\"prodid\", \"-\u002F\u002FExample Reports\u002F\u002FSchedule export\u002F\u002FEN\")\n    personal.add(\"version\", \"2.0\")\n    for ev in check.walk(\"VEVENT\"):\n        if ev[\"uid\"] in {stable_uid(i) for i in group[\"ID\"]}:\n            personal.add_component(ev)\n    personal.add_missing_timezones()\n    (out \u002F f\"{owner.lower()}.ics\").write_bytes(personal.to_ical())\nprint(sorted(p.name for p in out.iterdir()))\n",[14,1852,1853,1868,1883,1900,1909,1922,1934,1949,1981,1986,1991,2016],{"__ignoreMap":42},[46,1854,1855,1858,1860,1863,1866],{"class":48,"line":49},[46,1856,1857],{"class":107},"out ",[46,1859,143],{"class":103},[46,1861,1862],{"class":107}," Path(",[46,1864,1865],{"class":56},"\"calendars\"",[46,1867,164],{"class":107},[46,1869,1870,1873,1876,1878,1881],{"class":48,"line":117},[46,1871,1872],{"class":107},"out.mkdir(",[46,1874,1875],{"class":155},"exist_ok",[46,1877,143],{"class":103},[46,1879,1880],{"class":283},"True",[46,1882,164],{"class":107},[46,1884,1885,1887,1890,1892,1895,1898],{"class":48,"line":130},[46,1886,319],{"class":103},[46,1888,1889],{"class":107}," owner, group ",[46,1891,325],{"class":103},[46,1893,1894],{"class":107}," df.groupby(",[46,1896,1897],{"class":56},"\"Owner\"",[46,1899,1010],{"class":107},[46,1901,1902,1905,1907],{"class":48,"line":137},[46,1903,1904],{"class":107},"    personal ",[46,1906,143],{"class":103},[46,1908,950],{"class":107},[46,1910,1911,1914,1916,1918,1920],{"class":48,"line":167},[46,1912,1913],{"class":107},"    personal.add(",[46,1915,958],{"class":56},[46,1917,152],{"class":107},[46,1919,963],{"class":56},[46,1921,164],{"class":107},[46,1923,1924,1926,1928,1930,1932],{"class":48,"line":217},[46,1925,1913],{"class":107},[46,1927,972],{"class":56},[46,1929,152],{"class":107},[46,1931,977],{"class":56},[46,1933,164],{"class":107},[46,1935,1936,1939,1941,1943,1945,1947],{"class":48,"line":238},[46,1937,1938],{"class":103},"    for",[46,1940,1680],{"class":107},[46,1942,325],{"class":103},[46,1944,1685],{"class":107},[46,1946,1688],{"class":56},[46,1948,1010],{"class":107},[46,1950,1951,1954,1957,1959,1961,1963,1966,1968,1971,1973,1976,1978],{"class":48,"line":259},[46,1952,1953],{"class":103},"        if",[46,1955,1956],{"class":107}," ev[",[46,1958,1028],{"class":56},[46,1960,225],{"class":107},[46,1962,325],{"class":103},[46,1964,1965],{"class":107}," {stable_uid(i) ",[46,1967,319],{"class":103},[46,1969,1970],{"class":107}," i ",[46,1972,325],{"class":103},[46,1974,1975],{"class":107}," group[",[46,1977,386],{"class":56},[46,1979,1980],{"class":107},"]}:\n",[46,1982,1983],{"class":48,"line":264},[46,1984,1985],{"class":107},"            personal.add_component(ev)\n",[46,1987,1988],{"class":48,"line":277},[46,1989,1990],{"class":107},"    personal.add_missing_timezones()\n",[46,1992,1993,1996,1998,2001,2003,2005,2008,2010,2013],{"class":48,"line":299},[46,1994,1995],{"class":107},"    (out ",[46,1997,1407],{"class":103},[46,1999,2000],{"class":103}," f",[46,2002,922],{"class":56},[46,2004,913],{"class":912},[46,2006,2007],{"class":107},"owner.lower()",[46,2009,919],{"class":912},[46,2011,2012],{"class":56},".ics\"",[46,2014,2015],{"class":107},").write_bytes(personal.to_ical())\n",[46,2017,2018,2020,2022,2025,2028,2030,2033,2035],{"class":48,"line":304},[46,2019,380],{"class":283},[46,2021,1223],{"class":107},[46,2023,2024],{"class":283},"sorted",[46,2026,2027],{"class":107},"(p.name ",[46,2029,319],{"class":103},[46,2031,2032],{"class":107}," p ",[46,2034,325],{"class":103},[46,2036,2037],{"class":107}," out.iterdir()))\n",[10,2039,2040,2041,2045,2046,2049],{},"Each file can be attached to an email — see ",[26,2042,2044],{"href":2043},"\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Fsend-each-recipient-their-own-excel-report\u002F","Send Each Recipient Their Own Excel Report"," for the mailing loop. Use the MIME type ",[14,2047,2048],{},"text\u002Fcalendar"," for the attachment so mail clients offer to add the events.",[432,2051,441,2056,441,2059,441,2062,441,2065,441,2070,441,2080,441,2085,441,2090,441,2094,441,2101,441,2107,441,2112,441,2115,441,2118,441,2120,441,2126,441,2130,441,2134,441,2137,441,2140,441,2143,441,2148,441,2152,441,2156,441,2159],{"viewBox":2052,"role":435,"ariaLabelledBy":2053,"xmlns":439,"style":1319},"4 0 752 332",[2054,2055],"ics-owners-t","ics-owners-d",[443,2057,2058],{"id":2054},"One schedule, one calendar file per owner",[447,2060,2061],{"id":2055},"schedule.xlsx feeds schedule.ics with every event, plus adaeze.ics with the two training sessions and the office closure, and jonas.ics with the month-end close and the budget workshop.",[451,2063],{"x":593,"y":454,"width":1328,"height":2064,"fill":457},"332",[451,2066],{"x":1332,"y":2067,"width":2068,"height":2069,"rx":1336,"fill":513,"stroke":514,"style":657},"119.5","220.0","93.0",[2071,2072,2074],"g",{"transform":2073},"translate(118.0,132.5) scale(1.000)",[2075,2076],"path",{"d":2077,"fill":1385,"stroke":2078,"style":2079},"M6 3h9l4 4v14H6z M15 3v4h4 M9.5 11.5l4.5 6 M14 11.5l-4.5 6","var(--brand-strong,#4338ca)","stroke-width:1.80px;stroke-linecap:round;stroke-linejoin:round",[472,2081,75],{"x":2082,"y":2083,"style":2084},"130.0","179.0","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle",[472,2086,2089],{"x":2082,"y":2087,"style":2088},"198.5","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","5 events",[451,2091],{"x":2092,"y":1333,"width":2093,"height":508,"rx":1336,"fill":464,"stroke":465,"style":657},"490.0","250.0",[2071,2095,2097],{"transform":2096},"translate(603.0,29.0) scale(1.000)",[2075,2098],{"d":2099,"fill":1385,"stroke":2100,"style":2079},"M4 5h16v15H4z M4 10h16 M8 3v4 M16 3v4","var(--text,#172033)",[472,2102,2106],{"x":2103,"y":2104,"style":2105},"615.0","75.0","font-size:13px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","schedule.ics",[472,2108,2111],{"x":2103,"y":2109,"style":2110},"94.0","font-size:11px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","everything",[2075,2113],{"d":2114,"fill":1385,"stroke":514,"style":657},"M244.0,166.0 C364.0,166.0 364.0,62.0 480.0,62.0",[659,2116],{"points":2117,"fill":662},"484.0,62.0 475.0,67.0 475.0,57.0",[451,2119],{"x":2092,"y":564,"width":2093,"height":508,"rx":1336,"fill":623,"stroke":624,"style":657},[2071,2121,2123],{"transform":2122},"translate(603.0,133.0) scale(1.000)",[2075,2124],{"d":2099,"fill":1385,"stroke":2125,"style":2079},"var(--teal-ink,#0b6157)",[472,2127,2129],{"x":2103,"y":2083,"style":2128},"font-size:13px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","adaeze.ics",[472,2131,2133],{"x":2103,"y":2132,"style":2110},"198.0","2 trainings, office closed",[2075,2135],{"d":2136,"fill":1385,"stroke":514,"style":657},"M244.0,166.0 C364.0,166.0 364.0,166.0 480.0,166.0",[659,2138],{"points":2139,"fill":662},"484.0,166.0 475.0,171.0 475.0,161.0",[451,2141],{"x":2092,"y":2142,"width":2093,"height":508,"rx":1336,"fill":623,"stroke":624,"style":657},"224.0",[2071,2144,2146],{"transform":2145},"translate(603.0,237.0) scale(1.000)",[2075,2147],{"d":2099,"fill":1385,"stroke":2125,"style":2079},[472,2149,2151],{"x":2103,"y":2150,"style":2128},"283.0","jonas.ics",[472,2153,2155],{"x":2103,"y":2154,"style":2110},"302.0","month-end close, workshop",[2075,2157],{"d":2158,"fill":1385,"stroke":514,"style":657},"M244.0,166.0 C364.0,166.0 364.0,270.0 480.0,270.0",[659,2160],{"points":2161,"fill":662},"484.0,270.0 475.0,275.0 475.0,265.0",[32,2163,2165],{"id":2164},"step-6-add-a-reminder","Step 6 — Add a reminder",[10,2167,2168,2169,2172],{},"People import calendar files so that their calendar reminds them. A ",[14,2170,2171],{},"VALARM"," component inside each event asks the app to show a notification a set time before the start:",[37,2174,2176],{"className":94,"code":2175,"language":96,"meta":42,"style":42},"from icalendar import Alarm\n\nfor ev in check.walk(\"VEVENT\"):\n    if isinstance(ev.decoded(\"dtstart\"), dt.datetime):\n        alarm = Alarm()\n        alarm.add(\"action\", \"DISPLAY\")\n        alarm.add(\"description\", f\"Reminder: {ev['summary']}\")\n        alarm.add(\"trigger\", dt.timedelta(minutes=-30))\n        ev.add_component(alarm)\nPath(\"schedule_reminders.ics\").write_bytes(check.to_ical())\nprint(check.to_ical().decode().count(\"BEGIN:VALARM\"), \"reminders added\")\n",[14,2177,2178,2189,2193,2207,2221,2231,2246,2274,2294,2299,2310],{"__ignoreMap":42},[46,2179,2180,2182,2184,2186],{"class":48,"line":49},[46,2181,692],{"class":103},[46,2183,877],{"class":107},[46,2185,104],{"class":103},[46,2187,2188],{"class":107}," Alarm\n",[46,2190,2191],{"class":48,"line":117},[46,2192,134],{"emptyLinePlaceholder":133},[46,2194,2195,2197,2199,2201,2203,2205],{"class":48,"line":130},[46,2196,319],{"class":103},[46,2198,1680],{"class":107},[46,2200,325],{"class":103},[46,2202,1685],{"class":107},[46,2204,1688],{"class":56},[46,2206,1010],{"class":107},[46,2208,2209,2211,2213,2216,2218],{"class":48,"line":137},[46,2210,1063],{"class":103},[46,2212,1722],{"class":283},[46,2214,2215],{"class":107},"(ev.decoded(",[46,2217,1105],{"class":56},[46,2219,2220],{"class":107},"), dt.datetime):\n",[46,2222,2223,2226,2228],{"class":48,"line":167},[46,2224,2225],{"class":107},"        alarm ",[46,2227,143],{"class":103},[46,2229,2230],{"class":107}," Alarm()\n",[46,2232,2233,2236,2239,2241,2244],{"class":48,"line":217},[46,2234,2235],{"class":107},"        alarm.add(",[46,2237,2238],{"class":56},"\"action\"",[46,2240,152],{"class":107},[46,2242,2243],{"class":56},"\"DISPLAY\"",[46,2245,164],{"class":107},[46,2247,2248,2250,2252,2254,2256,2259,2261,2264,2266,2268,2270,2272],{"class":48,"line":238},[46,2249,2235],{"class":107},[46,2251,1196],{"class":56},[46,2253,152],{"class":107},[46,2255,906],{"class":103},[46,2257,2258],{"class":56},"\"Reminder: ",[46,2260,913],{"class":912},[46,2262,2263],{"class":107},"ev[",[46,2265,1757],{"class":56},[46,2267,1820],{"class":107},[46,2269,919],{"class":912},[46,2271,922],{"class":56},[46,2273,164],{"class":107},[46,2275,2276,2278,2281,2284,2287,2290,2292],{"class":48,"line":259},[46,2277,2235],{"class":107},[46,2279,2280],{"class":56},"\"trigger\"",[46,2282,2283],{"class":107},", dt.timedelta(",[46,2285,2286],{"class":155},"minutes",[46,2288,2289],{"class":103},"=-",[46,2291,1817],{"class":283},[46,2293,416],{"class":107},[46,2295,2296],{"class":48,"line":264},[46,2297,2298],{"class":107},"        ev.add_component(alarm)\n",[46,2300,2301,2304,2307],{"class":48,"line":277},[46,2302,2303],{"class":107},"Path(",[46,2305,2306],{"class":56},"\"schedule_reminders.ics\"",[46,2308,2309],{"class":107},").write_bytes(check.to_ical())\n",[46,2311,2312,2314,2317,2320,2322,2325],{"class":48,"line":299},[46,2313,380],{"class":283},[46,2315,2316],{"class":107},"(check.to_ical().decode().count(",[46,2318,2319],{"class":56},"\"BEGIN:VALARM\"",[46,2321,1258],{"class":107},[46,2323,2324],{"class":56},"\"reminders added\"",[46,2326,164],{"class":107},[10,2328,2329,2330,2333],{},"The negative trigger means \"30 minutes before the start\". All-day events are skipped here because a reminder relative to midnight is rarely useful; give them a trigger such as ",[14,2331,2332],{},"-P1D"," (one day before) instead if your audience wants one. Calendar apps are free to ignore alarms in imported files, and some apply the user's own default notifications instead, so treat the reminder as a suggestion rather than a guarantee.",[32,2335,2337],{"id":2336},"common-pitfalls","Common pitfalls",[1268,2339,2340,2348,2354,2360,2369],{},[1271,2341,2342,1300,2345,2347],{},[1274,2343,2344],{},"Same-day end for all-day events.",[14,2346,1440],{}," is exclusive; add a day.",[1271,2349,2350,2353],{},[1274,2351,2352],{},"Random UIDs."," A new UID on each export creates duplicates on re-import; derive it from a stable row key.",[1271,2355,2356,2359],{},[1274,2357,2358],{},"Fixed UTC offsets."," Use a zone name so daylight-saving changes are handled.",[1271,2361,2362,2365,2366,2368],{},[1274,2363,2364],{},"Hand-written files."," Unescaped commas and unfolded long lines break strict parsers; let ",[14,2367,20],{}," write the text.",[1271,2370,2371,2374,2375,2377,2378,30],{},[1274,2372,2373],{},"Subscriptions versus imports."," An imported ",[14,2376,16],{}," is a one-off copy. For a calendar that refreshes itself, publish the file at a stable URL and subscribe to it — see ",[26,2379,2381],{"href":2380},"\u002Fautomating-reporting-workflows\u002Fpublishing-excel-reports-to-cloud-storage\u002F","Publishing Excel Reports to Cloud Storage",[32,2383,2385],{"id":2384},"performance-notes","Performance notes",[10,2387,2388],{},"Building events is cheap: a few thousand rows convert in well under a second, and the time zone definitions are added once per zone, not per event. The practical limits are on the receiving side. Importing a file with thousands of events into a personal calendar is slow and hard to undo, so for large schedules publish per-person or per-team files, and keep past events out of the export unless someone needs the history.",[32,2390,2392],{"id":2391},"conclusion","Conclusion",[10,2394,2395,2396,2398],{},"Read the schedule with pandas, combine date and time columns into timestamps, attach a named time zone, and write each row as an iCalendar event with a UID derived from the row's ID. Give all-day events an exclusive end date, let ",[14,2397,1303],{}," add the zone definitions, parse the file back as a check, and split it per owner when each person should only see their own entries.",[32,2400,2402],{"id":2401},"frequently-asked-questions","Frequently asked questions",[10,2404,2405,2408],{},[1274,2406,2407],{},"How do I convert Excel rows into calendar events?","\nRead the sheet with pandas, build start and end datetimes for each row, and write them as VEVENT components with the icalendar package. Save the result as an .ics file, which Outlook, Google Calendar and Apple Calendar can import.",[10,2410,2411,2414],{},[1274,2412,2413],{},"How do I create all-day events in an .ics file?","\nUse date values instead of datetimes for DTSTART and DTEND, and set DTEND to the day after the last day, because the end date is exclusive.",[10,2416,2417,2420],{},[1274,2418,2419],{},"Why does re-importing my .ics file create duplicate events?","\nEach export generated new UIDs. Give every event a UID derived from a stable key, such as the row's ID, so calendar apps recognise the event and update it.",[10,2422,2423,2426],{},[1274,2424,2425],{},"How do I set the time zone for events exported from Excel?","\nAttach a named zone with zoneinfo, for example ZoneInfo(\"Europe\u002FLondon\"), to each start and end time, and call add_missing_timezones() on the calendar so the file includes the matching VTIMEZONE definition.",[32,2428,2430],{"id":2429},"related","Related",[1268,2432,2433,2439,2446,2453],{},[1271,2434,2435,2436,2438],{},"Up one level: ",[26,2437,29],{"href":28}," — converting sheets to other formats.",[1271,2440,2441,2445],{},[26,2442,2444],{"href":2443},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fstyling-excel-cells-with-openpyxl\u002Fbuild-a-calendar-sheet-in-excel-with-python\u002F","Build a Calendar Sheet in Excel with Python"," — a printable month grid instead of calendar events.",[1271,2447,2448,2452],{},[26,2449,2451],{"href":2450},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Fhandle-timezones-in-excel-timestamps-with-python\u002F","Handle Timezones in Excel Timestamps with Python"," — zone conversion in depth.",[1271,2454,2455,2459],{},[26,2456,2458],{"href":2457},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Fconvert-excel-time-values-and-durations\u002F","Convert Excel Time Values and Durations"," — when time columns arrive as fractions or text.",[2461,2462,2463],"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 .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}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":42,"searchDepth":117,"depth":117,"links":2465},[2466,2467,2468,2469,2470,2471,2472,2473,2474,2475,2476,2477],{"id":34,"depth":117,"text":35},{"id":79,"depth":117,"text":80},{"id":678,"depth":117,"text":679},{"id":857,"depth":117,"text":858},{"id":1633,"depth":117,"text":1634},{"id":1843,"depth":117,"text":1844},{"id":2164,"depth":117,"text":2165},{"id":2336,"depth":117,"text":2337},{"id":2384,"depth":117,"text":2385},{"id":2391,"depth":117,"text":2392},{"id":2401,"depth":117,"text":2402},{"id":2429,"depth":117,"text":2430},"2026-10-02","Turn a schedule kept in Excel — training sessions, deadlines, shifts, release dates — into an .ics file that Outlook, Google Calendar and Apple Calendar import: combine date and time columns, handle all-day rows with exclusive end dates, attach the right time zone, give each event a stable UID so re-imports update instead of duplicating, and split the file per person.","md",[2482,2484,2486,2488],{"q":2407,"a":2483},"Read the sheet with pandas, build start and end datetimes for each row, and write them as VEVENT components with the icalendar package. Save the result as an .ics file, which Outlook, Google Calendar and Apple Calendar can import.",{"q":2413,"a":2485},"Use date values instead of datetimes for DTSTART and DTEND, and set DTEND to the day after the last day, because the end date is exclusive.",{"q":2419,"a":2487},"Each export generated new UIDs. Give every event a UID derived from a stable key, such as the row's ID, so calendar apps recognise the event and update it.",{"q":2425,"a":2489},"Attach a named zone with zoneinfo, for example ZoneInfo(\"Europe\u002FLondon\"), to each start and end time, and call add_missing_timezones() on the calendar so the file includes the matching VTIMEZONE definition.","create calendar events from an excel sheet",null,{"breadcrumb":2493},[2494,2496,2499],{"name":2495,"item":1407},"Home",{"name":2497,"item":2498},"Getting Started with Python Excel Automation","\u002Fgetting-started-with-python-excel-automation\u002F",{"name":29,"item":28},"\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fcreate-calendar-events-from-an-excel-sheet",{"title":2502,"description":2503},"Create Calendar (.ics) Events from an Excel Sheet with Python","Convert Excel rows into iCalendar events with pandas and the icalendar package: date + time columns, all-day events, time zones with VTIMEZONE, stable UIDs for updates, escaping, per-person files, and a round-trip check.","create-calendar-events-from-an-excel-sheet","getting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fcreate-calendar-events-from-an-excel-sheet\u002Findex",[2507,2508],"file-formats","dates-and-times","how-to","WkY3Ho39y0rIMAqlrwc33xbKUnR5Vj0o8U6gQQC7jOk",[2512,2516],{"title":2513,"path":2514,"stem":2515,"children":-1},"Convert XML to Excel with pandas read_xml","\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fconvert-xml-to-excel-with-pandas","getting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fconvert-xml-to-excel-with-pandas\u002Findex",{"title":2517,"path":2518,"stem":2519,"children":-1},"Extract Tables from a PDF into Excel with Python","\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fextract-tables-from-a-pdf-into-excel-with-python","getting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fextract-tables-from-a-pdf-into-excel-with-python\u002Findex",[2521,2523],{"path":2522,"title":29},"\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions",{"path":2524,"title":2497},"\u002Fgetting-started-with-python-excel-automation",[],[2527,2530,2533,2537,2540,2544],{"path":2528,"title":2529,"kind":2509},"\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":2531,"title":2532,"kind":2509},"\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":2534,"title":2535,"kind":2536},"\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":2538,"title":2539,"kind":2536},"\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":2541,"title":2542,"kind":2543},"\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":2545,"title":2546,"kind":2509},"\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",1790972059242]