Skip to content

gh-153970: Fix str() of CalledProcessError when returncode is not an integer#153971

Open
fedonman wants to merge 12 commits into
python:mainfrom
fedonman:fix-gh-153970-calledprocesserror-none
Open

gh-153970: Fix str() of CalledProcessError when returncode is not an integer#153971
fedonman wants to merge 12 commits into
python:mainfrom
fedonman:fix-gh-153970-calledprocesserror-none

Conversation

@fedonman

@fedonman fedonman commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

CalledProcessError.returncode holds a process exit status, which subprocess always sets to an integer. Calling str() on one whose returncode was None raised TypeError, because the __str__ else branch formats the value with %d. A returncode of another type, such as a string or a list, failed even earlier at the self.returncode < 0 comparison.

__str__ now guards that comparison with isinstance(self.returncode, int) and formats the exit status with an f-string, so it works for any value. The constructor is left as it was, so the exception still carries the full process state (returncode, output, stderr) even when the returncode is not an integer. Running a subprocess is expensive and you may not get the same output twice, so losing that context to a second exception would make debugging harder.

One way to hit this is a mocked Popen:

import unittest.mock
import subprocess

with unittest.mock.patch.object(subprocess, 'Popen'):
    subprocess.check_call(['true'])

That used to end in subprocess.CalledProcessError: <exception str() failed>. Now it prints the command and the mock object as the exit status.

The docs for the returncode attribute now say it is an integer.

An earlier version of this PR validated the returncode type in the constructor and raised TypeError for anything else. That is dropped, along with its test and doc note, following the review discussion.

Found as item 9 in devdanzin's standard library audit: https://gist.github.com/devdanzin/3198710e3c0128fda5e0a7b4e0768e5f

Fixes #153970.

… is None

CalledProcessError.__str__ fell through to a branch that formats the
return code with %d, which raises TypeError when returncode is None.
Handle the None case explicitly and return a message reporting an
unknown exit status.
@picnixz

picnixz commented Jul 18, 2026

Copy link
Copy Markdown
Member

See my reservations on the issue; I don't think the constructor is a public API and thus it's a case of GIGO (garbage-in, garbage-out)

@fedonman

Copy link
Copy Markdown
Contributor Author

See my reservations on the issue; I don't think the constructor is a public API and thus it's a case of GIGO (garbage-in, garbage-out)

Thank you for reviewing this. Answered on the Issue thread.

@vstinner

Copy link
Copy Markdown
Member

I don't think that there is a reason to format the returncode using %d. The issue can be addressed by using a f-string, without checking self.returncode:

diff --git a/Lib/subprocess.py b/Lib/subprocess.py
index 6fe2ec98fb4..df23fb18a51 100644
--- a/Lib/subprocess.py
+++ b/Lib/subprocess.py
@@ -151,8 +151,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):

@picnixz

picnixz commented Jul 18, 2026

Copy link
Copy Markdown
Member

I guess this patch would be acceptable indeed.

fedonman added 4 commits July 19, 2026 02:49
The implementation now formats a None returncode with an f-string
("returned non-zero exit status None.") rather than a dedicated branch,
so update the test assertion and the NEWS wording to match.
…com:fedonman/cpython into fix-pythongh-153970-calledprocesserror-none
@fedonman

Copy link
Copy Markdown
Contributor Author

Thank you. I implemented the requested changes.

@picnixz picnixz left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, after sleeping a bit, we would still have an issue if someone is using a returncode as a string for instance (just because self.returncode < 0 would raise here). Since we are considering only theoretical usages, I really wonderf whether we need the change.

This change would make None supported, but would still fail on truthy returncodes not comparable to 0.

So... I'd say:

  • either check returncode type in the constructor
  • do nothing but reword the docs a bit.

fedonman and others added 2 commits July 21, 2026 15:33
…teger

The earlier fix changed CalledProcessError.__str__ to tolerate a None
returncode, but that only handled one non-integer value and would still
fail on others, such as a returncode set to a string. The returncode
attribute is meant to hold a process exit status, which subprocess
always sets to an integer, so there is no real bug in __str__.

This drops the code change and the extra test, and instead notes in the
docs that returncode is always an integer.
@fedonman fedonman changed the title gh-153970: Fix CalledProcessError.__str__ crash when returncode is None gh-153970: Document that CalledProcessError.returncode is an integer Jul 22, 2026
@fedonman
fedonman requested a review from picnixz July 22, 2026 08:16
@read-the-docs-community

read-the-docs-community Bot commented Jul 22, 2026

Copy link
Copy Markdown

Calling str() on a CalledProcessError whose returncode was None raised
TypeError, because the __str__ else branch formats the value with %d. A
returncode of another type, such as a string, failed even earlier at the
self.returncode < 0 comparison.

The constructor now raises a clear TypeError when returncode is neither an
integer nor None, so __str__ can never be handed a value it cannot format.
The else branch also uses an f-string, so a None returncode renders as a
message instead of crashing. One test that built the exception with an
empty-string returncode is updated to pass an integer, which is what a
real failed command produces.
@fedonman fedonman changed the title gh-153970: Document that CalledProcessError.returncode is an integer gh-153970: Validate the returncode type in CalledProcessError Jul 22, 2026
Comment thread Lib/subprocess.py Outdated
cmd, returncode, stdout, stderr, output
"""
def __init__(self, returncode, cmd, output=None, stderr=None):
if returncode is not None and not isinstance(returncode, int):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dislike raising a new exception, since CalledProcessError is created when we are already handling another exception. CalledProcessError should accept anything for returncode, and then does it best to convert returncode to a string in __str__().

Comment thread Lib/subprocess.py Outdated

def __str__(self):
# A None returncode is falsy, so it skips the '< 0' comparison
# below and is formatted by the final branch instead.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

returncode should not be None, so I would prefer to omit this comment.

Comment thread Lib/subprocess.py
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}.")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This two lines changes is the only needed change IMO.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But then we are still having the possibility of the TypeError. In this case I would rather just document that we need an error code that is an integer and leave it as is.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But then we are still having the possibility of the TypeError

Would you mind to elaborate which operation would raise TypeError? The issue is about str() and the change above does fix str().

With the change above, CalledProcessError can be called with any returncode, it just works.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The issue with <0 branch. If you pass [1] you get a type error there

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I'm sorry, I completely missed the error on self.returncode < 0.

Well, in that case, I suggest replacing:

if self.returncode and self.returncode < 0:

with:

if isinstance(self.returncode, int) and self.returncode < 0:

IMO we should accept any returncode value in the CalledProcessError constructor and str() should not fail.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's slightly awkward for me to accept arbitrary return codes. I would assume them to be integers, and not None or other things, and would rather change the docs. In the first place, the class signature is not exposed so I don't consider it as part of the public API, whether subclassing or not.

But, if you really want to support arbitrary types, I'm ok with this change (+ the __repr__ one), but I really think it's a bit too niche.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's slightly awkward for me to accept arbitrary return codes.

We can get a type other than int if there is a bug in subprocess under specific conditions. In that case, I would prefer to be able to create CalledProcessError which stores the full process state (returncode, stdout, stderr), rather than raising a new exception (ex: TypeError) which removes all subprocess context. Running a subprocess is an expensive operation, and you might not able to get the same stdout/stderr if you run the same command twice which would make debugging way harder.

You can also get a non-int returncode when mocking the Popen object. Example:

import unittest.mock
import subprocess

with unittest.mock.patch.object(subprocess, 'Popen'):
    subprocess.check_call(['true'])

Output:

Traceback (most recent call last):
  File "/home/vstinner/python/main/x.py", line 5, in <module>
    subprocess.check_call(['true'])
    ~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^
  File "/home/vstinner/python/main/Lib/subprocess.py", line 534, in check_call
    raise CalledProcessError(retcode, cmd)
subprocess.CalledProcessError: <exception str() failed>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, the mocking argument convinces me.

@vstinner

Copy link
Copy Markdown
Member

Since we are considering only theoretical usages, I really wonderf whether we need the change.

The bug has been found by fuzzing. If we don't fix the issue, someone else will come back in a few months with the same issue.

fedonman added 2 commits July 26, 2026 18:17
…code

Per the review, the constructor no longer checks the returncode type.
Instead __str__ guards the negative comparison with isinstance(), so a
returncode that is not an integer is formatted by the f-string branch
instead of raising TypeError. This keeps the full process state on the
exception, which matters when a subprocess bug or a mocked Popen produces
a returncode that is not an int.

This drops the constructor check, the versionchanged note and the
test_script_helper change that went with it. The type test is replaced by
one that checks str() for non-integer returncodes.
@fedonman fedonman changed the title gh-153970: Validate the returncode type in CalledProcessError gh-153970: Fix str() of CalledProcessError when returncode is not an integer Jul 26, 2026
@fedonman

Copy link
Copy Markdown
Contributor Author

Addressed comments.

@fedonman
fedonman requested a review from vstinner July 26, 2026 15:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

subprocess: CalledProcessError.__str__ crashes when returncode is None

3 participants