Skip to content

feat: add built-in frozendict support for Python 3.15+#274

Open
XuehaiPan wants to merge 24 commits into
metaopt:mainfrom
XuehaiPan:frozendict
Open

feat: add built-in frozendict support for Python 3.15+#274
XuehaiPan wants to merge 24 commits into
metaopt:mainfrom
XuehaiPan:frozendict

Conversation

@XuehaiPan

@XuehaiPan XuehaiPan commented Mar 21, 2026

Copy link
Copy Markdown
Member

Description

Register frozendict (PEP 814, Python 3.15.0a7+) as a built-in PyTree node type with key-sorted traversal, matching the behavior of dict and collections.defaultdict. The dict_insertion_ordered context manager also affects frozendict.

Also improves the empty deque representation: deque() instead of deque([]).

Motivation and Context

Python 3.15 introduces frozendict as a built-in immutable mapping type (PEP 814). As a fundamental container type, it should be supported as a built-in PyTree node — just like dict, OrderedDict, and defaultdict.

  • I have raised an issue to propose this change (required for new features and bug fixes)

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds core functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation (update in the documentation)

Implemented Tasks

  • C++ PyTreeKind::FrozenDict enum and PyFrozenDictTypeObject macro (guarded by PY_VERSION_HEX >= 0x030F00A7)
  • All treespec switch statements updated (flatten, unflatten, serialize, hash, compare, traverse, construct, paths, accessors, entries)
  • Python registry with sorted and insertion-ordered flatten/unflatten functions
  • treespec_frozendict() constructor, STANDARD_DICT_TYPES expansion, PyTree[T] type union
  • optree.treespec.frozendict namespace alias
  • README (linked to PEP 814), Sphinx docs, and spelling wordlist
  • Test data extended (3 frozendict entries in TREES fixture, subinterpreter test tree, registry size checks)
  • Empty deque repr improvement

Checklist

  • I have read the CONTRIBUTION guide. (required)
  • My change requires a change to the documentation.
  • I have updated the tests accordingly. (required for a bug fix or a new feature)
  • I have updated the documentation accordingly.
  • I have reformatted the code using make format. (required)
  • I have checked the code using make lint. (required)
  • I have ensured make test pass. (required)

@XuehaiPan XuehaiPan self-assigned this Mar 21, 2026
@XuehaiPan XuehaiPan added enhancement New feature or request cxx Something related to the CXX source code python Something related to the Python source code labels Mar 21, 2026
@codecov

codecov Bot commented Mar 21, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (cf8498f) to head (7964f43).

Additional details and impacted files
@@            Coverage Diff            @@
##              main      #274   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           15        15           
  Lines         1582      1614   +32     
  Branches       210       218    +8     
