From fd8fc3a0c39003a919d2db82ff165c7885f5571a Mon Sep 17 00:00:00 2001 From: Timothy Poon <62692924+ptim0626@users.noreply.github.com> Date: Sat, 18 Jul 2026 10:09:59 +0100 Subject: [PATCH 01/12] Handle invalid file object when passed explicitly --- Lib/argparse.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/Lib/argparse.py b/Lib/argparse.py index 29e6ebb9634261a..cda1abc926c9707 100644 --- a/Lib/argparse.py +++ b/Lib/argparse.py @@ -86,6 +86,7 @@ ] +import io as _io import os as _os import re as _re import sys as _sys @@ -2885,12 +2886,13 @@ def print_help(self, file=None): self._print_message(help_text, file) def _print_message(self, message, file=None): - if message: - file = file or _sys.stderr - try: - file.write(message) - except (AttributeError, OSError): - pass + if file is None: + if _sys.stderr is not None: + _sys.stderr.write(message) + else: + if not isinstance(file, _io.IOBase): + raise ValueError(f"invalid file object {file}") + file.write(message) def _get_theme(self, file=None): # If self.color is False, _colorize is not imported From 259fa8e8c42def36a3bb11035658d2cadf3946b8 Mon Sep 17 00:00:00 2001 From: Timothy Poon <62692924+ptim0626@users.noreply.github.com> Date: Sat, 18 Jul 2026 14:08:25 +0100 Subject: [PATCH 02/12] Add tests for valid/invalid file with stdout/err as None --- Lib/test/test_argparse.py | 54 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/Lib/test/test_argparse.py b/Lib/test/test_argparse.py index 1dc3f538f4ad8ba..d5eac17725b173e 100644 --- a/Lib/test/test_argparse.py +++ b/Lib/test/test_argparse.py @@ -79,6 +79,60 @@ def test_skip_invalid_stdout(self): func() self.assertRegex(mocked_stderr.getvalue(), r'usage:') + def test_invalid_file_only(self): + parser = argparse.ArgumentParser() + for func in ( + parser.print_usage, + parser.print_help, + ): + self.assertRaises(ValueError, func, "invalid file") + + def test_valid_file_invalid_stdout(self): + parser = argparse.ArgumentParser() + for func in ( + parser.print_usage, + parser.print_help, + ): + with ( + self.subTest(func=func), + contextlib.redirect_stdout(None), + StdIOBuffer() as f, + mock.patch('argparse._sys.exit'), + ): + func(file=f) + self.assertRegex(f.getvalue(), r'usage:') + + def test_valid_file_invalid_stderr(self): + parser = argparse.ArgumentParser() + for func in ( + parser.print_usage, + parser.print_help, + ): + with ( + self.subTest(func=func), + contextlib.redirect_stderr(None), + StdIOBuffer() as f, + mock.patch('argparse._sys.exit'), + ): + func(file=f) + self.assertRegex(f.getvalue(), r'usage:') + + def test_valid_file_invalid_stdout_stderr(self): + parser = argparse.ArgumentParser() + for func in ( + parser.print_usage, + parser.print_help, + ): + with ( + self.subTest(func=func), + contextlib.redirect_stdout(None), + contextlib.redirect_stderr(None), + StdIOBuffer() as f, + mock.patch('argparse._sys.exit'), + ): + func(file=f) + self.assertRegex(f.getvalue(), r'usage:') + class TestLazyImports(unittest.TestCase): LAZY_IMPORTS = { From 84bc1109defbca55e03f44d483682d0346e93839 Mon Sep 17 00:00:00 2001 From: Timothy Poon <62692924+ptim0626@users.noreply.github.com> Date: Sat, 18 Jul 2026 15:13:19 +0100 Subject: [PATCH 03/12] Update assertRaise to use context-manager format --- Lib/test/test_argparse.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_argparse.py b/Lib/test/test_argparse.py index d5eac17725b173e..17ac4fad56a7bf6 100644 --- a/Lib/test/test_argparse.py +++ b/Lib/test/test_argparse.py @@ -85,7 +85,8 @@ def test_invalid_file_only(self): parser.print_usage, parser.print_help, ): - self.assertRaises(ValueError, func, "invalid file") + with self.assertRaises(ValueError): + func(file="invalid file") def test_valid_file_invalid_stdout(self): parser = argparse.ArgumentParser() From 2f2261049f83a26ddc85bb4e94698bea23e186ec Mon Sep 17 00:00:00 2001 From: Timothy Poon <62692924+ptim0626@users.noreply.github.com> Date: Sat, 18 Jul 2026 15:33:50 +0100 Subject: [PATCH 04/12] Remove redundant tests --- Lib/test/test_argparse.py | 46 --------------------------------------- 1 file changed, 46 deletions(-) diff --git a/Lib/test/test_argparse.py b/Lib/test/test_argparse.py index 17ac4fad56a7bf6..a06ddc066c64213 100644 --- a/Lib/test/test_argparse.py +++ b/Lib/test/test_argparse.py @@ -88,52 +88,6 @@ def test_invalid_file_only(self): with self.assertRaises(ValueError): func(file="invalid file") - def test_valid_file_invalid_stdout(self): - parser = argparse.ArgumentParser() - for func in ( - parser.print_usage, - parser.print_help, - ): - with ( - self.subTest(func=func), - contextlib.redirect_stdout(None), - StdIOBuffer() as f, - mock.patch('argparse._sys.exit'), - ): - func(file=f) - self.assertRegex(f.getvalue(), r'usage:') - - def test_valid_file_invalid_stderr(self): - parser = argparse.ArgumentParser() - for func in ( - parser.print_usage, - parser.print_help, - ): - with ( - self.subTest(func=func), - contextlib.redirect_stderr(None), - StdIOBuffer() as f, - mock.patch('argparse._sys.exit'), - ): - func(file=f) - self.assertRegex(f.getvalue(), r'usage:') - - def test_valid_file_invalid_stdout_stderr(self): - parser = argparse.ArgumentParser() - for func in ( - parser.print_usage, - parser.print_help, - ): - with ( - self.subTest(func=func), - contextlib.redirect_stdout(None), - contextlib.redirect_stderr(None), - StdIOBuffer() as f, - mock.patch('argparse._sys.exit'), - ): - func(file=f) - self.assertRegex(f.getvalue(), r'usage:') - class TestLazyImports(unittest.TestCase): LAZY_IMPORTS = { From b25daae1d7a5d5f1eef1f9f515c8513912b33d27 Mon Sep 17 00:00:00 2001 From: Timothy Poon <62692924+ptim0626@users.noreply.github.com> Date: Sat, 18 Jul 2026 16:07:04 +0100 Subject: [PATCH 05/12] Add NEWS entry --- .../next/Library/2026-07-18-16-05-38.gh-issue-153967.-OUNXe.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2026-07-18-16-05-38.gh-issue-153967.-OUNXe.rst diff --git a/Misc/NEWS.d/next/Library/2026-07-18-16-05-38.gh-issue-153967.-OUNXe.rst b/Misc/NEWS.d/next/Library/2026-07-18-16-05-38.gh-issue-153967.-OUNXe.rst new file mode 100644 index 000000000000000..54add434bb635e2 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-18-16-05-38.gh-issue-153967.-OUNXe.rst @@ -0,0 +1,2 @@ +:func:`argparse.print_usage` and :func:`argparse.print_help` won't silently +fail when an invalid file object is specified. Patch by Timothy Poon. From 27787e9aa40f0063e6949dc2116518a8ca095527 Mon Sep 17 00:00:00 2001 From: Timothy Poon <62692924+ptim0626@users.noreply.github.com> Date: Sat, 18 Jul 2026 19:27:40 +0100 Subject: [PATCH 06/12] Modify invalid file test to include falsy object --- Lib/test/test_argparse.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Lib/test/test_argparse.py b/Lib/test/test_argparse.py index a06ddc066c64213..86a7d969400d1cf 100644 --- a/Lib/test/test_argparse.py +++ b/Lib/test/test_argparse.py @@ -81,13 +81,13 @@ def test_skip_invalid_stdout(self): def test_invalid_file_only(self): parser = argparse.ArgumentParser() - for func in ( - parser.print_usage, - parser.print_help, - ): - with self.assertRaises(ValueError): - func(file="invalid file") - + for func in (parser.print_usage, parser.print_help): + for invalid_f in ("invalid file", "", 0): + with ( + self.subTest(func=func, invalid_f=invalid_f), + self.assertRaises(ValueError), + ): + func(file=invalid_f) class TestLazyImports(unittest.TestCase): LAZY_IMPORTS = { From 516ff2096b0324303a0c8074dc203a980cec9562 Mon Sep 17 00:00:00 2001 From: Timothy Poon <62692924+ptim0626@users.noreply.github.com> Date: Sat, 18 Jul 2026 19:32:12 +0100 Subject: [PATCH 07/12] Refactor to a flatter structure and silently pass with falsy message --- Lib/argparse.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Lib/argparse.py b/Lib/argparse.py index cda1abc926c9707..f6a2fd14efb69a2 100644 --- a/Lib/argparse.py +++ b/Lib/argparse.py @@ -2886,12 +2886,13 @@ def print_help(self, file=None): self._print_message(help_text, file) def _print_message(self, message, file=None): + if not message: + return if file is None: - if _sys.stderr is not None: - _sys.stderr.write(message) - else: - if not isinstance(file, _io.IOBase): - raise ValueError(f"invalid file object {file}") + file = _sys.stderr + elif not isinstance(file, _io.IOBase): + raise ValueError(f"invalid file object {file}") + if file is not None: file.write(message) def _get_theme(self, file=None): From d0af2f431f69c80c6f83011f757b8f8eb4780aa6 Mon Sep 17 00:00:00 2001 From: Timothy Poon <62692924+ptim0626@users.noreply.github.com> Date: Sun, 19 Jul 2026 11:37:38 +0100 Subject: [PATCH 08/12] Fix docs parser issue --- .../Library/2026-07-18-16-05-38.gh-issue-153967.-OUNXe.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Misc/NEWS.d/next/Library/2026-07-18-16-05-38.gh-issue-153967.-OUNXe.rst b/Misc/NEWS.d/next/Library/2026-07-18-16-05-38.gh-issue-153967.-OUNXe.rst index 54add434bb635e2..c3bdc3f35b1aaf5 100644 --- a/Misc/NEWS.d/next/Library/2026-07-18-16-05-38.gh-issue-153967.-OUNXe.rst +++ b/Misc/NEWS.d/next/Library/2026-07-18-16-05-38.gh-issue-153967.-OUNXe.rst @@ -1,2 +1,2 @@ -:func:`argparse.print_usage` and :func:`argparse.print_help` won't silently -fail when an invalid file object is specified. Patch by Timothy Poon. +:meth:`ArgumentParser.print_usage` and :meth:`ArgumentParser.print_help` won't +silently fail when an invalid file object is specified. Patch by Timothy Poon. From 97c6b58a72974c1f242beea7f6a99b33633422a8 Mon Sep 17 00:00:00 2001 From: Timothy Poon <62692924+ptim0626@users.noreply.github.com> Date: Sun, 19 Jul 2026 11:46:26 +0100 Subject: [PATCH 09/12] Fix missing file name in meth ref --- .../Library/2026-07-18-16-05-38.gh-issue-153967.-OUNXe.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Misc/NEWS.d/next/Library/2026-07-18-16-05-38.gh-issue-153967.-OUNXe.rst b/Misc/NEWS.d/next/Library/2026-07-18-16-05-38.gh-issue-153967.-OUNXe.rst index c3bdc3f35b1aaf5..5f520140f43a8ea 100644 --- a/Misc/NEWS.d/next/Library/2026-07-18-16-05-38.gh-issue-153967.-OUNXe.rst +++ b/Misc/NEWS.d/next/Library/2026-07-18-16-05-38.gh-issue-153967.-OUNXe.rst @@ -1,2 +1,3 @@ -:meth:`ArgumentParser.print_usage` and :meth:`ArgumentParser.print_help` won't -silently fail when an invalid file object is specified. Patch by Timothy Poon. +:meth:`argparse.ArgumentParser.print_usage` and +:meth:`argparse.ArgumentParser.print_help` won't silently fail when an invalid +file object is specified. Patch by Timothy Poon. From 0d22e85b8dbc508044061338f460ee9845a3ac61 Mon Sep 17 00:00:00 2001 From: Timothy Poon <62692924+ptim0626@users.noreply.github.com> Date: Fri, 24 Jul 2026 23:38:01 +0100 Subject: [PATCH 10/12] Remove _IOBase check to support writable objects --- Lib/argparse.py | 3 --- Lib/test/test_argparse.py | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/Lib/argparse.py b/Lib/argparse.py index f6a2fd14efb69a2..66e901bfb42cfad 100644 --- a/Lib/argparse.py +++ b/Lib/argparse.py @@ -86,7 +86,6 @@ ] -import io as _io import os as _os import re as _re import sys as _sys @@ -2890,8 +2889,6 @@ def _print_message(self, message, file=None): return if file is None: file = _sys.stderr - elif not isinstance(file, _io.IOBase): - raise ValueError(f"invalid file object {file}") if file is not None: file.write(message) diff --git a/Lib/test/test_argparse.py b/Lib/test/test_argparse.py index 86a7d969400d1cf..b6f61adbea84cd2 100644 --- a/Lib/test/test_argparse.py +++ b/Lib/test/test_argparse.py @@ -85,7 +85,7 @@ def test_invalid_file_only(self): for invalid_f in ("invalid file", "", 0): with ( self.subTest(func=func, invalid_f=invalid_f), - self.assertRaises(ValueError), + self.assertRaises(AttributeError), ): func(file=invalid_f) From 772a8e18c1319e00d71fd4ffc949935ec4184caa Mon Sep 17 00:00:00 2001 From: Timothy Poon <62692924+ptim0626@users.noreply.github.com> Date: Thu, 6 Aug 2026 11:26:23 +0100 Subject: [PATCH 11/12] Add test to ensure argparse.exit always raise SystemExit --- Lib/test/test_argparse.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Lib/test/test_argparse.py b/Lib/test/test_argparse.py index b6f61adbea84cd2..18348bd841edffa 100644 --- a/Lib/test/test_argparse.py +++ b/Lib/test/test_argparse.py @@ -89,6 +89,15 @@ def test_invalid_file_only(self): ): func(file=invalid_f) + def test_exit_when_stderr_oserror(self): + parser = argparse.ArgumentParser() + with (mock.patch('argparse._sys.stderr.write', + side_effect=OSError('not raise this')), + self.assertRaises(SystemExit), + ): + parser.exit(status=0, message='foo') + + class TestLazyImports(unittest.TestCase): LAZY_IMPORTS = { "_colorize", From 6206743529c54e7f6e8a5edeefde02567a54e561 Mon Sep 17 00:00:00 2001 From: Timothy Poon <62692924+ptim0626@users.noreply.github.com> Date: Thu, 6 Aug 2026 11:27:54 +0100 Subject: [PATCH 12/12] Suppess OSError in _print_message --- Lib/argparse.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Lib/argparse.py b/Lib/argparse.py index 66e901bfb42cfad..caa9da9eb7687fe 100644 --- a/Lib/argparse.py +++ b/Lib/argparse.py @@ -2890,7 +2890,10 @@ def _print_message(self, message, file=None): if file is None: file = _sys.stderr if file is not None: - file.write(message) + try: + file.write(message) + except OSError: + pass def _get_theme(self, file=None): # If self.color is False, _colorize is not imported