-
-
Notifications
You must be signed in to change notification settings - Fork 30.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
gh-116938: Clarify documentation of dict
and dict.update
regarding the positional argument they accept
#125213
Conversation
…garding the positional argument they accept
Wording can be tweaked of course. The creator of the issue also suggested mentioning a I'm also wondering what will happen to the typeshed definitions, currently it expects |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! This seems reasonable to me: it doesn't make the docs significantly more verbose, but it does make them more precise.
It feels a bit unfortunate that the implementation detail of how CPython determines what constitutes a "mapping" is leaking out a bit here. But I think that probably just reflects the fact that people are having to take account of the exact details of this kind of thing in their own code, so it deserves to be documented, I think
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, we don't usually add NEWS entries for changes that only update docs; could you remove this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah sorry I got confused with the blurb Documentation
entry
The typeshed definition already tries to take account of this detail... Curious if you have any specific examples where type checkers have false positives or false negatives as a result of typeshed's current definitions :-) but that's a better discussion for the typeshed issue tracker than here! |
@@ -962,7 +962,7 @@ def clear(self): | |||
|
|||
def update(self, other=(), /, **kwds): | |||
''' D.update([E, ]**F) -> None. Update D from mapping/iterable E and F. | |||
If E present and has a .keys() method, does: for k in E: D[k] = E[k] | |||
If E present and has a .keys() method, does: for k in E.keys(): D[k] = E[k] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As per #116938 (comment)
Turns out I got confused here: having a |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks!
Thanks @Viicos for the PR, and @AlexWaygood for merging it 🌮🎉.. I'm working now to backport this PR to: 3.12, 3.13. |
…garding the positional argument they accept (pythonGH-125213) (cherry picked from commit 21ac0a7) Co-authored-by: Victorien <65306057+Viicos@users.noreply.github.com> Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
…garding the positional argument they accept (pythonGH-125213) (cherry picked from commit 21ac0a7) Co-authored-by: Victorien <65306057+Viicos@users.noreply.github.com> Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
GH-125336 is a backport of this pull request to the 3.13 branch. |
GH-125337 is a backport of this pull request to the 3.12 branch. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am a bit late to the party, but here is some issues I would like to address.
If a positional argument is given and it defines a ``keys()`` method, a | ||
dictionary is created by calling :meth:`~object.__getitem__` on the argument with | ||
each returned key from the method. Otherwise, the positional argument must be an | ||
:term:`iterable` object. Each item in the iterable must itself be an iterable | ||
with exactly two elements. The first element of each item becomes a key in the | ||
new dictionary, and the second element the corresponding value. If a key occurs | ||
more than once, the last value for that key becomes the corresponding value in | ||
the new dictionary. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is the point of using double spaces after periods? I see this is also present in the original .rst. Is there a special meaning for this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There used to be a convention to put two spaces after a period. It's been slowly dying out for a long time now, but you still see it a lot in older pieces of writing. (This article in the Python docs is very old.) The most important thing is to stay consistent in whatever article you're editing at any one point in time; consistency between articles on this point is not a goal for CPython's docs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see. There was two spaces after period on line 4673 (4675 right now), but after this PR it is gone, should it be added back?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I really wouldn't worry about it too much :-)
:meth:`update` accepts either another object with a ``keys()`` method (in | ||
which case :meth:`~object.__getitem__` is called with every key returned from | ||
the method). or an iterable of key/value pairs (as tuples or other iterables | ||
of length two). If keyword arguments are specified, the dictionary is then | ||
updated with those key/value pairs: ``d.update(red=1, blue=2)``. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is a premature period on line 4674 after closing brace.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch! Want to file a PR to fix it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If E present and has a .keys() method, does: for k in E.keys(): D[k] = E[k] | ||
If E present and lacks .keys() method, does: for (k, v) in E: D[k] = v | ||
In either case, this is followed by: for k, v in F.items(): D[k] = v |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here I would like it to be more specific:
If E present and has a .keys() method, does: for k in E.keys(): D[k] = E[k] | |
If E present and lacks .keys() method, does: for (k, v) in E: D[k] = v | |
In either case, this is followed by: for k, v in F.items(): D[k] = v | |
If E present and has keys attribute, does: for k in E.keys(): D[k] = E[k] | |
If E present and lacks keys attribute, does: for (k, v) in E: D[k] = v | |
In either case, this is followed by: for k, v in F.items(): D[k] = v |
In the web docs the requirements are strict:
dict
: If a positional argument is given and it defines akeys()
method, <...>. Otherwise, the positional argument must be an iterable object.update
: Accepts either another object with akeys()
method <...> or <...>.
Usage of words must
and accept
indicate that any object with non-callable keys
is not a valid argument. But phrase If E present and lacks .keys() method
does not eliminate cases with non-callable keys
.
For example:
class Object:
keys = []
def __iter__(self, /):
return iter(self.keys)
This class clearly lacks .keys() method because keys
is a list, but passing it to dict.update
results in TypeError: 'list' object is not callable
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is fine IMO. It's much less important for docstrings to be completely precise than it is for the ReST documentation. The most important thing for the docstrings is for them to be concise and easily readable in a terminal or editor.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're calling help(dict.update)
there, which has a different docstring to collections.abc.Mapping.update
(the one being edited here).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, my bad. In that case dict.update
should also be updated as it works the same way (i.e. it calls keys
attribute).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, but the docstring for dict.update
already appears to be correct, unless I'm misunderstanding.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In this PR the first sentence was updated from for k in E: D[k] = E[k]
to for k in E.keys(): D[k] = E[k]
for MutableMapping. dict.update
has the same behavior and its docstring has the same flaw, so it would be correct to fix it too.
Lines 4521 to 4525 in 2982bdb
PyDoc_STRVAR(update__doc__, | |
"D.update([E, ]**F) -> None. Update D from dict/iterable E and F.\n\ | |
If E is present and has a .keys() method, then does: for k in E: D[k] = E[k]\n\ | |
If E is present and lacks a .keys() method, then does: for k, v in E: D[k] = v\n\ | |
In either case, this is followed by: for k in F: D[k] = F[k]"); |
Consider:
class Object:
def keys(self):
return [1, 2]
def __iter__(self):
return [3, 4]
def __getitem__(self, item):
match item:
case 1: return 'one'
case 2: return 'two'
case 3: return 'three'
case 4: return 'four'
return item
print(dict(Object()))
d = {}
d.update(Object())
print(d)
Both times the result is {1: 'one', 2: 'two'}
, i.e., both times keys()
is called, the object is not iterated.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, I see what you mean now. Yes, I suppose that could also be clarified.
dict
anddict.update
treat the first argument as a mapping when it has attributekeys
without attribute__getitem__
#116938📚 Documentation preview 📚: https://cpython-previews--125213.org.readthedocs.build/