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.
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 😉
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:
Take on a backports and tzdata dependency on Python < 3.9 and migrate over to using the backport.
Take on a dependency to tzdata in python_version >= "3.9" and opportunistically upgrade to zoneinfo if present, otherwise use dateutil.tz.gettz.
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:
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.
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:
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()
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!
Activity
jklymak commented on May 29, 2020
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 😉
pganssle commented on May 29, 2020
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:
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:
I imagine no one is using POSIX strings in their rcParams, but if we're worried about that as well, we can always do:
tacaswell commented on Jun 17, 2020
I am in favor of option 3.
QuLogic commented on Feb 19, 2021
This is something that's wanted downstream in Fedora eventually: https://fedoraproject.org/wiki/Changes/DeprecatePytz
jklymak commented on Feb 19, 2021
Just bear in mind that our datetime support will not drop dateutil anytime soon because we depend on dateutil.rrule.
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:
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!
timhoffm commented on Jul 14, 2026
This should now be easy to adopt as zoneinfo is meanwhile available in all Python versions we support.