https://github.com/python/cpython/commit/b35c37910a4595f22458cf0291a9b0252b3c6c70
commit: b35c37910a4595f22458cf0291a9b0252b3c6c70
branch: main
author: Benjy Wiener <[email protected]>
committer: vstinner <[email protected]>
date: 2026-06-24T16:11:02+02:00
summary:

gh-151485: Fix command quoting in subprocess.CalledProcessError.__str__ 
(#151486)

CalledProcessError previously formatted cmd as `"... '%s' ..."`. This lead to
unbalanced quoting when cmd contains single-quotes or, more commonly, when cmd
is a list. This change updates the relevant format strings to use %r instead.

Co-authored-by: Benjy Wiener <[email protected]>
Co-authored-by: blurb-it[bot] <43283697+blurb-it[bot]@users.noreply.github.com>

files:
A Misc/NEWS.d/next/Library/2026-06-15-07-42-12.gh-issue-151485.NiYQPZ.rst
M Lib/subprocess.py
M Lib/test/test_subprocess.py

diff --git a/Lib/subprocess.py b/Lib/subprocess.py
index 38b655f2f7b9d2..6fe2ec98fb4088 100644
--- a/Lib/subprocess.py
+++ b/Lib/subprocess.py
@@ -145,13 +145,13 @@ def __init__(self, returncode, cmd, output=None, 
stderr=None):
     def __str__(self):
         if self.returncode and self.returncode < 0:
             try:
-                return "Command '%s' died with %r." % (
+                return "Command %r died with %r." % (
                         self.cmd, signal.Signals(-self.returncode))
             except ValueError:
-                return "Command '%s' died with unknown signal %d." % (
+                return "Command %r died with unknown signal %d." % (
                         self.cmd, -self.returncode)
         else:
-            return "Command '%s' returned non-zero exit status %d." % (
+            return "Command %r returned non-zero exit status %d." % (
                     self.cmd, self.returncode)
 
     @property
diff --git a/Lib/test/test_subprocess.py b/Lib/test/test_subprocess.py
index d41cb1294a3daf..d066ae85dfc51a 100644
--- a/Lib/test/test_subprocess.py
+++ b/Lib/test/test_subprocess.py
@@ -2428,26 +2428,26 @@ def test_run_abort(self):
             p.wait()
         self.assertEqual(-p.returncode, signal.SIGABRT)
 
-    def test_CalledProcessError_str_signal(self):
+    def test_CalledProcessError_str(self):
+        # command string
+        err = subprocess.CalledProcessError(2, "fake cmd")
+        self.assertEqual(str(err), "Command 'fake cmd' returned non-zero exit 
status 2.")
+
+        # command string with a single-quote
+        err = subprocess.CalledProcessError(2, "fake ' cmd")
+        self.assertEqual(str(err), 'Command "fake \' cmd" returned non-zero 
exit status 2.')
+
+        # command list
+        err = subprocess.CalledProcessError(2, ["fake", "cmd"])
+        self.assertEqual(str(err), "Command ['fake', 'cmd'] returned non-zero 
exit status 2.")
+
+        # signal
         err = subprocess.CalledProcessError(-int(signal.SIGABRT), "fake cmd")
-        error_string = str(err)
-        # We're relying on the repr() of the signal.Signals intenum to provide
-        # the word signal, the signal name and the numeric value.
-        self.assertIn("signal", error_string.lower())
-        # We're not being specific about the signal name as some signals have
-        # multiple names and which name is revealed can vary.
-        self.assertIn("SIG", error_string)
-        self.assertIn(str(signal.SIGABRT), error_string)
-
-    def test_CalledProcessError_str_unknown_signal(self):
-        err = subprocess.CalledProcessError(-9876543, "fake cmd")
-        error_string = str(err)
-        self.assertIn("unknown signal 9876543.", error_string)
+        self.assertEqual(str(err), f"Command 'fake cmd' died with 
{signal.SIGABRT!r}.")
 
-    def test_CalledProcessError_str_non_zero(self):
-        err = subprocess.CalledProcessError(2, "fake cmd")
-        error_string = str(err)
-        self.assertIn("non-zero exit status 2.", error_string)
+        # unknown signal
+        err = subprocess.CalledProcessError(-9876543, "fake cmd")
+        self.assertEqual(str(err), "Command 'fake cmd' died with unknown 
signal 9876543.")
 
     def test_preexec(self):
         # DISCLAIMER: Setting environment variables is *not* a good use
diff --git 
a/Misc/NEWS.d/next/Library/2026-06-15-07-42-12.gh-issue-151485.NiYQPZ.rst 
b/Misc/NEWS.d/next/Library/2026-06-15-07-42-12.gh-issue-151485.NiYQPZ.rst
new file mode 100644
index 00000000000000..134499107ab2b7
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-06-15-07-42-12.gh-issue-151485.NiYQPZ.rst
@@ -0,0 +1 @@
+Fix command quoting in :exc:`subprocess.CalledProcessError`. Contributed by 
Benjy Wiener.

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to