ChatGPT解决这个技术问题 Extra ChatGPT

Which exception should I raise on bad/illegal argument combinations in Python?

I was wondering about the best practices for indicating invalid argument combinations in Python. I've come across a few situations where you have a function like so:

def import_to_orm(name, save=False, recurse=False):
    """
    :param name: Name of some external entity to import.
    :param save: Save the ORM object before returning.
    :param recurse: Attempt to import associated objects as well. Because you
        need the original object to have a key to relate to, save must be
        `True` for recurse to be `True`.
    :raise BadValueError: If `recurse and not save`.
    :return: The ORM object.
    """
    pass

The only annoyance with this is that every package has its own, usually slightly differing BadValueError. I know that in Java there exists java.lang.IllegalArgumentException -- is it well understood that everybody will be creating their own BadValueErrors in Python or is there another, preferred method?


B
Bonifacio2

I would just raise ValueError, unless you need a more specific exception..

def import_to_orm(name, save=False, recurse=False):
    if recurse and not save:
        raise ValueError("save must be True if recurse is True")

There's really no point in doing class BadValueError(ValueError):pass - your custom class is identical in use to ValueError, so why not use that?


> "so why not use that?" - Specificity. Perhaps I want to catch at some outer layer "MyValueError", but not any/all "ValueError".
Yeah, so part of the question of specificity is where else ValueError is raised. If the callee function likes your arguments but calls math.sqrt(-1) internally, a caller may be catching ValueError expect that its arguments were inappropriate. Maybe you just check the message in this case...
I'm not sure that argument holds: if someone is calling math.sqrt(-1), that's a programming error that needs to be fixed anyway. ValueError is not intended to be caught in normal program execution or it would derive from RuntimeError.
If the error is on the NUMBER of arguments, for a function with a variable number of arguments ... for example a function where the arguments must be an even number of arguments, then you should raise a TypeError, to be consistent. And don't make your own class unless a) you have a use case or b) you are exporting the library to be used by others. Premature functionality is the death of code.
Would an assertion also be acceptable in this case or is there a specific reason to use ValueError instead?
M
Markus Jarderot

I would inherit from ValueError

class IllegalArgumentError(ValueError):
    pass

It is sometimes better to create your own exceptions, but inherit from a built-in one, which is as close to what you want as possible.

If you need to catch that specific error, it is helpful to have a name.


Stop writing classes and custom exceptions - pyvideo.org/video/880/stop-writing-classes
@HamishGrubijan that video is terrible. When anyone suggested a good use of a class, he just bleated "Don't use classes." Brilliant. Classes are good. But don't take my word for it.
@RobertGrant No, you don't get it. That video is not really about literally "don't use classes". It is about don't over-complicate things.
@RayLuo you may have sanity-checked what the video's saying and converted it into a palateable, sensible alternative message, but that is what the video says, and it's what someone who doesn't have a lot of experience and common sense will come away with.
@SamuelSantana as I said, any time anyone put their hand up and said "what about X?" where X was a good idea, he just said, "don't make another class." Pretty clear. I agree the key is balance; the problem is that's just too vague to actually live by :-)
J
J Bones

I think the best way to handle this is the way python itself handles it. Python raises a TypeError. For example:

$ python -c 'print(sum())'
Traceback (most recent call last):
File "<string>", line 1, in <module>
TypeError: sum expected at least 1 arguments, got 0

Our junior dev just found this page in a google search for "python exception wrong arguments" and I'm surprised that the obvious (to me) answer wasn't ever suggested in the decade since this question was asked.


Nothing surprises me but I agree 100% that TypeError is the correct exception if the type is wrong on some of the arguments passed into the function. A ValueError would be suitable if the variables are of correct type but their contents and values does not make sense.
I think this is probably for missing or uncalled for arguments, while the question is about arguments that are given correctly, but are incorrect on a higher abstraction level involving the value of the given argument. But as I was actually looking for the former, so have an upvote anyway.
As @user3504575 and @Nobody said, TypeError is used if the arguments don't match the function signature (wrong number of positional arguments, keyword arguments with the wrong name, wrong type of argument), but a ValueError is used when the function call matches the signature but the argument values are invalid (e.g., calling int('a')). source
As the OP's question referred to "invalid argument combinations", it seems a TypeError would be appropriate as this would be a case where the function signature is essentially wrong for the passed arguments.
Your example calls sum() with no arguments, which is a TypeError, but the OP was concerned with "illegal" combinations of argument values when the argument types are correct. In this case, both save and recurse are bools, but if recurse is True then save should not be False. This is a ValueError. I agree that some interpretation of the question's title would be answered by TypeError, but not for the example that's presented.
G
Gloweye

