8000 PEP 692: Introduce proposed enhancements and expand Motivation section by franekmagiera · Pull Request #2732 · python/peps · GitHub
[go: up one dir, main page]

Skip to content

PEP 692: Introduce proposed enhancements and expand Motivation section #2732

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

Merged
merged 15 commits into from
Aug 23, 2022
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Expand Motivation section
  • Loading branch information
franekmagiera committed Jul 25, 2022
commit 2b2973f1304979e4261356fb8acf01e7fd93e6bf
76 changes: 73 additions & 3 deletions pep-0692.rst
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,66 @@ type annotating ``**kwargs`` is not possible. This is especially a problem for
already existing codebases where the need of refactoring the code in order to
introduce proper type annotations may be considered not worth the effort. This
in turn prevents the project from getting all of the benefits that type hinting
can provide. As a consequence, there has been a `lot of discussion <mypyIssue4441_>`__
around supporting more precise ``**kwargs`` typing and it became a
feature that would be valuable for a large part of the Python community.
can provide.

Moreover, one pattern that ``**kwargs`` are especially useful for is when a
function should accommodate optional keyword-only arguments that don't have
default values. A need for a pattern like that can arise when values that are
Copy link
Member

Choose a reason for hiding this comment

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

I find this a particularly weak argument -- optional keyword args without defaults seems an obscure edge case, and marker objects usually work just fine. Even if there are real-world examples of this need (as you show below), I don't think it's worth nearly 50 lines of text in the PEP.

Copy link
Member

Choose a reason for hiding this comment

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

For what its worth, as a random datapoint from a random Python user, I don't think I've ever seen this pattern in real life—I'm sure it happens, but "especially useful" seems like it might be overstating it and this might be more helpful nearer the end of the section, particularly considering the utility of the other two (I see them absolutely everywhere).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That makes sense, I deleted the detailed example and just linked the httpx issue. Also, moved the paragraphs around and changed the "especially useful" bit to "also convenient".

usually used as defaults to indicate no user input, such as ``None``, can be
passed in by a user and should result in a valid, non-default behavior.

As an example, let's consider a hypothetical ``HTTPClient`` class::

class HTTPClient:
def __init__(
self,
*,
auth: AuthType,
timeout: TimeoutType,
base_url: UrlType,
# And some more arguments...
): ...

def request(
self,
*,
auth,
timeout
# And some more arguments...
): ...

# And a bunch of other methods...

In this case, the default authorization and timeout strategies are passed in
via the constructor. However, calls to the ``request`` method can override the
defaults. For example, passing ``auth=None`` explicitly, could mean that no
authorization strategy should be used at all, and the same could be true for
``timeout``. In this situation there are no obvious default values for those
parameters.

This example is based on real world use cases taken from popular `httpx
<httpxSentinel_>`__ and `urllib3 <urllib3Sentinel_>`__ libraries. Those
libraries deal with this problem by introducing sentinel objects that are used
as defaults. One issue of such a solution is that if the clients subclass a
class that is part of a public API and want to override the ``request`` method,
they may need to import that sentinel object, which usually should not be made
public. Using ``**kwargs`` in this example would not have this problem and is
arguably simpler. However, currently there is no way to properly type hint such
a solution.

In addition, ``**kwargs`` can be used to reduce the amount of code needed in
cases when there is a top-level method that is a part of a public API and it
calls a bunch of helper functions, all of which expect the same keyword
arguments. Unfortunately, if those helper functions were to use ``**kwargs``,
there is no way to properly type hint them if the keyword arguments they expect
are of different types.
Copy link
Member

Choose a reason for hiding this comment

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

This strikes me as the main use case -- the need to avoid repetition when one function calls another with an identical signature. Maybe you could elaborate more on how frequent this pattern is? I've encountered it enough to think it's quite often -- e.g. I recall writing many wrappers for open() that were a pain to write. I've also seen real bugs reported that were due to such a situation where the helper was updated to accept a new keyword and the wrapper wasn't.

(The other legit use case is adding type annotations to a legacy code base that uses **kwargs wrappers a lot -- I agree with what you wrote at the top of the Motivation section about exceeding the time budget for the migration.)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added the example you mentioned to the paragraph. Also, in the next paragraph I linked mypy issue 4441 and mentioned that it links to specific examples - on one hand feels sort of lazy but I think it is a nice way to show that there is indeed a real need for that proposal.


As described in the :ref:`Intended Usage <pep-692-intended-usage>` section,
using ``**kwargs`` is not always the best tool for the job, but still it is a
widely used pattern. As a consequence, there has been a
`lot of discussion <mypyIssue4441_>`__ around supporting more precise
``**kwargs`` typing and it became a feature that would be valuable for a large
part of the Python community.
Copy link
Member

Choose a reason for hiding this comment

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

This paragraph is kind of hanging here by itself. Maybe it needs to be made part of the Introduction instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Moved it so it appears earlier in the Motivation section - without a detailed example of "facilitating optional keyword arguments that don't have default values" I think it is a nice transition from the previous two paragraphs.


Rationale
=========
Expand Down Expand Up @@ -345,6 +402,8 @@ to perform the function call::
name = kwargs["name"]
takes_name(name)

.. _pep-692-intended-usage:

Intended Usage
--------------

Expand All @@ -368,6 +427,12 @@ Another useful pattern that justifies using and typing ``**kwargs`` as proposed
is when the function's API should allow for optional keyword arguments that
don't have default values.

Moreover, using ``**kwargs`` with more precise type hinting would allow for
less code and less copy pasting in cases when a module contains a bunch of
functions that expect similar keyword arguments. Using ``**kwargs`` with a
``TypedDict`` as propsed in this PEP is much more concise than explicitly
repeating the keyword arguments every time.

However, it has to be pointed out that in some cases there are better tools
for the job than using ``TypedDict`` to type ``**kwargs`` as proposed in this
PEP. For example, when writing new code if all the keyword arguments are
Expand Down Expand Up @@ -399,6 +464,9 @@ explicitly as::

def foo(name: str, year: int): ...

Also, for the benefit of IDEs and documentation pages, functions that are part
of the public API should prefer explicit keyword arguments whenever possible.

Grammar Changes
===============

Expand Down Expand Up @@ -562,6 +630,8 @@ overloaded::
References
==========

.. _httpxSentinel: https://github.com/encode/httpx/issues/1384
.. _urllib3Sentinel: https://github.com/urllib3/urllib3/issues/2252
.. _mypyIssue4441: https://github.com/python/mypy/issues/4441
.. _mypyPull10576: https://github.com/python/mypy/pull/10576
.. _mypyExtensionsPull22: https://github.com/python/mypy_extensions/pull/22/files
Expand Down
0