 Thanks, everyone. As the partner said, I'm Paul Gansel. I'm a software developer in the New York office in Bloomberg. And I also maintain Python Datetail. So just a warning for this talk. I am dialectically challenged. And so it's really not going to be possible for me to mentally transform these Zs to Zeds. And I'm going to say it a lot. So you should just be prepared every time I say Z. Just think Zed. OK, so I'm just going to get right into it. Because as everyone knows, time zones, when you want to learn about them, they should probably take a half an hour, 40 minutes at most to learn. But I only have 20 minutes, so I'm going to go fast. So for starters, we have UTC. Just to get everyone on the same page, UTC is sort of the reference time zone. No one lives in UTC. No one really does anything in UTC. But it's supposed to be an easy time zone that you can transform your local times into. It's supposed to be monotonic, but it's really monotonic-ish. Because for some ridiculous reason, they decided that leap seconds should be part of civil time. Don't ask me why. Another concept that you should know about is time zones versus offsets. So right now, the offset from UTC in London is one hour, UTC plus one. That's an offset. Whether that's in London now or in France in six months makes no difference. That's always, if you see UTC plus one, just add or subtract, I always forget, one. And that gets you to UTC. Europe slash London is a time zone. It's a set of rules for transforming your local time into UTC. Now, what's BST? So if you print your current daytime, you get BST. Is that a time zone as an offset? It's kind of an offset in the sense that it only refers to the part of your London time that is one hour ahead of UTC. But the problem with these is that these are very highly context dependent. Because if you're here and you say BST, that means British summertime. If you're in Bangladesh, it means UTC plus six. And if you're in Papua New Guinea, it's Bougainville Standard Time, which is UTC plus 11. So as a general rule, people will like to see these if you display them. But don't count on them to mean anything. And don't try and parse them. So now here's everyone's favorite part, where I go through all the crazy edge cases. And honestly, there's not actually time to go through all the crazy edge cases. So I'm going to start us off easy with Australia. They have a non-integer number of hours offset in certain parts. Like, oh, that's real spooky. Then we move on to Nepal. They have not even an integer number of half hours. They're off on a 15 minute. And if you go into history in Liberia before 1979, they don't even have an integer number of minutes. It's off by 30 seconds. So you can say, OK, well, at least we know that daylight saving time, the only reason you're going to change is daylight saving time, right? Wrong. The base offset can also change. So in Portugal, in 1992, the Portuguese decided instead of being in Western European time, they wanted to be in Central European time. So they wanted to move over by one hour. And sensibly, they did this when they were already transitioning from summertime back to standard time. So what they did was they said, OK, in September of 1992, we're going to stop being on Western European summertime, and we're just going to start being on central time, which means that the UTC offset did not change, but the DST offset did change. So that can happen. And you can note that this first change didn't go so well. They were having sunsets that were after midnight. So about four years later, they did the same thing in reverse, with another offset change, DST change with no offset change. So OK, maybe you say, all right, well, yeah, there's that one weird edge case. But maybe at least if you're on daylight saving time, you're only going to transition once per year, right? Incorrect. In Morocco, since 2012, what happens is they go onto daylight saving time, like many people do, in the spring. So you have your standard transition there. And then at the end of July, they go back to standard time for about one month. And then they go back onto daylight saving time. So what's going on here? Well, Morocco has a large Muslim population. And Ramadan falls in the middle of summer. And in Ramadan, Muslims aren't allowed to eat until sunset. And so their solution to this is to say, OK, we'll just have sunset an hour earlier during Ramadan. So you have to keep that in mind. You can say, OK, well, all right, we have that. But everyone at least does daylight saving time at 2 in the morning or 1 in the morning. So I'm never going to see an ambiguous date time that is in the middle of the day, right? Incorrect. In Kiribati, or Christmas Island, in 1994, the Kiribatians, they decided that they preferred one trading partner, the other Australia, or the US. I forget which. I'm terrible at details. But essentially what they said was they live right on the international date line. So they said, all right, we're just going to switch from being on one side of the date line to the other. So we're just going to skip December 31. It's just like December 30, 1159. And then one minute later, happy New Year. So you say, OK, maybe. Yeah, that happened. It was the 90s. We were watching a lot of Seinfeld, water cooler talk. But no, it can happen today. It happened, again, in Samoa in 2011. And if you go back pretty far in time, you can see in the Quadrillion Atoll, they went the other way. So in 1969, there were two September 30 in the Quadrillion Atoll. So OK, now you say, all right, fine. Yeah, the offsets are all crazy. The people will do crazy stuff with their offsets. But at least we know that if we're in one place, there will always be one time zone. I can always say what the time is in a certain time. China, in an effort to rationalize their time zones, said we're going to have one time zone throughout all of China. It's going to be UTC plus 8. So over here in Beijing, that's pretty good. If you're over here in Xinjiang, that's not so great, because the sun rises at 4 in the morning. So in Xinjiang, a lot of people start using what's called Xinjiang time, which is UTC plus 6. But the Chinese government doesn't recognize Xinjiang time and doesn't really care for it. So all the trains still run on UTC plus 8, and all the planes do, and all the official offices. And if you look on Wikipedia, it seems that the breakdown between whether you use Xinjiang time or Beijing time is your race. If you're part of the Uyghur minority, you use Xinjiang time. So this is a racial time zone. Get that through GDPR. All right, so now I thoroughly scared you. Time zones are terrifying. They're crazy. Why should we work with them at all? Why can't we just go UTC for everything? Well, you can imagine that it would be a common occurrence in, say, Bloomberg to say, I would like to know when trading ended in New York on any given day. So I can write this nice recurrence rule that says Friday, 5 o'clock, trading ends. I could maybe say Friday, 10 o'clock UTC, but is that really going to work when I change my UTC offset because of daylight saving time? It's not, right? So I still have to say, there are still times where there's something important in looking at the civil time. The civil time is 5 o'clock. That's what people actually use. And when you want to find out when that time actually is, you need to make the mapping there. So how does Python deal with this? So Python has a very specific time zone model. And the way it works is there's an abstract-based class called tzinfo. This is one of these z's. And you're supposed to subclass it. And your subclass should implement three functions, tzname, UTC offset, and dst. And these are functions of the naive daytime. So it's supposed to be one sort of singleton object that represents the set of rules. And those rules are implemented as functions. So here's an example. This is Eastern Time. I've implemented it. I have this little isdaylight function that uses actual US rules. And if I go on one side of the daylight saving time transition and then on the other, I get the appropriate thing, right? So then, if I go through this appropriate rules type situation, I say, all right, now I go from UTC. I get the first one, the Eastern Standard Time. And then I go one hour ahead in UTC. And now it shows up, instead of as 130 minus 5, it shows up as 230 minus 5. So what's up with that? So to explain why that's broken, you have to understand ambiguous times. So ambiguous times are times where the wall time occurs twice. As you remember, the functions that you're implementing are functions of the wall time. So obviously, if you have two wall times that are differentiated only by their offset, there's no function that you can write that will map wall times to the correct offset. So here's an example. You have two 130s, one on one side and one on the other. So how does this work in Python? Up until maybe a year or two ago, it just didn't. It was just broken. It was like, you just can't do that. But Python 3.6 introduced the fold attribute with PEP 4.95. And the fold is a property of the date time that tells you which side of any given fold that you're on. So here, I have this implementation that says 130, and then I pass it fold. And if I put fold equals 0, it's on the daylight time because that's the first one that occurred. And if I pass it fold equals 1, that's the second time that time occurred. And so I can write a function that takes this fold and turns it into an offset. So now I'm going to take a brief digression into the semantics of comparison. So if you have two day times and you want to know, are they the same daytime? What does that mean? In Python, what it means is that if they're in the same zone, and this is a bit complicated, but if they're in the same zone, what it does is it imagines that they're not in any zone at all. It just takes the naive portions and compares them, which is a fine rule when you don't have ambiguous day times because that always means the same thing. When you do have ambiguous day times, you have to decide whether the fold is part of the thing you're comparing or not. And I don't know why this decision was made, but it was decided that that is not part of what you're comparing. So if you have two things, you have your 130 minus 4 and 130 minus 5, those are considered equal because the naive portion of the daytime is the same. Now, if you have different zones, those are still considered comparable. And what happens is they just convert them both to UTC and compare that. So you can have a situation like this, where it's almost exactly an identical situation where the naive portion is the same and the offset is different. But the difference is that one of them is in New York and the other one is in Chicago. Whereas in the previous example, they were both in New York. So in Chicago, because they're in different zones, they get converted with the offset. And then the other problem here is that I guess there's some kind of weird thing with hash invariance. I really can't explain this. I don't know why I mentioned it in the slides. But if either daytime is ambiguous, Python just throws up its hands. It's like, no, I don't know. They're not the same, whatever. Maybe who cares? Really, come on. You're trying to pull a fast one on me? Give me an ambiguous daytime? No way, buddy. So here's a curious case. This came into the issue tracker. So I have this x equals daytime. It's March 25th at 1 AM in London. I convert it to a UTC offset. I'm now UTC an epoch time. So I convert it to a timestamp. And then I convert it back. And I put it in the same time zone, same exact time zone object. And then I create another one where I just get a new instance of the same time zone, still Europe-London. So now I'm going to compare these. They should all be true, because they all represent the same time. So x equals y false, not off to a great start. x equals z true, maybe a little better, but that's still a little puzzling. But I guess that means y equals z is false, right? Oh, no, wait, it's true. So what's happening here? Why is there this non-transitive comparison property of time zones? So this comes down to what's known as imaginary times. So we already talked about ambiguous times. Now there can be a gap on the other side when you go from standard to daylight. And so I have an example here of where you jump from 1 to 3 AM. So now let's look at the values from our original example. So we have this 1 AM. It's 1 AM plus 1. But if you try and turn that into UTC, if you go to that same time at UTC, it's midnight. And then one second later, or one hour later, it's 2 AM. So that 1 AM didn't actually exist. You can just construct a non-existent time. But why was it non-transitive? So we can assume that it's some sort of undefined behavior otherwise, but why was it not transitive? So if you look here, you see that when you take the London date, you turn it to UCC, you get this midnight date. And when you turn it back, you also get midnight at a different offset, but it's in Europe. Now when you look at y and z, y and z are both the existing time, the UTC time. So you maybe know where this is going. If you look at x.tzinfo and y.tzinfo, they're the same object. So what's the rule for the same object? You compare the naive portion. So the x has 1 AM, y has midnight, and so they're not the same. But when you go x versus z, they're actually different objects. And that's what Python uses to determine whether two day times are in the same time zone is the is relationship. And so what happens there is they go, oh, these are not the same time zone. Let's convert them to UTC. And x can go to UTC just fine. It's just UTC that can't go to x. And so they both go to UTC, they get the same daytime, and they're the same. So that's your non-transitive daytime. So how do you work with actual time zone objects? I don't think I'm going to have time to go into the details of how to use date utils, particular time zones, but I'll tell you right now I recommend that you use date util over PyTZ. PyTZ was doing the right thing for like 14 years before date util was even able to do the right thing. But now date util is able to do the right thing, and it's a little more pythonic. So if you take, date util provides a number of time zone objects. I'm going to take this one that represents specific time. I can just pass it to the constructor as you would expect from the documentation. So you set tz info equals this thing, and it works just fine. Now if I want to take that and just keep the wall time, but say this is actually a wall time that's not in Pacific time, it's in Eastern time, I can use the dot replace. And that just sort of drops off the tz info and gives me a new date time with Eastern time in it. But if you want to know the same moment in time represented in Eastern time, so I want to say you're scheduling a call, and it's represented in Pacific time, you use as time zone. And what as time zone does is it goes to UTC, and then it goes to whatever other time zone you told it to go to. So that's how it works with date util zones. Now with pi tz, if you attach a pi tz zone, if you take a date time and you do as time zone for a pi tz zone, it'll work just fine. It's exactly what you expect. But if you construct a date time directly that way, or use replace as I mentioned, it fails in this horrible way. You see this should say Eastern standard time or Eastern daylight time or something, but it says LMT. The offset is minus 4.93 hours. What is that? Well, that's local mean time. That's what it was before time zones were standardized, and that's what it was like in New York. And pi tz has a strange time zone model. All of pi tz tz infos that get attached to date times are static offsets. So what happens is pi tz will just take the date time, figure out what offset to attach, and then attach it directly to the date time. This has some advantages, such as being able to disambiguate between ambiguous times and imaginary times as you're attaching the date time. So what they do is they take the time zone object, then they localize to the date time, and that attaches the right one. But if you do some arithmetic on it, like you add six months, there's no hooks for it to update that information. And so what happens is it just still keeps returning the daylight time. So you have to use the time zone object again, and you do normalize, which is essentially saying, figure out why this is wrong and do the right thing. I have way more detail on this in my blog posts on blog.cancel.io. It's called pi tz, the fastest foot gun in the West. So how do you handle ambiguous times? So if you're using the as time zone, where you have something that's already UTC and you want it as an aware date time, that'll just work with both day util and pi tz. Pi tz will set the fold attribute. Pi tz will not, because pi tz doesn't need the fold attribute and doesn't do anything with it. If you're working in something before 3.6, and who really is doing that these days, then day util provides a backwards compatibility mode that you can use the tz.infold method. You pass it a date time, and then you pass it what value fold is gonna be. The default is just setting it to one. It'll set the fold method if you're in 3.6 plus, or if it's not, if you're in 2.7, it'll create this sort of daytime subclass that has the fold attribute back ported. So this has most of the same semantics, and you can use it as appropriate. And also, as time zone, we'll still work that way. If you wanna detect ambiguous times, day util has to detect ambiguous, and so, or it's just called daytime ambiguous. So you pass it a daytime, it tells you whether or not it's ambiguous. If you want, so if you wanna say something, you have it coming in, say you're adding an hour each time, and if it's an ambiguous daytime, you wanna be on one side or the other, you wanna be on the standard side, you can sort of say, if it's ambiguous, set fold to one. But you actually don't have to do this because the spec says that if fold is one on a non-ambiguous time or a non-gap time, then what you get is just whatever the value is. It has no effect. So you can just set fold equals one all the time, and it'll get you standard. But if you want it to be neat and not see folds, then don't set fold equals one. So handling imaginary times, day util provides a daytime exists. It's the same as a daytime ambiguous. You ask if a daytime exists. If it exists, it'll say true, if not false. Generally, the kind of hard thing to do is if you're sort of adding one hour at a time to an aware daytime, and you're at 130, you go one hour ahead, it should be 330 instead of 230, then the thing that you usually wanna do is not skip back, you wanna skip forward by an hour. So day util added during one of the sprints by this young lady right here in the second row has this resolve imaginary function. You pass it something, and if it's not imaginary, it just gives you the same thing back. If it is imaginary, it skips forward by whatever the gap was. So if that gap is one hour, it skips forward one hour, and in the case of Christmas Island, if it's one day, you skip forward one day. And both these functions will actually work just fine with pi tz time zones. All right, so I'm gonna skip over this part because I wanna be able to take questions. Just a parting response. A lot of people ask me things about what should I do with time zones and things like that, and I'm happy afterwards to take all kinds of questions. I could talk about this stuff for hours. The things that I would keep in mind are civil times versus timestamps. So a civil time is the time on the wall. If you have something where it matters what the time on the wall is, such as a meeting, almost every date that you care about in the future, other than the asteroid will hit on Monday at 2 a.m. UTC. If it's human related, store it as actually a local time with the time zone. Don't convert it to UTC because that mapping is not stable. You could, as was in the case of Egypt all the time when they decide whether or not to have daylight saving time or whether to have daylight saving time during Ramadan or not, they will give you three days notice. Your phone is not necessarily gonna be updated there. You could have scheduled the meeting three days ahead of time, even if you managed to update your phone correctly. If you stored it in UTC, it's now off by an hour. So for things in the past, you can store them as UTC just fine. As long as you care about the time between two events or how long ago it happened, like the exact time that it happened, you can store it in UTC or UTC with an offset. And with the INA, never rely on these three letter abbreviations. You can use the INA keys, but if you're localizing, like if you wanna show someone something that's some time in Ukraine, you probably wanna use CLDR because those keys are not intended to really be human readable. All right, so that's it. You can try and help with DayUtil. I'm open for a lot of pull requests. We've been running sprints here and they've been going fantastically well. Check out my website or my blog. And thanks to Bloomberg for flying me out here and hosting this conference and everything. All right. So I think we have two minutes for questions. I just wanna thank you, Paul. That was quite interesting, especially because I'm sure we all have one or two nightmare stories about working with time zones. Anyone? Yeah. Thanks for talking. What have been some of the practical ways you've faced or overcome some of these challenges in your work here? Well, so to be honest, I hit most of these problems when people come to the issue tracker of DayUtil, which is why I'm always old hat by the time I have to do something. If I ever have to use this stuff myself, it's like I know where the pitfalls are because I had to fix 10 other people's bugs. A lot of times it comes down to just making assumptions it comes down to making assumptions about what the time zones are doing, not really understanding the model. And after every time I give this talk or any related talk, a lightning talk or anything, what happens is someone comes after me afterwards and says, oh, I have some bugs in prod because I've been using PyTZ by just attaching it to the TZ info. So watch out for that. Like do a search, a control F over your whole code base for PyTZ time zones and check to see that they're not being used that way. Okay, I think that's all we have for time. You can just find Paul afterwards if you have any more questions. We have a five minute break and then we'll start the next session.