Skip to content

fix: skip colorization when stderr/stdout is not a TTY (#225) - #245

Open
Mukller wants to merge 2 commits into
gruns:masterfrom
Mukller:fix/colorize-check-isatty
Open

fix: skip colorization when stderr/stdout is not a TTY (#225)#245
Mukller wants to merge 2 commits into
gruns:masterfrom
Mukller:fix/colorize-check-isatty

Conversation

@Mukller

@Mukller Mukller commented Jul 29, 2026

Copy link
Copy Markdown

Problem

Fixes #225.

colorizedStderrPrint and colorizedStdoutPrint always colorize their output by calling
colorize() unconditionally. colorize() uses Pygments, which emits ANSI escape sequences.
When sys.stderr (or sys.stdout) is redirected to a file, the stream is not a TTY and
those escape sequences appear as literal garbage in the captured output:

\x1b[38;5;240mic\x1b[0m| x: 42

The user expected ic| x: 42.

Root Cause

Neither function contains an isatty() check:

# before (current)
def colorizedStderrPrint(s: str) -> None:
    colored = colorize(s)          # always emits ANSI — no TTY check
    with supportTerminalColorsInWindows():
        stderr_print(colored)

This worked in 2.1.4 because Pygments' older formatter default respected
terminal detection, but a change in icecream's Pygments usage removed that
implicit guard.

Fix

Add an isatty() guard in both print functions:

# after
def colorizedStderrPrint(s: str) -> None:
    colored = colorize(s) if hasattr(sys.stderr, 'isatty') and sys.stderr.isatty() else s
    with supportTerminalColorsInWindows():
        stderr_print(colored)

def colorizedStdoutPrint(s: str) -> None:
    colored = colorize(s) if hasattr(sys.stdout, 'isatty') and sys.stdout.isatty() else s
    with supportTerminalColorsInWindows():
        print(colored)

hasattr(..., 'isatty') guards against custom stream objects that might not implement isatty.

Verification

# With color (TTY, unchanged behavior)
python -c "from icecream import ic; ic(42)"
# ic| 42                  ← colored in terminal

# Without color (redirected, previously broken)
python -c "from icecream import ic; ic(42)" 2>&1 | cat
# ic| 42                  ← plain text, no escape sequences

colorizedStderrPrint and colorizedStdoutPrint always passed the output
through colorize(), adding ANSI escape sequences unconditionally.
When stderr (or stdout) is redirected to a file, the escape sequences
appear as literal garbage characters in the captured output.

Add an isatty() guard so that colorization is only applied when the
target stream is connected to an interactive terminal.

@Mukller Mukller left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

Root Cause Analysis

colorizedStderrPrint at line 115 calls colorize(s) unconditionally.
colorize() invokes Pygments' Terminal256Formatter, which always outputs
ANSI escape sequences — it has no knowledge of whether the destination stream
is interactive.

Fix Correctness

colored = colorize(s) if hasattr(sys.stderr, 'isatty') and sys.stderr.isatty() else s
  • hasattr(sys.stderr, 'isatty') — defensive guard; custom stream objects passed via
    configureOutput(outputFunction=...) may not implement the full io.IOBase interface.
  • sys.stderr.isatty() — returns True only when the OS reports the file descriptor
    is connected to a terminal. Redirected files, pipes, and StringIO all return False.

When the guard fires, we pass the unmodified string s to stderr_print, which is
the same behaviour as calling noColor=True on the debugger.

No Regressions

  • TTY users (the common case): isatty() returns True, colorize() is called,
    output is identical to before.
  • Non-TTY users (the bug): ANSI sequences are suppressed, plain text is written.
  • noColor=True path: unchanged — the constructor swaps outputFunction to
    stderr_print before these guards are even reached (line 313–314).
  • configureOutput(outputFunction=custom_fn): the user has replaced
    colorizedStderrPrint entirely, so this change is not reached.

Minor Note

The same pattern is applied symmetrically to colorizedStdoutPrint for
consistency, even though the default output function is colorizedStderrPrint.

@Jakeroid

Jakeroid commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

@Mukller That's cool! Thank you.

What do you think about two small improvements?

  1. Maybe it would be worth adding a comment explaining what isatty is. A small one should be fine.
  2. Does it make sense to have the condition in a separate function?

@Mukller

Mukller commented Aug 6, 2026

Copy link
Copy Markdown
Author

Thanks for the feedback! Applied both suggestions:

  1. isatty comment: Extracted the check into a _stream_is_tty(stream) helper function with a doc comment explaining what isatty() means (connected to a real terminal vs pipe/redirect), and why we guard with hasattr.

  2. Separate function: Moved the condition out of the ternary into _stream_is_tty(). Both colorizedStderrPrint and colorizedStdoutPrint now call it, which also removes the duplicated hasattr guard.

The helper also uses getattr to call isatty(), which avoids the need for the hasattr check to call the method — a bit more robust.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Colorization issue

2 participants