GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — Replace module level mutable containers with immutable containers · Issue #139003 · python/cpython

Replace module level mutable containers with immutable containers #139003

New issue
Copy link
New issue
Copy link
Open
Open
Replace module level mutable containers with immutable containers#139003
Copy link
Labels
performancePerformance or resource usagePerformance or resource usagestdlibStandard Library Python modules in the Lib/ directoryStandard Library Python modules in the Lib/ directorytopic-free-threading

Description

@eendebakpt
eendebakpt
opened on Sep 16, 2025
Issue body actions

There are many module level containers with constants. By replacing the mutable containers with immutable variants (e.g. replace a set with a frozenset or a list with a tuple) we improve performance (especially in the free-threaded build, see for example #138429) and avoid accidental modification of these containers.

The number of module level lists, dicts and sets on current main is:

number of mutable module level containers by type:
<class 'dict'>: 266
<class 'list'>: 150
<class 'set'>: 63
<class 'collections.defaultdict'>: 2
<class '_strptime.TimeRE'>: 1
<class 'email._encoded_words._QByteMap'>: 1
Script to list all the module level mutable containers
import sys
import importlib
import pkgutil
from collections import Counter

blacklist = ('idlelib.idle')
excluded_submodule_names = ('__main__')
search_submodules = 2
mutable_containers = (list, dict,  set)

def list_container_types(module, mcc):
    print_module = False
    for a in dir(module):
        if a in ('__all__', '__path__', '__builtins__', '__annotations__', '__conditional_annotations__'):
            # why is __all__ a list and not a tuple?
            continue
        # if not a.startswith('_'):
        #    continue
        attr = getattr(module, a)
        tp = type(attr)
        if issubclass(tp, mutable_containers):
            if not print_module:
                print(f'{module}:')
                print_module = True
            print(f'  {a}: {tp}')
            mcc.update([tp])


def search_modules(module_names, search_submodules: int, mcc):
    for name in module_names:
        if name in blacklist:
            continue
        try:
            module = importlib.import_module(name)
        except:
            print(f'{name}: error on import')
            module = None
        list_container_types(module, mcc)

        if search_submodules:
            try:
                sub_names = list(z.name for z in pkgutil.iter_modules(module.__path__))
            except Exception as ex:
                sub_names = []
            mm = [name + '.' + sub_name for sub_name in sub_names if sub_name not in excluded_submodule_names]
            search_modules(mm, search_submodules - 1, mcc)

mcc = Counter()
module_names = sorted(list(sys.builtin_module_names)) + sorted(list(sys.stdlib_module_names))
search_modules(module_names, search_submodules=2, mcc = mcc)

print()
print('number of module level containers by type:')
for key, value in mcc.items():
    print(f'{key}: {value}')

Not all the mutable containers can be replaced by immutable containers. Some of them need to be mutable (e.g. copyreg.dispatch_table). And some of them are part of the public API and we might not want to change the type only for performance reasons.

Example candidates: _pydatetime._DAYNAMES (would improve performance of date.cdate), token.EXACT_TOKEN_TYPES, xml.etree.ElementTree.HTML_EMPTY

Linked PRs

  • gh-139003: Use frozenset for module level attributes in _pyrepl.utils #139004

Activity

  1. eendebakpt
    added
    performancePerformance or resource usagePerformance or resource usage
    topic-free-threading
    on Sep 16, 2025
  2. StanFromIreland
    added
    stdlibStandard Library Python modules in the Lib/ directoryStandard Library Python modules in the Lib/ directory
    on Sep 16, 2025
  3. JanEricNitschke commented on Sep 17, 2025

    @JanEricNitschke
    JanEricNitschke
    on Sep 17, 2025
    Contributor
    More actions

    I tried turning all the lists in _pydatetime into tuple, but didnt really get a speedup here:

    (.wsl-venv) janeric@Jan-Eric-PC:/mnt/d/Programming/Projects/cpython$ python
    Python 3.15.0a0 (heads/main:c919d02ede, Sep  6 2025, 15:55:27) [GCC 13.3.0] on linux
    Type "help", "copyright", "credits" or "license" for more information.
    >>> import timeit
    >>> timeit.timeit("d.ctime()", setup="from _pydatetime import date; d = date.today()")
    4.49665271799995
    >>> timeit.timeit("d.ctime()", setup="from _pydatetime import date; d = date.today()")
    4.522444048000125
    >>> timeit.timeit("d.ctime()", setup="from _pydatetime import date; d = date.today()")
    4.512530989999959
    >>> timeit.timeit("d.ctime()", setup="from _pydatetime import date; d = date.today()")
    4.507625565000126
    >>> exit
    (.wsl-venv) janeric@Jan-Eric-PC:/mnt/d/Programming/Projects/cpython$ python
    Python 3.15.0a0 (heads/main:c919d02ede, Sep  6 2025, 15:55:27) [GCC 13.3.0] on linux
    Type "help", "copyright", "credits" or "license" for more information.
    >>> import timeit
    >>> timeit.timeit("d.ctime()", setup="from _pydatetime import date; d = date.today()")
    4.494325492999906
    >>> timeit.timeit("d.ctime()", setup="from _pydatetime import date; d = date.today()")
    4.547055193000006
    >>> timeit.timeit("d.ctime()", setup="from _pydatetime import date; d = date.today()")
    4.518809389999888
    >>> timeit.timeit("d.ctime()", setup="from _pydatetime import date; d = date.today()")
    4.655416744999911
    >>>
    
    

    First set of runs is with tuples, second is with lists.

  4. JanEricNitschke commented on Sep 17, 2025

    @JanEricNitschke
    JanEricNitschke
    on Sep 17, 2025
    Contributor
    More actions

    Do we have a frozen dict type for "token.EXACT_TOKEN_TYPES"?

  5. eendebakpt commented on Sep 17, 2025

    @eendebakpt
    eendebakpt
    on Sep 17, 2025
    ContributorAuthor
    More actions

    Do we have a frozen dict type for "token.EXACT_TOKEN_TYPES"?

    Not yet. See for example https://peps.python.org/pep-0603/ or discussions on discuss.python.org

  6. JanEricNitschke commented on Sep 17, 2025

    @JanEricNitschke
    JanEricNitschke
    on Sep 17, 2025
    Contributor
    More actions

    Ok, thats what i thought.

    I also just noticed that i didnt test performance in the freethreaded build...

    Should i create a benchmark for each module/PR replacing mutables with immutables or is that not necessary?

    if not I'd just go through a bunch where the global isnt publically documented and the tests still pass after making them immutable

  7. eendebakpt commented on Sep 17, 2025

    @eendebakpt
    eendebakpt
    on Sep 17, 2025
    ContributorAuthor
    More actions

    @JanEricNitschke Thanks for helping out. I am not sure yet whether PRs will be accepted or not, but that will probably determined on a case by case basis. For the copy module the PR was accepted as there was a clear performance gain the the attribute in question was not public (and recently added). I would therefore start with the "easy" cases: a set (or list) that is private and is related to functionality where performance would matter. (there are a few suggestions in the OP, but also urllib.parse might be worthwhile)

    Performance impact may be the greatest for sets (in the FT build they use locks, while lists and dicts try to be safe without locks). You can use the ftscalingbench.py to quickly test whether code scales in the free-threading build.

  8. pablogsal
    added a commit that references this issue on Mar 28, 2026

    gh-139003: Use frozenset for module level attributes in _pyrepl.utils (…

    1fd66ea
  9. maurycy
    added a commit that references this issue on Mar 30, 2026

    Merge remote-tracking branch 'upstream/main' into tachyon-ndjson-kole…

    6a0ea81
  10. clin1234
    added a commit that references this issue on Apr 16, 2026

    pythongh-139003: Use frozenset for module level attributes in _pyrepl…

    78d8612
  11. ljfp
    added a commit that references this issue on Apr 25, 2026

    pythongh-139003: Use frozenset for module level attributes in _pyrepl…

    b2646c1
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

    performancePerformance or resource usagePerformance or resource usagestdlibStandard Library Python modules in the Lib/ directoryStandard Library Python modules in the Lib/ directorytopic-free-threading

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      You are visitor 01402503 — GeezerVison does not store the pages you look up.
      Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.