Add a dict method to convert to a frozendict?

I don’t see why such code should call .as_frozendict_and_clear() method. This method doesn’t fit all use cases. It fits well in some specific use cases. Also, I didn’t talk about modifying inputs.

I didn’t give examples where inputs are modified. The typical usecase is a function creating a temporary dict, converts the dict to frozendict, returns the frozendict, and discards the dict.

Maybe it’s easier to understand with examples from the stdlib.

Example: copy.deepcopy()

Extract of the Lib/copy.py code:

def _deepcopy_frozendict(x, memo, deepcopy=deepcopy):
    y = {}  # dict local to the function
    for key, value in x.items():
        y[deepcopy(key, memo)] = deepcopy(value, memo)
    ...
    return frozendict(y)
    # discard dict 'y'
d[frozendict] = _deepcopy_frozendict

See the last return frozendict(y) instruction converting the dict to a frozendict.

Example: json.loads()

Another example is the object_pairs_hook parameter of json.loads() used to create frozendict instead of dict:

>>> json.loads('{"x": [1, 2, 3]}', object_pairs_hook=frozendict, array_hook=tuple)
frozendict({'x': (1, 2, 3)})

For better performance, object_pairs_hook=frozendict could be replaced with object_pairs_hook=dict.as_frozendict_and_clear. Again, the dict is discarded anyway, the dict is a fresh object created by json.

The function creates a dict, fill the dict, converts the dict to a frozendict, and then discards the dict.

Example: pickle.loads()

To (re)create a frozendict, the pickle protocol first creates a mutable dict, converts it to a frozendict, and then discards the dict.

>>> fd = frozendict(x=1, y=2)
>>> fd.__reduce_ex__(4)
(<function __newobj__ at 0x7f25714fd610>, (<class 'frozendict'>, {'x': 1, 'y': 2}), None, None, None)

>>> import pickle
>>> s=pickle.dumps(fd)
>>> import pickletools
>>> pickletools.dis(s)
    0: \x80 PROTO      5
    2: \x95 FRAME      47
   11: \x8c SHORT_BINUNICODE 'builtins'
   21: \x94 MEMOIZE    (as 0)
   22: \x8c SHORT_BINUNICODE 'frozendict'
   34: \x94 MEMOIZE    (as 1)
   35: \x93 STACK_GLOBAL
   36: \x94 MEMOIZE    (as 2)
   37: }    EMPTY_DICT
   38: \x94 MEMOIZE    (as 3)
   39: (    MARK
   40: \x8c     SHORT_BINUNICODE 'x'
   43: \x94     MEMOIZE    (as 4)
   44: K        BININT1    1
   46: \x8c     SHORT_BINUNICODE 'y'
   49: \x94     MEMOIZE    (as 5)
   50: K        BININT1    2
   52: u        SETITEMS   (MARK at 39)
   53: \x85 TUPLE1
   54: \x94 MEMOIZE    (as 6)
   55: \x81 NEWOBJ
   56: \x94 MEMOIZE    (as 7)
   57: .    STOP

Example: marshal.loads()

An example in C, from Python/marshal.c (simplified code):

    case TYPE_DICT:
    case TYPE_FROZENDICT:
        v = PyDict_New();
        for (;;) {
            key = r_object(p);
            val = r_object(p);
            if (PyDict_SetItem(v, key, val) < 0) {
                ...
                break;
            }
            ...
        }
        if (type == TYPE_FROZENDICT && v != NULL) {
            Py_SETREF(v, PyFrozenDict_New(v));
        }
        retval = v;
        break;

Py_SETREF(v, PyFrozenDict_New(v)) replaces the dict with a frozendict and discards the dict.


Cody Maloney conducted a great analysis of functions of the io module, to see which functions create a temporary bytearray, and has to return a bytes since the API return type is bytes. He found multiple functions using this pattern in the _pyio module. The idea would be similar, but for dict and frozendict.

Well, right now, there are less functions using the return frozendict(dict) pattern since frozendict was just added to Python 3.15 :wink: I expect to see this pattern more often in new code.

Adding bytearray.take_bytes() doesn’t mean that all functions using bytearray must now call it. It depends on the usecase. For example, the method should not be used if the bytearray is a function argument.

3 Likes