Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Add frozendict support for 3.15+ - #1052

Merged
sobolevn merged 19 commits into
msgspec:mainfrom
sobolevn:add-frozendict-support
Jun 30, 2026
Merged

Add frozendict support for 3.15+#1052
sobolevn merged 19 commits into
msgspec:mainfrom
sobolevn:add-frozendict-support

Conversation

@sobolevn

Copy link
Copy Markdown
Member

Closes #1029

@provinzkraut provinzkraut left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Great stuff @sobolevn! Couple of questions :)

Comment thread src/msgspec/_core.c
Comment thread src/msgspec/_core.c Outdated
Comment thread src/msgspec/_core.c
Comment thread src/msgspec/_core.c Outdated
Comment thread src/msgspec/_core.c
Comment thread src/msgspec/inspect.py Outdated
Comment thread src/msgspec/inspect.py Outdated
Comment thread tests/unit/test_convert.py
Comment thread tests/unit/test_to_builtins.py
@sobolevn

Copy link
Copy Markdown
Member Author

Done! Very helpful feedback, thanks a lot!
I've added a new doc section.

@provinzkraut provinzkraut left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Just some thoughts on the docs, the rest looks good :)

Comment thread docs/perf-tips.rst Outdated
Comment thread docs/perf-tips.rst Outdated
Comment thread docs/perf-tips.rst Outdated
Comment thread docs/perf-tips.rst
To achieve the best performance,
use native ``json`` types: ``list`` and ``dict``.
use collection types that can be constructed natively:
``list``, ``set``, ``frozenset``, fixed-sized ``tuple``, and ``dict``.

@provinzkraut provinzkraut Jun 12, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Just curious, do you know why PySet_Add can add to a frozenset, but PyDict_SetItem cannot be used on a frozendict?

Was there a reasoning behind it?

provinzkraut
provinzkraut previously approved these changes Jun 12, 2026

@provinzkraut provinzkraut left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🔥

@sobolevn

Copy link
Copy Markdown
Member Author

Let's keep this open for some time to collect more feedback 👍
Thanks for your help!

@ofek

ofek commented Jun 12, 2026

Copy link
Copy Markdown
Member

I don't think Siyet has done this yet so would either of you be interested in tackling my comment here?

Specifically, as a first step I would want to improve the performance checks so that the results of each commit to main are persisted (perhaps on a dedicated branch) and can be used on PRs to detect regressions.

We need to do the following, in order, for both performance (#310) and startup time (#884):

  1. Set up some sort of rudimentary measurement in CI (profiling was introduced in Add performance profiling #923 and improved in subsequent PRs like Add dedicated profiling workflow #934)
  2. Manifest the results in a comment on PRs that gets updated after every commit
  3. Fail CI for regressions (e.g. Add CI gate for non-trivial regressions in startup time #885)
  4. Improve upon the measurements themselves and measure more things we care about (for example Memray is included as a profiling dependency but not actually used anywhere because I didn't have time and it doesn't support Windows)

Pretty please try to prioritize this over other tasks and be sure to manually look at the drift of the existing profiling jobs before the next release 🙏

@sobolevn

Copy link
Copy Markdown
Member Author

I created a public discussion about new potential C-API to efficently convert dict into frozendict objects: https://discuss.python.org/t/provide-a-c-api-to-effectively-convert-dict-into-frozendict/107754

@Siyet Siyet left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Took a careful pass on this (including building the branch against 3.15.0a7) and found two issues to fix before merge. Nice feature otherwise, the type-system integration is clean.

1. Reference leak in every frozendict construction path.

PyFrozenDict_New(dict) does not steal its argument: for a plain dict it copies (via PyTuple_Pack + frozendict_new) and leaves the caller's reference intact (CPython Objects/dictobject.c, which is exactly why you proposed PyFrozenDict_FromDictSteal upstream). The wrappers do return PyFrozenDict_New(dict); without releasing the intermediate dict, so it leaks on every call: mpack_decode_frozendict, json_decode_frozendict, to_builtins_frozendict, convert_dict_to_frozendict, and the frozendict branch in convert_other.

Reproduced on 3.15.0a7, counting live dict objects after 5000 ops:

decode -> dict         : +0
decode -> frozendict   : +5000
convert -> frozendict  : +5000
to_builtins frozendict : +5000

Fix at each site:

PyObject *res = PyFrozenDict_New(dict);
Py_DECREF(dict);
return res;

(this also handles the case where New returns NULL). Worth a refcount regression test, since the functional tests pass straight through the leak.

2. msgspec.yaml.encode raises on frozendict.

to_builtins yields a frozendict, and PyYAML's SafeDumper has no representer for it:

msgspec.yaml.encode(frozendict({"a": 1}))
# RepresenterError: ('cannot represent an object', frozendict({'a': 1}))

Same for a struct or dict containing a frozendict. (stdlib json.dumps(to_builtins(...)) happens to work, so it is specific to consumers that do not special-case frozendict, but yaml.encode is public API.) The yaml path likely needs to emit a plain dict for frozendict. A yaml roundtrip test would catch it.

Both reproduced on a local 3.15.0a7 build of this branch.

@sobolevn
sobolevn force-pushed the add-frozendict-support branch from 7fefc02 to 8928e21 Compare June 14, 2026 19:17
provinzkraut
provinzkraut previously approved these changes Jun 30, 2026
@sobolevn
sobolevn requested a review from ofek June 30, 2026 17:34

@ofek ofek left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for working on this! I wish we had better benchmarks (or added back the old profiling test dependency) so that we could assess the impact on performance.

Comment thread docs/api.rst Outdated
Comment thread src/msgspec/inspect.py
max_length: int | None = None


class FrozenDictType(Type):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can we only define this on 3.15+?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I don't think that this is a good idea. Users might want to provide an API based on these types. It would require them to use conditional imports as well. I don't think that this extra complexity is needed. Saying that it won't be issued is enough IMO.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

For typing purposes, shouldn't we act as the responsible party here and enforce that? It seems to me that it's more correct to conditionally define this (just as our extension has conditional support) and far better to have static analysis catch errors rather than tests of runtime behavior.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

What runtime errors could arise from this?

Since this is inspection code, pre 3.15, you just never get back this type.
However, for consumers of this inspection, a conditional define would be awkward, e.g.

if isinstance(o, (inspect.FrozenDictType, inspect.DictType)):
 ...

would have to become

if isinstance(o, inspect.DictType) or (sys.version_info >= (3, 15) and isinstance(o, inspect.FrozenDictType)):
 ...

Comment thread tests/unit/test_common.py Outdated
Comment thread tests/unit/test_common.py Outdated
Comment thread src/msgspec/_core.c Outdated
Comment thread src/msgspec/_core.c
@sobolevn

Copy link
Copy Markdown
Member Author

Done! Thanks a lot for the review!

ofek
ofek previously approved these changes Jun 30, 2026
Comment thread tests/unit/test_common.py Outdated
@sobolevn

Copy link
Copy Markdown
Member Author

(I need one more approve now after the changes) 😅

@sobolevn
sobolevn added this pull request to the merge queue Jun 30, 2026
Merged via the queue into msgspec:main with commit 8f7ac06 Jun 30, 2026
27 checks passed
@sobolevn
sobolevn deleted the add-frozendict-support branch June 30, 2026 19:53
Comment thread tests/unit/test_common.py
Comment on lines +4296 to +4299
if use_encoder:
res = proto.Encoder(order=order).encode(msg)
else:
res = proto.encode(msg, order=order)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Shouldn't msg be converted to frozendict here?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in #1107

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

support for python 3.15 frozendict

5 participants