Get now() with local system timezone without knowing the timezone first

Currently now() and now(None) would return a naive datetime in the local system timezone (inherited from long ago), while now(tz) gets you an aware datetime in the timezone tz. What’s missing though is a way to get an aware datetime in the local system timezone. This value can be obtained in a roundabout way:

# Or use ZoneInfo("UTC") instead.
datetime.now(timezone.utc).astimezone()

Not exactly obvious, and also wastes some time re-calculating UTC offsets (not significant but technically unnecessary).


Since changing the behaviour of now(None) is likely off-limits, I can think of three possible interface additions; new ideas are very much welcomed as well.

  1. Add a nowtz() that always returns an aware datetime, matching the astimezone() interface. The name is inspired by timetz().
  2. Add a special sentinel LOCAL so now(LOCAL) returns an aware datetime in the local system timezone. This can also be used in astimezone(LOCAL) (behaving the same as astimezone(None)).
  3. Add a separate function to return the current local system timezone, for example timezone.local(), so we can write datetime.now(timezone.local()). This does not eliminate the UTC offset calculation, but provides a slightly more obvious interface than the currently available approach.
8 Likes

Another possibility is to add an interface to zoneinfo which can be used to give you a ZoneInfo object in your local timezone. It might even make sense to allow ZoneInfo() to take zero arguments, defaulting to the local timezone. Assuming (though I don’t know for sure) that there are system APIs on all the major OSes to return that information.

Could do datetime.now(local=True) and have local be keyword-only.

There are definitely options at the Python API level, and they aren’t necessarily mutually exclusive [1]. I haven’t researched it, but what are the system level APIs to get the current timezone on Linux, macOS, and Windows? Once you have that, and can translate that into zoneinfo names, it should be easy to add the APIs.


  1. like, you could have zero-arg ZoneInfo() under the hood of datetime.now(local=True) ↩︎

Depends on how you define timezone. All systems have a way to return the currently active offset and timezone code, but what exactly that timezone maps to a ZoneInfo is not guaranteed to be available. So adding an interface to ZoneInfo would not universally work (although could still be a good idea on platforms where it does work), while an interface on datetime (either on class datetime or timezone) would.

1 Like

Great point.

Looks like on macOS, this would be the relevant API?

Yes I think so. I have no idea whether that uses the same underlying system config as the C API though.

On Windows there’s GetDynamicTimeZoneInformation (I think this is the correct one). On Linux I think only localtime is guaranteed, which Python already exposes in time.localtime(), to return a code and an offset.

Does:

>>> import time
>>> time.tzname
('Pacific Standard Time', 'Pacific Daylight Time')

Work for getting ‘my timezone’?

The C runtime’s _tzset() function[1] calls GetTimeZoneInformation() to compute the time zone’s UTC offset and DST bias, but only if the “TZ” environment variable isn’t set. For example:

>>> time.strftime('%z (%Z)')
'+0000 (GMT Standard Time)'
>>> os.environ['TZ'] = 'ZZZ+10:00'
>>> ctypes.CDLL('ucrtbase')._tzset()
0
>>> time.strftime('%z (%Z)')
'-1000 (ZZZ)'

  1. For some unknown reason, Python’s time.tzset() has never been implemented on Windows. ↩︎

time.tzname tells you what timezone rule your system is configured to use, but does not tell you which timezone is active (DST or not) right now, which is needed for now. Another problem is the value it gives is system-dependent, may not be compatible to the IANA database, and thus cannot always be converted into a useful timezone offset.

Python daylight savings time - Stack Overflow shows how to tell if dst is active or not.

Combine that with my answer to get the timezone and a name lookup table of sorts and you’d probably get it working easier than trying to hit more low level apis directly.

On Windows, time zone names are non-standard (e.g. not “PST” or “PDT”), and they’re localized to the user’s preferred UI language. For example, with the preferred UI language and locale set to Japanese, check time.tzname and time.strftime('%Z')[1].

>>> import time, locale
>>> time.tzname
('太平洋標準時', '太平洋夏時間')
>>> locale.setlocale(locale.LC_CTYPE, 'ja_JP.utf8')
'ja_JP.utf8'
>>> locale.setlocale(locale.LC_TIME, 'ja_JP.utf8')
'ja_JP.utf8'
>>> time.strftime('%Z')
'太平洋標準時'

The C runtime provides functions to get the time zone variables that _tzset() computes.

>>> ucrt = ctypes.CDLL('ucrtbase', use_errno=True)
>>> timezone = ctypes.c_long()
>>> dstbias = ctypes.c_long()
>>> ucrt._get_timezone(ctypes.byref(timezone))
0
>>> ucrt._get_dstbias(ctypes.byref(dstbias))
0
>>> timezone.value
28800
>>> dstbias.value
-3600

  1. time.strftime() has serious issues with the locale encoding on Windows. There wouldn’t be an issue if Python used C wcsftime() instead of C strftime() on Windows (using C strftime() was a work around for a bug that hasn’t existed for a very long time). I had to modify both locale categories because time.strftime() incorrectly decodes the result using the encoding of the LC_CTYPE category, but C strftime() encodes the native wcsftime() wide-character result using the LC_TIME encoding. I had to modify the locale categories to explicitly use Japanese because the default locale, i.e. locale.setlocale(locale.LC_TIME, ""), combines the user locale (e.g. “Japanese_Japan”) with the encoding from the system locale, and my system locale is set to use 1252 as the ANSI code page. I have this crazy idea that if the runtime defaults to using the user locale, then it should default to using the code page that’s defined for that locale, i.e. code page 932 or UTF-8 for Japanese. Windows is multi-user, so it’s extremely bad design to assume that all users select a locale and preferred UI language that matches the system locale. ↩︎

I never said it’s impossible to get the timezone; I already propose a way to get a functional equivalent in the original post. The point of this thread is it’s not obvious, and using combining localtime with tzname is still much less obvious than using astimezone.

And if you want to use localtime in the first place, there is actually a tm_zone field to directly get the currently active timezone name, and a tm_offset to construct a datetime. This is what astimezone uses under the hood, in fact. Checking tzname is entirely unnecessary for this use case. (But getting the timezone name either localtime and tzname has the same IANA compatibility issue, as both Eryk and I mentioned.)

I presume you meant tm_gmtoff. A tm record in the Windows C runtime doesn’t have the tm_zone and tm_gmtoff fields that are found on BSD, macOS and Linux systems. These fields aren’t part of the POSIX standard. The local_timezone_from_timestamp() function thus has to take the long path of calling strftime("%Z") to get the time zone name, and it has to manually compute the UTC offset.

On Windows, we could call _get_timezone() to get the UTC offset. If tm_isdst is set in the local time, also add the DST bias from _get_dstbias(). We should use wcsftime(L"%Z", ...) to get the time zone name as a wide-character string, which avoids encoding problems[1].


  1. The time zone name from strftime("%Z") is potentially decoded incorrectly. The C runtime encodes the name using the LC_TIME encoding, but Python decodes it using the LC_CTYPE encoding. Even if both categories are set to the default locale, the C runtime’s default locale uses the ANSI code page of the system, while the time zone name is in the preferred UI language of whichever thread last called _tzset(). (The default UI language of a thread is that of the current user, but this can be overridden for the current process or current thread via SetProcessPreferredUILanguages() and SetThreadPreferredUILanguages().) It’s by no means guaranteed that the time zone name can be encoded using the system ANSI code page. ↩︎

I like this option out of all the options presented:

  • No exclusive parameter introduced, which helps with my mental model of the function
  • Removed redundant computation
  • No new methods introduced

As for the variable’s name, I prefer longer and more descriptive names (eg USE_LOCAL), but standard library devs seen to prefer shorter memnomics.

1 Like