=========================================
+ Hits          1582      1614   +32     
Flag Coverage Δ
unittests 100.00% <100.00%> (ø)
unittests-cp310-Linux 100.00% <100.00%> (ø)
unittests-cp310-Windows 100.00% <100.00%> (ø)
unittests-cp310-macOS 100.00% <100.00%> (ø)
unittests-cp311-Linux 100.00% <100.00%> (ø)
unittests-cp311-Windows 100.00% <100.00%> (ø)
unittests-cp311-macOS 100.00% <100.00%> (ø)
unittests-cp312-Linux 100.00% <100.00%> (ø)
unittests-cp312-Windows 100.00% <100.00%> (ø)
unittests-cp312-macOS 100.00% <100.00%> (ø)
unittests-cp313-Linux 100.00% <100.00%> (ø)
unittests-cp313-Windows 100.00% <100.00%> (ø)
unittests-cp313-macOS 100.00% <100.00%> (ø)
unittests-cp313t-Linux 100.00% <100.00%> (ø)
unittests-cp313t-Windows 100.00% <100.00%> (ø)
unittests-cp313t-macOS 100.00% <100.00%> (ø)
unittests-cp314-Linux 100.00% <100.00%> (ø)
unittests-cp314-Windows 100.00% <100.00%> (ø)
unittests-cp314-macOS 100.00% <100.00%> (ø)
unittests-cp314t-Linux 100.00% <100.00%> (ø)
unittests-cp314t-Windows 100.00% <100.00%> (ø)
unittests-cp314t-macOS 100.00% <100.00%> (ø)
unittests-cp315-Linux 99.92% <96.97%> (?)
unittests-cp315-Windows 99.92% <96.97%> (?)
unittests-cp315-macOS 99.92% <96.97%> (?)
unittests-cp315t-Linux 99.92% <96.97%> (?)
unittests-cp315t-Windows 99.92% <96.97%> (?)
unittests-cp315t-macOS 99.92% <96.97%> (?)
unittests-cp39-Linux 100.00% <100.00%> (ø)
unittests-cp39-Windows 100.00% <100.00%> (ø)
unittests-cp39-macOS 100.00% <100.00%> (ø)
unittests-pp311-Linux 100.00% <100.00%> (ø)
unittests-pp311-Windows ?
unittests-pp311-macOS 100.00% <100.00%> (ø)
unittests-pydebug 100.00% <100.00%> (ø)
unittests-pydebug-cp310d-Linux 100.00% <100.00%> (ø)
unittests-pydebug-cp310d-Windows 100.00% <100.00%> (ø)
unittests-pydebug-cp310d-macOS 100.00% <100.00%> (ø)
unittests-pydebug-cp311d-Linux 100.00% <100.00%> (ø)
unittests-pydebug-cp311d-Windows 100.00% <100.00%> (ø)
unittests-pydebug-cp311d-macOS 100.00% <100.00%> (ø)
unittests-pydebug-cp312d-Linux 100.00% <100.00%> (ø)
unittests-pydebug-cp312d-Windows 100.00% <100.00%> (ø)
unittests-pydebug-cp312d-macOS 100.00% <100.00%> (ø)
unittests-pydebug-cp313d-Linux 100.00% <100.00%> (ø)
unittests-pydebug-cp313d-Windows 100.00% <100.00%> (ø)
unittests-pydebug-cp313d-macOS 100.00% <100.00%> (ø)
unittests-pydebug-cp313td-Linux 100.00% <100.00%> (ø)
unittests-pydebug-cp313td-Windows 100.00% <100.00%> (ø)
unittests-pydebug-cp313td-macOS 100.00% <100.00%> (ø)
unittests-pydebug-cp314d-Linux 100.00% <100.00%> (ø)
unittests-pydebug-cp314d-Windows 100.00% <100.00%> (ø)
unittests-pydebug-cp314d-macOS 100.00% <100.00%> (ø)
unittests-pydebug-cp314td-Linux 100.00% <100.00%> (ø)
unittests-pydebug-cp314td-Windows 100.00% <100.00%> (ø)
unittests-pydebug-cp314td-macOS 100.00% <100.00%> (ø)
unittests-pydebug-cp315d-Linux 99.92% <96.97%> (?)
unittests-pydebug-cp315d-Windows 99.92% <96.97%> (?)
unittests-pydebug-cp315d-macOS 99.92% <96.97%> (?)
unittests-pydebug-cp315td-Linux 99.92% <96.97%> (?)
unittests-pydebug-cp315td-Windows 99.92% <96.97%> (?)
unittests-pydebug-cp315td-macOS 99.92% <96.97%> (?)
unittests-pydebug-cp39d-Linux 100.00% <100.00%> (ø)
unittests-pydebug-cp39d-Windows 100.00% <100.00%> (ø)
unittests-pydebug-cp39d-macOS 100.00% <100.00%> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

@XuehaiPan XuehaiPan force-pushed the frozendict branch 4 times, most recently from 3426921 to 12a3276 Compare March 25, 2026 15:25
@XuehaiPan XuehaiPan force-pushed the frozendict branch 4 times, most recently from fe42e10 to 0faccef Compare April 3, 2026 06:14
@XuehaiPan XuehaiPan force-pushed the frozendict branch 5 times, most recently from 19952e3 to 7656326 Compare May 11, 2026 17:39
XuehaiPan added 9 commits May 12, 2026 01:59
Register `frozendict` (Python 3.15.0a7+) as a built-in PyTree node type
with key-sorted traversal, matching the behavior of `dict` and
`collections.defaultdict`. The `dict_insertion_ordered` context manager
also affects `frozendict`.

Changes across 29 files:
- C++ enum `PyTreeKind::FrozenDict` and `PyFrozenDictTypeObject` macro
  (guarded by `PY_VERSION_HEX >= 0x030F00A7`)
- All treespec switch statements updated (flatten, unflatten, serialize,
  hash, compare, traverse, construct, paths, accessors, entries)
- Python registry with sorted and insertion-ordered flatten/unflatten
- `treespec_frozendict()` constructor, `STANDARD_DICT_TYPES` expansion,
  `PyTree[T]` type union update
- `optree.treespec.frozendict` namespace alias
- README, Sphinx docs, and spelling wordlist
- Test data extended (3 frozendict entries in TREES fixture),
  subinterpreter test tree includes frozendict, registry size checks
- Minor: empty deque repr improved (`deque()` instead of `deque([])`)
Sphinx autodoc/autosummary imports symbols during the reading phase
regardless of ``.. only::`` directives, causing import errors on
Python < 3.15 where ``treespec_frozendict`` does not exist. Remove the
entries for now with TODO comments to add them back when building with
Python 3.15+.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@XuehaiPan XuehaiPan requested a review from Copilot May 13, 2026 18:01

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 33 out of 34 changed files in this pull request and generated 14 comments.

Comments suppressed due to low confidence (2)

tests/test_prefix_errors.py:1

  • The dots in collections.OrderedDict and collections.defaultdict are unescaped regex metacharacters. They will match any character. Escape them as collections\.OrderedDict / collections\.defaultdict to avoid accidental matches and for consistency with the escaping used elsewhere in this file.
    tests/helpers.py:1
  • Hard-coding 9 if … else 8 and repeating the same expression in tests/concurrent/test_subinterpreters.py (twice) couples three test sites to the same magic constant. Consider exposing the expected size from helpers.py (e.g., EXPECTED_REGISTRY_SIZE) and importing it where needed, so future builtin additions only need to update one place.

Comment thread optree/ops.py Outdated
Comment thread optree/registry.py
Comment thread optree/ops.py
Comment thread optree/treespec.py
Comment thread README.md
Comment thread src/treespec/serialization.cpp
Comment thread .github/workflows/build.yml Outdated
Comment thread optree/ops.py
Comment thread optree/typing.py
Comment thread tests/concurrent/test_subinterpreters.py
…all time

Address Copilot PR review on `docs/source/ops.rst` and `docs/source/treespec.rst`:
the previous `TODO(frozendict): Add ... when building docs with Python 3.15+` comments
meant the new API was perpetually undocumented even on toolchains that could support
it, and conditional Sphinx directives (`only::`, `ifconfig::`) don't actually suppress
the autodoc/autosummary import warnings emitted when the symbol is missing — those
extensions resolve symbols at directive-parse / pre-scan time, before any
rendering-side filter applies.

Move the version gating from definition-time to call-time so the symbol always
exists. `autodoc` and `autosummary` can introspect it on every supported Python; the
docstring's `.. note::` block explicitly states the runtime requirement. Calling the
function on a build without `frozendict` support raises a clear `RuntimeError`
naming the version requirement, rather than the previous `AttributeError` users got
when the symbol was conditionally defined.

API changes:

- `optree.treespec_frozendict` is now defined unconditionally (always callable,
  always in `optree.__all__`), with a runtime guard that raises `RuntimeError` if
  `_C.OPTREE_HAS_FROZENDICT` is unset.
- `optree.treespec.frozendict` mirrors the same: unconditional re-export of
  `treespec_frozendict`, always in `optree.treespec.__all__`.
- Dropped now-unused `import sys` / `import optree._C as _C` / `del sys` from
  `optree/__init__.py` and `optree/treespec.py`.
