Skip to content

Black silently fails with exit code 1 when passed in too many files in the Python 3.14.2 on Linux #5083

@emperorcezar

Description

@emperorcezar

Describe the bug

When a large number of files are passed into black for formatting, it will process them and succeed, but will exit with code 1.

First noticed this in our CI when upgrading from python 3.14.2 to 3.14.3. It does not happen in 3.14.2

To Reproduce

I have the following dockerfile that can be used to reproduce the error. It generates 5000 file to pass into black. I'm not sure how many files need to be passed in, but I think it is above 2000.

You can build the container with docker build -f Dockerfile -t black-error-test . then run it with docker run -it black-error-test /bin/bash. Once in the container run black $(git ls-files '*.py'); echo "Black exit code is: $?"

# syntax=docker.io/docker/dockerfile-upstream:1.6.0-labs
FROM python:3.14.3-trixie
SHELL ["/bin/bash", "-c"]
RUN mkdir -p /usr/app
WORKDIR /usr/app

RUN <<EOS
set -eu
cat <<EOL > generate_python_files.py
"""
Generate ~5000 fake Python files for testing black's file scanning behavior.
Files are placed in ./generated_py_files/ by default.
"""

import os
import random
import string

OUTPUT_DIR = "generated_py_files"
NUM_FILES = 5000

# Templates for variety so files aren't all identical
TEMPLATES = [
    """\
def func_{name}(x, y):
    \"\"\"A simple function.\"\"\"
    return x + y


def helper_{name}(items):
    result = []
    for item in items:
        result.append(item * 2)
    return result


class Class_{name}:
    def __init__(self, value):
        self.value = value

    def compute(self):
        return self.value ** 2
""",
    """\
import os
import sys


CONSTANT_{name} = {value}


def process_{name}(data):
    if not data:
        return None
    return [d for d in data if d is not None]


def main():
    print("Running {name}")


if __name__ == "__main__":
    main()
""",
    """\
from typing import List, Optional


def transform_{name}(items: List[int], factor: int = 1) -> List[int]:
    return [i * factor for i in items]


def validate_{name}(value: Optional[str]) -> bool:
    if value is None:
        return False
    return len(value) > 0


class Manager_{name}:
    def __init__(self):
        self._data = []

    def add(self, item):
        self._data.append(item)

    def get_all(self):
        return list(self._data)
""",
    """\
# Module {name}

X_{name} = {value}
Y_{name} = {value2}


def add_{name}(a, b):
    return a + b


def subtract_{name}(a, b):
    return a - b


def multiply_{name}(a, b):
    return a * b
""",
    """\
class Base_{name}:
    pass


class Child_{name}(Base_{name}):
    def method(self):
        return "{name}"

    @staticmethod
    def static_method():
        return True

    @classmethod
    def class_method(cls):
        return cls()


def standalone_{name}():
    obj = Child_{name}()
    return obj.method()
""",
]


def random_suffix(length=6):
    return "".join(random.choices(string.ascii_lowercase, k=length))


def generate_file_content(index):
    template = random.choice(TEMPLATES)
    name = f"{index:04d}_{random_suffix()}"
    return template.format(
        name=name,
        value=random.randint(1, 9999),
        value2=random.randint(1, 9999),
    )


def main():
    os.makedirs(OUTPUT_DIR, exist_ok=True)

    # Optionally spread files across subdirectories to mimic a real project
    use_subdirs = True
    subdir_count = 50  # ~100 files per subdir

    if use_subdirs:
        subdirs = [f"pkg_{i:03d}" for i in range(subdir_count)]
        for sd in subdirs:
            path = os.path.join(OUTPUT_DIR, sd)
            os.makedirs(path, exist_ok=True)
            # Add __init__.py so black treats them as packages
            open(os.path.join(path, "__init__.py"), "w").close()

    print(f"Generating {NUM_FILES} Python files in '{OUTPUT_DIR}/'...")

    for i in range(NUM_FILES):
        content = generate_file_content(i)
        if use_subdirs:
            subdir = subdirs[i % subdir_count]
            filepath = os.path.join(OUTPUT_DIR, subdir, f"mod_{i:05d}.py")
        else:
            filepath = os.path.join(OUTPUT_DIR, f"mod_{i:05d}.py")

        with open(filepath, "w") as f:
            f.write(content)

        if (i + 1) % 500 == 0:
            print(f"  {i + 1}/{NUM_FILES} files created...")

    print(f"Done! {NUM_FILES} files written to '{OUTPUT_DIR}/'.")
    print(f"\nTo run black against them:")
    print(f"  black {OUTPUT_DIR}/")
    print(f"  black --check {OUTPUT_DIR}/")


if __name__ == "__main__":
    main()
EOL
EOS

#COPY generate_python_files.py ./generate_python_files.py
RUN pip install "black @ git+https://github.com/psf/black.git@e079b7e100d1e181d4ee860ee4512bf3326f32c3"

RUN git config --global user.email "you@example.com" && \
    git config --global user.name "Your Name" && \
    git init && \
    git add .

RUN python generate_python_files.py
RUN git add .
# Make sure everything is formatting correctly with black, ignoring the exit 1 from black when it finds unformatted files
RUN black . || true

# Remove the cache so black will have to re-scan and re-parse all the files on the next run, which is what we want for testing the file scanning behavior
RUN rm -rf /root/.cache/black

Expected behavior
Black should return exit code 0 on success

Environment

Python 3.14.3 on Debian Trixie in the official container. Seen in it CI with AMD64 and on a Mac M1 with the built container. I pulled in the latest main sha for black

  • Black's version: main@e079b7e100d1e181d4ee860ee4512bf3326f32c3
  • OS and Python version: Linux/Python 3.14.3

Additional context

Metadata

Metadata

Assignees

No one assigned

    Labels

    C: crashBlack is crashingT: bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions