Skip to content

subprocess: CalledProcessError.__str__ crashes when returncode is None #153970

Description

@fedonman

Calling str() on a CalledProcessError crashes if its returncode is None.

The __str__ method uses if self.returncode and self.returncode < 0 for the "died with a signal" case, and otherwise formats the return code with %d. When returncode is None, that first check is falsy, so it falls through to the %d branch, and %d can't format None, so it raises TypeError. Having the exception's own string representation blow up is a pretty bad way to fail.

import subprocess
err = subprocess.CalledProcessError(None, "cmd")
str(err)  # TypeError: %d format: a real number is required, not NoneType

The fix is to check for returncode is None before the %d branch and return a plain message instead.

Found while going through devdanzin's audit of the standard library, item 9: https://gist.github.com/devdanzin/3198710e3c0128fda5e0a7b4e0768e5f

Linked PRs

Activity

  1. added
    type-bugAn unexpected behavior, bug, or error
    stdlibStandard Library Python modules in the Lib/ directory
    on Jul 18, 2026
  2. picnixz commented on Jul 18, 2026

    @picnixz
    Member

    I'm not entirely sure you're meant to create CalledProcessError yourself. The docs say:

    Subclass of SubprocessError, raised when a process run by check_call(), check_output(), or run() (with check=True) returns a non-zero exit status

    So I don't thinkt he constructor is meant to be part of the public API. Note that the constructor is not documented either.

  3. added
    pendingThe issue will be closed if no feedback is provided
    on Jul 18, 2026
  4. fedonman commented on Jul 18, 2026

    @fedonman
    ContributorAuthor

    @picnixz Fair point, and you're right that the constructor isn't documented, and that subprocess itself always builds this with a real integer return code.

    The reasoning is less "people construct this directly" and more that an exception's __str__ shouldn't itself raise. CalledProcessError and its returncode attribute are public and documented, and exceptions do get constructed and re-raised by test code and by third-party libraries that imitate subprocess. If one of those ends up with returncode=None and propagates, the traceback machinery calls str() on it and you get a confusing chained TypeError that hides the real error instead of a readable message.

  5. devdanzin commented on Jul 18, 2026

    @devdanzin
    Member

    I'm not entirely sure you're meant to create CalledProcessError yourself.

    Me neither, but if someone is creating a new subclass they could benefit from not having an admittedly hard to hit footgun. E.g.:

    class StrictCompletedProcess(subprocess.CompletedProcess):
        def check_returncode(self):
            if self.returncode != 0:          # looks more explicit/correct...
                raise subprocess.CalledProcessError(
                    self.returncode, self.args, self.stdout, self.stderr)
    
    cp = StrictCompletedProcess(args=["mycommand"], returncode=None)
    cp.check_returncode()
  6. picnixz commented on Jul 18, 2026

    @picnixz
    Member

    I'm still unsure about this. In this case, I would rather leave it as is because it's already documented as being an exit code, so an integer. We don't do overly defensive programming in the stdlib.

  7. fedonman commented on Jul 18, 2026

    @fedonman
    ContributorAuthor

    Should I close the issue?

  8. picnixz commented on Jul 18, 2026

    @picnixz
    Member

    I personally think it's not relevant here. We can improve the documentation though, but I don't see a reason for raising a ValueError in __str__ for None exit codes. If such code can be legitimately obtained by the public API, then we ought to fix it, sure. But such fix should not raise a ValueError either and rather present a different __str__.

    cc @gpshead

  9. picnixz commented on Jul 18, 2026

    @picnixz
    Member

    If one of those ends up with returncode=None and propagates, the traceback machinery calls str() on it and you get a confusing chained TypeError that hides the real error instead of a readable message.

    FTR, we would still consider a GIGO case. The exception would still point at the __str__ method so it wouldn't help for much more for debugging either.

  10. removed
    pendingThe issue will be closed if no feedback is provided
    on Jul 18, 2026
  11. picnixz commented on Jul 18, 2026

    @picnixz
    Member

    After looking at Victor's suggestion on the PR, I think we can just use %s instead of %d (or an f-string) for the return code. It would make it more friendly while allowing arbitrary types.

  12. added
    triagedThe issue has been accepted as valid by a triager.
    on Jul 18, 2026
  13. added a commit that references this issue on Jul 18, 2026
  14. 5 remaining items

  15. added 4 commits that reference this issue on Jul 22, 2026
  16. added a commit that references this issue on Aug 7, 2026
  17. added a commit that references this issue on Aug 14, 2026
  18. added a commit that references this issue on Aug 17, 2026
  19. serhiy-storchaka commented on Aug 21, 2026

    @serhiy-storchaka
    Member

    It needs a backport to 3.15 (which may be blocked by other backports).

  20. vstinner commented on Aug 31, 2026

    @vstinner
    Member

    It needs a backport to 3.15 (which may be blocked by other backports).

    Yes. I already had this PR in my "3.15 backport queue". The PR cannot be created just with git cherry-pick -x 7c653e2540cbe4180efb3bd83b63865a1f675650, other changes should be backported to 3.15 first.

  21. added a commit that references this issue on Sep 29, 2026
  22. added a commit that references this issue on Oct 9, 2026
  23. vstinner commented on Oct 9, 2026

    @vstinner
    Member

    It needs a backport to 3.15 (which may be blocked by other backports).

    I just merged the 3.15 backport. Thanks for the reminder!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    stdlibStandard Library Python modules in the Lib/ directorytopic-subprocessSubprocess issues.triagedThe issue has been accepted as valid by a triager.type-bugAn unexpected behavior, bug, or error

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions