But sometimes a function is more aesthetically pleasing if the default is not None. Then we get to argue about subjective judgements of beauty in code, which is always fun
And Raymond is right that we should make AC and Signature objects smarter and better able to deal with alternative APIs, and so reduce the pressure that every API must use defaults.
VB/COM had a sentinel for âmissingâ argument. But it sounds an overkill to introduce such a sentinel to Python given the ecosystem has grown well without it.
The problem is how to not document a default value in the documentation, especially in introspection APIs like inspect.signature(). I donât think that accepting a surprising but valid default value like -1 or None is a big deal: as Guido explained, itâs just convenient and practical.
When APIs were documented manually, we used syntax like math.log(value[, base]) for functions accepting 1 or 2 arguments. I never liked this syntax, itâs barely readable with many arguments, but so far, no one came with a better syntax
Right now, the inspect.signature() API has no way to express âthe default value should not be documentedâ. See also PEP 661 Sentinel Values: many functions use sentinel values which should not be documented.
Personally Iâve never been a fan of functions with mystery internal-only defaults. I see this as an anachronism when Python function declaration syntax wasnât as rich, and a lot of functions were written in C with handmade argument parsing or PyArg_ParseTuple argument parsing, and maybe from before signature introspection. If I could wave my magic wand and change the world, Iâd change Python so that optional arguments always had default values visible in the functionâs signature.
Occasionally I stub my toe on this. For example, I might want to call a function both with an explicit value or with the default value of a parameter, but because the function has this mystery default value, I have to work a little harderâconditionally passing in the argument by splatting it in using *args or **kwargs, or maybe just writing two function calls with an if`. I admit I havenât run into this recently enough for me to remember an example.
PEP 671 works in the same kind of field, trying to make defaults more visible even when they arenât simple values. So I definitely agree with the principle (although with C-implemented functions, thereâs probably no single approach that would work for all of them).
The issue is, this breaks for builtins. It seems pretty awkward trying to explain, e.g. the range constructor in these terms. Whatâs clearer:
range.__init__(start, stop, step=1) separately allows you to call it with only one argument, as range.__init__(stop), in which case start defaults to 0 and step to 1
or
range.__init__(start, stop=NOT_PROVIDED, step=1) will check if stop is NOT_PROVIDED and, in this case, reassign the provided start value to stop and assign 0 for start
? Granted, the latter is still clearer than showing the defaults as None. But itâs still quite awkward, and then it turns out that there isnât an actual sentinel NOT_PROVIDED value you could use explicitly (even if you had access to it), and the internals actually take more of a *args-equivalent approach anyway, and in fact you canât pass arguments by keyword at allâŚ
Granted, if we had a time machine we could make it so that the built-ins only ever have Python-styled function signatures. (Maybe we could incorporate PEP 570 from the start while weâre at it.) But then it seems like youâre either stuck with the NOT_PROVIDED hack and the weird explanation in the doc (and then, do you actually want people to be able to write it with the sentinel explicitly?) Or maybe you donât get a 1-arg form at all. Or you document the start/stop parameters as *endpoints and have to explain that actually it can only be either 1 or 2 elements etc.
On the other hand, being able to write range(10, step=2) does seem kinda neatâŚ
I think this reflects more on range than on my position. As you point out, the signature of range canât be easily expressed with a Python signatureâand yet itâs a Python function. I regard the behavior of range as a historical artifact, the result of an early experiment in alternative approaches to argument processing from the early prehistory of Python. I assert that if we added range to the language today its signature would look very different.
Of course, I donât have my magic wand, and I canât retroactively change things, so my opinion there isnât really actionable. We need to live with the situation as it exists. In order to non-awkwardly express the signature of range in Python, either weâd need to change the signature of range, or change the definiton of a Python function signature so it encompasses the existing behavior of range. Since the former is a non-starter, our only remaining choices are âenhance Python function signatures to encompass rangeâ or âlive with it being awkwardâ.