Skip to content
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

Merged
merged 3 commits into from
Oct 11, 2024

Conversation

Viicos
Copy link
Contributor

@Viicos Viicos commented Oct 9, 2024

…garding the positional argument they accept
@Viicos
Copy link
Contributor Author

Viicos commented Oct 10, 2024

Wording can be tweaked of course. The creator of the issue also suggested mentioning a keys attribute, not necessarily a method.

I'm also wondering what will happen to the typeshed definitions, currently it expects _typeshed.SupportsKeysAndGetitem.

Copy link
Member

@AlexWaygood AlexWaygood left a 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

Doc/library/stdtypes.rst Outdated Show resolved Hide resolved
Doc/library/stdtypes.rst Outdated Show resolved Hide resolved
Copy link
Member

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?

Copy link
Contributor Author

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

@AlexWaygood AlexWaygood added needs backport to 3.12 bug and security fixes needs backport to 3.13 bugs and security fixes labels Oct 10, 2024
@AlexWaygood
Copy link
Member

I'm also wondering what will happen to the typeshed definitions, currently it expects _typeshed.SupportsKeysAndGetitem.

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]
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Viicos
Copy link
Contributor Author

Viicos commented Oct 11, 2024

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!

Turns out I got confused here: having a .keys() method is the only requirement for dict() to consider the argument as a mapping, but __getitem__ must be defined or it will crash at runtime. So the current definition is good!

Copy link
Member

@AlexWaygood AlexWaygood left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

@AlexWaygood AlexWaygood enabled auto-merge (squash) October 11, 2024 22:41
@AlexWaygood AlexWaygood self-assigned this Oct 11, 2024
@AlexWaygood AlexWaygood merged commit 21ac0a7 into python:main Oct 11, 2024
36 checks passed
@miss-islington-app
Copy link

Thanks @Viicos for the PR, and @AlexWaygood for merging it 🌮🎉.. I'm working now to backport this PR to: 3.12, 3.13.
🐍🍒⛏🤖

miss-islington pushed a commit to miss-islington/cpython that referenced this pull request Oct 11, 2024
…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>
miss-islington pushed a commit to miss-islington/cpython that referenced this pull request Oct 11, 2024
…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>
@bedevere-app
Copy link

bedevere-app bot commented Oct 11, 2024

GH-125336 is a backport of this pull request to the 3.13 branch.

@bedevere-app bedevere-app bot removed the needs backport to 3.13 bugs and security fixes label Oct 11, 2024
@bedevere-app
Copy link

bedevere-app bot commented Oct 11, 2024

GH-125337 is a backport of this pull request to the 3.12 branch.

@bedevere-app bedevere-app bot removed the needs backport to 3.12 bug and security fixes label Oct 11, 2024
AlexWaygood added a commit that referenced this pull request Oct 11, 2024
…egarding the positional argument they accept (GH-125213) (#125337)

Co-authored-by: Victorien <65306057+Viicos@users.noreply.github.com>
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
AlexWaygood added a commit that referenced this pull request Oct 11, 2024
…egarding the positional argument they accept (GH-125213) (#125336)

Co-authored-by: Victorien <65306057+Viicos@users.noreply.github.com>
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
@Viicos Viicos deleted the fix-issue-116938 branch October 12, 2024 11:09
Copy link
Contributor

@Prometheus3375 Prometheus3375 left a 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.

Comment on lines +4508 to +4515
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.
Copy link
Contributor

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?

Copy link
Member

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.

Copy link
Contributor

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?

Copy link
Member

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 :-)

Comment on lines +4672 to +4676
: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)``.
Copy link
Contributor

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.

Copy link
Member

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?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines +965 to 967
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
Copy link
Contributor

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:

Suggested change
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 a keys() method, <...>. Otherwise, the positional argument must be an iterable object.
  • update: Accepts either another object with a keys() 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.

Copy link
Member

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.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. Any particular reason why is there no a before .keys() method in the second sentence? It is present in 3.12, but looks like it was removed prior this PR.
image

Copy link
Member

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).

Copy link
Contributor

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).

Copy link
Member

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.

Copy link
Contributor

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.

cpython/Objects/dictobject.c

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.

Copy link
Member

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
docs Documentation in the Doc dir skip news
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants