Add two builder (“writer”) C APIs, PyFrozenSetWriter and PyFrozenDictWriter, following the design of PyBytesWriter (PEP 782). A writer collects items internally; *_Finish() produces the immutable object — a frozenset or a frozendict (PEP 814) — in a single pass, without ever exposing a mutable intermediate object.
Add two builder (“writer”) C APIs, PyFrozenSetWriter and
PyFrozenDictWriter, following the design of PyBytesWriter
(PEP 782). A writer collects items internally;
*_Finish() produces the immutable object — a frozenset or a
frozendict (PEP 814) — in a single pass, without ever exposing
a mutable intermediate object.
In addition, calling PySet_Add() on a frozenset is soft
deprecated (PEP 387) in favor of PyFrozenSetWriter.
The C API offers no way to build a frozenset or a frozendict
item by item without either an intermediate container or mutating
the object after creation:
frozenset
There are only two ways to build a frozenset in C today:
PyFrozenSet_New(iterable): works well when all items already
sit in one iterable. When items are produced one at a time in C,
or come from more than one collection, callers must first collect
them into an intermediate mutable container (set, list, tuple)
and then copy it, which costs a second allocation and a second
iteration.PySet_Add() on a newly
created frozenset before it is exposed to other code. This
mutates an object of an immutable type after creation and forces
the implementation to keep frozensets mutable internally.frozendict
PEP 814 added the frozendict builtin type, which can be
created in C with PyFrozenDict_New(iterable). As with
PyFrozenSet_New(), code that produces items one at a time, or
merges more than one mapping, must first build an intermediate dict
and then copy it.
CPython itself does not build frozendicts this way: the
frozendict() constructor fills the new object directly, using
private dict functions, before exposing it. Extension modules
cannot use this path. The writer API makes it public.
Applying the writer pattern of PEP 782 to the two immutable containers based on hash tables gives:
Finish() knows the final number of items and
can build a table of exactly the right size with no resizing.Finish() may compute and cache the
hash, decide GC tracking at creation time, and the implementation
may trust that the object never changes after creation.typedef struct PyFrozenSetWriter PyFrozenSetWriter; PyAPI_FUNC(PyFrozenSetWriter *) PyFrozenSetWriter_Create( Py_ssize_t size_hint); PyAPI_FUNC(int) PyFrozenSetWriter_Add( PyFrozenSetWriter *writer, PyObject *item); PyAPI_FUNC(int) PyFrozenSetWriter_Update( PyFrozenSetWriter *writer, PyObject *iterable); PyAPI_FUNC(PyObject *) PyFrozenSetWriter_Finish( PyFrozenSetWriter *writer); PyAPI_FUNC(void) PyFrozenSetWriter_Discard( PyFrozenSetWriter *writer);
PyFrozenSetWriter_Create(size_hint)0 is allowed); it is a hint, not a limit. Return NULL
with an exception set on error.PyFrozenSetWriter_Add(writer, item)set.add. The writer holds a strong
reference to item. Return 0 on success, -1 with an
exception set on error; on error the writer remains valid.PyFrozenSetWriter_Update(writer, iterable)Add.
Update can be called any number of times and mixed with
Add, so a frozenset can be built from several collections in
one pass — something PyFrozenSet_New() cannot do without an
intermediate mutable set.PyFrozenSetWriter_Finish(writer)frozenset containing the collected items and
destroy the writer. Finish does not copy the items again.
On failure, return NULL with an exception set; the writer is
destroyed in all cases, matching PyBytesWriter_Finish.PyFrozenSetWriter_Discard(writer)Discard(NULL) does nothing.typedef struct PyFrozenDictWriter PyFrozenDictWriter; PyAPI_FUNC(PyFrozenDictWriter *) PyFrozenDictWriter_Create( Py_ssize_t size_hint); PyAPI_FUNC(int) PyFrozenDictWriter_SetItem( PyFrozenDictWriter *writer, PyObject *key, PyObject *value); PyAPI_FUNC(int) PyFrozenDictWriter_Update( PyFrozenDictWriter *writer, PyObject *mapping); PyAPI_FUNC(PyObject *) PyFrozenDictWriter_Finish( PyFrozenDictWriter *writer); PyAPI_FUNC(void) PyFrozenDictWriter_Discard( PyFrozenDictWriter *writer);
Creation, error handling, Finish and Discard behave the same
as PyFrozenSetWriter. PyFrozenDictWriter_Finish() returns a
new frozendict. SetItem requires a hashable key and
overwrites an existing key, keeping the position of the first
insertion, like frozendict. Update accepts anything
PyFrozenDict_New() accepts.
PySet_Add() on frozensets
Calling PySet_Add() on a frozenset is soft deprecated
(PEP 387): the documentation recommends PyFrozenSetWriter
instead; no warning is emitted and no removal is scheduled.
PySet_Add() on set objects remains fully supported.
Removing frozenset support from PySet_Add(), which would allow
the implementation to assume that frozensets never change after
creation, is left to a future PEP.
PyObject and must never be exposed to Python
code.PyBytesWriter.Finish() or Discard() is undefined
behavior.Create() must be paired with exactly one
Finish() or Discard().PyBytesWriter is.PyObject * build_keywords(const char *const *names, Py_ssize_t n) { PyFrozenSetWriter *w = PyFrozenSetWriter_Create(n); if (w == NULL) { return NULL; } for (Py_ssize_t i = 0; i < n; i++) { PyObject *s = PyUnicode_FromString(names[i]); if (s == NULL || PyFrozenSetWriter_Add(w, s) < 0) { Py_XDECREF(s); PyFrozenSetWriter_Discard(w); return NULL; } Py_DECREF(s); } return PyFrozenSetWriter_Finish(w); }
Only new APIs are added. The soft deprecation of PySet_Add() on
frozensets is limited to documentation: existing extensions keep
compiling and running unchanged.
None known.
How to Teach ThisBoth APIs will be documented in the C API reference, with example code.
Rejected Ideas Hard deprecation ofPySet_Add() on frozensets
Emitting a DeprecationWarning would break extensions using the
documented pattern. This PEP limits itself to soft deprecation;
removal is left to a future PEP.
CPython’s own C code contains all three patterns this PEP replaces. These sites would be migrated as part of the reference implementation.
Pattern 1 —PySet_Add() on a newly created frozenset
Python/marshal.c (TYPE_FROZENSET): also needs delayed
reference registration to keep the frozenset hidden while it is
mutated.Modules/_hashopenssl.c (openssl_md_meth_names)Modules/_ssl.c (ssl_enum_certificates)Modules/_abc.c (__abstractmethods__)Modules/_asynciomodule.c (_asyncio_awaited_by getter)PyFrozenSet_New()
Python/initconfig.c (PyConfig_Names): via a listObjects/codeobject.c, Python/compile.c,
Python/flowgraph.c (constant interning and folding): via a
tupleModules/_pickle.c (load_frozenset): via a listPyFrozenDict_New()
Python/marshal.c (TYPE_FROZENDICT): fills a dict, then
copies the entire table with PyFrozenDict_New().Objects/dictobject.c already builds frozendicts in a single pass
internally; this PEP makes that construction path available through a
supported API.
Example migration (Python/marshal.c, TYPE_FROZENDICT):
// Before: build a dict, then copy it into a frozendict v = PyDict_New(); for (;;) { ... PyDict_SetItem(v, key, val) ... } Py_SETREF(v, PyFrozenDict_New(v)); // After: build the frozendict directly, one pass, exact size PyFrozenDictWriter *w = PyFrozenDictWriter_Create(n); for (;;) { ... PyFrozenDictWriter_SetItem(w, key, val) ... } v = PyFrozenDictWriter_Finish(w);
This document is placed in the public domain or under the CC0-1.0-Universal license, whichever is more permissive.
| # | Наименование новости | Тональность | Информативность | Дата публикации |
|---|---|---|---|---|
| 1 | PEP 841: Adding Frozen Syntax to Optimize Immutable Types | 0 | 12.33 | 20-07-2026 |
| 2 | PEP 836: JIT Go Brrr: The Path to a Supported JIT Compiler for CPython | 0 | 12.09 | 02-07-2026 |
| 3 | PEP 837: Extensible JSON serialization | 0 | 11.26 | 12-07-2026 |
| 4 | PEP 844: ``public`` and ``private`` builtins | 0 | 5.72 | 05-08-2026 |
| 5 | What Every Python Developer Should Know About the CPython ABI | 0 | 10 | 19-07-2026 |
| 6 | Как устроен словарь в CPython: compact dict, key sharing и что с ним делает free-threading | 0 | 12.25 | 09-05-2026 |
| 7 | PEP 842: Module Exports | 0 | 11.23 | 25-07-2026 |
| 8 | Scaling NumPy on Free-Threaded Python | 0 | 14.69 | 09-08-2026 |
| 9 | PEP 840: Name Resolution in Class Namespaces | 0 | 13.38 | 15-07-2026 |
| 10 | Ускорение пересборки llama.cpp | 0 | 2.5 | 27-01-2026 |