- Moved `import builtins` from `TYPE_CHECKING` to module-top in `optree/ops.py`
  since it's accessed at runtime via `builtins.frozendict`.

Docs:

- `docs/source/ops.rst`: replace the TODO with the actual
  `.. autofunction:: treespec_frozendict` directive and add the symbol to the
  surrounding autosummary block.
- `docs/source/treespec.rst`: replace the TODO with a `frozendict` entry in the
  autosummary list.
- Result: `make docs` is warning-free on every supported Python — the symbol is
  always importable so `autodoc`/`autosummary` succeed.

Docstring polish: rewrite the opening line of every `treespec_*` constructor's
docstring to the consistent form `Make a treespec representing a :class:`X` node
from child treespecs.` and use `:class:` markup uniformly in the `Returns:` block
(tuple, list, dict, OrderedDict, defaultdict, deque).

Tests:

- New `tests/test_treespec.py::test_treespec_frozendict_runtime_error_on_unsupported_interpreter`:
  on Python < 3.15 (or wheels without `frozendict` support), both
  `optree.treespec_frozendict()` and `optree.treespec.frozendict(...)` raise a
  `RuntimeError` whose message contains the exact version requirement.

Drive-by: add `# pylint: disable=redefined-builtin` to two `sentinel`
references in `tree_replace_nones` (parameter) and `tree_broadcast_common` (local).
Python 3.15 added `builtins.sentinel` (PEP 661), which makes pylint flag prior
uses of the name; surfaced now that the lint venv runs on 3.15t.
@XuehaiPan XuehaiPan force-pushed the frozendict branch 3 times, most recently from 0795f05 to 2792b40 Compare May 14, 2026 17:31
@XuehaiPan XuehaiPan added the test-with-nightly-pybind11 Use nightly pybind11 in tests label May 29, 2026
XuehaiPan added 5 commits June 6, 2026 02:15
PyPy/Windows jobs intermittently fail in pip steps with transient network
errors (e.g. `OSError: Connection broken: IncompleteRead`) while a nested
build-isolation pip downloads build dependencies such as `scikit-build-core`
for nightly `pybind11`.

Set `PIP_RETRIES=10` (default 5) and `PIP_TIMEOUT=120` (default 15s) in the
`env:` block of all test/build/lint workflows. As environment variables these
propagate into nested build-isolation pip subprocesses (unlike CLI flags on the
outer `pip install`), covering connection setup, DNS, and metadata-fetch
hiccups that account for most CI pip failures. Also add both vars to
cibuildwheel's `environment-pass` so they cross into the isolated (manylinux)
wheel-build containers, which would otherwise not inherit the host environment.

Note: a mid-download `IncompleteRead` can still escape pip's internal retry;
a command-level retry would be the next lever if it recurs.
XuehaiPan added 3 commits July 4, 2026 15:02
The pyenv-win bundled `.versions_cache.xml` lags behind python.org, and the `pyenv update` command is unusable on GitHub runners: its VBScript updater parses release listings via the Internet Explorer `htmlfile` COM object, which modern Windows images reject with "This command is not supported".

Run the pyenv-win stdlib updater (`.github/scripts/update_versions_cache.py`) right after cloning so `pyenv install` can resolve the newest CPython release (3.15.0b3), which ships the gh-149786 fix for the free-threaded venv launcher that previously broke the 3.15 free-threaded debug build venv/ensurepip.
…urface

Extend three existing tests with `frozendict` cases (gated on Python 3.15+)
to exercise paths that were previously untested:

- `test_tree_broadcast_common`: the `PyTreeKind::FrozenDict` arm in
  `BroadcastToCommonSuffix`.
- `test_different_metadata`: the `frozendict:` key-mismatch message emitted
  by `FlattenUpTo`.
- `test_pytree_typing`: the `optree.typing.FrozenDict` alias, its `__all__`
  position, and the `frozendict[Any, ...]` member of the `PyTree` generic union.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cxx Something related to the CXX source code enhancement New feature or request python Something related to the Python source code test-with-nightly-pybind11 Use nightly pybind11 in tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants