Repository: python/mypy

Issue:
Piping mypy into a closed pipe results in BrokenPipeError
## Bug Report

Executing mypy and piping the result into another process like head which closes the pipe whilst mypy still wants to print something to stdout results in an unhandled BrokenPipeError.

## To Reproduce

(Write your steps here:)

1. Get a file on which mypy outputs a few errors (`echo 'a = 0\nb = "1"\na + b' > test.py`)
1. Call mypy on the file and pipe the result into a process which closes the pipe before mypy is finished (`mypy test.py | head -n 0`)

## Actual Behavior (expected are no errors)

For `head -n 0` mypy returns:

```
Exception ignored in: <_io.TextIOWrapper name='<stdout>' mode='w' encoding='utf-8'>
BrokenPipeError: [Errno 32] Broken pipe
```

For `head -n 1` mypy returns:

```
test.py:3: error: Unsupported operand types for + ("int" and "str")
Traceback (most recent call last):
  File "/home/septatrix/.local/bin/mypy", line 8, in <module>
    sys.exit(console_entry())
  File "/home/septatrix/.local/lib/python3.8/site-packages/mypy/__main__.py", line 8, in console_entry
    main(None, sys.stdout, sys.stderr)
  File "mypy/main.py", line 122, in main
BrokenPipeError: [Errno 32] Broken pipe
Exception ignored in: <_io.TextIOWrapper name='<stdout>' mode='w' encoding='utf-8'>
BrokenPipeError: [Errno 32] Broken pipe
```

Where the first line is the output from head to stdio. This can be removed by piping everything into `/dev/null` leaving only the error from mypy

## Your Environment

- Mypy version used: `mypy 0.782`
- Python version used: `Python 3.8.2`
- Head version used: `head (GNU coreutils) 8.30`

## Related

This seems to be the same as #2893. However the bug apparently got reintroduced as there are no degradation tests for this in the test suite.


Discussion:
If you want to help fixing this, maybe the place to add the try/except is in `__main__` (probably both functions there).
Alternatively I could also wrap https://github.com/python/mypy/blob/4325aae8ba2231eafd50331e2b9ee22b52a5cf61/mypy/main.py#L98-L103 and https://github.com/python/mypy/blob/4325aae8ba2231eafd50331e2b9ee22b52a5cf61/mypy/main.py#L113-L122. This way every possible entry point would be protected. However I do not know if this is even desired should someone else want to import mypy. Regardless of which way you prefer I think it should be consistent i.e. either move all try/except inside of `main.py` or move all of them into `__main__.py` (including the one which already exists at https://github.com/python/mypy/blob/4325aae8ba2231eafd50331e2b9ee22b52a5cf61/mypy/main.py#L78-L85)

Resolving patch:
diff --git a/mypy/__main__.py b/mypy/__main__.py
--- a/mypy/__main__.py
+++ b/mypy/__main__.py
@@ -1,12 +1,23 @@
 """Mypy type checker command line tool."""
 
 import sys
+import os
+
 from mypy.main import main
 
 
 def console_entry() -> None:
-    main(None, sys.stdout, sys.stderr)
+    try:
+        main(None, sys.stdout, sys.stderr)
+        sys.stdout.flush()
+        sys.stderr.flush()
+    except BrokenPipeError:
+        # Python flushes standard streams on exit; redirect remaining output
+        # to devnull to avoid another BrokenPipeError at shutdown
+        devnull = os.open(os.devnull, os.O_WRONLY)
+        os.dup2(devnull, sys.stdout.fileno())
+        sys.exit(2)
 
 
 if __name__ == '__main__':
-    main(None, sys.stdout, sys.stderr)
+    console_entry()
diff --git a/mypy/main.py b/mypy/main.py
--- a/mypy/main.py
+++ b/mypy/main.py
@@ -75,14 +75,11 @@ def flush_errors(new_messages: List[str], serious: bool) -> None:
             new_messages = formatter.fit_in_terminal(new_messages)
         messages.extend(new_messages)
         f = stderr if serious else stdout
-        try:
-            for msg in new_messages:
-                if options.color_output:
-                    msg = formatter.colorize(msg)
-                f.write(msg + '\n')
-            f.flush()
-        except BrokenPipeError:
-            sys.exit(2)
+        for msg in new_messages:
+            if options.color_output:
+                msg = formatter.colorize(msg)
+            f.write(msg + '\n')
+        f.flush()
 
     serious = False
     blockers = False


Test patch: