Bind `frozendict` with new unpacking syntax?

With the new Python 3.15 builtin frozendict, decorators that pass parameters directly into a wrapped function (which is a common pattern), for one, would potentially benefit from a new unpacking syntax that collects keyword arguments into a frozendict rather than a dict. This is because unpacking a dict, even without modifying it, inevitably creates a new dict, which can be expensive. I have, however, not much idea what syntax can be used. To illustrate:

import functools
def log_arguments(func, /):
    @functools.wraps(func)
    def wrapper(*a, **k): # creates one dictionary
        print(f'{func.__name__} called: positional: {a}, keywords: {k}')
        return func(*a, **k) # creates another
    return wrapper

frozendict unpacking, theoretically would call the frozendict constructor on the already-created frozendict, which would be an identity operation rather than a shallow copy.

If this is where your performance bottleneck lies, that implies that you’re wrapping extremely small functions, which is probably your real issue.

I suggest that you do some benchmarking on real-world workloads, to find out the actual impact of using a dict here. If there’s enough performance impact to indicate that using a frozendict instead would be beneficial, then you can try implementing it and see what the savings are like.

But even then, if you’re proposing that in your example code k would be a frozendict rather than a dict, that would be a breaking change which could affect a lot of code. So it’s not likely to happen unless you come up with an approach that avoids breaking existing, working code.

1 Like

But even then, if you’re proposing that in your example code k would be a frozendict rather than a dict, that would be a breaking change which could affect a lot of code.

I am not proposing that. I am putting this out to gather ideas about how this would work at the language level, because it is sure to require new syntax to represent.

1 Like

OK. So the answer I’d give is “demonstrate it’s something people would get actual benefits from, and then we can talk about syntax”.

It’s worth remembering that this would be a very deep change to the language - keyword arguments are passed as dictionaries all the way down to the C API. So you’d either have to change all of that (affecting every 3rd party extension) or you’d still incur a conversion from frozendict to dict when the Python-level call is passed to the C API.

(Disclaimer - it’s been a long time since I looked at the code for the interpreter main loop, and there’s been a lot of optimisation work. So things could have changed from what I recall. You should check the details for yourself.)

1 Like

There are common idioms in Python that prevent a change in the semantics of **kwargs.

A simple example:

def err_print(*args, **kwargs):
    kwargs['file'] = stderr
    kwargs.pop('end', None)
    print(*args, **kwargs)

Python programmers count on a kwargs behaving like a dict. As you mentioned, changing the semantics would break who knows what.

(I would be worse than the 2.x → 3.0 transition)

Let me reiterate that I never tried to change the semantics of double-star unpacking. I am proposing to introduce a yet undecided new syntax.

2 Likes

A new syntax will involve two uphill battles, one for whether there there is an actual benefit for optimizing this (@pf_moore’s point) as well as what the new syntax should be (see other proposals for syntax changes to see just how steep this hill can be).

If you do quantify the benefits and proceed to an actual proposal to optimize this, consider whether a copy-on-write dict would achieve sufficient improvements without requiring a syntax change. Can the cost of copying the dict be deferred until it is actually changed?

As for real world workloads, I would look for cases with long decorator chains so that a single frozen dict can be passed through a bunch of decorators that would copy it each time without your feature. For example, I’ve worked in code (closed source, sorry) that used decorators to process web requests and responses (validation, caching, backward compatibility patching…I don’t recall exactly what) that passed the kwargs through unchanged. Five to ten decorators was not uncommon.

That said, I’m skeptical optimizing this will help…in the cases I’ve seen the dicts are small (frequently empty) and the processing at the start and end of the chain vastly outweighs the cost of small dict copies (ie (de)serializing requests, ORM database access). I think you’d be hard pressed to find a real workload that this optimization would be statistically significant for. It would be trivial to create a false workload to show a huge gain, but that isn’t very compelling for the increased complexity and maintenance burden.

1 Like

I consider optimization in nanonseconds range and with special syntax a distraction for a developer.

If there is potential for some gain (thank you for spotting it), in this case I would prefer if the interpreter would auto-detect whether it can generate simpler/quicker bytecode. It’s probably the case only for the wrappers that just pass **kw without using it. I expect such wrappers are quite common.

7 Likes

It would of course be great if the interpreter can do that, but any function call (even invoking operators on arbitrary objects, for that matter) could allow the callee to look into the caller’s frame and access the dictionary, invalidating the possibility to optimize in most cases.