dougthor42: Whoops, wasn't working off the latest master. Fixed and rebased.
zsol: FWIW I like this feature. How about dropping the command line flag and always outputting colored diff if we're writing to a terminal (`sys.stdout.isatty()`)?

As for Windows support, I'd be fine with adding `colorama` as an extra (`extras_require` in `setup.py`)
dougthor42: > dropping the command line flag

I think it would be prudent to keep *some* CLI flag because I'm sure there are cases where a user wants to write to a terminal but not colorize it. Perhaps we just invert it: `--no-color`.

> if we're writing to a terminal (`sys.stdout.isatty()`)

From what I can tell, whenever `WriteBack.DIFF` is used, black is already writing to `io.TextIOWrapper(sys.stdout.buffer, ...)`. Do I even need to check `isatty()`?
diff --git a/CHANGES.md b/CHANGES.md
index b94d806..ebd1dd1 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -3,6 +3,7 @@
 ### Unreleased
 
 - reindent docstrings when reindenting code around it (#1053)
+- show colored diffs (#1266)
 
 ### 19.10b0
 
diff --git a/black.py b/black.py
index 087683a..2a913fc 100644
--- a/black.py
+++ b/black.py
@@ -39,6 +39,7 @@
     TypeVar,
     Union,
     cast,
+    TYPE_CHECKING,
 )
 from typing_extensions import Final
 from mypy_extensions import mypyc_attr
@@ -59,6 +60,9 @@
 
 from _black_version import version as __version__
 
+if TYPE_CHECKING:
+    import colorama  # noqa: F401
+
 DEFAULT_LINE_LENGTH = 88
 DEFAULT_EXCLUDES = r"/(\.eggs|\.git|\.hg|\.mypy_cache|\.nox|\.tox|\.venv|\.svn|_build|buck-out|build|dist)/"  # noqa: B950
 DEFAULT_INCLUDES = r"\.pyi?$"
@@ -140,12 +144,18 @@ class WriteBack(Enum):
     YES = 1
     DIFF = 2
     CHECK = 3
+    COLOR_DIFF = 4
 
     @classmethod
-    def from_configuration(cls, *, check: bool, diff: bool) -> "WriteBack":
+    def from_configuration(
+        cls, *, check: bool, diff: bool, color: bool = False
+    ) -> "WriteBack":
         if check and not diff:
             return cls.CHECK
 
+        if diff and color:
+            return cls.COLOR_DIFF
+
         return cls.DIFF if diff else cls.YES
 
 
@@ -380,6 +390,11 @@ def target_version_option_callback(
     is_flag=True,
     help="Don't write the files back, just output a diff for each file on stdout.",
 )
+@click.option(
+    "--color/--no-color",
+    is_flag=True,
+    help="Show colored diff. Only applies when `--diff` is given.",
+)
 @click.option(
     "--fast/--safe",
     is_flag=True,
@@ -458,6 +473,7 @@ def main(
     target_version: List[TargetVersion],
     check: bool,
     diff: bool,
+    color: bool,
     fast: bool,
     pyi: bool,
     py36: bool,
@@ -470,7 +486,7 @@ def main(
     config: Optional[str],
 ) -> None:
     """The uncompromising code formatter."""
-    write_back = WriteBack.from_configuration(check=check, diff=diff)
+    write_back = WriteBack.from_configuration(check=check, diff=diff, color=color)
     if target_version:
         if py36:
             err("Cannot use both --target-version and --py36")
@@ -718,12 +734,15 @@ def format_file_in_place(
     if write_back == WriteBack.YES:
         with open(src, "w", encoding=encoding, newline=newline) as f:
             f.write(dst_contents)
-    elif write_back == WriteBack.DIFF:
+    elif write_back in (WriteBack.DIFF, WriteBack.COLOR_DIFF):
         now = datetime.utcnow()
         src_name = f"{src}\t{then} +0000"
         dst_name = f"{src}\t{now} +0000"
         diff_contents = diff(src_contents, dst_contents, src_name, dst_name)
 
+        if write_back == write_back.COLOR_DIFF:
+            diff_contents = color_diff(diff_contents)
+
         with lock or nullcontext():
             f = io.TextIOWrapper(
                 sys.stdout.buffer,
@@ -731,12 +750,57 @@ def format_file_in_place(
                 newline=newline,
                 write_through=True,
             )
+            f = wrap_stream_for_windows(f)
             f.write(diff_contents)
             f.detach()
 
     return True
 
 
+def color_diff(contents: str) -> str:
+    """Inject the ANSI color codes to the diff."""
+    lines = contents.split("\n")
+    for i, line in enumerate(lines):
+        if line.startswith("+++") or line.startswith("---"):
+            line = "\033[1;37m" + line + "\033[0m"  # bold white, reset
+        if line.startswith("@@"):
+            line = "\033[36m" + line + "\033[0m"  # cyan, reset
+        if line.startswith("+"):
+            line = "\033[32m" + line + "\033[0m"  # green, reset
+        elif line.startswith("-"):
+            line = "\033[31m" + line + "\033[0m"  # red, reset
+        lines[i] = line
+    return "\n".join(lines)
+
+
+def wrap_stream_for_windows(
+    f: io.TextIOWrapper,
+) -> Union[io.TextIOWrapper, "colorama.AnsiToWin32.AnsiToWin32"]:
+    """
+    Wrap the stream in colorama's wrap_stream so colors are shown on Windows.
+
+    If `colorama` is not found, then no change is made. If `colorama` does
+    exist, then it handles the logic to determine whether or not to change
+    things.
+    """
+    try:
+        from colorama import initialise
+
+        # We set `strip=False` so that we can don't have to modify
+        # test_express_diff_with_color.
+        f = initialise.wrap_stream(
+            f, convert=None, strip=False, autoreset=False, wrap=True
+        )
+
+        # wrap_stream returns a `colorama.AnsiToWin32.AnsiToWin32` object
+        # which does not have a `detach()` method. So we fake one.
+        f.detach = lambda *args, **kwargs: None  # type: ignore
+    except ImportError:
+        pass
+
+    return f
+
+
 def format_stdin_to_stdout(
     fast: bool, *, write_back: WriteBack = WriteBack.NO, mode: Mode
 ) -> bool:
@@ -762,11 +826,15 @@ def format_stdin_to_stdout(
         )
         if write_back == WriteBack.YES:
             f.write(dst)
-        elif write_back == WriteBack.DIFF:
+        elif write_back in (WriteBack.DIFF, WriteBack.COLOR_DIFF):
             now = datetime.utcnow()
             src_name = f"STDIN\t{then} +0000"
             dst_name = f"STDOUT\t{now} +0000"
-            f.write(diff(src, dst, src_name, dst_name))
+            d = diff(src, dst, src_name, dst_name)
+            if write_back == WriteBack.COLOR_DIFF:
+                d = color_diff(d)
+                f = wrap_stream_for_windows(f)
+            f.write(d)
         f.detach()
 
 
diff --git a/setup.py b/setup.py
index 6192131..c9db684 100644
--- a/setup.py
+++ b/setup.py
@@ -77,7 +77,11 @@ def get_long_description() -> str:
         "typing_extensions>=3.7.4",
         "mypy_extensions>=0.4.3",
     ],
-    extras_require={"d": ["aiohttp>=3.3.2", "aiohttp-cors"]},
+    extras_require={
+        "d": ["aiohttp>=3.3.2", "aiohttp-cors"],
+        "colorama": ["colorama>=0.4.3"],
+    },
+    test_suite="tests.test_black",
     classifiers=[
         "Development Status :: 4 - Beta",
         "Environment :: Console",

Coloured `--diff` output
There is an option in [diffutils](https://www.gnu.org/software/diffutils/manual/html_node/diff-Options.html):

> **--color [=_when_]**
> 
> Specify whether to use color for distinguishing different contexts, like header, added or removed lines. _when_ may be omitted, or one of:
>  - `none` Do not use color at all. This is the default when no `--color` option is specified.
>  - `auto` Use color only if standard output is a terminal.
>  - `always` Always use color. 
> 
> Specifying `--color` and no _when_ is equivalent to `--color=auto`.

It would be nice if Black had a similar option, probably with a different name.
diff --git a/tests/test_black.py b/tests/test_black.py
index fdd1994..11bba1f 100644
--- a/tests/test_black.py
+++ b/tests/test_black.py
@@ -264,6 +264,28 @@ def test_piping_diff(self) -> None:
         actual = actual.rstrip() + "\n"  # the diff output has a trailing space
         self.assertEqual(expected, actual)
 
+    def test_piping_diff_with_color(self) -> None:
+        source, _ = read_data("expression.py")
+        config = THIS_DIR / "data" / "empty_pyproject.toml"
+        args = [
+            "-",
+            "--fast",
+            f"--line-length={black.DEFAULT_LINE_LENGTH}",
+            "--diff",
+            "--color",
+            f"--config={config}",
+        ]
+        result = BlackRunner().invoke(
+            black.main, args, input=BytesIO(source.encode("utf8"))
+        )
+        actual = result.output
+        # Again, the contents are checked in a different test, so only look for colors.
+        self.assertIn("\033[1;37m", actual)
+        self.assertIn("\033[36m", actual)
+        self.assertIn("\033[32m", actual)
+        self.assertIn("\033[31m", actual)
+        self.assertIn("\033[0m", actual)
+
     @patch("black.dump_to_file", dump_to_stderr)
     def test_function(self) -> None:
         source, expected = read_data("function")
@@ -352,6 +374,25 @@ def test_expression_diff(self) -> None:
             )
             self.assertEqual(expected, actual, msg)
 
+    def test_expression_diff_with_color(self) -> None:
+        source, _ = read_data("expression.py")
+        expected, _ = read_data("expression.diff")
+        tmp_file = Path(black.dump_to_file(source))
+        try:
+            result = BlackRunner().invoke(
+                black.main, ["--diff", "--color", str(tmp_file)]
+            )
+        finally:
+            os.unlink(tmp_file)
+        actual = result.output
+        # We check the contents of the diff in `test_expression_diff`. All
+        # we need to check here is that color codes exist in the result.
+        self.assertIn("\033[1;37m", actual)
+        self.assertIn("\033[36m", actual)
+        self.assertIn("\033[32m", actual)
+        self.assertIn("\033[31m", actual)
+        self.assertIn("\033[0m", actual)
+
     @patch("black.dump_to_file", dump_to_stderr)
     def test_fstring(self) -> None:
         source, expected = read_data("fstring")

pip install -e .[d]
parse_log_pytest
requirements.txt
pytest pytest-cov pytest-xdist pytest-mock pytest-asyncio
apt-get update apt-get install -y gcc
3.9
requirements.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
aiohappyeyeballs==2.6.1
aiohttp==3.11.14
aiohttp-cors==0.8.0
aiosignal==1.3.2
appdirs==1.4.4
async-timeout==5.0.1
attrs==25.3.0
-e git+https://github.com/psf/black.git@1382eabb3f27d7c9cd5328fb7fddd1ded98121fb#egg=black
click==8.1.8
coverage==7.8.0
exceptiongroup==1.2.2
execnet==2.1.1
frozenlist==1.5.0
idna==3.10
iniconfig==2.1.0
multidict==6.2.0
mypy-extensions==1.0.0
packaging==24.2
pathspec==0.12.1
pluggy==1.5.0
propcache==0.3.1
pytest==8.3.5
pytest-asyncio==0.26.0
pytest-cov==6.0.0
pytest-mock==3.14.0
pytest-xdist==3.6.1
regex==2024.11.6
toml==0.10.2
tomli==2.2.1
typed-ast==1.5.5
typing_extensions==4.13.0
yarl==1.18.3

name: black
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:
      - aiohappyeyeballs==2.6.1
      - aiohttp==3.11.14
      - aiohttp-cors==0.8.0
      - aiosignal==1.3.2
      - appdirs==1.4.4
      - async-timeout==5.0.1
      - attrs==25.3.0
      - black==19.10b1.dev64+g1382eab
      - click==8.1.8
      - coverage==7.8.0
      - exceptiongroup==1.2.2
      - execnet==2.1.1
      - frozenlist==1.5.0
      - idna==3.10
      - iniconfig==2.1.0
      - multidict==6.2.0
      - mypy-extensions==1.0.0
      - packaging==24.2
      - pathspec==0.12.1
      - pluggy==1.5.0
      - propcache==0.3.1
      - pytest==8.3.5
      - pytest-asyncio==0.26.0
      - pytest-cov==6.0.0
      - pytest-mock==3.14.0
      - pytest-xdist==3.6.1
      - regex==2024.11.6
      - toml==0.10.2
      - tomli==2.2.1
      - typed-ast==1.5.5
      - typing-extensions==4.13.0
      - yarl==1.18.3
prefix: /opt/conda/envs/black

tests/test_black.py::BlackTestCase::test_piping_diff_with_color
tests/test_black.py::BlackTestCase::test_expression tests/test_black.py::BlackTestCase::test_expression_diff tests/test_black.py::BlackTestCase::test_expression_diff_with_color tests/test_black.py::BlackTestCase::test_expression_ff tests/test_black.py::BlackTestCase::test_shhh_click
tests/test_black.py::BlackTestCase::test_assertFormatEqual tests/test_black.py::BlackTestCase::test_assert_equivalent_different_asts tests/test_black.py::BlackTestCase::test_async_as_identifier tests/test_black.py::BlackTestCase::test_beginning_backslash tests/test_black.py::BlackTestCase::test_black tests/test_black.py::BlackTestCase::test_blackd_main tests/test_black.py::BlackTestCase::test_bracket_match tests/test_black.py::BlackTestCase::test_broken_symlink tests/test_black.py::BlackTestCase::test_cache_broken_file tests/test_black.py::BlackTestCase::test_cache_multiple_files tests/test_black.py::BlackTestCase::test_cache_single_file_already_cached tests/test_black.py::BlackTestCase::test_cantfit tests/test_black.py::BlackTestCase::test_check_diff_use_together tests/test_black.py::BlackTestCase::test_collections tests/test_black.py::BlackTestCase::test_comment_after_escaped_newline tests/test_black.py::BlackTestCase::test_comments tests/test_black.py::BlackTestCase::test_comments2 tests/test_black.py::BlackTestCase::test_comments3 tests/test_black.py::BlackTestCase::test_comments4 tests/test_black.py::BlackTestCase::test_comments5 tests/test_black.py::BlackTestCase::test_comments6 tests/test_black.py::BlackTestCase::test_comments7 tests/test_black.py::BlackTestCase::test_composition tests/test_black.py::BlackTestCase::test_conv tests/test_black.py::BlackTestCase::test_debug_visitor tests/test_black.py::BlackTestCase::test_detect_pos_only_arguments tests/test_black.py::BlackTestCase::test_docstring tests/test_black.py::BlackTestCase::test_driver tests/test_black.py::BlackTestCase::test_empty tests/test_black.py::BlackTestCase::test_empty_exclude tests/test_black.py::BlackTestCase::test_empty_ff tests/test_black.py::BlackTestCase::test_empty_include tests/test_black.py::BlackTestCase::test_empty_lines tests/test_black.py::BlackTestCase::test_endmarker tests/test_black.py::BlackTestCase::test_failed_formatting_does_not_get_cached tests/test_black.py::BlackTestCase::test_filter_cached tests/test_black.py::BlackTestCase::test_fmtonoff tests/test_black.py::BlackTestCase::test_fmtonoff2 tests/test_black.py::BlackTestCase::test_fmtonoff3 tests/test_black.py::BlackTestCase::test_fmtonoff4 tests/test_black.py::BlackTestCase::test_format_file_contents tests/test_black.py::BlackTestCase::test_fstring tests/test_black.py::BlackTestCase::test_function tests/test_black.py::BlackTestCase::test_function2 tests/test_black.py::BlackTestCase::test_function_trailing_comma tests/test_black.py::BlackTestCase::test_get_features_used tests/test_black.py::BlackTestCase::test_get_future_imports tests/test_black.py::BlackTestCase::test_gitignore_exclude tests/test_black.py::BlackTestCase::test_grammar tests/test_black.py::BlackTestCase::test_import_spacing tests/test_black.py::BlackTestCase::test_include_exclude tests/test_black.py::BlackTestCase::test_invalid_config_return_code tests/test_black.py::BlackTestCase::test_invalid_include_exclude tests/test_black.py::BlackTestCase::test_lib2to3_parse tests/test_black.py::BlackTestCase::test_literals tests/test_black.py::BlackTestCase::test_long_strings tests/test_black.py::BlackTestCase::test_long_strings__edge_case tests/test_black.py::BlackTestCase::test_long_strings__regression tests/test_black.py::BlackTestCase::test_multi_file_force_py36 tests/test_black.py::BlackTestCase::test_multi_file_force_pyi tests/test_black.py::BlackTestCase::test_new_line_between_class_and_code tests/test_black.py::BlackTestCase::test_no_cache_when_stdin tests/test_black.py::BlackTestCase::test_no_cache_when_writeback_diff tests/test_black.py::BlackTestCase::test_no_files tests/test_black.py::BlackTestCase::test_numeric_literals tests/test_black.py::BlackTestCase::test_numeric_literals_ignoring_underscores tests/test_black.py::BlackTestCase::test_numeric_literals_py2 tests/test_black.py::BlackTestCase::test_parse tests/test_black.py::BlackTestCase::test_pep_570 tests/test_black.py::BlackTestCase::test_pep_572 tests/test_black.py::BlackTestCase::test_pep_572_version_detection tests/test_black.py::BlackTestCase::test_pgen tests/test_black.py::BlackTestCase::test_pipe_force_py36 tests/test_black.py::BlackTestCase::test_pipe_force_pyi tests/test_black.py::BlackTestCase::test_piping tests/test_black.py::BlackTestCase::test_piping_diff tests/test_black.py::BlackTestCase::test_preserves_line_endings tests/test_black.py::BlackTestCase::test_preserves_line_endings_via_stdin tests/test_black.py::BlackTestCase::test_pygram tests/test_black.py::BlackTestCase::test_python2 tests/test_black.py::BlackTestCase::test_python2_print_function tests/test_black.py::BlackTestCase::test_python2_unicode_literals tests/test_black.py::BlackTestCase::test_python37 tests/test_black.py::BlackTestCase::test_python38 tests/test_black.py::BlackTestCase::test_pytree tests/test_black.py::BlackTestCase::test_read_cache_line_lengths tests/test_black.py::BlackTestCase::test_read_cache_no_cachefile tests/test_black.py::BlackTestCase::test_remove_empty_parentheses_after_class tests/test_black.py::BlackTestCase::test_remove_parens tests/test_black.py::BlackTestCase::test_report_normal tests/test_black.py::BlackTestCase::test_report_quiet tests/test_black.py::BlackTestCase::test_report_verbose tests/test_black.py::BlackTestCase::test_root_logger_not_used_directly tests/test_black.py::BlackTestCase::test_self tests/test_black.py::BlackTestCase::test_setup tests/test_black.py::BlackTestCase::test_single_file_force_py36 tests/test_black.py::BlackTestCase::test_single_file_force_pyi tests/test_black.py::BlackTestCase::test_slices tests/test_black.py::BlackTestCase::test_string_prefixes tests/test_black.py::BlackTestCase::test_string_quotes tests/test_black.py::BlackTestCase::test_stub tests/test_black.py::BlackTestCase::test_symlink_out_of_root_directory tests/test_black.py::BlackTestCase::test_tab_comment_indentation tests/test_black.py::BlackTestCase::test_token tests/test_black.py::BlackTestCase::test_tokenize tests/test_black.py::BlackTestCase::test_tricky_unicode_symbols tests/test_black.py::BlackTestCase::test_tuple_assign tests/test_black.py::BlackTestCase::test_write_cache_creates_directory_if_needed tests/test_black.py::BlackTestCase::test_write_cache_read_cache tests/test_black.py::BlackTestCase::test_write_cache_write_fail tests/test_black.py::BlackDTestCase::test_blackd_diff tests/test_black.py::BlackDTestCase::test_blackd_invalid_line_length tests/test_black.py::BlackDTestCase::test_blackd_invalid_python_variant tests/test_black.py::BlackDTestCase::test_blackd_line_length tests/test_black.py::BlackDTestCase::test_blackd_pyi tests/test_black.py::BlackDTestCase::test_blackd_python_variant tests/test_black.py::BlackDTestCase::test_blackd_request_needs_formatting tests/test_black.py::BlackDTestCase::test_blackd_request_no_change tests/test_black.py::BlackDTestCase::test_blackd_request_syntax_error tests/test_black.py::BlackDTestCase::test_blackd_response_black_version_header tests/test_black.py::BlackDTestCase::test_blackd_supported_version tests/test_black.py::BlackDTestCase::test_blackd_unsupported_version
setup.py black.py CHANGES.md
setup.py black.py CHANGES.md