[{"data":1,"prerenderedAt":2215},["ShallowReactive",2],{"doc:\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Fhandle-timezones-in-excel-timestamps-with-python":3,"surround:\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Fhandle-timezones-in-excel-timestamps-with-python":2208},{"id":4,"title":5,"body":6,"dateModified":2184,"datePublished":2184,"description":2185,"extension":2186,"faq":2187,"meta":2199,"navigation":293,"path":2200,"seo":2201,"slug":2204,"stem":2205,"type":2206,"__hash__":2207},"docs\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Fhandle-timezones-in-excel-timestamps-with-python\u002Findex.md","Handle Timezones in Excel Timestamps with Python",{"type":7,"value":8,"toc":2172},"minimark",[9,28,165,170,200,207,246,252,256,259,382,389,515,519,522,807,817,882,891,895,898,1011,1178,1192,1200,1204,1321,1324,1568,1571,1762,1765,1769,1900,1904,1910,1921,2052,2065,2073,2077,2083,2087,2098,2104,2110,2121,2127,2131,2168],[10,11,12,13,17,18,21,22,27],"p",{},"Excel has no timezone. A cell holds a day count and nothing else, so the instant ",[14,15,16],"code",{},"2026-08-15 18:00+02:00"," and the instant ",[14,19,20],{},"2026-08-15 18:00Z"," are indistinguishable once written. openpyxl handles this honestly — it raises rather than silently dropping the offset — which means every script that writes timestamps has to make a decision. This guide covers the three-step pattern that keeps those timestamps unambiguous, the daylight-saving edges that break naive code, and how to read the values back into aware datetimes. It expands on the timezone section of ",[23,24,26],"a",{"href":25},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002F","Working with Dates and Times in Excel Data",".",[29,30,39,40,39,44,39,48,39,55,39,64,39,71,39,76,39,81,39,85,39,90,39,96,39,101,39,106,39,110,39,113,39,116,39,120,39,123,39,127,39,131,39,136,39,140,39,145,39,150,39,155,39,159],"svg",{"viewBox":31,"role":32,"ariaLabel":33,"ariaLabelledBy":34,"xmlns":37,"style":38},"0 0 800 236","img","The convert, strip and label pattern: an aware UTC timestamp is converted to the target zone, the offset is removed so Excel can store it, and the zone name is written into the sheet as a label.",[35,36],"tzpat-t","tzpat-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:800px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[41,42,43],"title",{"id":35},"Convert, strip, label — writing timestamps Excel can hold",[45,46,47],"desc",{"id":36},"Three stages. An aware timestamp of six in the evening UTC is first converted to the target zone, becoming eight in the evening Berlin time while representing the same instant. The offset is then removed with tz_localize None, leaving a naive value Excel can store. Finally the zone name is written into the sheet as a header so the reader knows what the bare timestamp means. Skipping the third stage is what makes reports unreproducible.",[49,50],"rect",{"x":51,"y":51,"width":52,"height":53,"fill":54},"0","800","236","#ffffff",[49,56],{"x":57,"y":58,"width":59,"height":60,"rx":57,"fill":61,"stroke":62,"style":63},"14","66","216","104","#ebebfd","var(--brand,#5b5cf0)","stroke-width:2px",[65,66,70],"text",{"x":67,"y":68,"style":69},"122","50","font-size:11px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","1 · convert",[65,72,75],{"x":67,"y":73,"style":74},"96","font-size:11.5px;fill:var(--text,#172033);text-anchor:middle","2026-08-15 18:00+00:00",[65,77,80],{"x":67,"y":78,"style":79},"120","font-size:14px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","↓",[65,82,84],{"x":67,"y":83,"style":74},"144","2026-08-15 20:00+02:00",[65,86,89],{"x":67,"y":87,"style":88},"162","font-size:10px;fill:var(--muted,#5b6780);text-anchor:middle","same instant",[91,92],"line",{"x1":93,"y1":94,"x2":95,"y2":94,"stroke":62,"style":63},"230","118","266",[97,98],"polygon",{"points":99,"fill":100},"274,118 262,112 262,124","#5b5cf0",[49,102],{"x":103,"y":58,"width":59,"height":60,"rx":57,"fill":104,"stroke":105,"style":63},"282","#fdefd8","var(--gold,#b4740a)",[65,107,109],{"x":108,"y":68,"style":69},"390","2 · strip",[65,111,112],{"x":108,"y":73,"style":74},"tz_localize(None)",[65,114,80],{"x":108,"y":78,"style":115},"font-size:14px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle",[65,117,119],{"x":108,"y":83,"style":118},"font-size:11.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","2026-08-15 20:00",[65,121,122],{"x":108,"y":87,"style":88},"naive — Excel can store it",[91,124],{"x1":125,"y1":94,"x2":126,"y2":94,"stroke":105,"style":63},"498","534",[97,128],{"points":129,"fill":130},"542,118 530,112 530,124","#b4740a",[49,132],{"x":133,"y":58,"width":53,"height":60,"rx":57,"fill":134,"stroke":135,"style":63},"550","#d9f4f1","var(--teal,#0f9488)",[65,137,139],{"x":138,"y":68,"style":69},"668","3 · label",[65,141,144],{"x":138,"y":142,"style":143},"98","font-size:11.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","\"All times Europe\u002FBerlin\"",[65,146,149],{"x":138,"y":147,"style":148},"124","font-size:11px;fill:var(--text,#172033);text-anchor:middle","written into the sheet",[65,151,154],{"x":138,"y":152,"style":153},"148","font-size:10.5px;fill:var(--muted,#5b6780);text-anchor:middle","without this the file is",[65,156,158],{"x":138,"y":157,"style":153},"164","unreproducible later",[65,160,164],{"x":161,"y":162,"style":163},"400","206","font-size:11px;fill:var(--muted,#5b6780);text-anchor:middle","Excel stores a day count. The zone lives in the sheet, or it lives nowhere.",[166,167,169],"h2",{"id":168},"prerequisites","Prerequisites",[171,172,177],"pre",{"className":173,"code":174,"language":175,"meta":176,"style":176},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install pandas openpyxl xlsxwriter\n","bash","",[14,178,179],{"__ignoreMap":176},[180,181,183,187,191,194,197],"span",{"class":91,"line":182},1,[180,184,186],{"class":185},"sMTad","pip",[180,188,190],{"class":189},"srMev"," install",[180,192,193],{"class":189}," pandas",[180,195,196],{"class":189}," openpyxl",[180,198,199],{"class":189}," xlsxwriter\n",[10,201,202,203,206],{},"Python 3.9 and later ship ",[14,204,205],{},"zoneinfo"," in the standard library, so no third-party timezone package is needed:",[171,208,212],{"className":209,"code":210,"language":211,"meta":176,"style":176},"language-python shiki shiki-themes github-light github-dark-high-contrast","from zoneinfo import ZoneInfo\nprint(ZoneInfo(\"Europe\u002FBerlin\"))\n","python",[14,213,214,230],{"__ignoreMap":176},[180,215,216,220,224,227],{"class":91,"line":182},[180,217,219],{"class":218},"s-kum","from",[180,221,223],{"class":222},"skGVy"," zoneinfo ",[180,225,226],{"class":218},"import",[180,228,229],{"class":222}," ZoneInfo\n",[180,231,233,237,240,243],{"class":91,"line":232},2,[180,234,236],{"class":235},"sP0c6","print",[180,238,239],{"class":222},"(ZoneInfo(",[180,241,242],{"class":189},"\"Europe\u002FBerlin\"",[180,244,245],{"class":222},"))\n",[10,247,248,249,27],{},"On a bare Linux container without the system tz database, install the fallback: ",[14,250,251],{},"pip install tzdata",[166,253,255],{"id":254},"step-1-see-the-failure-clearly","Step 1 — See the failure clearly",[10,257,258],{},"openpyxl refuses aware datetimes outright:",[171,260,262],{"className":209,"code":261,"language":211,"meta":176,"style":176},"from datetime import datetime, timezone\nfrom openpyxl import Workbook\n\nwb = Workbook()\nws = wb.active\nws[\"A1\"] = datetime(2026, 8, 15, 18, 0, tzinfo=timezone.utc)\n# ValueError: Excel does not support timezones in datetimes.\n# The tzinfo in the datetime\u002Ftime object must be set to None.\n",[14,263,264,276,288,295,307,318,369,376],{"__ignoreMap":176},[180,265,266,268,271,273],{"class":91,"line":182},[180,267,219],{"class":218},[180,269,270],{"class":222}," datetime ",[180,272,226],{"class":218},[180,274,275],{"class":222}," datetime, timezone\n",[180,277,278,280,283,285],{"class":91,"line":232},[180,279,219],{"class":218},[180,281,282],{"class":222}," openpyxl ",[180,284,226],{"class":218},[180,286,287],{"class":222}," Workbook\n",[180,289,291],{"class":91,"line":290},3,[180,292,294],{"emptyLinePlaceholder":293},true,"\n",[180,296,298,301,304],{"class":91,"line":297},4,[180,299,300],{"class":222},"wb ",[180,302,303],{"class":218},"=",[180,305,306],{"class":222}," Workbook()\n",[180,308,310,313,315],{"class":91,"line":309},5,[180,311,312],{"class":222},"ws ",[180,314,303],{"class":218},[180,316,317],{"class":222}," wb.active\n",[180,319,321,324,327,330,332,335,338,341,344,346,349,351,354,356,358,360,364,366],{"class":91,"line":320},6,[180,322,323],{"class":222},"ws[",[180,325,326],{"class":189},"\"A1\"",[180,328,329],{"class":222},"] ",[180,331,303],{"class":218},[180,333,334],{"class":222}," datetime(",[180,336,337],{"class":235},"2026",[180,339,340],{"class":222},", ",[180,342,343],{"class":235},"8",[180,345,340],{"class":222},[180,347,348],{"class":235},"15",[180,350,340],{"class":222},[180,352,353],{"class":235},"18",[180,355,340],{"class":222},[180,357,51],{"class":235},[180,359,340],{"class":222},[180,361,363],{"class":362},"sa561","tzinfo",[180,365,303],{"class":218},[180,367,368],{"class":222},"timezone.utc)\n",[180,370,372],{"class":91,"line":371},7,[180,373,375],{"class":374},"s-wDw","# ValueError: Excel does not support timezones in datetimes.\n",[180,377,379],{"class":91,"line":378},8,[180,380,381],{"class":374},"# The tzinfo in the datetime\u002Ftime object must be set to None.\n",[10,383,384,385,388],{},"pandas is quieter and therefore more dangerous. ",[14,386,387],{},"to_excel"," on a tz-aware column will raise on some engine and dtype combinations and silently write the naive local wall-clock time on others — so never rely on the default. Make the conversion explicit:",[171,390,392],{"className":209,"code":391,"language":211,"meta":176,"style":176},"import pandas as pd\n\nevents = pd.DataFrame({\n    \"event\": [\"login\", \"export\", \"logout\"],\n    \"at\": pd.to_datetime([\n        \"2026-08-15T16:04:00Z\",\n        \"2026-08-15T17:30:00Z\",\n        \"2026-08-15T19:15:00Z\",\n    ], utc=True),\n})\nprint(events[\"at\"].dtype)      # datetime64[ns, UTC]\n",[14,393,394,407,411,421,445,453,461,468,475,492,498],{"__ignoreMap":176},[180,395,396,398,401,404],{"class":91,"line":182},[180,397,226],{"class":218},[180,399,400],{"class":222}," pandas ",[180,402,403],{"class":218},"as",[180,405,406],{"class":222}," pd\n",[180,408,409],{"class":91,"line":232},[180,410,294],{"emptyLinePlaceholder":293},[180,412,413,416,418],{"class":91,"line":290},[180,414,415],{"class":222},"events ",[180,417,303],{"class":218},[180,419,420],{"class":222}," pd.DataFrame({\n",[180,422,423,426,429,432,434,437,439,442],{"class":91,"line":297},[180,424,425],{"class":189},"    \"event\"",[180,427,428],{"class":222},": [",[180,430,431],{"class":189},"\"login\"",[180,433,340],{"class":222},[180,435,436],{"class":189},"\"export\"",[180,438,340],{"class":222},[180,440,441],{"class":189},"\"logout\"",[180,443,444],{"class":222},"],\n",[180,446,447,450],{"class":91,"line":309},[180,448,449],{"class":189},"    \"at\"",[180,451,452],{"class":222},": pd.to_datetime([\n",[180,454,455,458],{"class":91,"line":320},[180,456,457],{"class":189},"        \"2026-08-15T16:04:00Z\"",[180,459,460],{"class":222},",\n",[180,462,463,466],{"class":91,"line":371},[180,464,465],{"class":189},"        \"2026-08-15T17:30:00Z\"",[180,467,460],{"class":222},[180,469,470,473],{"class":91,"line":378},[180,471,472],{"class":189},"        \"2026-08-15T19:15:00Z\"",[180,474,460],{"class":222},[180,476,478,481,484,486,489],{"class":91,"line":477},9,[180,479,480],{"class":222},"    ], ",[180,482,483],{"class":362},"utc",[180,485,303],{"class":218},[180,487,488],{"class":235},"True",[180,490,491],{"class":222},"),\n",[180,493,495],{"class":91,"line":494},10,[180,496,497],{"class":222},"})\n",[180,499,501,503,506,509,512],{"class":91,"line":500},11,[180,502,236],{"class":235},[180,504,505],{"class":222},"(events[",[180,507,508],{"class":189},"\"at\"",[180,510,511],{"class":222},"].dtype)      ",[180,513,514],{"class":374},"# datetime64[ns, UTC]\n",[166,516,518],{"id":517},"step-2-convert-strip-label","Step 2 — Convert, strip, label",[10,520,521],{},"The whole pattern is three lines of transformation and one line of documentation:",[171,523,525],{"className":209,"code":524,"language":211,"meta":176,"style":176},"import pandas as pd\n\nREPORT_ZONE = \"Europe\u002FBerlin\"\n\n# 1. Convert — same instants, expressed in the readers' zone.\nevents[\"at\"] = events[\"at\"].dt.tz_convert(REPORT_ZONE)\n\n# 2. Strip — drop the offset so Excel can hold the value.\nevents[\"at\"] = events[\"at\"].dt.tz_localize(None)\n\n# 3. Label — write the zone where a reader will see it.\nwith pd.ExcelWriter(\"events.xlsx\", engine=\"xlsxwriter\",\n                    datetime_format=\"yyyy-mm-dd hh:mm\") as writer:\n    events.to_excel(writer, sheet_name=\"Events\", index=False, startrow=1)\n\n    book, sheet = writer.book, writer.sheets[\"Events\"]\n    note = book.add_format({\"italic\": True, \"font_color\": \"#5b6780\"})\n    sheet.write(0, 0, f\"All times {REPORT_ZONE}\", note)\n    sheet.set_column(\"B:B\", 19)\n",[14,526,527,537,541,552,556,561,585,589,594,616,620,625,649,668,704,709,725,756,791],{"__ignoreMap":176},[180,528,529,531,533,535],{"class":91,"line":182},[180,530,226],{"class":218},[180,532,400],{"class":222},[180,534,403],{"class":218},[180,536,406],{"class":222},[180,538,539],{"class":91,"line":232},[180,540,294],{"emptyLinePlaceholder":293},[180,542,543,546,549],{"class":91,"line":290},[180,544,545],{"class":235},"REPORT_ZONE",[180,547,548],{"class":218}," =",[180,550,551],{"class":189}," \"Europe\u002FBerlin\"\n",[180,553,554],{"class":91,"line":297},[180,555,294],{"emptyLinePlaceholder":293},[180,557,558],{"class":91,"line":309},[180,559,560],{"class":374},"# 1. Convert — same instants, expressed in the readers' zone.\n",[180,562,563,566,568,570,572,575,577,580,582],{"class":91,"line":320},[180,564,565],{"class":222},"events[",[180,567,508],{"class":189},[180,569,329],{"class":222},[180,571,303],{"class":218},[180,573,574],{"class":222}," events[",[180,576,508],{"class":189},[180,578,579],{"class":222},"].dt.tz_convert(",[180,581,545],{"class":235},[180,583,584],{"class":222},")\n",[180,586,587],{"class":91,"line":371},[180,588,294],{"emptyLinePlaceholder":293},[180,590,591],{"class":91,"line":378},[180,592,593],{"class":374},"# 2. Strip — drop the offset so Excel can hold the value.\n",[180,595,596,598,600,602,604,606,608,611,614],{"class":91,"line":477},[180,597,565],{"class":222},[180,599,508],{"class":189},[180,601,329],{"class":222},[180,603,303],{"class":218},[180,605,574],{"class":222},[180,607,508],{"class":189},[180,609,610],{"class":222},"].dt.tz_localize(",[180,612,613],{"class":235},"None",[180,615,584],{"class":222},[180,617,618],{"class":91,"line":494},[180,619,294],{"emptyLinePlaceholder":293},[180,621,622],{"class":91,"line":500},[180,623,624],{"class":374},"# 3. Label — write the zone where a reader will see it.\n",[180,626,628,631,634,637,639,642,644,647],{"class":91,"line":627},12,[180,629,630],{"class":218},"with",[180,632,633],{"class":222}," pd.ExcelWriter(",[180,635,636],{"class":189},"\"events.xlsx\"",[180,638,340],{"class":222},[180,640,641],{"class":362},"engine",[180,643,303],{"class":218},[180,645,646],{"class":189},"\"xlsxwriter\"",[180,648,460],{"class":222},[180,650,652,655,657,660,663,665],{"class":91,"line":651},13,[180,653,654],{"class":362},"                    datetime_format",[180,656,303],{"class":218},[180,658,659],{"class":189},"\"yyyy-mm-dd hh:mm\"",[180,661,662],{"class":222},") ",[180,664,403],{"class":218},[180,666,667],{"class":222}," writer:\n",[180,669,671,674,677,679,682,684,687,689,692,694,697,699,702],{"class":91,"line":670},14,[180,672,673],{"class":222},"    events.to_excel(writer, ",[180,675,676],{"class":362},"sheet_name",[180,678,303],{"class":218},[180,680,681],{"class":189},"\"Events\"",[180,683,340],{"class":222},[180,685,686],{"class":362},"index",[180,688,303],{"class":218},[180,690,691],{"class":235},"False",[180,693,340],{"class":222},[180,695,696],{"class":362},"startrow",[180,698,303],{"class":218},[180,700,701],{"class":235},"1",[180,703,584],{"class":222},[180,705,707],{"class":91,"line":706},15,[180,708,294],{"emptyLinePlaceholder":293},[180,710,712,715,717,720,722],{"class":91,"line":711},16,[180,713,714],{"class":222},"    book, sheet ",[180,716,303],{"class":218},[180,718,719],{"class":222}," writer.book, writer.sheets[",[180,721,681],{"class":189},[180,723,724],{"class":222},"]\n",[180,726,728,731,733,736,739,742,744,746,749,751,754],{"class":91,"line":727},17,[180,729,730],{"class":222},"    note ",[180,732,303],{"class":218},[180,734,735],{"class":222}," book.add_format({",[180,737,738],{"class":189},"\"italic\"",[180,740,741],{"class":222},": ",[180,743,488],{"class":235},[180,745,340],{"class":222},[180,747,748],{"class":189},"\"font_color\"",[180,750,741],{"class":222},[180,752,753],{"class":189},"\"#5b6780\"",[180,755,497],{"class":222},[180,757,759,762,764,766,768,770,773,776,780,782,785,788],{"class":91,"line":758},18,[180,760,761],{"class":222},"    sheet.write(",[180,763,51],{"class":235},[180,765,340],{"class":222},[180,767,51],{"class":235},[180,769,340],{"class":222},[180,771,772],{"class":218},"f",[180,774,775],{"class":189},"\"All times ",[180,777,779],{"class":778},"sSjpA","{",[180,781,545],{"class":235},[180,783,784],{"class":778},"}",[180,786,787],{"class":189},"\"",[180,789,790],{"class":222},", note)\n",[180,792,794,797,800,802,805],{"class":91,"line":793},19,[180,795,796],{"class":222},"    sheet.set_column(",[180,798,799],{"class":189},"\"B:B\"",[180,801,340],{"class":222},[180,803,804],{"class":235},"19",[180,806,584],{"class":222},[10,808,809,812,813,816],{},[14,810,811],{},"tz_convert"," and ",[14,814,815],{},"tz_localize"," are easy to confuse and do opposite things:",[818,819,820,836],"table",{},[821,822,823],"thead",{},[824,825,826,830,833],"tr",{},[827,828,829],"th",{},"Method",[827,831,832],{},"Requires",[827,834,835],{},"Does",[837,838,839,858,871],"tbody",{},[824,840,841,847,850],{},[842,843,844],"td",{},[14,845,846],{},"tz_localize(\"Europe\u002FBerlin\")",[842,848,849],{},"naive input",[842,851,852,853,857],{},"Asserts these wall-clock times ",[854,855,856],"em",{},"are"," Berlin times",[824,859,860,865,868],{},[842,861,862],{},[14,863,864],{},"tz_convert(\"Europe\u002FBerlin\")",[842,866,867],{},"aware input",[842,869,870],{},"Re-expresses the same instant in Berlin",[824,872,873,877,879],{},[842,874,875],{},[14,876,112],{},[842,878,867],{},[842,880,881],{},"Discards the offset, keeping the wall clock",[10,883,884,885,887,888,890],{},"Calling ",[14,886,815],{}," on already-aware data raises; calling ",[14,889,811],{}," on naive data raises. The error messages are clear, but the conceptual mistake — localising when you meant to convert — silently shifts every timestamp by the offset when you get it the other way round.",[166,892,894],{"id":893},"step-3-survive-daylight-saving","Step 3 — Survive daylight saving",[10,896,897],{},"Twice a year, local time is not a function of itself. In the spring one hour does not exist; in the autumn one hour happens twice. Any code that builds aware timestamps from local strings meets this eventually.",[29,899,39,905,39,908,39,911,39,914,39,919,39,925,39,931,39,935,39,940,39,946,39,951,39,954,39,958,39,963,39,966,39,969,39,973,39,975,39,978,39,980,39,984,39,987,39,991,39,993,39,995,39,999,39,1003,39,1007],{"viewBox":900,"role":32,"ariaLabel":901,"ariaLabelledBy":902,"xmlns":37,"style":38},"0 0 800 244","Daylight saving edges: in spring the local hour from two to three a.m. never occurs, and in autumn the hour from two to three occurs twice with different offsets.",[903,904],"dst-t","dst-d",[41,906,907],{"id":903},"The two hours where local time is not a function",[45,909,910],{"id":904},"Two timelines. On the spring transition the clock jumps from one fifty-nine to three a.m., so any local time in the two o'clock hour is nonexistent and localising it raises unless a policy is given. On the autumn transition the two o'clock hour repeats, first at plus two and then at plus one, so a local time in that hour is ambiguous and maps to two distinct instants.",[49,912],{"x":51,"y":51,"width":52,"height":913,"fill":54},"244",[65,915,918],{"x":161,"y":916,"style":917},"26","font-size:12px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","local wall-clock time across a transition",[49,920],{"x":57,"y":68,"width":58,"height":921,"rx":922,"fill":923,"stroke":924},"36","7","#f0f2f5","var(--line,#cdd5e6)",[65,926,930],{"x":927,"y":928,"style":929},"47","73","font-size:11.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","spring",[49,932],{"x":933,"y":68,"width":934,"height":921,"rx":343,"fill":134,"stroke":135,"style":63},"90","130",[65,936,939],{"x":937,"y":938,"style":143},"155","74","01:00 – 01:59",[49,941],{"x":942,"y":68,"width":943,"height":921,"rx":343,"fill":944,"stroke":945,"style":63},"228","200","#fee8f2","var(--accent,#f43f8f)",[65,947,950],{"x":948,"y":938,"style":949},"328","font-size:11.5px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","02:00 – 02:59 never happens",[49,952],{"x":953,"y":68,"width":934,"height":921,"rx":343,"fill":134,"stroke":135,"style":63},"436",[65,955,957],{"x":956,"y":938,"style":143},"501","03:00 – 03:59",[65,959,962],{"x":960,"y":938,"style":961},"600","font-size:11px;fill:var(--muted,#5b6780)","nonexistent=",[65,964,965],{"x":960,"y":933,"style":961},"\"shift_forward\"",[49,967],{"x":57,"y":968,"width":58,"height":921,"rx":922,"fill":923,"stroke":924},"142",[65,970,972],{"x":927,"y":971,"style":929},"165","autumn",[49,974],{"x":933,"y":968,"width":934,"height":921,"rx":343,"fill":134,"stroke":135,"style":63},[65,976,939],{"x":937,"y":977,"style":143},"166",[49,979],{"x":942,"y":968,"width":73,"height":921,"rx":343,"fill":104,"stroke":105,"style":63},[65,981,983],{"x":982,"y":977,"style":118},"276","02:00 +02",[49,985],{"x":986,"y":968,"width":73,"height":921,"rx":343,"fill":104,"stroke":105,"style":63},"332",[65,988,990],{"x":989,"y":977,"style":118},"380","02:00 +01",[49,992],{"x":953,"y":968,"width":934,"height":921,"rx":343,"fill":134,"stroke":135,"style":63},[65,994,957],{"x":956,"y":977,"style":143},[65,996,998],{"x":960,"y":997,"style":961},"160","ambiguous=",[65,1000,1002],{"x":960,"y":1001,"style":961},"176","True or False",[65,1004,1006],{"x":161,"y":59,"style":1005},"font-size:11px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","the reliable fix: store UTC upstream and only ever tz_convert",[65,1008,1010],{"x":161,"y":1009,"style":153},"234","converting from an instant is total; localising a wall clock is not",[171,1012,1014],{"className":209,"code":1013,"language":211,"meta":176,"style":176},"import pandas as pd\n\nlocal = pd.to_datetime([\"2026-10-25 02:30\", \"2026-03-29 02:30\"])\n\n# Default: raises on both the ambiguous and the nonexistent value.\ntry:\n    local.tz_localize(\"Europe\u002FBerlin\")\nexcept Exception as exc:\n    print(type(exc).__name__, exc)\n\n# State the policy explicitly instead.\nresolved = local.tz_localize(\n    \"Europe\u002FBerlin\",\n    ambiguous=False,               # autumn repeat: take the second (winter) pass\n    nonexistent=\"shift_forward\",   # spring gap: move to the first valid instant\n)\nprint(resolved)\n",[14,1015,1016,1026,1030,1051,1055,1060,1068,1077,1091,1111,1115,1120,1130,1137,1152,1167,1171],{"__ignoreMap":176},[180,1017,1018,1020,1022,1024],{"class":91,"line":182},[180,1019,226],{"class":218},[180,1021,400],{"class":222},[180,1023,403],{"class":218},[180,1025,406],{"class":222},[180,1027,1028],{"class":91,"line":232},[180,1029,294],{"emptyLinePlaceholder":293},[180,1031,1032,1035,1037,1040,1043,1045,1048],{"class":91,"line":290},[180,1033,1034],{"class":222},"local ",[180,1036,303],{"class":218},[180,1038,1039],{"class":222}," pd.to_datetime([",[180,1041,1042],{"class":189},"\"2026-10-25 02:30\"",[180,1044,340],{"class":222},[180,1046,1047],{"class":189},"\"2026-03-29 02:30\"",[180,1049,1050],{"class":222},"])\n",[180,1052,1053],{"class":91,"line":297},[180,1054,294],{"emptyLinePlaceholder":293},[180,1056,1057],{"class":91,"line":309},[180,1058,1059],{"class":374},"# Default: raises on both the ambiguous and the nonexistent value.\n",[180,1061,1062,1065],{"class":91,"line":320},[180,1063,1064],{"class":218},"try",[180,1066,1067],{"class":222},":\n",[180,1069,1070,1073,1075],{"class":91,"line":371},[180,1071,1072],{"class":222},"    local.tz_localize(",[180,1074,242],{"class":189},[180,1076,584],{"class":222},[180,1078,1079,1082,1085,1088],{"class":91,"line":378},[180,1080,1081],{"class":218},"except",[180,1083,1084],{"class":235}," Exception",[180,1086,1087],{"class":218}," as",[180,1089,1090],{"class":222}," exc:\n",[180,1092,1093,1096,1099,1102,1105,1108],{"class":91,"line":477},[180,1094,1095],{"class":235},"    print",[180,1097,1098],{"class":222},"(",[180,1100,1101],{"class":235},"type",[180,1103,1104],{"class":222},"(exc).",[180,1106,1107],{"class":235},"__name__",[180,1109,1110],{"class":222},", exc)\n",[180,1112,1113],{"class":91,"line":494},[180,1114,294],{"emptyLinePlaceholder":293},[180,1116,1117],{"class":91,"line":500},[180,1118,1119],{"class":374},"# State the policy explicitly instead.\n",[180,1121,1122,1125,1127],{"class":91,"line":627},[180,1123,1124],{"class":222},"resolved ",[180,1126,303],{"class":218},[180,1128,1129],{"class":222}," local.tz_localize(\n",[180,1131,1132,1135],{"class":91,"line":651},[180,1133,1134],{"class":189},"    \"Europe\u002FBerlin\"",[180,1136,460],{"class":222},[180,1138,1139,1142,1144,1146,1149],{"class":91,"line":670},[180,1140,1141],{"class":362},"    ambiguous",[180,1143,303],{"class":218},[180,1145,691],{"class":235},[180,1147,1148],{"class":222},",               ",[180,1150,1151],{"class":374},"# autumn repeat: take the second (winter) pass\n",[180,1153,1154,1157,1159,1161,1164],{"class":91,"line":706},[180,1155,1156],{"class":362},"    nonexistent",[180,1158,303],{"class":218},[180,1160,965],{"class":189},[180,1162,1163],{"class":222},",   ",[180,1165,1166],{"class":374},"# spring gap: move to the first valid instant\n",[180,1168,1169],{"class":91,"line":711},[180,1170,584],{"class":222},[180,1172,1173,1175],{"class":91,"line":727},[180,1174,236],{"class":235},[180,1176,1177],{"class":222},"(resolved)\n",[10,1179,1180,1181,1184,1185,1187,1188,1191],{},"Choosing ",[14,1182,1183],{},"ambiguous=False"," versus ",[14,1186,488],{}," is a business decision, not a technical one — it decides whether a 02:30 event on transition night is recorded before or after the clocks go back. Passing ",[14,1189,1190],{},"ambiguous=\"NaT\""," is the honest option when you genuinely cannot tell, because it marks the rows rather than guessing.",[10,1193,1194,1195,1199],{},"All of this disappears if timestamps arrive as UTC instants. Conversion from an instant is always well defined; localisation of a wall clock is not. Where you control the upstream — a database extract, an API — capture UTC and convert only for display. See ",[23,1196,1198],{"href":1197},"\u002Fadvanced-data-transformation-and-cleaning\u002Fmoving-data-between-excel-and-databases\u002Fexport-sql-query-results-to-excel-with-python\u002F","exporting SQL query results to Excel"," for pushing that decision into the query.",[166,1201,1203],{"id":1202},"step-4-read-the-timestamps-back","Step 4 — Read the timestamps back",[29,1205,39,1211,39,1214,39,1217,39,1219,39,1225,39,1229,39,1233,39,1238,39,1241,39,1243,39,1247,39,1250,39,1254,39,1257,39,1264,39,1269,39,1273,39,1280,39,1285,39,1290,39,1294,39,1298,39,1302,39,1306,39,1310,39,1313,39,1316],{"viewBox":1206,"role":32,"ariaLabel":1207,"ariaLabelledBy":1208,"xmlns":37,"style":38},"0 0 800 230","Round trip: a UTC instant is converted and stripped for writing, and reading it back requires re-localising to the documented zone before converting to UTC again.",[1209,1210],"round-t","round-d",[41,1212,1213],{"id":1209},"The full round trip, and where the zone information lives",[45,1215,1216],{"id":1210},"A UTC instant is converted to the report zone and stripped of its offset to be written into the workbook, where only a naive wall-clock time is stored. On the way back, that naive value is re-localised using the zone recorded in the sheet's header, then converted to UTC so it can be joined with other systems. The header label is the only thing carrying the zone across the boundary; without it, the return leg is guesswork.",[49,1218],{"x":51,"y":51,"width":52,"height":93,"fill":54},[49,1220],{"x":57,"y":1221,"width":1222,"height":1223,"rx":1224,"fill":61,"stroke":62,"style":63},"40","168","64","12",[65,1226,1228],{"x":142,"y":58,"style":1227},"font-size:11.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","UTC instant",[65,1230,1232],{"x":142,"y":1231,"style":153},"87","16:04+00:00",[91,1234],{"x1":1235,"y1":1236,"x2":1237,"y2":1236,"stroke":62,"style":63},"182","72","212",[97,1239],{"points":1240,"fill":100},"220,72 208,66 208,78",[49,1242],{"x":942,"y":1221,"width":1001,"height":1223,"rx":1224,"fill":104,"stroke":105,"style":63},[65,1244,1246],{"x":1245,"y":58,"style":118},"316","convert + strip",[65,1248,1249],{"x":1245,"y":1231,"style":153},"18:04, naive",[91,1251],{"x1":1252,"y1":1236,"x2":1253,"y2":1236,"stroke":105,"style":63},"404","434",[97,1255],{"points":1256,"fill":130},"442,72 430,66 430,78",[49,1258],{"x":1259,"y":1260,"width":1261,"height":1262,"rx":1263,"fill":923,"stroke":924,"style":63},"450","28","334","88","13",[65,1265,1268],{"x":1266,"y":1267,"style":929},"617","54","in the workbook",[65,1270,1272],{"x":1266,"y":1271,"style":153},"76","cell value: 18:04 · no offset anywhere",[49,1274],{"x":1275,"y":1276,"width":1277,"height":1278,"rx":1279,"fill":134,"stroke":135},"486","84","262","24","6",[65,1281,1284],{"x":1266,"y":1282,"style":1283},"101","font-size:10.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","header: \"All times Europe\u002FBerlin\"",[1286,1287],"path",{"d":1288,"fill":1289,"stroke":135,"style":63},"M617 116 V 140 H 316 V 158","none",[97,1291],{"points":1292,"fill":1293},"316,166 309,154 323,154","#0f9488",[49,1295],{"x":1235,"y":977,"width":1296,"height":1297,"rx":1224,"fill":134,"stroke":135,"style":63},"268","52",[65,1299,1301],{"x":1245,"y":1300,"style":143},"188","tz_localize(zone from header)",[65,1303,1305],{"x":1245,"y":1304,"style":153},"207","then tz_convert(\"UTC\")",[91,1307],{"x1":1235,"y1":1308,"x2":1309,"y2":1308,"stroke":135,"style":63},"192","152",[97,1311],{"points":1312,"fill":1293},"144,192 156,186 156,198",[49,1314],{"x":57,"y":977,"width":1315,"height":1297,"rx":1224,"fill":61,"stroke":62,"style":63},"126",[65,1317,1320],{"x":1318,"y":1319,"style":1227},"77","197","UTC again",[10,1322,1323],{},"A naive timestamp read from Excel is meaningless until you re-apply the zone the file was written in. If step 3 put the zone in the sheet, the round trip is exact:",[171,1325,1327],{"className":209,"code":1326,"language":211,"meta":176,"style":176},"import pandas as pd\n\n# The label written at row 0; the table starts at row 1.\nheader = pd.read_excel(\"events.xlsx\", sheet_name=\"Events\", nrows=0, header=None)\nlabel = str(header.iloc[0, 0]) if not header.empty else \"\"\nzone = label.replace(\"All times\", \"\").strip() or \"UTC\"\n\ndf = pd.read_excel(\"events.xlsx\", sheet_name=\"Events\", skiprows=1)\n\n# Re-localise into the documented zone, then normalise to UTC for joining.\ndf[\"at\"] = (\n    df[\"at\"]\n    .dt.tz_localize(zone, ambiguous=False, nonexistent=\"shift_forward\")\n    .dt.tz_convert(\"UTC\")\n)\nprint(df[\"at\"].dtype)      # datetime64[ns, UTC]\n",[14,1328,1329,1339,1343,1348,1388,1425,1452,1456,1486,1490,1495,1509,1518,1541,1551,1555],{"__ignoreMap":176},[180,1330,1331,1333,1335,1337],{"class":91,"line":182},[180,1332,226],{"class":218},[180,1334,400],{"class":222},[180,1336,403],{"class":218},[180,1338,406],{"class":222},[180,1340,1341],{"class":91,"line":232},[180,1342,294],{"emptyLinePlaceholder":293},[180,1344,1345],{"class":91,"line":290},[180,1346,1347],{"class":374},"# The label written at row 0; the table starts at row 1.\n",[180,1349,1350,1353,1355,1358,1360,1362,1364,1366,1368,1370,1373,1375,1377,1379,1382,1384,1386],{"class":91,"line":297},[180,1351,1352],{"class":222},"header ",[180,1354,303],{"class":218},[180,1356,1357],{"class":222}," pd.read_excel(",[180,1359,636],{"class":189},[180,1361,340],{"class":222},[180,1363,676],{"class":362},[180,1365,303],{"class":218},[180,1367,681],{"class":189},[180,1369,340],{"class":222},[180,1371,1372],{"class":362},"nrows",[180,1374,303],{"class":218},[180,1376,51],{"class":235},[180,1378,340],{"class":222},[180,1380,1381],{"class":362},"header",[180,1383,303],{"class":218},[180,1385,613],{"class":235},[180,1387,584],{"class":222},[180,1389,1390,1393,1395,1398,1401,1403,1405,1407,1410,1413,1416,1419,1422],{"class":91,"line":309},[180,1391,1392],{"class":222},"label ",[180,1394,303],{"class":218},[180,1396,1397],{"class":235}," str",[180,1399,1400],{"class":222},"(header.iloc[",[180,1402,51],{"class":235},[180,1404,340],{"class":222},[180,1406,51],{"class":235},[180,1408,1409],{"class":222},"]) ",[180,1411,1412],{"class":218},"if",[180,1414,1415],{"class":218}," not",[180,1417,1418],{"class":222}," header.empty ",[180,1420,1421],{"class":218},"else",[180,1423,1424],{"class":189}," \"\"\n",[180,1426,1427,1430,1432,1435,1438,1440,1443,1446,1449],{"class":91,"line":320},[180,1428,1429],{"class":222},"zone ",[180,1431,303],{"class":218},[180,1433,1434],{"class":222}," label.replace(",[180,1436,1437],{"class":189},"\"All times\"",[180,1439,340],{"class":222},[180,1441,1442],{"class":189},"\"\"",[180,1444,1445],{"class":222},").strip() ",[180,1447,1448],{"class":218},"or",[180,1450,1451],{"class":189}," \"UTC\"\n",[180,1453,1454],{"class":91,"line":371},[180,1455,294],{"emptyLinePlaceholder":293},[180,1457,1458,1461,1463,1465,1467,1469,1471,1473,1475,1477,1480,1482,1484],{"class":91,"line":378},[180,1459,1460],{"class":222},"df ",[180,1462,303],{"class":218},[180,1464,1357],{"class":222},[180,1466,636],{"class":189},[180,1468,340],{"class":222},[180,1470,676],{"class":362},[180,1472,303],{"class":218},[180,1474,681],{"class":189},[180,1476,340],{"class":222},[180,1478,1479],{"class":362},"skiprows",[180,1481,303],{"class":218},[180,1483,701],{"class":235},[180,1485,584],{"class":222},[180,1487,1488],{"class":91,"line":477},[180,1489,294],{"emptyLinePlaceholder":293},[180,1491,1492],{"class":91,"line":494},[180,1493,1494],{"class":374},"# Re-localise into the documented zone, then normalise to UTC for joining.\n",[180,1496,1497,1500,1502,1504,1506],{"class":91,"line":500},[180,1498,1499],{"class":222},"df[",[180,1501,508],{"class":189},[180,1503,329],{"class":222},[180,1505,303],{"class":218},[180,1507,1508],{"class":222}," (\n",[180,1510,1511,1514,1516],{"class":91,"line":627},[180,1512,1513],{"class":222},"    df[",[180,1515,508],{"class":189},[180,1517,724],{"class":222},[180,1519,1520,1523,1526,1528,1530,1532,1535,1537,1539],{"class":91,"line":651},[180,1521,1522],{"class":222},"    .dt.tz_localize(zone, ",[180,1524,1525],{"class":362},"ambiguous",[180,1527,303],{"class":218},[180,1529,691],{"class":235},[180,1531,340],{"class":222},[180,1533,1534],{"class":362},"nonexistent",[180,1536,303],{"class":218},[180,1538,965],{"class":189},[180,1540,584],{"class":222},[180,1542,1543,1546,1549],{"class":91,"line":670},[180,1544,1545],{"class":222},"    .dt.tz_convert(",[180,1547,1548],{"class":189},"\"UTC\"",[180,1550,584],{"class":222},[180,1552,1553],{"class":91,"line":706},[180,1554,584],{"class":222},[180,1556,1557,1559,1562,1564,1566],{"class":91,"line":711},[180,1558,236],{"class":235},[180,1560,1561],{"class":222},"(df[",[180,1563,508],{"class":189},[180,1565,511],{"class":222},[180,1567,514],{"class":374},[10,1569,1570],{},"When rows genuinely originate in different zones, a label cannot describe them. Carry the zone per row instead:",[171,1572,1574],{"className":209,"code":1573,"language":211,"meta":176,"style":176},"import pandas as pd\n\ndf = pd.DataFrame({\n    \"site\": [\"berlin\", \"denver\", \"singapore\"],\n    \"local_time\": pd.to_datetime([\"2026-08-15 20:00\", \"2026-08-15 12:00\",\n                                  \"2026-08-16 02:00\"]),\n    \"zone\": [\"Europe\u002FBerlin\", \"America\u002FDenver\", \"Asia\u002FSingapore\"],\n})\n\n# groupby keeps each zone's rows together so tz_localize is vectorised per group.\ndf[\"utc\"] = (\n    df.groupby(\"zone\", group_keys=False)\n      .apply(lambda g: g[\"local_time\"].dt.tz_localize(g.name).dt.tz_convert(\"UTC\"))\n)\nprint(df[[\"site\", \"local_time\", \"zone\", \"utc\"]])\n",[14,1575,1576,1586,1590,1598,1620,1638,1646,1667,1671,1675,1680,1693,1712,1733,1737],{"__ignoreMap":176},[180,1577,1578,1580,1582,1584],{"class":91,"line":182},[180,1579,226],{"class":218},[180,1581,400],{"class":222},[180,1583,403],{"class":218},[180,1585,406],{"class":222},[180,1587,1588],{"class":91,"line":232},[180,1589,294],{"emptyLinePlaceholder":293},[180,1591,1592,1594,1596],{"class":91,"line":290},[180,1593,1460],{"class":222},[180,1595,303],{"class":218},[180,1597,420],{"class":222},[180,1599,1600,1603,1605,1608,1610,1613,1615,1618],{"class":91,"line":297},[180,1601,1602],{"class":189},"    \"site\"",[180,1604,428],{"class":222},[180,1606,1607],{"class":189},"\"berlin\"",[180,1609,340],{"class":222},[180,1611,1612],{"class":189},"\"denver\"",[180,1614,340],{"class":222},[180,1616,1617],{"class":189},"\"singapore\"",[180,1619,444],{"class":222},[180,1621,1622,1625,1628,1631,1633,1636],{"class":91,"line":309},[180,1623,1624],{"class":189},"    \"local_time\"",[180,1626,1627],{"class":222},": pd.to_datetime([",[180,1629,1630],{"class":189},"\"2026-08-15 20:00\"",[180,1632,340],{"class":222},[180,1634,1635],{"class":189},"\"2026-08-15 12:00\"",[180,1637,460],{"class":222},[180,1639,1640,1643],{"class":91,"line":320},[180,1641,1642],{"class":189},"                                  \"2026-08-16 02:00\"",[180,1644,1645],{"class":222},"]),\n",[180,1647,1648,1651,1653,1655,1657,1660,1662,1665],{"class":91,"line":371},[180,1649,1650],{"class":189},"    \"zone\"",[180,1652,428],{"class":222},[180,1654,242],{"class":189},[180,1656,340],{"class":222},[180,1658,1659],{"class":189},"\"America\u002FDenver\"",[180,1661,340],{"class":222},[180,1663,1664],{"class":189},"\"Asia\u002FSingapore\"",[180,1666,444],{"class":222},[180,1668,1669],{"class":91,"line":378},[180,1670,497],{"class":222},[180,1672,1673],{"class":91,"line":477},[180,1674,294],{"emptyLinePlaceholder":293},[180,1676,1677],{"class":91,"line":494},[180,1678,1679],{"class":374},"# groupby keeps each zone's rows together so tz_localize is vectorised per group.\n",[180,1681,1682,1684,1687,1689,1691],{"class":91,"line":500},[180,1683,1499],{"class":222},[180,1685,1686],{"class":189},"\"utc\"",[180,1688,329],{"class":222},[180,1690,303],{"class":218},[180,1692,1508],{"class":222},[180,1694,1695,1698,1701,1703,1706,1708,1710],{"class":91,"line":627},[180,1696,1697],{"class":222},"    df.groupby(",[180,1699,1700],{"class":189},"\"zone\"",[180,1702,340],{"class":222},[180,1704,1705],{"class":362},"group_keys",[180,1707,303],{"class":218},[180,1709,691],{"class":235},[180,1711,584],{"class":222},[180,1713,1714,1717,1720,1723,1726,1729,1731],{"class":91,"line":651},[180,1715,1716],{"class":222},"      .apply(",[180,1718,1719],{"class":218},"lambda",[180,1721,1722],{"class":222}," g: g[",[180,1724,1725],{"class":189},"\"local_time\"",[180,1727,1728],{"class":222},"].dt.tz_localize(g.name).dt.tz_convert(",[180,1730,1548],{"class":189},[180,1732,245],{"class":222},[180,1734,1735],{"class":91,"line":670},[180,1736,584],{"class":222},[180,1738,1739,1741,1744,1747,1749,1751,1753,1755,1757,1759],{"class":91,"line":706},[180,1740,236],{"class":235},[180,1742,1743],{"class":222},"(df[[",[180,1745,1746],{"class":189},"\"site\"",[180,1748,340],{"class":222},[180,1750,1725],{"class":189},[180,1752,340],{"class":222},[180,1754,1700],{"class":189},[180,1756,340],{"class":222},[180,1758,1686],{"class":189},[180,1760,1761],{"class":222},"]])\n",[10,1763,1764],{},"That two-column shape — a readable local time plus the IANA zone name — is the most robust thing you can put in a spreadsheet. It is readable by a human, and it reconstructs the exact instant for a machine.",[166,1766,1768],{"id":1767},"common-pitfalls-and-fixes","Common pitfalls and fixes",[818,1770,1771,1784],{},[821,1772,1773],{},[824,1774,1775,1778,1781],{},[827,1776,1777],{},"Symptom",[827,1779,1780],{},"Cause",[827,1782,1783],{},"Fix",[837,1785,1786,1801,1817,1837,1852,1863,1878,1889],{},[824,1787,1788,1793,1796],{},[842,1789,1790],{},[14,1791,1792],{},"ValueError: Excel does not support timezones",[842,1794,1795],{},"Writing an aware datetime",[842,1797,1798,1800],{},[14,1799,112],{}," after converting.",[824,1802,1803,1806,1814],{},[842,1804,1805],{},"Every timestamp shifted by the offset",[842,1807,1808,1810,1811,1813],{},[14,1809,815],{}," used where ",[14,1812,811],{}," was meant",[842,1815,1816],{},"Convert aware data; localise naive data.",[824,1818,1819,1824,1827],{},[842,1820,1821],{},[14,1822,1823],{},"AmbiguousTimeError",[842,1825,1826],{},"Autumn transition hour",[842,1828,1829,1830,1832,1833,1836],{},"Pass ",[14,1831,998],{}," explicitly, or ",[14,1834,1835],{},"\"NaT\""," to mark them.",[824,1838,1839,1844,1847],{},[842,1840,1841],{},[14,1842,1843],{},"NonExistentTimeError",[842,1845,1846],{},"Spring transition gap",[842,1848,1829,1849,27],{},[14,1850,1851],{},"nonexistent=\"shift_forward\"",[824,1853,1854,1857,1860],{},[842,1855,1856],{},"Nobody can tell what zone the file uses",[842,1858,1859],{},"Label step skipped",[842,1861,1862],{},"Write the zone into a header cell or a metadata sheet.",[824,1864,1865,1871,1874],{},[842,1866,1867,1870],{},[14,1868,1869],{},"ZoneInfoNotFoundError"," in a container",[842,1872,1873],{},"No system tz database",[842,1875,1876,27],{},[14,1877,251],{},[824,1879,1880,1883,1886],{},[842,1881,1882],{},"Times drift by seconds over a round trip",[842,1884,1885],{},"Binary fraction of a day",[842,1887,1888],{},"Round to the second after reading.",[824,1890,1891,1894,1897],{},[842,1892,1893],{},"Rows from different offices compared wrongly",[842,1895,1896],{},"One zone assumed for all",[842,1898,1899],{},"Carry a per-row zone column.",[166,1901,1903],{"id":1902},"performance-and-scale-notes","Performance and scale notes",[10,1905,1906,1907,1909],{},"Timezone conversion is vectorised and cheap — ",[14,1908,811],{}," on a million-row column is a single offset computation per distinct offset, not per row. The expensive operations are the ones that fall back to Python objects.",[10,1911,1912,1913,1916,1917,1920],{},"The ",[14,1914,1915],{},"groupby"," plus ",[14,1918,1919],{},"apply"," pattern above is the main one to watch: it runs once per distinct zone, which is fine for a handful of offices and slow for thousands of rows with high zone cardinality. When cardinality is high, map through the small set of distinct zones instead:",[171,1922,1924],{"className":209,"code":1923,"language":211,"meta":176,"style":176},"import pandas as pd\n\nout = pd.Series(pd.NaT, index=df.index, dtype=\"datetime64[ns, UTC]\")\nfor zone, idx in df.groupby(\"zone\").groups.items():\n    out.loc[idx] = (\n        df.loc[idx, \"local_time\"]\n          .dt.tz_localize(zone, ambiguous=False, nonexistent=\"shift_forward\")\n          .dt.tz_convert(\"UTC\")\n    )\ndf[\"utc\"] = out\n",[14,1925,1926,1936,1940,1967,1986,1995,2004,2025,2034,2039],{"__ignoreMap":176},[180,1927,1928,1930,1932,1934],{"class":91,"line":182},[180,1929,226],{"class":218},[180,1931,400],{"class":222},[180,1933,403],{"class":218},[180,1935,406],{"class":222},[180,1937,1938],{"class":91,"line":232},[180,1939,294],{"emptyLinePlaceholder":293},[180,1941,1942,1945,1947,1950,1952,1954,1957,1960,1962,1965],{"class":91,"line":290},[180,1943,1944],{"class":222},"out ",[180,1946,303],{"class":218},[180,1948,1949],{"class":222}," pd.Series(pd.NaT, ",[180,1951,686],{"class":362},[180,1953,303],{"class":218},[180,1955,1956],{"class":222},"df.index, ",[180,1958,1959],{"class":362},"dtype",[180,1961,303],{"class":218},[180,1963,1964],{"class":189},"\"datetime64[ns, UTC]\"",[180,1966,584],{"class":222},[180,1968,1969,1972,1975,1978,1981,1983],{"class":91,"line":297},[180,1970,1971],{"class":218},"for",[180,1973,1974],{"class":222}," zone, idx ",[180,1976,1977],{"class":218},"in",[180,1979,1980],{"class":222}," df.groupby(",[180,1982,1700],{"class":189},[180,1984,1985],{"class":222},").groups.items():\n",[180,1987,1988,1991,1993],{"class":91,"line":309},[180,1989,1990],{"class":222},"    out.loc[idx] ",[180,1992,303],{"class":218},[180,1994,1508],{"class":222},[180,1996,1997,2000,2002],{"class":91,"line":320},[180,1998,1999],{"class":222},"        df.loc[idx, ",[180,2001,1725],{"class":189},[180,2003,724],{"class":222},[180,2005,2006,2009,2011,2013,2015,2017,2019,2021,2023],{"class":91,"line":371},[180,2007,2008],{"class":222},"          .dt.tz_localize(zone, ",[180,2010,1525],{"class":362},[180,2012,303],{"class":218},[180,2014,691],{"class":235},[180,2016,340],{"class":222},[180,2018,1534],{"class":362},[180,2020,303],{"class":218},[180,2022,965],{"class":189},[180,2024,584],{"class":222},[180,2026,2027,2030,2032],{"class":91,"line":378},[180,2028,2029],{"class":222},"          .dt.tz_convert(",[180,2031,1548],{"class":189},[180,2033,584],{"class":222},[180,2035,2036],{"class":91,"line":477},[180,2037,2038],{"class":222},"    )\n",[180,2040,2041,2043,2045,2047,2049],{"class":91,"line":494},[180,2042,1499],{"class":222},[180,2044,1686],{"class":189},[180,2046,329],{"class":222},[180,2048,303],{"class":218},[180,2050,2051],{"class":222}," out\n",[10,2053,2054,2055,2057,2058,2061,2062,2064],{},"Two other habits keep large jobs fast. Convert once, at the boundary, rather than inside every function that touches the column — repeated ",[14,2056,811],{}," calls allocate a new array each time. And avoid ",[14,2059,2060],{},".dt.tz_localize"," inside an ",[14,2063,1919],{}," over rows; it constructs a fresh timezone object per call, which is roughly two orders of magnitude slower than the column-level operation.",[10,2066,2067,2068,2072],{},"For workbooks large enough that memory matters, do the timezone work chunk by chunk as you read, using the streaming approach in ",[23,2069,2071],{"href":2070},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-large-excel-files-in-python\u002Fread-large-excel-file-in-chunks-with-pandas\u002F","reading large Excel files in chunks with pandas",". Timezone conversion is stateless per row, so it parallelises across chunks with no coordination.",[166,2074,2076],{"id":2075},"conclusion","Conclusion",[10,2078,2079,2080,2082],{},"Excel cannot hold a timezone, so your script has to. Convert aware timestamps to the one zone your readers think in, strip the offset with ",[14,2081,112],{}," so openpyxl will accept them, and write the zone name into the sheet where a human will find it six months later. Keep upstream data in UTC wherever you can, because converting from an instant is always well defined while localising a wall clock is not. And when rows really do come from different places, carry the IANA zone name in its own column — that is the one representation that survives every round trip.",[166,2084,2086],{"id":2085},"frequently-asked-questions","Frequently asked questions",[10,2088,2089,2097],{},[2090,2091,2092,2093,2096],"strong",{},"Why does openpyxl raise ",[14,2094,2095],{},"ValueError"," on a timezone-aware datetime?","\nBecause the Excel file format has no field for an offset. Rather than silently discarding information, openpyxl refuses the write and makes you decide which zone the value represents.",[10,2099,2100,2103],{},[2090,2101,2102],{},"Should I store UTC or local time in the spreadsheet?","\nStore what the readers will reason about. Operational reports read by one office should carry that office's local time; anything joined with other systems or spanning regions should carry UTC. Whichever you pick, say so in the sheet.",[10,2105,2106,2109],{},[2090,2107,2108],{},"How do I record the zone so the file is self-describing?","\nPut it in a header cell above the table, in the sheet name, or in a dedicated metadata sheet. A separate column holding the IANA zone name works when rows genuinely come from different zones.",[10,2111,2112,2115,2116,812,2118,2120],{},[2090,2113,2114],{},"What happens on the night the clocks change?","\nLocal times become ambiguous or non-existent for one hour. Convert from UTC rather than localising local strings, and where you must localise, pass the ",[14,2117,1525],{},[14,2119,1534],{}," arguments explicitly instead of accepting the default.",[10,2122,2123,2126],{},[2090,2124,2125],{},"Can I keep the UTC offset in a separate column?","\nYes, and it is a good pattern. Write the local timestamp for reading plus a text column holding the IANA zone name, so the original instant can be reconstructed exactly.",[166,2128,2130],{"id":2129},"related","Related",[2132,2133,2134,2141,2148,2155,2161],"ul",{},[2135,2136,2137,2138,2140],"li",{},"Up to the parent: ",[23,2139,26],{"href":25}," — the storage model and the wider date toolkit.",[2135,2142,2143,2147],{},[23,2144,2146],{"href":2145},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Fparse-excel-dates-into-python-datetimes-with-pandas\u002F","Parse Excel Dates into Python datetimes with pandas"," — getting the column typed before you localise it.",[2135,2149,2150,2154],{},[23,2151,2153],{"href":2152},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Fgroup-excel-rows-by-month-and-quarter-with-pandas\u002F","Group Excel Rows by Month and Quarter with pandas"," — why the zone choice changes which month a late-evening row lands in.",[2135,2156,2157,2160],{},[23,2158,2159],{"href":1197},"Export SQL Query Results to Excel with Python"," — capturing UTC at the source.",[2135,2162,2163,2167],{},[23,2164,2166],{"href":2165},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Ffix-excel-serial-numbers-showing-instead-of-dates\u002F","Fix Excel Serial Numbers Showing Instead of Dates"," — making the stripped timestamps display properly.",[2169,2170,2171],"style",{},"html pre.shiki code .sMTad, html code.shiki .sMTad{--shiki-default:#6F42C1;--shiki-dark:#FFB757}html pre.shiki code .srMev, html code.shiki .srMev{--shiki-default:#032F62;--shiki-dark:#ADDCFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s-kum, html code.shiki .s-kum{--shiki-default:#D73A49;--shiki-dark:#FF9492}html pre.shiki code .skGVy, html code.shiki .skGVy{--shiki-default:#24292E;--shiki-dark:#F0F3F6}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .s-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}",{"title":176,"searchDepth":232,"depth":232,"links":2173},[2174,2175,2176,2177,2178,2179,2180,2181,2182,2183],{"id":168,"depth":232,"text":169},{"id":254,"depth":232,"text":255},{"id":517,"depth":232,"text":518},{"id":893,"depth":232,"text":894},{"id":1202,"depth":232,"text":1203},{"id":1767,"depth":232,"text":1768},{"id":1902,"depth":232,"text":1903},{"id":2075,"depth":232,"text":2076},{"id":2085,"depth":232,"text":2086},{"id":2129,"depth":232,"text":2130},"2026-08-15","Excel cannot store a timezone. Convert aware datetimes to one zone, strip the offset for writing, label the sheet, and re-localise correctly when reading the file back.","md",[2188,2191,2193,2195,2197],{"q":2189,"a":2190},"Why does openpyxl raise ValueError on a timezone-aware datetime?","Because the Excel file format has no field for an offset. Rather than silently discarding information, openpyxl refuses the write and makes you decide which zone the value represents.",{"q":2102,"a":2192},"Store what the readers will reason about. Operational reports read by one office should carry that office's local time; anything joined with other systems or spanning regions should carry UTC. Whichever you pick, say so in the sheet.",{"q":2108,"a":2194},"Put it in a header cell above the table, in the sheet name, or in a dedicated metadata sheet. A separate column holding the IANA zone name works when rows genuinely come from different zones.",{"q":2114,"a":2196},"Local times become ambiguous or non-existent for one hour. Convert from UTC rather than localising local strings, and where you must localise, pass the ambiguous and nonexistent arguments explicitly instead of accepting the default.",{"q":2125,"a":2198},"Yes, and it is a good pattern. Write the local timestamp for reading plus a text column holding the IANA zone name, so the original instant can be reconstructed exactly.",{},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Fhandle-timezones-in-excel-timestamps-with-python",{"title":2202,"description":2203},"Timezones in Excel Timestamps with Python (openpyxl)","Why openpyxl raises on aware datetimes, and the convert-strip-label pattern that keeps Excel timestamps unambiguous — plus DST pitfalls and reading the values back.","handle-timezones-in-excel-timestamps-with-python","advanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Fhandle-timezones-in-excel-timestamps-with-python\u002Findex","how-to","yjGLbHorNWLMtZnYYe8K6SZZTF5es3QM2WUdFB-8C4A",[2209,2212],{"title":2153,"path":2210,"stem":2211,"children":-1},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Fgroup-excel-rows-by-month-and-quarter-with-pandas","advanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Fgroup-excel-rows-by-month-and-quarter-with-pandas\u002Findex",{"title":2146,"path":2213,"stem":2214,"children":-1},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Fparse-excel-dates-into-python-datetimes-with-pandas","advanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Fparse-excel-dates-into-python-datetimes-with-pandas\u002Findex",1786800027129]