Exception for parameter errors

At the moment, when you call a function, if there is any mismatch between the function parameters and the supplied arguments, TypeError is raised. For example: # Too few arguments len() # Too many arguments ord('a', 'b') # Invalid keywords min([], func=len) etc. (There are probably others.) This makes it harder to do feature detection in multi-version code than it need do. Here's an example of feature detection that does work: # math.gcd requires exactly two arguments prior to 3.9 # but soon to take arbitrary number of arguments in 3.9 try: gcd() except TypeError: ... But sometimes its not absolutely clear whether the TypeError is because I've passed the wrong number of arguments or the wrong type of argument: try: foobar(spam='a', eggs=0) except TypeError: ... When I read that, I have to check the documentation to see why a TypeError could be generated. Sure, a comment will help, but comments are notoriously untrustworthy: "At Resolver we've found it useful to short-circuit any doubt and just refer to comments in code as 'lies'." --Michael Foord paraphrases Christian Muirhead on python-dev, 2009-03-22 Proposal: add a new exception, ParameterError, for parameter errors. For backwards compatibility it would have to be a subclass of TypeError. Where the interpreter now raises TypeError for invalid parameters, it will switch to ParameterError. SyntaxErrors will remain SyntaxErrors. min(key=func, values) SyntaxError: positional argument follows keyword argument This will allow developers to distinguish genuine type errors (argument is the wrong type) from other calling errors (argument is the correct type, but passed in the wrong way). Should there be a series of separate exceptions for TooFewArguments, TooManyArguments, InvalidKeywordArgument, etc? No, I don't think so. We don't need to bloat the builtins with three (or more) new exceptions if only one will do. Normally it will be enough for the developer to read the error message, which will distinguish between the various kinds of parameter error. Most people won't care about programmatically distinguishing the cases. Reading the message will be enough. But for stability reasons (what if the error messages are localised, or change in the future?), we could give the exception a "reason" attribute, with a documented stable ID, and make that the official way to programmatically distinguish between the different kinds of parameter errors: except ParameterError as err: if err.reason == 1: # too few arguments The signature could be: ParameterError(*args, *, reason) Thoughts? -- Steven

On Mar 3, 2020, at 23:11, Steven D'Aprano <steve@pearwood.info> wrote:
I liked everything up to using magic numbers like this. Nobody will remember whether it’s 1 for too many arguments or too few; they’ll have to check the docs every time it’s been more than 20 minutes since they wrote the last handler. If you need to programmatically distinguish these, and you can’t use subclasses to do it, at least make it something like a named class attribute, so you can check if it’s TooFewArguments instead of 1. (That still isn’t as nice as an enum, but presumably 95% of the time when you display the reason you’ll also be displaying the error message, so that’s fine.) The other question: what exactly do you propose to change? I don’t think you can do this on the caller side (the ceval bytecode handler and the PyCall functions). But if you put it on the callee side, how do you handle everything? Presumably you want to handle C functions as well as Python functions; can you change the PyArg parsing functions? (I assume argclinic uses those?) That still leaves functions that parse *args and **kw and raise TypeError manually, whether in Python or in C, but I suppose you can say that in that case it really isn’t a parameter error (the signature really is *args, **kw so everything matches). What about inspect.Signature.bind?

Hi The present behaviour is: >>> len() TypeError: len() takes exactly one argument (0 given) >>> len(1, 2) TypeError: len() takes exactly one argument (2 given) This is nothing special about built-ins. The same goes for: >>> def f(): pass >>> f(1) TypeError: f() takes 0 positional arguments but 1 was given I find this behaviour perfectly satisfactory. The OP's problem can be resolved by looking at the exception, whose type is TypeError. For this to be completely reliable, certain aspects the exception message need to be stable. It may be worth stating that this will be done, at least for certain exception. By the way, using the exception instance will produce code that works with earlier versions of Python. -- Jonathan

Postscript: Things are not as clean as I have hoped and said. However, I don't think this changes the general form of the conclusion. Just that a little more work is needed to achieve the goal. Python 3.7 and 3.8: >>> def f(x): pass >>> f() TypeError: f() missing 1 required positional argument: 'x' Also note that in Python 3.8 we have: >>> def f(x,/): pass >>> f() TypeError: f() missing 1 required positional argument: 'x' Recall that we have >>> len() TypeError: len() takes exactly one argument (0 given) Note that the parameter of 'f' has a name, but not the parameter for 'len'. I think I was expecting >>> def f(x,/): pass >>> f() TypeError: f() takes exactly one argument (0 given) -- Jonathan

On Wed, Mar 04, 2020 at 10:12:34AM +0000, Jonathan Fine wrote:
Error messages are not part of the function API and are not guaranteed to be a constant part of the language. We shouldn't expect every single Python interpreter (CPython, IronPython, Jython, Nuitka, MicroPython, PyPy, Stackless etc) to use exactly the same error messages. It shouldn't be a *feature change* if an error message changes. We shouldn't have to use a deprecation period or a `__future__` import before we can improve error messages, fix spelling or grammatical errors, or add extra information to the error message. Error messages might be localised into the native language of the user. Parsing error messages is a fragile and error-prone technique that should be avoided if at all possible. -- Steven

