diff --git a/Doc/library/subprocess.rst b/Doc/library/subprocess.rst index fe64daa3291d67..5470d05bb11f91 100644 --- a/Doc/library/subprocess.rst +++ b/Doc/library/subprocess.rst @@ -236,8 +236,8 @@ underlying :class:`Popen` interface can be used directly. .. attribute:: returncode - Exit status of the child process. If the process exited due to a - signal, this will be the negative signal number. + Exit status of the child process, an integer. If the process + exited due to a signal, this will be the negative signal number. .. attribute:: cmd @@ -260,6 +260,10 @@ underlying :class:`Popen` interface can be used directly. .. versionchanged:: 3.5 *stdout* and *stderr* attributes added + .. versionchanged:: 3.16 + The constructor now raises :exc:`TypeError` if *returncode* is + neither an integer nor ``None``. + .. _frequently-used-arguments: diff --git a/Lib/subprocess.py b/Lib/subprocess.py index 6fe2ec98fb4088..7853413aff6f55 100644 --- a/Lib/subprocess.py +++ b/Lib/subprocess.py @@ -137,12 +137,18 @@ class CalledProcessError(SubprocessError): cmd, returncode, stdout, stderr, output """ def __init__(self, returncode, cmd, output=None, stderr=None): + if returncode is not None and not isinstance(returncode, int): + raise TypeError( + "returncode must be an integer or None, not " + f"{type(returncode).__name__}") self.returncode = returncode self.cmd = cmd self.output = output self.stderr = stderr def __str__(self): + # A None returncode is falsy, so it skips the '< 0' comparison + # below and is formatted by the final branch instead. if self.returncode and self.returncode < 0: try: return "Command %r died with %r." % ( @@ -151,8 +157,8 @@ def __str__(self): return "Command %r died with unknown signal %d." % ( self.cmd, -self.returncode) else: - return "Command %r returned non-zero exit status %d." % ( - self.cmd, self.returncode) + return (f"Command {self.cmd!r} returned non-zero " + f"exit status {self.returncode}.") @property def stdout(self): diff --git a/Lib/test/test_script_helper.py b/Lib/test/test_script_helper.py index eeea6c4842b488..3c5763b0b8374d 100644 --- a/Lib/test/test_script_helper.py +++ b/Lib/test/test_script_helper.py @@ -86,7 +86,7 @@ def tearDown(self): def test_interpreter_requires_environment_true(self, mock_check_call): with mock.patch.dict(os.environ): os.environ.pop('PYTHONHOME', None) - mock_check_call.side_effect = subprocess.CalledProcessError('', '') + mock_check_call.side_effect = subprocess.CalledProcessError(1, 'cmd') self.assertTrue(script_helper.interpreter_requires_environment()) self.assertTrue(script_helper.interpreter_requires_environment()) self.assertEqual(1, mock_check_call.call_count) diff --git a/Lib/test/test_subprocess.py b/Lib/test/test_subprocess.py index d066ae85dfc51a..d3368a745fc374 100644 --- a/Lib/test/test_subprocess.py +++ b/Lib/test/test_subprocess.py @@ -2449,6 +2449,20 @@ def test_CalledProcessError_str(self): err = subprocess.CalledProcessError(-9876543, "fake cmd") self.assertEqual(str(err), "Command 'fake cmd' died with unknown signal 9876543.") + # None return code (must not raise TypeError from __str__ itself) + err = subprocess.CalledProcessError(None, "fake cmd") + self.assertEqual(str(err), "Command 'fake cmd' returned non-zero exit status None.") + + def test_CalledProcessError_returncode_type(self): + # returncode must be an integer or None; other types are rejected + # at construction so that __str__ cannot fail later on. + subprocess.CalledProcessError(1, "fake cmd") + subprocess.CalledProcessError(None, "fake cmd") + for returncode in ("1", 1.5, [1], object()): + with self.subTest(returncode=returncode): + with self.assertRaises(TypeError): + subprocess.CalledProcessError(returncode, "fake cmd") + def test_preexec(self): # DISCLAIMER: Setting environment variables is *not* a good use # of a preexec_fn. This is merely a test. diff --git a/Misc/NEWS.d/next/Library/2026-07-22-12-00-00.gh-issue-153970.Kq7Wn3.rst b/Misc/NEWS.d/next/Library/2026-07-22-12-00-00.gh-issue-153970.Kq7Wn3.rst new file mode 100644 index 00000000000000..044982326adc53 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-22-12-00-00.gh-issue-153970.Kq7Wn3.rst @@ -0,0 +1,4 @@ +Calling :func:`str` on a :exc:`subprocess.CalledProcessError` whose +:attr:`!returncode` is ``None`` no longer raises :exc:`TypeError`. The +constructor now also raises :exc:`TypeError` when *returncode* is neither +an integer nor ``None``.