You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
argparse exits on error even when exit_on_error=False #103498
When ArgumentParser encounters certain errors, it calls self.error(...) which exits, even when exit_on_error is set to False.
This prevents catching the exception and writing a custom error message.
importargparseparser=argparse.ArgumentParser(exit_on_error=False)
parser.add_argument('filename')
try:
args=parser.parse_args([])
exceptException:
print("Bad argument, here's what you should do instead...")
Test on Python 3.11.3:
python argparse-bug/repro.pyusage: repro.py [-h] filenamerepro.py: error: the following arguments are required: filename
I written a quick fix which should be generic (instead of guarding all calls to self.error, I added the guard in the method itself). But this way, throwing custom exceptions is not possible. (for example in the PR implementing the feature there was a test expecting a argparse.ArgumentError exception). Is this acceptable or is throwing custom exceptions desired?
If it is desired, we could do the following without breaking the existing API:
def error(self, exception_or_message: Union[str, Exception]) - allow passing both messages and exceptions
def error(self, message: str, exception_class=None) - allow passing exception class information in a separate argument
Personally, I find the first one the simplest and requires little changes to the code. I would be glad if any maintainer could give an opinion.
As far as I can tell, all the self.error() usage is reporting an issue with argument parsing(well, its the argparse library of course). In _parse_known_args there are mixed usage of ArgumentError and self.error() - which is pretty bad.
Doing all the error handling in self.error() seems reasonable, but I don't think we need to change the signature in self.error(). Just raise ArgumentError with the message if self.exit_on_error is False - it's easier for user to catch it anyway.
We also need to replace all the raise ArgumentError with self.error() to keep the code clean.
Hmm, I did not realize that ArgumentError actually takes an action, that was my fault. I still think the current implementation is better, but there might be a backward compatibility issue involved. It's clearly stated that the user can override error() function. If you added an extra argument and used it, the user's current code might break. I think we need some opinions from the component owner.
Reacted by Mihail Feraru and Stephen Karl Larroque
No, I did not reply to the email directly, I just wanted to edit the comment because I noticed the compatibility issue after I written it, but I deleted it instead. 😅
Anyway, I followed your advice, but stumbled upon this action parameter that breaks the API for classes that inherit ArgumentParser. We could avoid this by using **kwargs, but it is not the cleanest way to solve it.
Taking at the look at the code, when this feature was intruduced, it was not very thoroughly tested/reviewed. Basically all the self.error() usage need to be vetted - by nature it contradicts this feature. So this feature either has to live in self.error(), or all the current self.error() usage need to be protected by checking this option.
For triage's sake, I think this is a duplicate of #85427, which has a PR waiting for review at #30832.
But this way, throwing custom exceptions is not possible. (for example in the PR implementing the feature there was a test expecting a argparse.ArgumentError exception). Is this acceptable or is throwing custom exceptions desired?
There's an even older duplicate with some discussion on this point that shaped the choices I made in #30832, see #30832 (comment).
I also came across the same issue where creating an ArgumentParser with exit_on_error=False and passing the wrong type of argument would raise an argparse.ArgumentError, but passing an unknown argument would exit the task.
Yes, as currently implemented exit_on_error only controls errors that raise a argparse.ArgumentError. The unrecognized arguments exit does not use ArgumentError (or an error Exception class) , and so cannot be caught in the same way.
A more comprehensive way of changing error handling is to customize either the exit or `error methods:
I think we are basically at a dead loop here - we want to keep the backward compatibility so we don't breaking anyone's existing code, but the current behavior is simply wrong. It's impossible to "fix" the behavior without breaking existing code.
The introduction of exit_on_error does not live well with all the sys.exit() usage in argparse. There could be a lot of similar traps unless we patch all the sys.exit()s (self.error()).
Anyone who had to make a decision about fixing this would carry plenty of pressure for breaking people's code and I believe according to @sobolevn none of the core devs are the experts of argparse now. I'm curious if/how we could proceed on this.
I took a deeper dive into the problem and I may have a solution. I think the main goals for the fix are:
raise exceptions for all kinds of errors ArgumentParser could encounter
prevent future bugs related to exit_on_error=False by calling .exit() indirectly by accident
stay as backward compatible as possible
First, to make the error handling consistent, I propose that we convert all calls to self.error() to raising exceptions (in case ArgumentError does not fit properly in some cases, we can think of a new public exception type, but I think we won't need that).
Second, we make all public methods of ArgumentParser (there are few) to handle exit_on_error in an obvious way. If exit_on_error=False, we allow exception to pass through, otherwise we pass their string representation to self.error which will do its thing and finally exit.
I think that if we use self.error only in simple try-catch blocks in the public methods, it would be easy enough to make the right behaviour happen and avoid future mistakes.
I will update the pull request to reflect what I was talking about and I'm looking for suggestions/opinions. Maybe we succeed to move this forward.
Below is a list of places where self.exit/self.error are used.
Calls to ArgumentParser.exit which are justified and probably do not need to be changed:
_HelpAction.__call__
_VersionAction.__call__
Calls to ArgumentParser.error that should be converted to raising exceptions:
I took a deeper dive into the problem and I may have a solution. I think the main goals for the fix are:
raise exceptions for all kinds of errors ArgumentParser could encounter
prevent future bugs related to exit_on_error=False by calling .exit() indirectly by accident
stay as backward compatible as possible
I'm sure there are ways to make the code better, but I believe the major issue here is not how, it is backward compatibility. For example, what if the user are subclassing ArgumentParser and using self.error() in their code? Your change would break the behavior.
Like I said above, the real issue here is the library was used too much by the users and all the implementation details are considered "documented feature". We either keep the old "wrong" behavior, or break users existing code.
The hard part is to make the decision - and that's where we are stuck I believe. We need core devs to support a reform, even if that means breaking some of the user code. Without that, I don't believe there would be an implemetation feasible.
The exit_on_error=False is also used to ignore undefined arguments and return args collection of what argparse succeeded to parse. It should not raise exception as then the parse will not be able to return its value.
`parse_known_args` can be used to return the unknowns along with the parsed
ones. This alternative was available long before this `exit_on_error`
parameter was introduced.
The original, default behavior was `exit_on_error=True`. I doubt if there
is much code that depends on the current incomplete implementation of the
`False` alternative. The question is whether we can make the `False`
coverage more complete without too much work, and without compromising the
`True` case.
I think before someone proposes a push, we need a comprehensive list of
errors that still exit.
``exit_on_error=False` currently redirects the `ArgumentError` cases, which
are tied to a specific argument. By default that error exits with a
`usage` and argument specific message.
`unknown arguments` exit is produced by `parse_args`, and isn't tied to any
one argument. `parse_known_args` has, and still is available for bypassing
that. I'm not sure that needs any further change.
Mutually_exclusive_groups can produce errors; I don't know if
`exit_on_error` addresses those
There's also a test for `required_arguments`. That can affect multiple
arguments, so it isn't an `ArgumentError`.
I'm working here from memory, so there may be others.
On Sun, Nov 5, 2023 at 2:07 PM paulj ***@***.***> wrote:
`parse_known_args` can be used to return the unknowns along with the
parsed ones. This alternative was available long before this
`exit_on_error` parameter was introduced.
The original, default behavior was `exit_on_error=True`. I doubt if there
is much code that depends on the current incomplete implementation of the
`False` alternative. The question is whether we can make the `False`
coverage more complete without too much work, and without compromising the
`True` case.
On Sun, Nov 5, 2023, 12:50 PM Alon Bar-Lev ***@***.***>
wrote:
> Hi,
>
> The exit_on_error=False is also used to ignore undefined arguments and
> return args collection of what argparse succeeded to parse. It should not
> raise exception as then the parse will not be able to return its value.
>
> Regards,
>
> —
> Reply to this email directly, view it on GitHub
> <#103498 (comment)>,
> or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/AAITB6ADV42JLQ32ZLWITZDYC73YVAVCNFSM6AAAAAAW4SRXYCVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMYTOOJTHA2DCOJTHA>
> .
> You are receiving this because you commented.Message ID:
> ***@***.***>
>
the issue still exists for me in version 1.4.0.
Error stack trace:
...
argparse.py:1829: in parse_args
self.error(msg % ' '.join(argv))
argparse.py:2587: in error
self.exit(2, _('%(prog)s: error: %(message)s\n') % args)
argparse.py:2574: in exit
_sys.exit(status)
As I can see, on line 1857 it actually tries to verify 'if self.exit_on_error' and then use try..catch but in fact it doesn't reach that block as at line 1827 it goes to self.error that later leads to exit
The preceding line is
msg = _('unrecognized arguments: %s')
This is the unrecognized arguments error that has been discussed in this
thread. Contrary to what the docs imply, the 'don;t exit' parameter does
not catch every kind of error. At the moment it just catches
`ArgumentError` ones that are tied to one specific Argument.
Broadening its coverage has been discussed, but, as far as I know, not been
implemented.
On Mon, Dec 11, 2023 at 9:50 AM Andrii ***@***.***> wrote:
Hi all,
the issue still exists for me in version 1.3.0.
Error stack trace:
...
argparse.py:1829: in parse_args
self.error(msg % ' '.join(argv))
argparse.py:2587: in error
self.exit(2, _('%(prog)s: error: %(message)s\n') % args)
argparse.py:2574: in exit
_sys.exit(status)
As I can see, on line 1857 it actually tries to verify 'if
self.exit_on_error' and then use try..catch but in fact it doesn't reach
that block as at line 1827 it goes to self.error that later leads to exit
—
Reply to this email directly, view it on GitHub
<#103498 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AAITB6FL6VKD6IJCNZWKVP3YI5BVHAVCNFSM6AAAAAAW4SRXYCVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMYTQNJQGU3TSMZQG4>
.
You are receiving this because you commented.Message ID:
***@***.***>
I wrote #121056 before discovering this issue. It fixes the issue in the way opposite to #103519: internal error() calls are replaced with raising ArgumentError. It is then caught and passed to error() at the higher level. Basically, it implements the idea proposed in #103498 (comment).
We now have 3 duplicate issues with 4 open PRs (and one incomplete PR was already merged). It is time to finish this.
Activity
sunmy2019 commentedon Apr 13, 2023
The feature was added at #15362
It looks like the author forgot about this case. A direct modification is acceptable here.
pythongh-103498: argparse.ArgumentParser will always throw instead of…
JustBeYou commentedon Apr 13, 2023
Hi, new contributor here 👋
I written a quick fix which should be generic (instead of guarding all calls to
self.error, I added the guard in the method itself). But this way, throwing custom exceptions is not possible. (for example in the PR implementing the feature there was a test expecting aargparse.ArgumentErrorexception). Is this acceptable or is throwing custom exceptions desired?If it is desired, we could do the following without breaking the existing API:
def error(self, exception_or_message: Union[str, Exception])- allow passing both messages and exceptionsdef error(self, message: str, exception_class=None)- allow passing exception class information in a separate argumentPersonally, I find the first one the simplest and requires little changes to the code. I would be glad if any maintainer could give an opinion.
gaogaotiantian commentedon Apr 13, 2023
As far as I can tell, all the
self.error()usage is reporting an issue with argument parsing(well, its theargparselibrary of course). In_parse_known_argsthere are mixed usage ofArgumentErrorandself.error()- which is pretty bad.Doing all the error handling in
self.error()seems reasonable, but I don't think we need to change the signature inself.error(). Just raiseArgumentErrorwith the message ifself.exit_on_errorisFalse- it's easier for user to catch it anyway.We also need to replace all the
raise ArgumentErrorwithself.error()to keep the code clean.pythongh-103498: argparse.ArgumentParser will always throw instead of…
pythongh-103498: argparse.ArgumentParser will always throw instead of…
pythongh-103498: argparse.ArgumentParser will always throw instead of…
gaogaotiantian commentedon Apr 13, 2023
Hmm, I did not realize that
ArgumentErroractually takes an action, that was my fault. I still think the current implementation is better, but there might be a backward compatibility issue involved. It's clearly stated that the user can overrideerror()function. If you added an extra argument and used it, the user's current code might break. I think we need some opinions from the component owner.JustBeYou commentedon Apr 13, 2023
No, I did not reply to the email directly, I just wanted to edit the comment because I noticed the compatibility issue after I written it, but I deleted it instead. 😅
Anyway, I followed your advice, but stumbled upon this
actionparameter that breaks the API for classes that inheritArgumentParser. We could avoid this by using**kwargs, but it is not the cleanest way to solve it.gaogaotiantian commentedon Apr 13, 2023
Taking at the look at the code, when this feature was intruduced, it was not very thoroughly tested/reviewed. Basically all the
self.error()usage need to be vetted - by nature it contradicts this feature. So this feature either has to live inself.error(), or all the currentself.error()usage need to be protected by checking this option.Merge branch 'main' into pythongh-103498
jacobtylerwalls commentedon Apr 15, 2023
For triage's sake, I think this is a duplicate of #85427, which has a PR waiting for review at #30832.
There's an even older duplicate with some discussion on this point that shaped the choices I made in #30832, see #30832 (comment).
2 remaining items
olgarithms commentedon Apr 24, 2023
I also came across the same issue where creating an
ArgumentParserwithexit_on_error=Falseand passing the wrong type of argument would raise anargparse.ArgumentError, but passing an unknown argument would exit the task.hpaulj commentedon Apr 24, 2023
Yes, as currently implemented
exit_on_erroronly controls errors that raise aargparse.ArgumentError. Theunrecognized argumentsexit does not useArgumentError(or an errorExceptionclass) , and so cannot be caught in the same way.A more comprehensive way of changing error handling is to customize either the
exitor `error methods:https://docs.python.org/3/library/argparse.html#exiting-methods
gaogaotiantian commentedon Apr 24, 2023
I think we are basically at a dead loop here - we want to keep the backward compatibility so we don't breaking anyone's existing code, but the current behavior is simply wrong. It's impossible to "fix" the behavior without breaking existing code.
The introduction of
exit_on_errordoes not live well with all thesys.exit()usage inargparse. There could be a lot of similar traps unless we patch all thesys.exit()s (self.error()).Anyone who had to make a decision about fixing this would carry plenty of pressure for breaking people's code and I believe according to @sobolevn none of the core devs are the experts of
argparsenow. I'm curious if/how we could proceed on this.JustBeYou commentedon May 4, 2023
I took a deeper dive into the problem and I may have a solution. I think the main goals for the fix are:
ArgumentParsercould encounterexit_on_error=Falseby calling.exit()indirectly by accidentFirst, to make the error handling consistent, I propose that we convert all calls to
self.error()to raising exceptions (in caseArgumentErrordoes not fit properly in some cases, we can think of a new public exception type, but I think we won't need that).Second, we make all public methods of
ArgumentParser(there are few) to handleexit_on_errorin an obvious way. Ifexit_on_error=False, we allow exception to pass through, otherwise we pass their string representation toself.errorwhich will do its thing and finally exit.I think that if we use
self.erroronly in simpletry-catchblocks in the public methods, it would be easy enough to make the right behaviour happen and avoid future mistakes.I will update the pull request to reflect what I was talking about and I'm looking for suggestions/opinions. Maybe we succeed to move this forward.
Below is a list of places where
self.exit/self.errorare used.Calls to ArgumentParser.exit which are justified and probably do not need to be changed:
Calls to ArgumentParser.error that should be converted to raising exceptions:
gaogaotiantian commentedon May 4, 2023
I'm sure there are ways to make the code better, but I believe the major issue here is not how, it is backward compatibility. For example, what if the user are subclassing
ArgumentParserand usingself.error()in their code? Your change would break the behavior.Like I said above, the real issue here is the library was used too much by the users and all the implementation details are considered "documented feature". We either keep the old "wrong" behavior, or break users existing code.
The hard part is to make the decision - and that's where we are stuck I believe. We need core devs to support a reform, even if that means breaking some of the user code. Without that, I don't believe there would be an implemetation feasible.
alonbl commentedon Nov 5, 2023
Hi,
The
exit_on_error=Falseis also used to ignore undefined arguments and return args collection of what argparse succeeded to parse. It should not raise exception as then the parse will not be able to return its value.Regards,
hpaulj commentedon Nov 5, 2023
hpaulj commentedon Nov 6, 2023
androidkh commentedon Dec 11, 2023
Hi all,
the issue still exists for me in version 1.4.0.
Error stack trace:
...
As I can see, on line 1857 it actually tries to verify 'if self.exit_on_error' and then use try..catch but in fact it doesn't reach that block as at line 1827 it goes to self.error that later leads to exit
hpaulj commentedon Dec 11, 2023
olgarithms commentedon May 20, 2024
@ericvsmith looking into this!
serhiy-storchaka commentedon Jun 26, 2024
I wrote #121056 before discovering this issue. It fixes the issue in the way opposite to #103519: internal
error()calls are replaced with raisingArgumentError. It is then caught and passed toerror()at the higher level. Basically, it implements the idea proposed in #103498 (comment).We now have 3 duplicate issues with 4 open PRs (and one incomplete PR was already merged). It is time to finish this.