On Wed, Mar 04, 2020 at 01:08:35AM -0800, Andrew Barnert wrote:
I liked everything up to using magic numbers like this.
I'm not wedded to the idea of error number. I chose them because I expect there will only be a few of them (three?) and the majority of the time you won't bother to check the number. If you are debugging an exception that occurs in code that's supposed to be working, you have a bug and you will read the error message to find out what it means, not inspect the error number. It's only when you intentionally capture the exception that the error number might matter, and even then probably not that often. I expect that in the majority of cases, there's only one possible failure mode. E.g. you can't have both too many and too few arguments in the same call, and you'll know which is going to be the case: try: spam(1, 2) except ParameterError: # Some versions of spam only accept 1 argument; # some accept 2. If we get a ParameterError, we # must be running the old 1-arg version. Another alternative is to testing for key phrases in the exception message: if "too few arguments" in err.args[0]: ... but that's fragile and only works until the message gets changed to say "not enough arguments". (The error message is not part of the API, so it can change without warning.) But as I said, I don't think this part of the proposal would be used very often, and if it becomes a real sticking point I'd be prepared to drop it. The important thing is to be able to programmatically distinguish the case where arguments don't match the parameters from the case where an argument is the wrong type.
Nobody will remember whether it’s 1 for too many arguments or too few;
If we use an error number, I will beg for the numbers to be documented in the exception docstring so that `help(ParameterError)` will show the list. I don't expect that there will be many. I can only think of three, even if there is triple that it's still small enough to put into the docstring. But this is just arguing about the colour of the bikeshed. Do you agree with me that the bikeshed itself is useful?
I don't know enough about the Python internals to give a definitive answer, but I'm assuming/hoping that there is a single, or at most a few, places in the interpreter that matches up arguments to formal parameters, and if there's a discrepency it currently raises TypeError. PyCall sounds promising :-)
Indeed. I don't have any expectations about those functions. If the maintainer of the function eventually changes whatever error they are currently raising to ParameterError, that will be grand, but I don't expect every third-party function that implements some custom variety of parameter handling to support this. If they do, great, if they don't, we're no worse off than the status quo.
What about inspect.Signature.bind?
Do you have a specific concern? Signature.bind promises to raise TypeError if the arguments don't match the parameters. A change to ParameterError should be completely backwards compatible since it will be a subclass of TypeError. -- Steven

Steven D'Aprano wrote: Another alternative is to testing for key phrases in the exception
We are talking here about an exception message that is generated by core Python. So there would be some warning of change, namely the review process for core Python. I agree that there is a problem to be solved here. Perhaps it is easier and better for core Python to decide to freeze certain messages, than to add a new exception type. -- Jonathan

On Wed, Mar 04, 2020 at 11:10:15AM +0000, Jonathan Fine wrote:
You can't call the error message a stable part of the language without forbidding changes to the error message. Essentially, you are asking for changes to error messages to be treated as a breaking change. That includes fixing typos and spelling errors, rewording clumsy phrases and adding extra or better quality information to the message. These are all breaking changes if you expect error messages to be stable. The precise wording of error messages would have to be documented. Not everyone follows the review process for interpreter changes, so changes would have to be listed in those docs, and the "What's New" for each release. In my own code I would never accept a request to freeze error messages. To be frank, I think it would be intolerable. I would never, ever ask the core developers to take on this additional burden. It would also rule out any possibility of having error messages localised to the user's native language.
How do you decide which functions should have their error messages frozen, and which do not? Let's suppose you make that decision, and you declare that len(), map(), zip(), itertools.count() and functools.wraps() will have their error message frozen, but the other functions don't. How does that help me when I want to use feature detection to detect a change in the calling parameters to somelibrary.somefunction()? -- Steven

On Wed, Mar 4, 2020 at 9:58 PM Steven D'Aprano <steve@pearwood.info> wrote:
Or where you're trying to call something that isn't callable. There are several distinct "phases" to calling a function, and TypeError could come up from any of them:
Being able to easily distinguish "that isn't callable" from "the parameters don't line up" from "the function, during execution, raised TypeError" would be useful. So, I agree that the bikeshed is of value here. As to colour, I'd be (weakly) inclined toward: 1) ParameterError and maybe UncallableError as well 2) Type flags with class constants eg ParameterError.TOO_MANY Even if TOO_MANY is technically just the integer 2 (as opposed to an IntEnum), encouraging people to use that will make it a lot easier to read. C code can use a #define for maximum performance, and then the cost of looking up the attribute is paid only if the exception is caught AND checked for that status - minimal overhead on creation. ChrisA

On Wed, Mar 4, 2020 at 2:58 AM Steven D'Aprano <steve@pearwood.info> wrote:
In my experience, it's sprinkled all over the place. For example, here are the pertinent chunks of one of our functions that converts VAX floats to IEEE... static PyObject *vax_data_floats(PyObject *self, PyObject **args, Py_ssize_t nargs) { if (nargs != 2) { PyErr_Format(PyExc_TypeError, "floats: expected 2 arguments, 'bytes' and 'count'"); return NULL; } ... if (!PyLong_Check(py_count)) { PyErr_Format(PyExc_TypeError, "floats: second argument must be an int"); return NULL; } ... if (count*(Py_ssize_t)sizeof(float) > bytes_size) { PyErr_Format(PyExc_TypeError, "floats: argument 2 is larger than data (only %d bytes for requested %d floats)", bytes_size, count); return NULL; }

