Skip to content

thread-safety issue in pure-Python zoneinfo cache on 3.13t #142763

Closed

Description

@ariebovenberg

Bug report

Bug description:

Summary

There seems to be a race condition in the cache underlying the pure Python ZoneInfo implementation, which causes errors in the latest 3.13t builds. It appears to have been fixed indirectly in 3.14t. Since 3.13t is still in bugfix mode, it might be worth patching.

I’m running on 3.13.11t specifically, under MacOS 26.2

Details

The zoneinfo cache uses an OrderedDict. Unlike dict, AFAIK OrderedDict is not thread-safe (at least, its methods defined in Python are not):

cls._strong_cache.popitem(last=False)

The popitem() method is defined in Python, and runs into problems when hit concurrently.

A minimal-ish demo. When run, it results in lots of KeyError and it often doesn’t terminate. In rare cases it even segfaults.

  File "/Users/arie/code/sandbox313-nogil/zoneinfo_crash.py", line 52, in touch_timezones
    zdt = ZoneInfo(tz)
  File "/Users/arie/.pyenv/versions/3.13.11t/lib/python3.13t/zoneinfo/_zoneinfo.py", line 50, in __new__
    cls._strong_cache.popitem(last=False)
    ~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^
KeyError: 'Africa/Brazzaville'
from threading import Thread
from zoneinfo._zoneinfo import ZoneInfo  # the pure Python version

NUM_THREADS = 16
NUM_ITERATIONS = 100
TIMEZONE_SAMPLE = [
    "UTC",
    "America/Guyana",
    "Etc/GMT-11",
    "Europe/Vienna",
    "America/Rainy_River",
    "Asia/Ulaanbaatar",
    "US/Alaska",
    "America/Rankin_Inlet",
    "Arctic/Longyearbyen",
    "Pacific/Bougainville",
    "Africa/Monrovia",
    "Europe/Copenhagen",
    "America/Hermosillo",
    "Africa/Brazzaville",
    "Asia/Tashkent",
    "Pacific/Saipan",
    "Europe/Tallinn",
    "Europe/Uzhgorod",
    "Africa/Nairobi",
    "America/Argentina/Ushuaia",
    "Brazil/Acre",
]
TZS = TIMEZONE_SAMPLE * (NUM_THREADS * NUM_ITERATIONS)


def touch_timezones(tzs):
    """A minimal function that triggers a timezone lookup"""
    for tz in tzs:
        zdt = ZoneInfo(tz)
        del zdt


def main(func):
    threads = []

    for n in range(NUM_THREADS):
        thread = Thread(target=func, args=(TZS[n::NUM_THREADS],))
        threads.append(thread)
        thread.start()

    for thread in threads:
        thread.join()


if __name__ == "__main__":
    main(touch_timezones)

Potential fixes

A sensible solution seems to synchronize all access to _strong_cache using a Lock. Side note: zoneinfo has another cache, but it uses WeakValueDictionary, which is thread-safe.

While I reliably get issues on 3.13.11t, it appears to be fixed in 3.14. Looking at the diff, both the zoneinfo and OrderedDict code remains mostly the same between these two versions. It might have been fixed due to changes in dictobject.c

CPython versions tested on:

3.13

Operating systems tested on:

macOS

Linked PRs

Activity

  1. colesbury commented on Dec 15, 2025

    @colesbury
    Contributor

    Thanks for the bug report. As you mentioned, OrderedDict was made thread-safe in 3.14. At this point, we're not backporting most free-threading thread-safety bug fixes to 3.13 because:

    • 3.13t was "experimental" and 3.14t is "supported"
    • Many of the thread safety bug fixes were large code changes and we're wary of accidentally introducing regressions in 3.13. (Bigger changes tend to be more risky.)

    3.14t is much more stable and faster than 3.13t, so I highly recommend using it instead of 3.13t.

    cc @kumaraditya303 and @Yhg1s in case we want to consider backporting the OrderedDict fix to 3.13. I think the PR is:

  2. added
    stdlibStandard Library Python modules in the Lib/ directory
    on Dec 15, 2025
  3. ariebovenberg commented on Dec 16, 2025

    @ariebovenberg
    ContributorAuthor

    @colesbury awesome, this was exactly the information I was looking for. Can we add this thread-safe behavior of OrderedDict to the API docs? Considering the growing adoption of free-threading builds, it'd be nice to have that assurance explicit.

  4. ariebovenberg commented on Dec 16, 2025

    @ariebovenberg
    ContributorAuthor

    Note there still remains a subtle thread-safety issue in the zoneinfo cache here:

    if len(cls._strong_cache) > cls._strong_cache_size:
    cls._strong_cache.popitem(last=False)

    We check the capacity, but potentially the cache may have already been emptied before reaching popitem()

    It doesn't trigger in my tests, but adding a fake sleep surfaces the race condition:

            if len(cls._strong_cache) > cls._strong_cache_size:
                time.sleep(0.001)
                cls._strong_cache.popitem(last=false)

    A simple fix is to catch and ignore any KeyError

  5. added a commit that references this issue on Feb 18, 2026
  6. added a commit that references this issue on Mar 10, 2026
  7. added 2 commits that reference this issue on Mar 10, 2026
  8. colesbury commented on Mar 10, 2026

    @colesbury
    Contributor

    Thanks @ariebovenberg. We are now catching and ignore the KeyError from cls._strong_cache.popitem(last=false).

  9. added 2 commits that reference this issue on Mar 10, 2026
  10. added a commit that references this issue on Apr 25, 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

    3.13only security fixesstdlibStandard Library Python modules in the Lib/ directorytopic-free-threadingtype-bugAn unexpected behavior, bug, or error

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions