diff --git a/pre_commit/util.py b/pre_commit/util.py
index 0f54e9e..8072042 100644
--- a/pre_commit/util.py
+++ b/pre_commit/util.py
@@ -74,36 +74,31 @@ def make_executable(filename):
 
 
 class CalledProcessError(RuntimeError):
-    def __init__(self, returncode, cmd, expected_returncode, output=None):
+    def __init__(self, returncode, cmd, expected_returncode, stdout, stderr):
         super(CalledProcessError, self).__init__(
-            returncode, cmd, expected_returncode, output,
+            returncode, cmd, expected_returncode, stdout, stderr,
         )
         self.returncode = returncode
         self.cmd = cmd
         self.expected_returncode = expected_returncode
-        self.output = output
+        self.stdout = stdout
+        self.stderr = stderr
 
     def to_bytes(self):
-        output = []
-        for maybe_text in self.output:
-            if maybe_text:
-                output.append(
-                    b'\n    ' +
-                    five.to_bytes(maybe_text).replace(b'\n', b'\n    '),
-                )
+        def _indent_or_none(part):
+            if part:
+                return b'\n    ' + part.replace(b'\n', b'\n    ')
             else:
-                output.append(b'(none)')
+                return b' (none)'
 
         return b''.join((
-            five.to_bytes(
-                'Command: {!r}\n'
-                'Return code: {}\n'
-                'Expected return code: {}\n'.format(
-                    self.cmd, self.returncode, self.expected_returncode,
-                ),
-            ),
-            b'Output: ', output[0], b'\n',
-            b'Errors: ', output[1],
+            'command: {!r}\n'
+            'return code: {}\n'
+            'expected return code: {}\n'.format(
+                self.cmd, self.returncode, self.expected_returncode,
+            ).encode('UTF-8'),
+            b'stdout:', _indent_or_none(self.stdout), b'\n',
+            b'stderr:', _indent_or_none(self.stderr),
         ))
 
     def to_text(self):
@@ -143,9 +138,7 @@ def cmd_output_b(*cmd, **kwargs):
         returncode = proc.returncode
 
     if retcode is not None and retcode != returncode:
-        raise CalledProcessError(
-            returncode, cmd, retcode, output=(stdout_b, stderr_b),
-        )
+        raise CalledProcessError(returncode, cmd, retcode, stdout_b, stderr_b)
 
     return returncode, stdout_b, stderr_b
 

pip install crashes can easily confuse newbies
those that are not familiar with the usual annoying messaging that pip presents can get pretty easily confused by the output that happens when `pip` fails to install

here's an example:

```console
$ pre-commit run flake8 --all-files
[INFO] Initializing environment for https://gitlab.com/pycqa/flake8:flake8-walrus.
[INFO] Installing environment for https://gitlab.com/pycqa/flake8.
[INFO] Once installed this environment will be reused.
[INFO] This may take a few minutes...
An unexpected error has occurred: CalledProcessError: Command: ('/home/asottile/.cache/pre-commit/repoi6ij0tyu/py_env-python3/bin/python', '/home/asottile/.cache/pre-commit/repoi6ij0tyu/py_env-python3/bin/pip', 'install', '.', 'flake8-walrus')
Return code: 1
Expected return code: 0
Output: 
    Processing /home/asottile/.cache/pre-commit/repoi6ij0tyu
    Collecting flake8-walrus
    
Errors: 
      ERROR: Could not find a version that satisfies the requirement flake8-walrus (from versions: none)
    ERROR: No matching distribution found for flake8-walrus
    WARNING: You are using pip version 19.2.3, however version 19.3.1 is available.
    You should consider upgrading via the 'pip install --upgrade pip' command.
    
Check the log at /home/asottile/.cache/pre-commit/pre-commit.log
```

this ~admittedly is a bit garbled for a number of reasons:
- pip's error message here isn't great (it _could_ say something about `python_requires` or that there are versions available for other versions) **(the actual error is that the python is python3.6 and the plugin requires python3.8)**
- pip is out of date (when is it not? but admittedly who cares) -- **this is what a lot of people try and fix** -- unfortunately there's not really anything to fix here, the version of `pip` is from inside the virtualenv and doesn't really matter all that much
- `pre-commit` is currently splitting the output from stdout and stderr making it harder to read what's going on

I can't really fix the first one, and the second one I could silence but it doesn't quite feel like the right thing to do (and admittedly knowing the pip version is sometimes useful when debugging).  The third however I can pretty easily fix!
diff --git a/tests/languages/docker_test.py b/tests/languages/docker_test.py
index 42616cd..4ea7679 100644
--- a/tests/languages/docker_test.py
+++ b/tests/languages/docker_test.py
@@ -10,7 +10,7 @@ from pre_commit.util import CalledProcessError
 def test_docker_is_running_process_error():
     with mock.patch(
         'pre_commit.languages.docker.cmd_output_b',
-        side_effect=CalledProcessError(*(None,) * 4),
+        side_effect=CalledProcessError(None, None, None, None, None),
     ):
         assert docker.docker_is_running() is False
 
diff --git a/tests/store_test.py b/tests/store_test.py
index 1833dee..c71c350 100644
--- a/tests/store_test.py
+++ b/tests/store_test.py
@@ -125,7 +125,7 @@ def test_clone_shallow_failure_fallback_to_complete(
 
     # Force shallow clone failure
     def fake_shallow_clone(self, *args, **kwargs):
-        raise CalledProcessError(None, None, None)
+        raise CalledProcessError(None, None, None, None, None)
     store._shallow_clone = fake_shallow_clone
 
     ret = store.clone(path, rev)
diff --git a/tests/util_test.py b/tests/util_test.py
index dd1ad37..647fd18 100644
--- a/tests/util_test.py
+++ b/tests/util_test.py
@@ -9,6 +9,7 @@ import pytest
 from pre_commit.util import CalledProcessError
 from pre_commit.util import clean_path_on_failure
 from pre_commit.util import cmd_output
+from pre_commit.util import cmd_output_b
 from pre_commit.util import cmd_output_p
 from pre_commit.util import parse_version
 from pre_commit.util import rmtree
@@ -16,30 +17,26 @@ from pre_commit.util import tmpdir
 
 
 def test_CalledProcessError_str():
-    error = CalledProcessError(
-        1, [str('git'), str('status')], 0, (str('stdout'), str('stderr')),
-    )
+    error = CalledProcessError(1, [str('exe')], 0, b'output', b'errors')
     assert str(error) == (
-        "Command: ['git', 'status']\n"
-        'Return code: 1\n'
-        'Expected return code: 0\n'
-        'Output: \n'
-        '    stdout\n'
-        'Errors: \n'
-        '    stderr'
+        "command: ['exe']\n"
+        'return code: 1\n'
+        'expected return code: 0\n'
+        'stdout:\n'
+        '    output\n'
+        'stderr:\n'
+        '    errors'
     )
 
 
 def test_CalledProcessError_str_nooutput():
-    error = CalledProcessError(
-        1, [str('git'), str('status')], 0, (str(''), str('')),
-    )
+    error = CalledProcessError(1, [str('exe')], 0, b'', b'')
     assert str(error) == (
-        "Command: ['git', 'status']\n"
-        'Return code: 1\n'
-        'Expected return code: 0\n'
-        'Output: (none)\n'
-        'Errors: (none)'
+        "command: ['exe']\n"
+        'return code: 1\n'
+        'expected return code: 0\n'
+        'stdout: (none)\n'
+        'stderr: (none)'
     )
 
 
@@ -90,8 +87,9 @@ def test_cmd_output_exe_not_found():
     assert out == 'Executable `dne` not found'
 
 
-def test_cmd_output_p_exe_not_found():
-    ret, out, _ = cmd_output_p('dne', retcode=None, stderr=subprocess.STDOUT)
+@pytest.mark.parametrize('fn', (cmd_output_b, cmd_output_p))
+def test_cmd_output_exe_not_found_bytes(fn):
+    ret, out, _ = fn('dne', retcode=None, stderr=subprocess.STDOUT)
     assert ret == 1
     assert out == b'Executable `dne` not found'
 

pip install -e .[dev]
parse_log_pytest
requirements.txt
pytest
apt-get update apt-get install -y gcc
3.9
requirements-dev.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
aspy.yaml==1.3.0
cfgv==3.4.0
coverage==7.8.0
distlib==0.3.9
exceptiongroup==1.2.2
filelock==3.18.0
identify==2.6.9
iniconfig==2.1.0
mock==5.2.0
nodeenv==1.9.1
packaging==24.2
platformdirs==4.3.7
pluggy==1.5.0
-e git+https://github.com/pre-commit/pre-commit.git@83f08025789c31de9168a69bd3b6f465305ef296#egg=pre_commit
pytest==8.3.5
pytest-env==1.1.5
PyYAML==6.0.2
six==1.17.0
toml==0.10.2
tomli==2.2.1
virtualenv==20.29.3

name: pre-commit
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - pip=25.0=py39h06a4308_0
  - python=3.9.21=he870216_1
  - readline=8.2=h5eee18b_0
  - setuptools=75.8.0=py39h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tzdata=2025a=h04d1e81_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - aspy-yaml==1.3.0
      - cfgv==3.4.0
      - coverage==7.8.0
      - distlib==0.3.9
      - exceptiongroup==1.2.2
      - filelock==3.18.0
      - identify==2.6.9
      - iniconfig==2.1.0
      - mock==5.2.0
      - nodeenv==1.9.1
      - packaging==24.2
      - platformdirs==4.3.7
      - pluggy==1.5.0
      - pytest==8.3.5
      - pytest-env==1.1.5
      - pyyaml==6.0.2
      - six==1.17.0
      - toml==0.10.2
      - tomli==2.2.1
      - virtualenv==20.29.3
prefix: /opt/conda/envs/pre-commit

tests/languages/docker_test.py::test_docker_is_running_process_error tests/store_test.py::test_clone_shallow_failure_fallback_to_complete tests/util_test.py::test_CalledProcessError_str tests/util_test.py::test_CalledProcessError_str_nooutput
tests/languages/docker_test.py::test_docker_fallback_user tests/store_test.py::test_our_session_fixture_works tests/store_test.py::test_get_default_directory_defaults_to_home tests/store_test.py::test_adheres_to_xdg_specification tests/store_test.py::test_uses_environment_variable_when_present tests/store_test.py::test_store_init tests/store_test.py::test_clone tests/store_test.py::test_clone_cleans_up_on_checkout_failure tests/store_test.py::test_clone_when_repo_already_exists tests/store_test.py::test_clone_tag_not_on_mainline tests/store_test.py::test_create_when_directory_exists_but_not_db tests/store_test.py::test_create_when_store_already_exists tests/store_test.py::test_db_repo_name tests/store_test.py::test_local_resources_reflects_reality tests/store_test.py::test_mark_config_as_used tests/store_test.py::test_mark_config_as_used_idempotent tests/store_test.py::test_mark_config_as_used_does_not_exist tests/store_test.py::test_select_all_configs_roll_forward tests/store_test.py::test_mark_config_as_used_roll_forward tests/util_test.py::test_clean_on_failure_noop tests/util_test.py::test_clean_path_on_failure_does_nothing_when_not_raising tests/util_test.py::test_clean_path_on_failure_cleans_for_normal_exception tests/util_test.py::test_clean_path_on_failure_cleans_for_system_exit tests/util_test.py::test_tmpdir tests/util_test.py::test_cmd_output_exe_not_found tests/util_test.py::test_cmd_output_exe_not_found_bytes[cmd_output_b] tests/util_test.py::test_cmd_output_exe_not_found_bytes[cmd_output_p] tests/util_test.py::test_parse_version tests/util_test.py::test_rmtree_read_only_directories
pre_commit/util.py
pre_commit/util.py