On Mar 4, 2020, at 03:00, Steven D'Aprano <steve@pearwood.info> wrote:
OK, but what’s the downside of giving you a named constant instead of making you use the magic number 2? Making CPython 400 bytes bigger? Forcing the help for ParameterError to display the kinds of error the same way all named constants are displayed when you had a better way to display them?
But this is just arguing about the colour of the bikeshed. Do you agree with me that the bikeshed itself is useful?
I’m +0 on this (if there’s an implementation that can cover all of the common cases). If people are switching on the error message string in real code (and I think I’ve done that once, for Python 2.7/3.3 code where I auto-generated wrappers around appscript proxies), there ought to be another way, and having a new error subclass is the obvious pythonic better way. The only question is whether it comes up often enough to be worth fixing.
I’m pretty sure there isn’t a single place, and PyCall is not useful—but I think there may be a small handful of places that’s good enough. IIRC, it works like this: The PyCall functions and the bytecode handler eventually call the same code, but that code just prepares the arguments in a generic way that the callee can match them: as a tuple of positional args and a dict of keyword args. It doesn’t even know anything about the callee. The parsing of that tuple and dict is up to the callee. Which can be anything. But usually, it’s either: * a function (or type or thing with __call__) written in Python, in which case the PyFunction object handles matching args to params and creating exceptions * a C function that just passes the tuple and dict to one of a small set of PyArg parse functions, which do the matching and exceptions * a C function with auto-generated argclinic code, which I think is the same as above, but if not argclinic is just one more place to change A C function could just manually parse the tuple and dict, but at that point it’s like a Python function that asks for *args, **kw and parses those, and I think you’re right that we don’t need to worry about those. Especially since in most cases they’re just proxies or bridges to some other function that will parse the args the normal way and they’ll just pass that exception up. So, I think those 2 or 3 places (that may turn out to be like 6 places in the CPython source) may be sufficient. But you’d have to check that, not just go by my memory. :) Also, it’s probably worth checking some very popular special cases: ctypes (with argtypes), cffi, a couple of proxy callables like partial and MethodType, Cython, the code generator from NumPy, a few static and dynamic bridge libraries like boost::python and PyObjC, … I think most of them will already just work, except maybe for some dynamic bridges (e.g., if PyObjC is just converting the tuple and dict to an NSInvocation, calling that via ObjC, and then parsing the ObjC error to generate the Python one…), which I think would be perfectly acceptable. But it’s worth knowing whether there will be such cases.
Just whether you’re proposing to change it as part of the proposal. I think you do want to, and I think it’ll be easy, but I don’t think either answer would be a deal breaker. More generally, I’m just trying to make sure things like inspect.signature, and partial and so on, have been considered before anyone makes a decision.

Can you explain where `except ParameterError` would produce better code than the best current alternative? It seems to me that using this is like stating "this code might be wrong" and would generally produce bad code. For example if I wanted to use math.gcd on multiple values but I need to support Python versions that only allow two arguments, then I'll just apply it to two arguments at a time, maybe with functools.reduce. And in general if I want to distinguish between versions I'd much rather just check the version and thus assert "this code is correct for this version". If you catch ParameterError, how do you know that it came directly from the line you caught it from and not deeper within? If you want to check that you're passing parameters properly, I suggest writing code like this: import inspect signature = inspect.signature(func) try: signature.bind(*args, **kwargs) except TypeError as e: result = ??? else: result = func(*args, **kwargs) That's basically code that I've actually used in practice. If you want it to look prettier, you could probably write a decorator like this: @check_params def func(...): ... try: result = func(*args, **kwargs) except func.ParameterError as e: result = ???

On Wed, Mar 04, 2020 at 12:19:25PM +0200, Alex Hall wrote:
Can you explain where `except ParameterError` would produce better code than the best current alternative?
Serhiy came up with a good use-case that I hadn't considered: functions which take arbitrary callback functions as argument, e.g. sorted() with a key function. Both of these calls raise TypeError, but for different reasons: # right number of arguments passed to the key function, # but the wrong argument type sorted([3, 5, 1], key=len) # wrong number of arguments passed to the key function sorted([3, 5, 1], key=isinstance) It might be useful to be able to distinguish the two cases. My earlier use-case still stands: feature detection where a function has changed its parameter list. More on this below.
It seems to me that using this is like stating "this code might be wrong" and would generally produce bad code.
It seems to me that your comment above could be equally said about *any* use of exceptions: "this code might be wrong, so we'll stick it in a try...except block". Only that's not how most of us actually use exceptions.
I've done that myself. Not specifically with gcd, but with other functions, which I don't remember off the top of my head. gcd is a particularly simple example because it is so easy to wrap. Excluding function header and docstring, the code is just six lines: if len(args) == 0: return 1 elif len(args) == 1: return abs(args[0]) else: return reduce(_gcd, args) Not all cases will be that simple, or they may involve a serious performance cost. In general, the official version is going to be faster and better tested than the wrapper version. Why shouldn't I use the fast, well-tested standard version if it is available? I just need a reliable way to detect the needed feature. This is not specifically about gcd. It could be about functions of arbitrary complexity: try: # Test whether the aardvark function supports a # `hovercraft` parameter. result = aardvark( spam, eggs, cheese, hovercraft=1 ) except ParameterError: # No hovercraft parameter allowed, create a wrapper. ... TypeError is less satisfactory because I cannot easily distinguish the two cases: 1. The 'hovercraft' parameter is not available; 2. The 'hovercraft' parameter is available, but I have made some other error in the function call which results in a TypeError.
Feature detection is more reliable. What if the caller of my library has back-ported the new, advanced version of aardvark() to their Python? Instead of using the faster, better tested official backport, my code will use my wrapper. What if the caller is using my library with another interpreter, say PyPy or Jython, which doesn't support the feature I need? With feature detection, my code will simply fall back to the wrapper. With version checking, it will wrongly assert the feature is available, and then crash later. As far as I can tell, feature detection has more or less completely overshadowed version checks in the browser and Javascript space. In my opinion, checking the version should only be used when it is too hard to detect the feature.
If you catch ParameterError, how do you know that it came directly from the line you caught it from and not deeper within?
Does it matter? If the call aardvark(*args, hovercraft=1) leaks a ParameterError from deep inside its internals, then even if the feature is technically available according to a version check, it is too buggy to use and I need to use my wrapper.
And what if the function I'm checking doesn't support signature metadata? https://docs.python.org/3/library/inspect.html#inspect.signature I can wrap the call to signature() in try...except, but what's my fallback if I can't check the signature? py> inspect.signature(math.gcd) Traceback (most recent call last): [...] TypeError: 'feature_version' is an invalid keyword argument for compile() -- Steven

I'm hearing a lot of hypotheticals and nothing close to what feels like a real use case. Both those cases are completely wrong and I don't know why I'd need to distinguish the reason, or even catch TypeError at all.
I don't understand, you seem to be contradicting yourself between "any use of exceptions" and "that's not how most of us actually use exceptions". The examples you're giving explicitly look like "this code might be wrong". The examples of exceptions I'm looking at in my own code are more like "the inputs might be wrong" or "something might go wrong beyond my control".
This is a weird hypothetical. Please provide a real example. Code that you have actually written vs what you wish you could have written. I have certainly never wanted to do something like this. And if I was in such a situation that's still not the way I'd want to solve it.
I don't really understand how this would work, can you elaborate?
But maybe the function `spam` that you passed to aardvark didn't take the right number of parameters? To make it more concrete, imagine code like this from a time when newer versions of `sort` accepted `key` but older versions could only accept `cmp` (I'm not sure if this is actually what happened, it's not important). try: lst.sort(key=foo_key) except ParameterError: lst.sort(cmp=foo_cmp) Does `except ParameterError:` mean that `sort` doesn't accept `key`, or does it mean that `foo_key` is wrong?
That's interesting, what version of Python gives you that? It works for me.

It seems to me that this thread is a bit too focused. I know I’ve found that often I wish I knew better exactly what caused a particular Exception— there are many that can be raised for various reasons, and you may need to know which one in order to handle it properly. At this point, parsing the error msg is often the only option. Though since you can tack on arbitrary data to an Exception, some have ways of knowing. So maybe a more formal and standardized way to to specify more detailed information is in order. But it shouldn’t be just about TypeError. On the other hand, subclassing exceptions works pretty well, so maybe that’s fine. It worked pretty well for OSError, for example. As for TypeError and calling functions. I believe the OP wanted to distinguish between calling the function the wrong way: e.g. the wrong [number of, name of, order of] arguments, and calling the function with the wrong type of a given parameter. And Python being dynamically typed, calling a function with the wrong Type is really an arbitrary somthing-went-wrong-inside-the-function, not an error with calling per se. And I think this is a useful distinction. It's actually a common problem, when you do: try: func(...) Except SomeError: do_something you never know where in the entire call stack SomeError came from. In the case at hand, a TypeError may have come from how you are calling func, or it may have come from somewhere inside func(), or indeed, anywhere else deep in that hierachy. So I think it would be very useful to have an Exception that is about the "how the function was called" part. Of course, it could still have come from another call inside the top-l;evel function, but I think it sill makes a useful distinction. -CHB On Wed, Mar 4, 2020 at 7:11 AM Alex Hall <alex.mojaki@gmail.com> wrote:

On 03/04/2020 06:24 AM, Steven D'Aprano wrote:
My earlier use-case still stands: feature detection where a function has changed its parameter list. More on this below.
It seems like `inspect.signature` is the right way to do this kind of feature detection.
On 03/04/2020 06:24 AM, Steven D'Aprano wrote:
This feels like a separate problem than "the function I'm calling changed it's signature".
Looks like ValueError and TypeError can be raised, so it seems like the question is how often? If it only fails rarely or seldom then the case for more built-in exceptions is weakened.
In those cases you'll need to use the existing TypeError -- and with unit tests to ensure things are working correctly on the different versions you're targeting you'll know you're good to go. By the way, `inspect.signature(math.gcd)` works for me on Python 3.8.2 -- which version were you testing with? -- ~Ethan~

04.03.20 09:06, Steven D'Aprano пише:
Proposal: add a new exception, ParameterError, for parameter errors. For backwards compatibility it would have to be a subclass of TypeError.
If add such exception, ArgumentError looks more appropriate name.
Look at this from other side. The working code does not contain something like len(a, b) or divmod(a), because the author of the code know how much arguments the used function accepts. It may be short-time mistake in process of writing code, but it will be fixed after the first test (or be caught by a linter). The way to get such error is when the caller does not know what function it calls. For example, functools.reduce(len, [1, 2]) sorted([1, 2], key=divmod) Authors of reduce() and sorted() do not know what function they call, it is passed as an argument. The error is on the side of the user of these function, it is passing a wrong *type* of the function. It is definitely a TypeError.

On Wed, Mar 04, 2020 at 02:09:00PM +0200, Serhiy Storchaka wrote:
If add such exception, ArgumentError looks more appropriate name.
Thanks Serhiy, I considered that but thought that ArgumentError was likely to be used in third-party libraries. A quick google shows that argparse and Boost both have an ArgumentError exception. [...]
That might be so, and I am not debating whether the error is really a TypeError or not. Backwards compatibility means that it must stay TypeError, or a subclass of TypeError. I had not thought of reduce(), map(), sorted() etc, that's another good use-case, thank you! -- Steven

Steven D'Aprano writes:
To me, that name evokes all kinds of things that linters and mypy do, but the Python interpreter can't or shouldn't. FunctionSignatureError, maybe? I'm not against the proposal, but for the sake of my own curiosity: ISTM that most use cases for this distinction (eg, functionals like reduce) should be left up to mypy, or to developers reading the error messages. A matter of taste, I guess, but assuming that leaves your original use case, feature testing. But the feature test is going to be done with a test case controlled by the developer, not taken from the environment. Is it common that TypeErrors are ambiguous in such a context?
I'm fine on that, but please give it human-readable values. Steve

On Mar 3, 2020, at 23:11, Steven D'Aprano <steve@pearwood.info> wrote:
I liked everything up to using magic numbers like this. Nobody will remember whether it’s 1 for too many arguments or too few; they’ll have to check the docs every time it’s been more than 20 minutes since they wrote the last handler. If you need to programmatically distinguish these, and you can’t use subclasses to do it, at least make it something like a named class attribute, so you can check if it’s TooFewArguments instead of 1. (That still isn’t as nice as an enum, but presumably 95% of the time when you display the reason you’ll also be displaying the error message, so that’s fine.) The other question: what exactly do you propose to change? I don’t think you can do this on the caller side (the ceval bytecode handler and the PyCall functions). But if you put it on the callee side, how do you handle everything? Presumably you want to handle C functions as well as Python functions; can you change the PyArg parsing functions? (I assume argclinic uses those?) That still leaves functions that parse *args and **kw and raise TypeError manually, whether in Python or in C, but I suppose you can say that in that case it really isn’t a parameter error (the signature really is *args, **kw so everything matches). What about inspect.Signature.bind?

Hi The present behaviour is: >>> len() TypeError: len() takes exactly one argument (0 given) >>> len(1, 2) TypeError: len() takes exactly one argument (2 given) This is nothing special about built-ins. The same goes for: >>> def f(): pass >>> f(1) TypeError: f() takes 0 positional arguments but 1 was given I find this behaviour perfectly satisfactory. The OP's problem can be resolved by looking at the exception, whose type is TypeError. For this to be completely reliable, certain aspects the exception message need to be stable. It may be worth stating that this will be done, at least for certain exception. By the way, using the exception instance will produce code that works with earlier versions of Python. -- Jonathan

Postscript: Things are not as clean as I have hoped and said. However, I don't think this changes the general form of the conclusion. Just that a little more work is needed to achieve the goal. Python 3.7 and 3.8: >>> def f(x): pass >>> f() TypeError: f() missing 1 required positional argument: 'x' Also note that in Python 3.8 we have: >>> def f(x,/): pass >>> f() TypeError: f() missing 1 required positional argument: 'x' Recall that we have >>> len() TypeError: len() takes exactly one argument (0 given) Note that the parameter of 'f' has a name, but not the parameter for 'len'. I think I was expecting >>> def f(x,/): pass >>> f() TypeError: f() takes exactly one argument (0 given) -- Jonathan

On Wed, Mar 04, 2020 at 10:12:34AM +0000, Jonathan Fine wrote:
Error messages are not part of the function API and are not guaranteed to be a constant part of the language. We shouldn't expect every single Python interpreter (CPython, IronPython, Jython, Nuitka, MicroPython, PyPy, Stackless etc) to use exactly the same error messages. It shouldn't be a *feature change* if an error message changes. We shouldn't have to use a deprecation period or a `__future__` import before we can improve error messages, fix spelling or grammatical errors, or add extra information to the error message. Error messages might be localised into the native language of the user. Parsing error messages is a fragile and error-prone technique that should be avoided if at all possible. -- Steven

On Wed, Mar 04, 2020 at 01:08:35AM -0800, Andrew Barnert wrote:
I liked everything up to using magic numbers like this.
I'm not wedded to the idea of error number. I chose them because I expect there will only be a few of them (three?) and the majority of the time you won't bother to check the number. If you are debugging an exception that occurs in code that's supposed to be working, you have a bug and you will read the error message to find out what it means, not inspect the error number. It's only when you intentionally capture the exception that the error number might matter, and even then probably not that often. I expect that in the majority of cases, there's only one possible failure mode. E.g. you can't have both too many and too few arguments in the same call, and you'll know which is going to be the case: try: spam(1, 2) except ParameterError: # Some versions of spam only accept 1 argument; # some accept 2. If we get a ParameterError, we # must be running the old 1-arg version. Another alternative is to testing for key phrases in the exception message: if "too few arguments" in err.args[0]: ... but that's fragile and only works until the message gets changed to say "not enough arguments". (The error message is not part of the API, so it can change without warning.) But as I said, I don't think this part of the proposal would be used very often, and if it becomes a real sticking point I'd be prepared to drop it. The important thing is to be able to programmatically distinguish the case where arguments don't match the parameters from the case where an argument is the wrong type.
Nobody will remember whether it’s 1 for too many arguments or too few;
If we use an error number, I will beg for the numbers to be documented in the exception docstring so that `help(ParameterError)` will show the list. I don't expect that there will be many. I can only think of three, even if there is triple that it's still small enough to put into the docstring. But this is just arguing about the colour of the bikeshed. Do you agree with me that the bikeshed itself is useful?
I don't know enough about the Python internals to give a definitive answer, but I'm assuming/hoping that there is a single, or at most a few, places in the interpreter that matches up arguments to formal parameters, and if there's a discrepency it currently raises TypeError. PyCall sounds promising :-)
Indeed. I don't have any expectations about those functions. If the maintainer of the function eventually changes whatever error they are currently raising to ParameterError, that will be grand, but I don't expect every third-party function that implements some custom variety of parameter handling to support this. If they do, great, if they don't, we're no worse off than the status quo.
What about inspect.Signature.bind?
Do you have a specific concern? Signature.bind promises to raise TypeError if the arguments don't match the parameters. A change to ParameterError should be completely backwards compatible since it will be a subclass of TypeError. -- Steven

Steven D'Aprano wrote: Another alternative is to testing for key phrases in the exception
We are talking here about an exception message that is generated by core Python. So there would be some warning of change, namely the review process for core Python. I agree that there is a problem to be solved here. Perhaps it is easier and better for core Python to decide to freeze certain messages, than to add a new exception type. -- Jonathan

On Wed, Mar 04, 2020 at 11:10:15AM +0000, Jonathan Fine wrote:
You can't call the error message a stable part of the language without forbidding changes to the error message. Essentially, you are asking for changes to error messages to be treated as a breaking change. That includes fixing typos and spelling errors, rewording clumsy phrases and adding extra or better quality information to the message. These are all breaking changes if you expect error messages to be stable. The precise wording of error messages would have to be documented. Not everyone follows the review process for interpreter changes, so changes would have to be listed in those docs, and the "What's New" for each release. In my own code I would never accept a request to freeze error messages. To be frank, I think it would be intolerable. I would never, ever ask the core developers to take on this additional burden. It would also rule out any possibility of having error messages localised to the user's native language.
How do you decide which functions should have their error messages frozen, and which do not? Let's suppose you make that decision, and you declare that len(), map(), zip(), itertools.count() and functools.wraps() will have their error message frozen, but the other functions don't. How does that help me when I want to use feature detection to detect a change in the calling parameters to somelibrary.somefunction()? -- Steven

On Wed, Mar 4, 2020 at 9:58 PM Steven D'Aprano <steve@pearwood.info> wrote:
Or where you're trying to call something that isn't callable. There are several distinct "phases" to calling a function, and TypeError could come up from any of them:
Being able to easily distinguish "that isn't callable" from "the parameters don't line up" from "the function, during execution, raised TypeError" would be useful. So, I agree that the bikeshed is of value here. As to colour, I'd be (weakly) inclined toward: 1) ParameterError and maybe UncallableError as well 2) Type flags with class constants eg ParameterError.TOO_MANY Even if TOO_MANY is technically just the integer 2 (as opposed to an IntEnum), encouraging people to use that will make it a lot easier to read. C code can use a #define for maximum performance, and then the cost of looking up the attribute is paid only if the exception is caught AND checked for that status - minimal overhead on creation. ChrisA

On Wed, Mar 4, 2020 at 2:58 AM Steven D'Aprano <steve@pearwood.info> wrote:
In my experience, it's sprinkled all over the place. For example, here are the pertinent chunks of one of our functions that converts VAX floats to IEEE... static PyObject *vax_data_floats(PyObject *self, PyObject **args, Py_ssize_t nargs) { if (nargs != 2) { PyErr_Format(PyExc_TypeError, "floats: expected 2 arguments, 'bytes' and 'count'"); return NULL; } ... if (!PyLong_Check(py_count)) { PyErr_Format(PyExc_TypeError, "floats: second argument must be an int"); return NULL; } ... if (count*(Py_ssize_t)sizeof(float) > bytes_size) { PyErr_Format(PyExc_TypeError, "floats: argument 2 is larger than data (only %d bytes for requested %d floats)", bytes_size, count); return NULL; }

On Mar 4, 2020, at 03:00, Steven D'Aprano <steve@pearwood.info> wrote:
OK, but what’s the downside of giving you a named constant instead of making you use the magic number 2? Making CPython 400 bytes bigger? Forcing the help for ParameterError to display the kinds of error the same way all named constants are displayed when you had a better way to display them?
But this is just arguing about the colour of the bikeshed. Do you agree with me that the bikeshed itself is useful?
I’m +0 on this (if there’s an implementation that can cover all of the common cases). If people are switching on the error message string in real code (and I think I’ve done that once, for Python 2.7/3.3 code where I auto-generated wrappers around appscript proxies), there ought to be another way, and having a new error subclass is the obvious pythonic better way. The only question is whether it comes up often enough to be worth fixing.
I’m pretty sure there isn’t a single place, and PyCall is not useful—but I think there may be a small handful of places that’s good enough. IIRC, it works like this: The PyCall functions and the bytecode handler eventually call the same code, but that code just prepares the arguments in a generic way that the callee can match them: as a tuple of positional args and a dict of keyword args. It doesn’t even know anything about the callee. The parsing of that tuple and dict is up to the callee. Which can be anything. But usually, it’s either: * a function (or type or thing with __call__) written in Python, in which case the PyFunction object handles matching args to params and creating exceptions * a C function that just passes the tuple and dict to one of a small set of PyArg parse functions, which do the matching and exceptions * a C function with auto-generated argclinic code, which I think is the same as above, but if not argclinic is just one more place to change A C function could just manually parse the tuple and dict, but at that point it’s like a Python function that asks for *args, **kw and parses those, and I think you’re right that we don’t need to worry about those. Especially since in most cases they’re just proxies or bridges to some other function that will parse the args the normal way and they’ll just pass that exception up. So, I think those 2 or 3 places (that may turn out to be like 6 places in the CPython source) may be sufficient. But you’d have to check that, not just go by my memory. :) Also, it’s probably worth checking some very popular special cases: ctypes (with argtypes), cffi, a couple of proxy callables like partial and MethodType, Cython, the code generator from NumPy, a few static and dynamic bridge libraries like boost::python and PyObjC, … I think most of them will already just work, except maybe for some dynamic bridges (e.g., if PyObjC is just converting the tuple and dict to an NSInvocation, calling that via ObjC, and then parsing the ObjC error to generate the Python one…), which I think would be perfectly acceptable. But it’s worth knowing whether there will be such cases.
Just whether you’re proposing to change it as part of the proposal. I think you do want to, and I think it’ll be easy, but I don’t think either answer would be a deal breaker. More generally, I’m just trying to make sure things like inspect.signature, and partial and so on, have been considered before anyone makes a decision.

Can you explain where `except ParameterError` would produce better code than the best current alternative? It seems to me that using this is like stating "this code might be wrong" and would generally produce bad code. For example if I wanted to use math.gcd on multiple values but I need to support Python versions that only allow two arguments, then I'll just apply it to two arguments at a time, maybe with functools.reduce. And in general if I want to distinguish between versions I'd much rather just check the version and thus assert "this code is correct for this version". If you catch ParameterError, how do you know that it came directly from the line you caught it from and not deeper within? If you want to check that you're passing parameters properly, I suggest writing code like this: import inspect signature = inspect.signature(func) try: signature.bind(*args, **kwargs) except TypeError as e: result = ??? else: result = func(*args, **kwargs) That's basically code that I've actually used in practice. If you want it to look prettier, you could probably write a decorator like this: @check_params def func(...): ... try: result = func(*args, **kwargs) except func.ParameterError as e: result = ???

On Wed, Mar 04, 2020 at 12:19:25PM +0200, Alex Hall wrote:
Can you explain where `except ParameterError` would produce better code than the best current alternative?
Serhiy came up with a good use-case that I hadn't considered: functions which take arbitrary callback functions as argument, e.g. sorted() with a key function. Both of these calls raise TypeError, but for different reasons: # right number of arguments passed to the key function, # but the wrong argument type sorted([3, 5, 1], key=len) # wrong number of arguments passed to the key function sorted([3, 5, 1], key=isinstance) It might be useful to be able to distinguish the two cases. My earlier use-case still stands: feature detection where a function has changed its parameter list. More on this below.
It seems to me that using this is like stating "this code might be wrong" and would generally produce bad code.
It seems to me that your comment above could be equally said about *any* use of exceptions: "this code might be wrong, so we'll stick it in a try...except block". Only that's not how most of us actually use exceptions.
I've done that myself. Not specifically with gcd, but with other functions, which I don't remember off the top of my head. gcd is a particularly simple example because it is so easy to wrap. Excluding function header and docstring, the code is just six lines: if len(args) == 0: return 1 elif len(args) == 1: return abs(args[0]) else: return reduce(_gcd, args) Not all cases will be that simple, or they may involve a serious performance cost. In general, the official version is going to be faster and better tested than the wrapper version. Why shouldn't I use the fast, well-tested standard version if it is available? I just need a reliable way to detect the needed feature. This is not specifically about gcd. It could be about functions of arbitrary complexity: try: # Test whether the aardvark function supports a # `hovercraft` parameter. result = aardvark( spam, eggs, cheese, hovercraft=1 ) except ParameterError: # No hovercraft parameter allowed, create a wrapper. ... TypeError is less satisfactory because I cannot easily distinguish the two cases: 1. The 'hovercraft' parameter is not available; 2. The 'hovercraft' parameter is available, but I have made some other error in the function call which results in a TypeError.
Feature detection is more reliable. What if the caller of my library has back-ported the new, advanced version of aardvark() to their Python? Instead of using the faster, better tested official backport, my code will use my wrapper. What if the caller is using my library with another interpreter, say PyPy or Jython, which doesn't support the feature I need? With feature detection, my code will simply fall back to the wrapper. With version checking, it will wrongly assert the feature is available, and then crash later. As far as I can tell, feature detection has more or less completely overshadowed version checks in the browser and Javascript space. In my opinion, checking the version should only be used when it is too hard to detect the feature.
If you catch ParameterError, how do you know that it came directly from the line you caught it from and not deeper within?
Does it matter? If the call aardvark(*args, hovercraft=1) leaks a ParameterError from deep inside its internals, then even if the feature is technically available according to a version check, it is too buggy to use and I need to use my wrapper.
And what if the function I'm checking doesn't support signature metadata? https://docs.python.org/3/library/inspect.html#inspect.signature I can wrap the call to signature() in try...except, but what's my fallback if I can't check the signature? py> inspect.signature(math.gcd) Traceback (most recent call last): [...] TypeError: 'feature_version' is an invalid keyword argument for compile() -- Steven

