FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

Use zoneinfo / backports.zoneinfo for time zones instead of dateutil.tz? · Issue #17538 · matplotlib/matplotlib · GitHub

Repository navigation

Use zoneinfo / backports.zoneinfo for time zones instead of dateutil.tz? #17538

Open

Description

PEP 615 introduces a new zoneinfo module into the standard library starting in Python 3.9. A backport to Python 3.6 is also available as backports.zoneinfo.

I don't think any support for pytz or dateutil.tz zones needs to be dropped, but as was done in #10443, I think we can start using it as the default time zone provider.

Upsides:
The biggest upside to zoneinfo is that it is much faster than dateutil (and also faster than pytz). dateutil is already a dependency for other reasons, so the fact that eventually relying on zoneinfo will not require a third-party dependency is more or less moot in this case. zoneinfo also has some nicer properties around where it searches for time zones, which is configurable in several ways. This will hopefully also be mooted in dateutil >= 3.0, since I plan to refactor dateutil to use essentially the same search path as zoneinfo when possible.

Additionally, zoneinfo currently supports Version 2 TZif files (including dates after 2038), whereas dateutil only uses the Version 1 subset. This will hopefully change in a future version of dateutil as well.

Downsides:
The downsides of switching to backports.zoneinfo is that (in Python <3.9) it will add a new dependency, and specifically one that is in a namespace package (backports.) and has a C extension. This shouldn't be a big deal considering how many of matplotlib's dependencies have C extensions (this one is particularly easy to build), but it is not uncommon for this sort of thing to add complications. You would also likely want a dependency on tzdata as well (though this will not be new because in dateutil >= 3.0, dateutil will be adding a dependency on that package).

On earlier versions of Python, it also brings in a dependency on importlib_resources (which itself depends on importlib_metadata and zipp). On newer versions of Python, these are in the standard library.

The last downside is that this introduces a semantic change you'd want to think about. Right now, tz.gettz() is used, and I believe the assumption is that you'd set your time zone to an IANA zone (since previously it was calling pytz.timezone), but dateutil.tz.gettz actually accepts a few other types of time zones, like POSIX TZ string formats like EST5EDT and such, plus it has the (unfortunate, in my mind) behavior that if it cannot find a time zone, it will return None, whereas ZoneInfo("Amreica/New_York") will raise an exception. We can pretty easily keep that behavior if desired.

Summary

The case for dropping dateutil in favor of zoneinfo is a lot less clear than the case for dropping pytz for dateutil was, because on >Python 3.9, this introduces rather than removes new dependencies. However, the case for using zoneinfo will get a lot stronger as more and more people adopt Python 3.9; it might not be a bad thing to get ahead of the curve there.

Another option might be adding this as an optional "extras" dependency, so that first you try zoneinfo, then backports.zoneinfo and if neither of those exists, use dateutil.tz.

Activity

  1. jklymak commented on May 29, 2020

    Member

    I don't know who among the core devs actually ever uses timezones. I certainly do not, it is usual in science to just use UTC and make your life a lot simpler. So from my point of view the only concern here is packaging, but otherwise we kind of depend on you for advice on timezones 😉

  2. pganssle commented on May 29, 2020

    MemberAuthor

    So from my point of view the only concern here is packaging, but otherwise we kind of depend on you for advice on timezones

    Well, I'm somewhat biased, in that zoneinfo is my big initiative and I'm eager to get people adopting it.

    I think the big question is the project's appetite for (at least temporarily) expanding the dependency tree in this way. I've tried to make it so that zoneinfo is the hand's down winner when choosing among the standard players — it should be as good or better than dateutil or pytz on every dimension in Python 3.9+. The three best options I see are:

    1. Take on a backports and tzdata dependency on Python < 3.9 and migrate over to using the backport.
    2. Take on a dependency to tzdata in python_version >= "3.9" and opportunistically upgrade to zoneinfo if present, otherwise use dateutil.tz.gettz.
    3. Same as option 2, but add a zoneinfo extra (or something of that nature) that pulls in backports.tzdata on < 3.9+ and tzdata unconditionally, then opportunistically upgrade to backports.zoneinfo / zoneinfo.

    With regards to the semantic differences between tz.gettz(s) and ZoneInfo(s), I think the most likely time someone will notice that something has changed is if:

    1. They were relying on something like EST working because that's one of the abbreviations on their local machine. They should probably be corrected on this anyway, because it is not reliable.
    2. They were relying on the fact that dateutil.tz.gettz() returns None for malformed keys. If this is really a common use case and matplotlib wants to support it, we can just do try/except around the zoneinfo call.

    I imagine no one is using POSIX strings in their rcParams, but if we're worried about that as well, we can always do:

    try:
        return ZoneInfo(s)
    except zoneinfo.ZoneInfoNotFound:
        return dateutil.tz.gettz(s)
  3. added this to the v3.4.0 milestone on Jun 17, 2020
  4. tacaswell commented on Jun 17, 2020

    Member

    I am in favor of option 3.

  5. modified the milestones: v3.4.0, v3.5.0 on Jan 27, 2021
  6. QuLogic commented on Feb 19, 2021

    Member

    This is something that's wanted downstream in Fedora eventually: https://fedoraproject.org/wiki/Changes/DeprecatePytz

  7. jklymak commented on Feb 19, 2021

    Member

    Just bear in mind that our datetime support will not drop dateutil anytime soon because we depend on dateutil.rrule.

  8. modified the milestones: v3.5.0, v3.6.0 on Sep 25, 2021
  9. modified the milestones: v3.6.0, v3.7.0 on Aug 24, 2022
  10. matzl commented on May 25, 2023

    It is currently not well possible to plot date-related data with localized dates after 2038 in many regions.

    Minimal example of my problem with dates after 2038:

    import pandas as pd
    from datetime import datetime
    import matplotlib.pyplot as plt
    import matplotlib.dates as mdates
    from zoneinfo import ZoneInfo
    
    TARGET_TIMEZONE = 'Europe/Berlin'
    plt.rcParams['timezone'] = TARGET_TIMEZONE
    
    timestamps = [[2066162400, 2066166000, 2066169600, 2066173200, 2066176800, 2066180400, 2066184000, 2066187600, 2066191200, 2066194800, 2066198400, 2066202000, 2066205600, 2066209200, 2066212800, 2066216400, 2066220000, 2066223600, 2066227200, 2066230800, 2066234400, 2066238000, 2066241600, 2066245200, 2066248800],
                  [2381781600, 2381785200, 2381788800, 2381792400, 2381796000, 2381799600, 2381803200, 2381806800, 2381810400, 2381814000, 2381817600, 2381821200, 2381824800, 2381828400, 2381832000, 2381835600, 2381839200, 2381842800, 2381846400, 2381850000, 2381853600, 2381857200, 2381860800, 2381864400, 2381868000]]
    # Sat Jun 23 2035 00:00:00 GMT+0200 to Sun Jun 24 2035 00:00:00 GMT+0200 (Central European Summer Time)
    # Fri Jun 23 2045 00:00:00 GMT+0200 to Sat Jun 24 2045 00:00:00 GMT+0200 (Central European Summer Time)
    
    data = [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.001555, 0.021241, 0.081962, 0.182555, 0.312131, 0.424202, 0.505196, 0.547307, 0.560841, 0.549523, 0.510746, 0.448988, 0.355547, 0.234808, 0.113397, 0.039914, 0.006440, 0.000016, 0.000000, 0.000000]
    
    
    if __name__ == "__main__":
        NUM_SUBPLOTS = len(timestamps)
        fig, axes = plt.subplots(1, NUM_SUBPLOTS)
        lim_x = [[0, 0]] * NUM_SUBPLOTS
    
        for i in range(0, NUM_SUBPLOTS):
            # conversion with pd.to_datetime(..., unit="s") and tz_localize('UTC') and tz_convert(TARGET_TIMEZONE) will fail
            time_df = pd.DataFrame(index=timestamps[i], columns=["datetime"])
            for k, this_time in time_df.iterrows():
                time_df.loc[k, "datetime"] = datetime.fromtimestamp(k, ZoneInfo(TARGET_TIMEZONE))
    
            axes[i].plot(time_df, data)
    
            axes[i].xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m-%d, %H:%M'))
            axes[i].grid(which='major')
            axes[i].grid(which='minor', alpha=0.2)
            axes[i].xaxis.set_major_locator(mdates.HourLocator(byhour=range(0, 24, 6)))
            axes[i].xaxis.set_minor_locator(mdates.HourLocator(byhour=range(0, 24, 1)))
            for tick in axes[i].get_xticklabels():
                tick.set_rotation(90)
            axes[i].set_xlim([time_df["datetime"].iloc[0], time_df["datetime"].iloc[-1]])
    
        fig.tight_layout()
        plt.show()
    

    Returns:

    Note how the start/end time and the ticks are wrong.

    EDIT - found this workaround: Don't plot in the destination/localized timezone but instead shift the datetime to UTC and plot in UTC. So basically matplotlib plots the same data but without the timezone issues - the result looks as expected, no timezone glitch after 2038:

    import pandas as pd
    from datetime import datetime
    import matplotlib.pyplot as plt
    import matplotlib.dates as mdates
    from zoneinfo import ZoneInfo
    
    TARGET_TIMEZONE = 'Europe/Berlin'
    
    # # ORIGINAL VERSION START
    # plt.rcParams['timezone'] = TARGET_TIMEZONE
    # # ORIGINAL VERSION END
    # WORKAROUND PART 1 START
    plt.rcParams['timezone'] = 'UTC'
    # WORKAROUND PART 1 END
    
    timestamps = [[2066162400, 2066166000, 2066169600, 2066173200, 2066176800, 2066180400, 2066184000, 2066187600, 2066191200, 2066194800, 2066198400, 2066202000, 2066205600, 2066209200, 2066212800, 2066216400, 2066220000, 2066223600, 2066227200, 2066230800, 2066234400, 2066238000, 2066241600, 2066245200, 2066248800],
                  [2381781600, 2381785200, 2381788800, 2381792400, 2381796000, 2381799600, 2381803200, 2381806800, 2381810400, 2381814000, 2381817600, 2381821200, 2381824800, 2381828400, 2381832000, 2381835600, 2381839200, 2381842800, 2381846400, 2381850000, 2381853600, 2381857200, 2381860800, 2381864400, 2381868000]]
    # Sat Jun 23 2035 00:00:00 GMT+0200 to Sun Jun 24 2035 00:00:00 GMT+0200 (Central European Summer Time)
    # Fri Jun 23 2045 00:00:00 GMT+0200 to Sat Jun 24 2045 00:00:00 GMT+0200 (Central European Summer Time)
    
    data = [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.001555, 0.021241, 0.081962, 0.182555, 0.312131, 0.424202, 0.505196, 0.547307, 0.560841, 0.549523, 0.510746, 0.448988, 0.355547, 0.234808, 0.113397, 0.039914, 0.006440, 0.000016, 0.000000, 0.000000]
    
    
    if __name__ == "__main__":
        NUM_SUBPLOTS = len(timestamps)
        fig, axes = plt.subplots(1, NUM_SUBPLOTS)
        lim_x = [[0, 0]] * NUM_SUBPLOTS
    
        for i in range(0, NUM_SUBPLOTS):
            # conversion with pd.to_datetime(..., unit="s") and tz_localize('UTC') and tz_convert(TARGET_TIMEZONE) will fail
            time_df = pd.DataFrame(index=timestamps[i], columns=["datetime"])
            for k, this_time in time_df.iterrows():
                # # ORIGINAL VERSION START
                # time_df.loc[k, "datetime"] = datetime.fromtimestamp(k, ZoneInfo(TARGET_TIMEZONE))
                # # ORIGINAL VERSION END
                # WORKAROUND PART 2 START
                tmp_dt = datetime.fromtimestamp(k, ZoneInfo(TARGET_TIMEZONE))
                time_df.loc[k, "datetime"] = datetime.fromtimestamp(k + tmp_dt.utcoffset().seconds, ZoneInfo('UTC'))
                # WORKAROUND PART 2 END
    
            axes[i].plot(time_df, data)
    
            axes[i].xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m-%d, %H:%M'))
            axes[i].grid(which='major')
            axes[i].grid(which='minor', alpha=0.2)
            axes[i].xaxis.set_major_locator(mdates.HourLocator(byhour=range(0, 24, 6)))
            axes[i].xaxis.set_minor_locator(mdates.HourLocator(byhour=range(0, 24, 1)))
            for tick in axes[i].get_xticklabels():
                tick.set_rotation(90)
            axes[i].set_xlim([time_df["datetime"].iloc[0], time_df["datetime"].iloc[-1]])
    
        fig.tight_layout()
        plt.show()
    
    

    Result:

  11. github-actions commented on Jul 13, 2026

    This issue has been marked "inactive" because it has been 365 days since the last comment. If this issue is still present in recent Matplotlib releases, or the feature request is still wanted, please leave a comment and this label will be removed. If there are no updates in another 30 days, this issue will be automatically closed, but you are free to re-open or create a new issue if needed. We value issue reports, and this procedure is meant to help us resurface and prioritize issues that have not been addressed yet, not make them disappear. Thanks for your help!

  12. timhoffm commented on Jul 14, 2026

    Member

    This should now be easy to adopt as zoneinfo is meanwhile available in all Python versions we support.

  13. added
    keepItems to be ignored by the “Stale” Github Action
    and removed
    status: inactiveMarked by the “Stale” Github Action
    on Jul 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    keepItems to be ignored by the “Stale” Github Actiontopic: date handling

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions


      Back | FazBrowse Home | New Git URL