Repository navigation
Dictionary union. (PEP 584) #80325
Copy link
Copy link
Closed
Labels
3.9 (EOL)end of lifeend of lifeinterpreter-core(Objects, Python, Grammar, and Parser dirs)(Objects, Python, Grammar, and Parser dirs)type-featureA feature request or enhancementA feature request or enhancement
Description
Note: these values reflect the state of the issue at the time it was migrated and might not reflect the current state.
Show more details
GitHub fields:
assignee = None
closed_at = <Date 2020-03-25.15:24:24.165>
created_at = <Date 2019-02-28.04:18:58.342>
labels = ['interpreter-core', 'type-feature', '3.9']
title = 'Dictionary union. (PEP 584)'
updated_at = <Date 2020-07-07.05:39:40.553>
user = 'https://github.com/brandtbucher'bugs.python.org fields:
activity = <Date 2020-07-07.05:39:40.553>
actor = 'Raps Uk'
assignee = 'none'
closed = True
closed_date = <Date 2020-03-25.15:24:24.165>
closer = 'brandtbucher'
components = ['Interpreter Core']
creation = <Date 2019-02-28.04:18:58.342>
creator = 'brandtbucher'
dependencies = []
files = []
hgrepos = []
issue_num = 36144
keywords = ['patch']
message_count = 66.0
messages = ['336798', '336803', '336808', '336810', '336811', '336812', '336816', '336820', '336847', '336848', '336849', '336854', '337094', '337107', '337266', '337267', '358305', '362619', '362620', '362621', '362626', '362645', '362660', '362663', '362666', '362726', '362729', '362740', '362757', '362759', '362761', '362774', '362779', '362810', '362813', '362816', '362817', '362819', '362821', '362823', '362827', '362828', '362831', '362835', '362852', '362855', '363526', '363527', '363528', '363613', '363625', '363889', '363953', '364106', '364107', '364195', '364196', '364887', '364900', '364969', '364992', '365245', '365333', '371520', '371549', '373205']
nosy_count = 15.0
nosy_names = ['gvanrossum', 'rhettinger', 'mark.dickinson', 'scoder', 'serhiy.storchaka', 'steve.dower', 'josh.r', 'Aaron Hall', 'slam', 'xtreak', 'brandtbucher', 'curtisbucher', 'chaburkland', 'justjais', 'Raps Uk']
pr_nums = ['12088', '18659', '18729', '18814', '18832', '18911', '18931', '18967', '19106', '19127', '19221']
priority = 'normal'
resolution = 'fixed'
stage = 'patch review'
status = 'closed'
superseder = None
type = 'enhancement'
url = 'https://bugs.python.org/issue36144'
versions = ['Python 3.9']Metadata
Metadata
Assignees
Labels
3.9 (EOL)end of lifeend of lifeinterpreter-core(Objects, Python, Grammar, and Parser dirs)(Objects, Python, Grammar, and Parser dirs)type-featureA feature request or enhancementA feature request or enhancement
Activity
brandtbucher commentedon Feb 28, 2019
...as discussed in python-ideas. Semantically:
d1 + d2 <-> d3 = d1.copy(); d3.update(d2); d3
d1 += d2 <-> d1.update(d2)
Attached is a working implementation with new/fixed tests for consideration. I've also updated collections.UserDict with the new __add__/radd/iadd methods.
rhettinger commentedon Feb 28, 2019
I believe that Guido rejected this when it was proposed a few years ago.
tirkarthi commentedon Feb 28, 2019
Python ideas discussion in 2015 : https://mail.python.org/pipermail/python-ideas/2015-February/031748.html
LWN summary : https://lwn.net/Articles/635397/
serhiy-storchaka commentedon Feb 28, 2019
I believe it was proposed and rejected multiple times.
rhettinger commentedon Feb 28, 2019
For the record, I'm opposed to the idea.
Use of the + operator is a temptation to produce new dictionaries rather than update an existing dict in-place which is usually what you want.
We already have ChainMap() which presents a single view of multiple mappings with any copying.
It is natural to expect the plus operator to be commutative, but this operation would necessarily be non-commutative.
Many other APIs are modeled on the dict API, so we should not grow the API unless there is a big win. The effects would be pervasive.
I don't see other languages going down this path, nor am I seeing dict subclasses that implement this functionality. Those are indications that this more of a "fun thing we could do" rather than a "thing that people need".
The existing code already reads nicely:
That reads more like self explanatory English than:
The latter takes more effort to correctly parse and
makes it less clear that you're working with dicts.
serhiy-storchaka commentedon Feb 28, 2019
In Python, the plus operator for sequences (strings, lists, tuples) is non-commutative.
But I have other arguments against it:
It conflicts with the plus operator of Counter (which is a specialized dict): Counter(a=2) + Counter(a=3) == Counter(a=5), but the proposed idea makes dict(a=2) + dict(a=3) == dict(a=3).
We already have a syntax for dict merging: {**d1, **d2}. It works with arbitrary mappings, in contrary to the plus operator, which needs a special support in argument types.
rhettinger commentedon Feb 28, 2019
For sequences, that is obvious and expected, but not so much with mappings where the order of overlapping keys is determined by the left operand and the value associated with those keys is determined by the right operand.
Also with sequences the + operator actually means "add to", but with dictionaries it means "add/or replace" which is contrary to the normal meaning of plus. I think that was one of Guido's reasons for favoring "|" instead of "+" for set-to-set operations.
This is a good point.
scoder commentedon Feb 28, 2019
Which doesn't mean that "d1 + d2" isn't much more intuitive than this special-character heavy version. It takes me a while to see the dict merge under that heap of stars. And that's already the shortest example.
The RHS of "d += M" doesn't have to be a dict IMHO, it could be any mapping. And even "dict(X) + M" doesn't look all too bad to me, even though there's "dict(X, **M)".
That's why there would be support for "+=". The exact same argument already fails for lists, where concatenation is usually much more performance critical than for the average little dict. (And remember that most code isn't performance critical at all.)
Which is a different use case that is unlikely to go away with this proposal.
This is a valid argument, although it always depends on the concrete code what the most readable way to express its intentions is. Again, this doesn't really differ for lists.
Let's wait for the PEP, I'd say.
MojoVampire commentedon Feb 28, 2019
scoder: dict(X, **M) is broken unless M is known to be string keyed (it used to work, but in Python 3, it will raise a TypeError). It's part of the argument for the additional unpacking generalizations from PEP-448; {**X, **M} does what dict(X, **M) is trying to do, but without abusing the keyword argument passing convention.
You also claim "It takes me a while to see the dict merge under that heap of stars", but that's at least as much about the newness of PEP-448 (and for many Python coders, a complete lack of familiarity with the pre-existing varargs unpacking rules for functions) as it is about the punctuation; after all, you clearly recognize dict(X, **M) even though it's been wrong in most contexts for years.
In any event, I'm a strong -1 on this, for largely the same reasons as Raymond and others:
It doesn't provide any new functionality, just one more way to do it; += is satisfied by .update, + is satisfied (more generally and efficiently) by the unpacking generalizations
It's needlessly confusing; addition is, for all existing types in the standard library I can think of, lossless; the information from both sides of the + is preserved in some form, either by addition or concatenation (and in the concatenation case, addition is happening, just to the length of the resulting sequence, and order is preserved). Addition for dictionaries would introduce new rules specific to dicts that do not exist for any other type regarding loss of values, non-additive resulting length, etc. Those rules would likely be similar to those of dict literals and the update method, but they'd need to be made explicit. By contrast, the PEP-448 unpacking generalization rules followed the existing rules for dict literals; no special rules occur, it just behaves intuitively (if you already knew the rules for dict literals without unpacking being involved).
Almost any generic, duck-typing based code for which addition makes sense will not make sense for dicts simply because it loosens the definition of addition too much to be useful, so best case, it still raises TypeError (when dicts added to non-dict things), worst case, it silently operates in a way that violates the rules of both addition and concatenation rather than raising a TypeError that the generic code could use to determine the correct thing to do.
The already mentioned conflict with Counter (which already has an addition operator, with lossless semantics)
(Minor) It means PyDict_Type needs a non-NULL tp_as_number, so now it's slightly slower to reject dicts as being non-numeric at the C layer
Problem #2 could be used to argue for allowing | instead of + (which would also resolve #4, and parts of #3), since | is already used for unioning with sets, and this operation is much closer to a union operation than addition or concatenation. Even so, it would still be misleading; at least with sets, there is no associated value, so it's still mostly lossless (you lose the input lengths, but the unique input data is kept); with dicts, you'd be losing values too.
Basically, I think the PEP-448 unpacking syntax should remain as the "one-- and preferably only one --obvious way to" combine dictionaries as a one-liner. It's more composable, since it allows adding arbitrary additional key/value pairs, and more efficient, since it allows combining more than two dicts at once with no additional temporaries: dicta + dictb + dictc requires "dictab" to be made first, then thrown away after dictab + dictc produces dictabc, while {**dicta, **dictb, **dictc} builds dictabc directly.
The only real argument I can see for not sticking to unpacking is that it doesn't allow for arbitrary dict-like things to produce new dict-like things directly; you'd have to rewrap as myspecialdict({**speciala, **specialb}). But I don't think that's a flaw worth fixing if it means major changes to the behavior of what I'm guessing is one of the three most commonly used types in Python (along with int and tuple, thanks to the integration of dicts into so many facets of the implementation).
gvanrossum commentedon Feb 28, 2019
I changed my mind and am now in favor. Most of the arguments against could also be used against list+list. Counter addition is actually a nice special case of this -- it produces the same keys but has a more sophisticated way of merging values for common keys. Please read the python-ideas thread!
MojoVampire commentedon Feb 28, 2019
Also note: That Python ideas thread that xtreak linked ( https://mail.python.org/pipermail/python-ideas/2015-February/031748.html ) largely rejected the proposal a couple weeks before PEP-448 was approved. At the time, the proposal wasn't just about +/+=; that was the initial proposal, but operator overloading was heavily criticized for the failure to adhere to either addition or concatenation semantics, so alternate constructors and top-level functions similar to sorted were proposed as alternatives (e.g. merged(dicta, dictb)). The whole thread ended up being about creating an approved, built-in way of one-lining: d3 = d1.copy(); d3.update(d2)
A key quote though is that this was needed because there was no other option without rolling your own merged function. Andrew Barnert summarized it best:
"I'm +1 on constructor, +0.5 on a function (whether it's called updated or merged, whether it's in builtins or collections), +0.5 on both constructor and function, -0.5 on a method, and -1 on an operator.
"Unless someone is seriously championing PEP-448 for 3.5, in which case I'm -0.5 on anything, because it looks like PEP-448 would already give us one obvious way to do it, and none of the alternatives are sufficiently nicer than that way to be worth having another."
As it happens, PEP-448 was put in 3.5, and we got the one obvious way to do it.
Side-note: It occurs to me there will be one more "way to do it" in 3.8 already, thanks to PEP-572:
(d3 := d1.copy()).update(d2)
I think I'll stick with d3 = {**d1, **d2} though. :-)
tirkarthi commentedon Feb 28, 2019
Current python-ideas thread for the issue : https://mail.python.org/pipermail/python-ideas/2019-February/055509.html
45 remaining items
gvanrossum commentedon Mar 13, 2020
New changeset d648ef1 by Charles Burkland in branch 'master':
bpo-36144: Update os.environ and os.environb for PEP-584 (bpo-18911)
d648ef1
gvanrossum commentedon Mar 13, 2020
New changeset 6d674a1 by Brandt Bucher in branch 'master':
bpo-36144: OrderedDict Union (PEP-584) (bpo-18967)
6d674a1
brandtbucher commentedon Mar 14, 2020
Three other MutableMappings we might want to update:
Shelf is up in the air, since it doesn't look like it defines a copy() equivalent... I also have no experience with it. Since it's a MutableMapping subclass, (not a dict subclass), we could in theory hold off on updating this until someone asks for it, without backward compatibility issues.
I think the other two should be updated, though. I can coordinate PRs for them this week.
gvanrossum commentedon Mar 14, 2020
I definitely think we should leave Shelf alone, it's a toy class from a different era.
It makes sense to update the weak dicts; hopefully the | and |= operators can be implemented in terms of other, more primitive operations, so we will have assurance that these classes' essential behavior is preserved.
gvanrossum commentedon Mar 23, 2020
New changeset f393b2c by Curtis Bucher in branch 'master':
bpo-36144: Add PEP-584 operators to collections.ChainMap (bpo-18832)
f393b2c
gvanrossum commentedon Mar 23, 2020
New changeset 25e580a by Curtis Bucher in branch 'master':
bpo-36144: Add union operators to WeakKeyDictionary (bpo-19106)
25e580a
gvanrossum commentedon Mar 25, 2020
New changeset 8f1ed21 by Curtis Bucher in branch 'master':
bpo-36144: Add union operators to WeakValueDictionary584 (bpo-19127)
8f1ed21
brandtbucher commentedon Mar 25, 2020
And... that's it! Big thanks to everybody who had a part in making this happen.
gvanrossum commentedon Mar 29, 2020
I'm guessing this can be closed?
gvanrossum commentedon Mar 30, 2020
I guess we should keep this open until Raymond Hettinger has given feedback on #18832 (where we have the option of changing to Brandt's proposal from #18832 (comment)).
justjais commentedon Jun 15, 2020
First off, thanks for adding the feature, it's much appreciated.
But it'd be great if you guys can enable list merge for the dict having list as its value, in current form I believe it's handling only "key: value" merge.
brandtbucher commentedon Jun 15, 2020
Similar behavior was considered and ultimately rejected by the PEP as being too specialized:
https://www.python.org/dev/peps/pep-0584/#concatenate-values-in-a-list
What you're asking for it subtly different, and even *more* specialized than that. I'd recommend just subclassing dict and overriding the operator, as the PEP suggests.
RapsUk commentedon Jul 7, 2020
Taking the union of items() in Python 3 (viewitems() in Python 2.7) will also fail when values are unhashable objects (like lists, for example). Even if your values are hashable, since sets are semantically unordered, the behavior is undefined in regards to precedence. So don't do this:
http://net-informations.com/python/ds/merge.htm