I'm hearing a lot of hypotheticals and nothing close to what feels like a real use case. Both those cases are completely wrong and I don't know why I'd need to distinguish the reason, or even catch TypeError at all.
I don't understand, you seem to be contradicting yourself between "any use of exceptions" and "that's not how most of us actually use exceptions". The examples you're giving explicitly look like "this code might be wrong". The examples of exceptions I'm looking at in my own code are more like "the inputs might be wrong" or "something might go wrong beyond my control".
This is a weird hypothetical. Please provide a real example. Code that you have actually written vs what you wish you could have written. I have certainly never wanted to do something like this. And if I was in such a situation that's still not the way I'd want to solve it.
I don't really understand how this would work, can you elaborate?
But maybe the function `spam` that you passed to aardvark didn't take the right number of parameters? To make it more concrete, imagine code like this from a time when newer versions of `sort` accepted `key` but older versions could only accept `cmp` (I'm not sure if this is actually what happened, it's not important). try: lst.sort(key=foo_key) except ParameterError: lst.sort(cmp=foo_cmp) Does `except ParameterError:` mean that `sort` doesn't accept `key`, or does it mean that `foo_key` is wrong?
That's interesting, what version of Python gives you that? It works for me.

It seems to me that this thread is a bit too focused. I know I’ve found that often I wish I knew better exactly what caused a particular Exception— there are many that can be raised for various reasons, and you may need to know which one in order to handle it properly. At this point, parsing the error msg is often the only option. Though since you can tack on arbitrary data to an Exception, some have ways of knowing. So maybe a more formal and standardized way to to specify more detailed information is in order. But it shouldn’t be just about TypeError. On the other hand, subclassing exceptions works pretty well, so maybe that’s fine. It worked pretty well for OSError, for example. As for TypeError and calling functions. I believe the OP wanted to distinguish between calling the function the wrong way: e.g. the wrong [number of, name of, order of] arguments, and calling the function with the wrong type of a given parameter. And Python being dynamically typed, calling a function with the wrong Type is really an arbitrary somthing-went-wrong-inside-the-function, not an error with calling per se. And I think this is a useful distinction. It's actually a common problem, when you do: try: func(...) Except SomeError: do_something you never know where in the entire call stack SomeError came from. In the case at hand, a TypeError may have come from how you are calling func, or it may have come from somewhere inside func(), or indeed, anywhere else deep in that hierachy. So I think it would be very useful to have an Exception that is about the "how the function was called" part. Of course, it could still have come from another call inside the top-l;evel function, but I think it sill makes a useful distinction. -CHB On Wed, Mar 4, 2020 at 7:11 AM Alex Hall <alex.mojaki@gmail.com> wrote:

On 03/04/2020 06:24 AM, Steven D'Aprano wrote:
My earlier use-case still stands: feature detection where a function has changed its parameter list. More on this below.
It seems like `inspect.signature` is the right way to do this kind of feature detection.
On 03/04/2020 06:24 AM, Steven D'Aprano wrote:
This feels like a separate problem than "the function I'm calling changed it's signature".
Looks like ValueError and TypeError can be raised, so it seems like the question is how often? If it only fails rarely or seldom then the case for more built-in exceptions is weakened.
In those cases you'll need to use the existing TypeError -- and with unit tests to ensure things are working correctly on the different versions you're targeting you'll know you're good to go. By the way, `inspect.signature(math.gcd)` works for me on Python 3.8.2 -- which version were you testing with? -- ~Ethan~

04.03.20 09:06, Steven D'Aprano пише:
Proposal: add a new exception, ParameterError, for parameter errors. For backwards compatibility it would have to be a subclass of TypeError.
If add such exception, ArgumentError looks more appropriate name.
Look at this from other side. The working code does not contain something like len(a, b) or divmod(a), because the author of the code know how much arguments the used function accepts. It may be short-time mistake in process of writing code, but it will be fixed after the first test (or be caught by a linter). The way to get such error is when the caller does not know what function it calls. For example, functools.reduce(len, [1, 2]) sorted([1, 2], key=divmod) Authors of reduce() and sorted() do not know what function they call, it is passed as an argument. The error is on the side of the user of these function, it is passing a wrong *type* of the function. It is definitely a TypeError.

On Wed, Mar 04, 2020 at 02:09:00PM +0200, Serhiy Storchaka wrote:
If add such exception, ArgumentError looks more appropriate name.
Thanks Serhiy, I considered that but thought that ArgumentError was likely to be used in third-party libraries. A quick google shows that argparse and Boost both have an ArgumentError exception. [...]
That might be so, and I am not debating whether the error is really a TypeError or not. Backwards compatibility means that it must stay TypeError, or a subclass of TypeError. I had not thought of reduce(), map(), sorted() etc, that's another good use-case, thank you! -- Steven

Steven D'Aprano writes:
To me, that name evokes all kinds of things that linters and mypy do, but the Python interpreter can't or shouldn't. FunctionSignatureError, maybe? I'm not against the proposal, but for the sake of my own curiosity: ISTM that most use cases for this distinction (eg, functionals like reduce) should be left up to mypy, or to developers reading the error messages. A matter of taste, I guess, but assuming that leaves your original use case, feature testing. But the feature test is going to be done with a test case controlled by the developer, not taken from the environment. Is it common that TypeErrors are ambiguous in such a context?
I'm fine on that, but please give it human-readable values. Steve
participants (10)
-
Alex Hall
-
Andrew Barnert
-
Chris Angelico
-
Christopher Barker
-
Eric Fahlgren
-
Ethan Furman
-
Jonathan Fine
-
Serhiy Storchaka
-
Stephen J. Turnbull
-
Steven D'Aprano