It depends on what the problem with the arguments is.

If the argument has the wrong type, raise a TypeError. For example, when you get a string instead of one of those Booleans.

if not isinstance(save, bool):
    raise TypeError(f"Argument save must be of type bool, not {type(save)}")

Note, however, that in Python we rarely make any checks like this. If the argument really is invalid, some deeper function will probably do the complaining for us. And if we only check the boolean value, perhaps some code user will later just feed it a string knowing that non-empty strings are always True. It might save him a cast.

If the arguments have invalid values, raise ValueError. This seems more appropriate in your case:

if recurse and not save:
    raise ValueError("If recurse is True, save should be True too")

Or in this specific case, have a True value of recurse imply a True value of save. Since I would consider this a recovery from an error, you might also want to complain in the log.

if recurse and not save:
    logging.warning("Bad arguments in import_to_orm() - if recurse is True, so should save be")
    save = True

I think this is the most accurate answer. This is obviously underrated (7 votes so far including mine).
E
Eli Courtwright

I've mostly just seen the builtin ValueError used in this situation.


t
thing10

You would most likely use ValueError (raise ValueError() in full) in this case, but it depends on the type of bad value. For example, if you made a function that only allows strings, and the user put in an integer instead, you would you TypeError instead. If a user inputted a wrong input (meaning it has the right type but it does not qualify certain conditions) a Value Error would be your best choice. Value Error can also be used to block the program from other exceptions, for example, you could use a ValueError to stop the shell form raising a ZeroDivisionError, for example, in this function:

def function(number):
    if not type(number) == int and not type(number) == float:
        raise TypeError("number must be an integer or float")
    if number == 5:
        raise ValueError("number must not be 5")
    else:
        return 10/(5-number)

P.S. For a list of python built-in exceptions, go here: https://docs.python.org/3/library/exceptions.html (This is the official python databank)


B
BobHy

Agree with Markus' suggestion to roll your own exception, but the text of the exception should clarify that the problem is in the argument list, not the individual argument values. I'd propose:

class BadCallError(ValueError):
    pass

Used when keyword arguments are missing that were required for the specific call, or argument values are individually valid but inconsistent with each other. ValueError would still be right when a specific argument is right type but out of range.

Shouldn't this be a standard exception in Python?

In general, I'd like Python style to be a bit sharper in distinguishing bad inputs to a function (caller's fault) from bad results within the function (my fault). So there might also be a BadArgumentError to distinguish value errors in arguments from value errors in locals.


I'd raise KeyError for keyword not found (since a missing explicit keyword is semantically identical to a **kwargs dict that is missing that key).
c
cdleary

I'm not sure I agree with inheritance from ValueError -- my interpretation of the documentation is that ValueError is only supposed to be raised by builtins... inheriting from it or raising it yourself seems incorrect.

Raised when a built-in operation or function receives an argument that has the right type but an inappropriate value, and the situation is not described by a more precise exception such as IndexError.

-- ValueError documentation


Compare google.com/codesearch?q=lang:python+class\+\wError(([^E]\w*|E[^x]\w)): with google.com/codesearch?q=lang:python+class\+\w*Error(Exception):
That blurb simply means that built-ins raise it, and not that only built-ins can raise it. It would not be entirely appropriate in this instance for the Python documentation to talk about what external libraries raise.
Every piece of Python software I've ever seen has used ValueError for this sort of thing, so I think you're trying to read too much into the documentation.
Err, if we're going to use Google Code searches to argue this: google.com/codesearch?q=lang%3Apython+raise%5C+ValueError # 66,300 cases of raising ValueError, including Zope, xen, Django, Mozilla (and that's just from the first page of results). If a builtin exception fits, use it..
As stated, the documentation is ambiguous. It should have been written as either "Raised when a built-in operation or built-in function receives" or as "Raised when a function or built-in operation receives". Of course, whatever the original intent, current practice has trumped it (as @dbr points out). So it should be rewritten as the second variant.