-
-
Notifications
You must be signed in to change notification settings - Fork 5.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
API: keywords only arguments #14714
Comments
I'm certainly in favor for new functions, but I'm perhaps less convinced that overhauling the signatures for older functions is a good place to spend energy for us or our consumers. I don't feel too strongly about it either way, but certainly may cause quite a bit of entropy on downstream code for an arguably-modest maintenance improvement. I suppose if scikit-learn went through with it we should at least consider the idea though. |
+1 for using in new code, keyword-only args are very helpful. For existing code, I don't think we can do it for the whole code base - too painful. We could look at it for a specific submodule if there's a good reason for it I think (for example, we want to add more keyword arguments and the order of arguments is becoming quite odd). |
Ok then, let's have this for new code and keep the current case-by-case changes we already do. What should be our workflow for proposals? Should we just close this? Have some dedicated section for proposals in the doc? I see that NumPy as something to keep track of changes https://github.com/numpy/numpy/tree/main/doc/release/upcoming_changes. Some other libraries have like a decision diary. If we would just add this in the doc, this would already be covered with gh-13955 (which IMO we should merge) |
One particular detail is that scikit-learn API is generally very involved so probably users are using keywords anyways (somehow VSCode is affecting if I have to guess). But on SciPy side this is not the case; If we force it on very often used functions this goes straight to annoyance, say So it is the predicted usage frequency should be the deciding factor and not the module in my opinion. |
For example, optimize.minimize is a very good candidate for kw-only usage to avoid misconceptions. |
I agree, it's a judgement call. It doesn't need complex signatures to be useful though. Even simple numpy function calls are often easier to read with keywords, like |
Another example for not using the keywords I'm using loc and scale in stats distributions most of the time as positional arguments to do something like Also it becomes more important to have keyword names that are consistent across similar functions. (In statsmodels copulas I switched to generic |
I personally disagree. This interface is not a good design and for Copulas we could have done something else. I had another design before you took over my PR. Having common interface is handy for objects which are relevant between each other like 2 Archimedean copulas. But wanting the same interface for any distribution for instance is pointless since a user would almost never consider switching from one distribution to any other. They would rather use some defined subset of possibilities. This is even more true when we have objects doing lot of very specialized work (like optimization methods or copulas where assumptions and use cases might be very different from each other). Just going through old matplotlib code is terrible and should be enough to convince anyone that args, kwargs should be avoided as much as possible. |
All the regression models in statsmodels use The same will apply once with have regression models estimated by MLE with copulas on the inside. @tupui Try to write some generic code with scipy distributions that uses shape parameters as keywords. They might be nice for users that work only with one distribution, but not if you want to write code that estimates a large number of distributions. |
My objective when writing code here is not to make my-as in a maintainer-life easier. Things like type hints are always a pain point for me to add, but I am the first one to be happy to have these while I am using the library. Hence I am pushing for it. As major libraries, we should have a strong focus on the user experience. Of course the developer experience should not be completely left aside, but I would argue that most things we would discuss here are in the end not that hard... Long story short, if it's not something totally obscure, I would favour user experience over mine (type hints again).
I hear your points, but if you read these articles you will not find
Same as above. As Ralf already said on the mailing list, there is lot of legacy here and we would probably not redo this if we would give the module a fresh start. In the meantime, you could just have a mapping. |
That's on of my favorites, hundreds of greek symbols and decorated characters and not consistently used across articles, books and fields. After reading 3 pages, I don't remember which is which. And I really like authors that don't use words in sentences that might give a hint about what this funny, squiggly character, that was defined many pages ago, stands for. What's phi and psi in archimedean copulas? More specific to scipy. In the fmin case, using arguments as positional wouldn't work either because keyword positions or ordering varies also across function. |
As with software, there are some inconsistencies. It's just a matter of taking side here. Just either pick phi or psi. My point is that there is not an infinite number of parameters.
It's exactly the use case of having keywords only arguments. The order does not matter. |
@tupui |
The consensus here and during meetings was: we do this for new code (as we've been doing), we don't update all old code. On a case by case basis, we can update old code if it really makes sense doing it. |
Thanks @tupui; I cannot read much more than what's in this issue (so it's hard to know what was discussed and how consensus was achieved), but I'd like to know if a lightweight (at the function-definition site at least) decorator has been considered that would let us allow to move to kwarg-only functions where we deem it useful (which, IMO, is the great majority): Say we have a function right now: # status as of 1.8
def scipy_func(x, y, kw1=None, kw2=None):
"""The docstring for scipy_func"""
return "a" * x + "b" * y + "c" * (kw1 is not None) + "d" * (kw2 is not None) We could transition this as follows: # transition in scipy 1.N: make main implementation kwarg-only (note the "*"!), but private
def _scipy_func(x, y, *, kw1=None, kw2=None):
"""The docstring for scipy_func"""
return "a" * x + "b" * y + "c" * (kw1 is not None) + "d" * (kw2 is not None)
# create wrapper with existing behaviour that warns if keyword arguments are passed as positional
@_pos_to_kwonly_decorator_factory(_scipy_func, pos_arg_names=["x", "y"],
kwonly_arg_names=["kw1", "kw2"])
def scipy_func(x, y, kw1=None, kw2=None):
pass # <- not just for exposition; we don't need to redefine the body here With this wrapper, the deprecation would work as expected:
After a suitable transition, we delete the wrapper and make the keyword-only function public: # future in 1.(N+2) after transition
def scipy_func(x, y, *, kw1=None, kw2=None):
"""The docstring for scipy_func"""
return "a" * x + "b" * y + "c" * (kw1 is not None) + "d" * (kw2 is not None) The only thing I've left out so far is the actual logic to do the decorator; while it can surely be improved/prettified, it's not that hard: import functools
import warnings
def _pos_to_kwonly_decorator_factory(kwonly_func, pos_arg_names, kwonly_arg_names):
# QoI: add some checks that the signatures of kwonly_func/dummy_func
# match the one that's declared by the factory
def actual_decorator_after_applying_factory_params(dummy_func):
# note that we intentionally do not use the function passed
# to the decorator but use the one given to the factory
@functools.wraps(kwonly_func)
def wrapper(*args, **kwargs):
num_pos = len(pos_arg_names)
if len(args) > num_pos:
warnings.warn(f"use of {kwonly_func.__name__.strip('_')} with "
f"positional arguments for the following "
f"keyword arguments is deprecated:\n"
f"{', '.join(kwonly_arg_names)}\n"
f"Please add the respective argument names to "
f"the call-site, e.g. '{kwonly_arg_names[0]}=...'",
DeprecationWarning)
pos_args = args[:num_pos]
kwonly_args = dict(zip(kwonly_arg_names, args[num_pos:])) | kwargs
else:
pos_args, kwonly_args = args, kwargs
return kwonly_func(*pos_args, **kwonly_args)
return wrapper
return actual_decorator_after_applying_factory_params |
@h-vetinari we did not have to discuss code. If we would have done something, sklearn already went down that route and we could just have taken their approach verbatim as it worked well. I don't think there would be any push back if you added such decorator in our utils, but as I said it was decided to not change anything in batch. I suggest the following, if you identify a function that you think we should refactor, open a discussion for this case. If the outcome is that we want to refactor, then we can introduce the decorator for the rewrite. |
OK cool. The scikit helper is also much prettier than the one I hacked together. I think it would be good idea to have something of a standing policy that if we're touching the signature of a function in a way that needs a deprecation cycle (e.g. remove/rename a keyword) to also use that to turn the keywords into keyword-only arguments, but for now I'll just try to remember that when I come across a case. |
@h-vetinari feel free to put this on the agenda of our next community meeting. |
Adding a note to http://scipy.github.io/devdocs/dev/api-dev/api-dev-toc.html about positional-only and keyword-only arguments sounds like a good idea to me. Re wholesale changes: that's going to break a ton of code, which isn't justified. It's also not the case that all keywords should be turned into keyword-only arguments, there are cases where positional usage is just fine. So it'd be a lot of work to make decisions as well. tl;dr let's not do that. |
What might be worth it if someone wants to spend effort on, is to check various functions which receive user callables: whether they handle keyword-only arguments. I suspect we do OTOH, there were no user bug reports, so maybe even this may wait until actual need arises. |
I'll try to look into that, but if someone gets there first, please don't hesitate!
I'm not sure if that's splitting hairs, but I don't consider the addition of a deprecation warning a breaking change. If there's enough time to transition then that's just part of the normal cycle. That said, I wasn't proposing wholesale conversion (neither in bulk nor per function - as my example showed, it's fine IMO if positional arguments stay positional), but certainly functions with many keyword arguments (say more >2) should IMO consider enforcing that "keywordness", at least long-term. |
That's not how it works. For some packages, deprecation warnings need to be addressed immediately. But more importantly, after 2 or more releases we will remove the deprecated features, and then it's a hard break. Which is a problem for end user code, snippets on SO, and all kinds of other static or not actively-maintained code. |
I respectfully disagree in turn. Projects choosing to treat warnings as errors voluntarily onboard the responsibility to deal with such breaks (they're warnings and not errors for the precise reason to not cause a hard break - if a project chooses to equate them then it's up to them to deal with the consequences). In turn, I don't think it's reasonable to call it a "hard break" if the behaviour was raising deprecation warnings before, aside from the fact that the deprecation-to-removal window could easily be more than 2 releases in cases that merit it. The SO questions are a muddier question, but that code as well is "maintained" by the community with a certain lag (aside from being clearly timestamped and thus associated with a given API epoch), and not IMO in itself a good enough reason to indefinitely forgo (sufficiently impactful) evolution. Backward compatibility is a good thing, but as with anything, dosis sola facit venenum, and I am opposed to elevate it to the point where worthwhile changes are stunted indefinitely based on the fact-of-life that unmaintained code will bitrot - it will do that in any case, whether we try to support that illusion or not. |
This is not something I disagree with. That said, keyword-only arguments aren't a significant enough improvement to do a very large scale deprecation. They don't add any new functionality nor do they change how idiomatic code is already written after all. |
This I can understand.
I'd say the provide an incremental API-safety benefit (e.g. putting supposed keyword args in the wrong order positionally); I hope we can iterate our way there on a per-function or per-module basis over time. |
Is your feature request related to a problem? Please describe.
Keywords only arguments are arguably clearer and allow easier refactoring on our side. When adding a new feature, we are already thinking about using
*
to declare keyword only arguments.Describe the solution you'd like.
scikit-learn
already went through this process and with their v1 coming up, the whole code base is already keywords-only arguments. Hence, if accepted, I propose we follow the same path.They did it in two phases:
f(a, *, b)
and you dof(1, 2)
instead off(1, b=2)
) then it will raise a deprecation warning. The function will still be working as the keyword arguments would not be enforced yet. The wrapper will convert the positional argument into a keyword argument.xref scikit-learn/scikit-learn#15005
The text was updated successfully, but these errors were encountered: