Skip to content

Commit ed83f61

Browse files
PEP 839: PyFrozenSetWriter and PyFrozenDictWriter C API (#5041)
* PEP 839: PyFrozenSetWriter and PyFrozenDictWriter C API * Update --------- Co-authored-by: Jelle Zijlstra <jelle.zijlstra@gmail.com>
1 parent 8605833 commit ed83f61

2 files changed

Lines changed: 312 additions & 0 deletions

File tree

.github/CODEOWNERS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -713,6 +713,7 @@ peps/pep-0835.rst @ilevkivskyi
713713
peps/pep-0836.rst @savannahostrowski @Fidget-Spinner @brandtbucher
714714
peps/pep-0837.rst @serhiy-storchaka
715715
peps/pep-0838.rst @AlexWaygood
716+
peps/pep-0839.rst @corona10
716717
peps/pep-0840.rst @jeremyhylton @gvanrossum
717718
peps/pep-0841.rst @corona10 @sobolevn
718719
peps/pep-0842.rst @ZeroIntensity

peps/pep-0839.rst

Lines changed: 311 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,311 @@
1+
PEP: 839
2+
Title: PyFrozenSetWriter and PyFrozenDictWriter C API
3+
Author: Donghee Na <donghee.na@python.org>
4+
Status: Draft
5+
Type: Standards Track
6+
Created: 15-Jul-2026
7+
Python-Version: 3.16
8+
9+
10+
Abstract
11+
========
12+
13+
Add two builder ("writer") C APIs, ``PyFrozenSetWriter`` and
14+
``PyFrozenDictWriter``, following the design of ``PyBytesWriter``
15+
(:pep:`782`). A writer collects items internally;
16+
``*_Finish()`` produces the immutable object — a ``frozenset`` or a
17+
``frozendict`` (:pep:`814`) — in a single pass, without ever exposing
18+
a mutable intermediate object.
19+
20+
In addition, calling ``PySet_Add()`` on a frozenset is soft
21+
deprecated (:pep:`387`) in favor of ``PyFrozenSetWriter``.
22+
23+
24+
Motivation
25+
==========
26+
27+
The C API offers no way to build a ``frozenset`` or a ``frozendict``
28+
item by item without either an intermediate container or mutating
29+
the object after creation:
30+
31+
``frozenset``
32+
-------------
33+
34+
There are only two ways to build a frozenset in C today:
35+
36+
1. ``PyFrozenSet_New(iterable)``: works well when all items already
37+
sit in one iterable. When items are produced one at a time in C,
38+
or come from more than one collection, callers must first collect
39+
them into an intermediate mutable container (set, list, tuple)
40+
and then copy it, which costs a second allocation and a second
41+
iteration.
42+
43+
2. The documented pattern of calling ``PySet_Add()`` on a newly
44+
created frozenset before it is exposed to other code. This
45+
mutates an object of an immutable type after creation and forces
46+
the implementation to keep frozensets mutable internally.
47+
48+
``frozendict``
49+
--------------
50+
51+
:pep:`814` added the ``frozendict`` builtin type, which can be
52+
created in C with ``PyFrozenDict_New(iterable)``. As with
53+
``PyFrozenSet_New()``, code that produces items one at a time, or
54+
merges more than one mapping, must first build an intermediate dict
55+
and then copy it.
56+
57+
CPython itself does not build frozendicts this way: the
58+
``frozendict()`` constructor fills the new object directly, using
59+
private dict functions, before exposing it. Extension modules
60+
cannot use this path. The writer API makes it public.
61+
62+
63+
Rationale
64+
=========
65+
66+
Applying the writer pattern of :pep:`782` to the two immutable
67+
containers based on hash tables gives:
68+
69+
* **Construction in a single pass** — no intermediate container, no
70+
copy.
71+
* **Exact sizing** — ``Finish()`` knows the final number of items and
72+
can build a table of exactly the right size with no resizing.
73+
* **A real immutability guarantee** — the returned object was never
74+
reachable while mutable, so ``Finish()`` may compute and cache the
75+
hash, decide GC tracking at creation time, and the implementation
76+
may trust that the object never changes after creation.
77+
* **A way to replace the pattern of calling ``PySet_Add()`` on a
78+
frozenset**, the last documented API in the set C API that mutates
79+
an immutable object.
80+
81+
82+
Specification
83+
=============
84+
85+
PyFrozenSetWriter
86+
-----------------
87+
88+
.. code-block:: c
89+
90+
typedef struct PyFrozenSetWriter PyFrozenSetWriter;
91+
92+
PyAPI_FUNC(PyFrozenSetWriter *) PyFrozenSetWriter_Create(
93+
Py_ssize_t size_hint);
94+
PyAPI_FUNC(int) PyFrozenSetWriter_Add(
95+
PyFrozenSetWriter *writer,
96+
PyObject *item);
97+
PyAPI_FUNC(int) PyFrozenSetWriter_Update(
98+
PyFrozenSetWriter *writer,
99+
PyObject *iterable);
100+
PyAPI_FUNC(PyObject *) PyFrozenSetWriter_Finish(
101+
PyFrozenSetWriter *writer);
102+
PyAPI_FUNC(void) PyFrozenSetWriter_Discard(
103+
PyFrozenSetWriter *writer);
104+
105+
``PyFrozenSetWriter_Create(size_hint)``
106+
Create a writer. *size_hint* is the expected number of items
107+
(``0`` is allowed); it is a hint, not a limit. Return ``NULL``
108+
with an exception set on error.
109+
110+
``PyFrozenSetWriter_Add(writer, item)``
111+
Add *item* (hashable) to the writer. Duplicate items are
112+
ignored, as with ``set.add``. The writer holds a strong
113+
reference to *item*. Return ``0`` on success, ``-1`` with an
114+
exception set on error; on error the writer remains valid.
115+
116+
``PyFrozenSetWriter_Update(writer, iterable)``
117+
Add all items of *iterable*. Same error handling as ``Add``.
118+
``Update`` can be called any number of times and mixed with
119+
``Add``, so a frozenset can be built from several collections in
120+
one pass — something ``PyFrozenSet_New()`` cannot do without an
121+
intermediate mutable set.
122+
123+
``PyFrozenSetWriter_Finish(writer)``
124+
Return a new ``frozenset`` containing the collected items and
125+
destroy the writer. ``Finish`` does not copy the items again.
126+
On failure, return ``NULL`` with an exception set; the writer is
127+
destroyed in all cases, matching ``PyBytesWriter_Finish``.
128+
129+
``PyFrozenSetWriter_Discard(writer)``
130+
Destroy the writer and release all references it holds, without
131+
producing an object. ``Discard(NULL)`` does nothing.
132+
133+
PyFrozenDictWriter
134+
------------------
135+
136+
.. code-block:: c
137+
138+
typedef struct PyFrozenDictWriter PyFrozenDictWriter;
139+
140+
PyAPI_FUNC(PyFrozenDictWriter *) PyFrozenDictWriter_Create(
141+
Py_ssize_t size_hint);
142+
PyAPI_FUNC(int) PyFrozenDictWriter_SetItem(
143+
PyFrozenDictWriter *writer,
144+
PyObject *key,
145+
PyObject *value);
146+
PyAPI_FUNC(int) PyFrozenDictWriter_Update(
147+
PyFrozenDictWriter *writer,
148+
PyObject *mapping);
149+
PyAPI_FUNC(PyObject *) PyFrozenDictWriter_Finish(
150+
PyFrozenDictWriter *writer);
151+
PyAPI_FUNC(void) PyFrozenDictWriter_Discard(
152+
PyFrozenDictWriter *writer);
153+
154+
Creation, error handling, ``Finish`` and ``Discard`` behave the same
155+
as ``PyFrozenSetWriter``. ``PyFrozenDictWriter_Finish()`` returns a
156+
new ``frozendict``. ``SetItem`` requires a hashable key and
157+
overwrites an existing key, keeping the position of the first
158+
insertion, like ``frozendict``. ``Update`` accepts anything
159+
``PyFrozenDict_New()`` accepts.
160+
161+
Soft deprecation of ``PySet_Add()`` on frozensets
162+
-------------------------------------------------
163+
164+
Calling ``PySet_Add()`` on a ``frozenset`` is *soft deprecated*
165+
(:pep:`387`): the documentation recommends ``PyFrozenSetWriter``
166+
instead; no warning is emitted and no removal is scheduled.
167+
``PySet_Add()`` on ``set`` objects remains fully supported.
168+
169+
Removing frozenset support from ``PySet_Add()``, which would allow
170+
the implementation to assume that frozensets never change after
171+
creation, is left to a future PEP.
172+
173+
Common rules
174+
------------
175+
176+
* A writer is not a ``PyObject`` and must never be exposed to Python
177+
code.
178+
* A writer must not be used from multiple threads at the same time,
179+
like ``PyBytesWriter``.
180+
* Using a writer after ``Finish()`` or ``Discard()`` is undefined
181+
behavior.
182+
* Every successful ``Create()`` must be paired with exactly one
183+
``Finish()`` or ``Discard()``.
184+
* Both APIs are excluded from the limited API at first, as
185+
``PyBytesWriter`` is.
186+
187+
Example
188+
-------
189+
190+
.. code-block:: c
191+
192+
PyObject *
193+
build_keywords(const char *const *names, Py_ssize_t n)
194+
{
195+
PyFrozenSetWriter *w = PyFrozenSetWriter_Create(n);
196+
if (w == NULL) {
197+
return NULL;
198+
}
199+
for (Py_ssize_t i = 0; i < n; i++) {
200+
PyObject *s = PyUnicode_FromString(names[i]);
201+
if (s == NULL || PyFrozenSetWriter_Add(w, s) < 0) {
202+
Py_XDECREF(s);
203+
PyFrozenSetWriter_Discard(w);
204+
return NULL;
205+
}
206+
Py_DECREF(s);
207+
}
208+
return PyFrozenSetWriter_Finish(w);
209+
}
210+
211+
212+
Backwards Compatibility
213+
=======================
214+
215+
Only new APIs are added. The soft deprecation of ``PySet_Add()`` on
216+
frozensets is limited to documentation: existing extensions keep
217+
compiling and running unchanged.
218+
219+
220+
Security Implications
221+
=====================
222+
223+
None known.
224+
225+
226+
How to Teach This
227+
=================
228+
229+
Both APIs will be documented in the `C API reference
230+
<https://docs.python.org/3/c-api/>`_, with example code.
231+
232+
233+
Rejected Ideas
234+
==============
235+
236+
Hard deprecation of ``PySet_Add()`` on frozensets
237+
-------------------------------------------------
238+
239+
Emitting a ``DeprecationWarning`` would break extensions using the
240+
documented pattern. This PEP limits itself to soft deprecation;
241+
removal is left to a future PEP.
242+
243+
244+
Appendix: Migration candidates in CPython
245+
=========================================
246+
247+
CPython's own C code contains all three patterns this PEP replaces.
248+
These sites would be migrated as part of the reference
249+
implementation.
250+
251+
Pattern 1 — ``PySet_Add()`` on a newly created frozenset
252+
--------------------------------------------------------
253+
254+
* ``Python/marshal.c`` (``TYPE_FROZENSET``): also needs delayed
255+
reference registration to keep the frozenset hidden while it is
256+
mutated.
257+
* ``Modules/_hashopenssl.c`` (``openssl_md_meth_names``)
258+
* ``Modules/_ssl.c`` (``ssl_enum_certificates``)
259+
* ``Modules/_abc.c`` (``__abstractmethods__``)
260+
* ``Modules/_asynciomodule.c`` (``_asyncio_awaited_by`` getter)
261+
262+
Pattern 2 — intermediate container copied by ``PyFrozenSet_New()``
263+
------------------------------------------------------------------
264+
265+
* ``Python/initconfig.c`` (``PyConfig_Names``): via a list
266+
* ``Objects/codeobject.c``, ``Python/compile.c``,
267+
``Python/flowgraph.c`` (constant interning and folding): via a
268+
tuple
269+
* ``Modules/_pickle.c`` (``load_frozenset``): via a list
270+
271+
Pattern 3 — mutable dict copied by ``PyFrozenDict_New()``
272+
---------------------------------------------------------
273+
274+
* ``Python/marshal.c`` (``TYPE_FROZENDICT``): fills a dict, then
275+
copies the entire table with ``PyFrozenDict_New()``.
276+
277+
``Objects/dictobject.c`` already builds frozendicts in a single pass
278+
internally; this PEP makes that construction path available through a
279+
supported API.
280+
281+
Example migration (``Python/marshal.c``, ``TYPE_FROZENDICT``):
282+
283+
.. code-block:: c
284+
285+
// Before: build a dict, then copy it into a frozendict
286+
v = PyDict_New();
287+
for (;;) {
288+
... PyDict_SetItem(v, key, val) ...
289+
}
290+
Py_SETREF(v, PyFrozenDict_New(v));
291+
292+
// After: build the frozendict directly, one pass, exact size
293+
PyFrozenDictWriter *w = PyFrozenDictWriter_Create(n);
294+
for (;;) {
295+
... PyFrozenDictWriter_SetItem(w, key, val) ...
296+
}
297+
v = PyFrozenDictWriter_Finish(w);
298+
299+
300+
References
301+
==========
302+
303+
* :pep:`782` — Add PyBytesWriter C API
304+
* :pep:`814` — Add frozendict built-in type
305+
306+
307+
Copyright
308+
=========
309+
310+
This document is placed in the public domain or under the
311+
CC0-1.0-Universal license, whichever is more permissive.

0 commit comments

Comments
 (0)