On the stable 8.4.x line, prompt() / confirm() with err=True still write part of the prompt interaction to stdout on Windows, because _readline_prompt's WIN branch bypasses the stderr redirect that the POSIX branch applies:
# src/click/termui.py (8.4.2)
def _readline_prompt(func, text, err):
if WIN:
echo(text[:-1], nl=False, err=err) # prompt body honors err ...
return func(text[-1:]) # ... but this call is NOT redirected
if err:
with redirect_stdout(sys.stderr): # POSIX: fully redirected
return func(text)
return func(text)
Consequences on Windows only:
- Real console:
input(text[-1:]) writes the prompt's last character (the trailing space of "... [y/N]: ") to stdout. For CLIs that promise "stdout is a single machine-readable document" under a --json flag (prompt chrome on stderr via err=True), that's a stray byte in the payload stream — benign for JSON parsers since it's whitespace, but the contract is broken.
CliRunner (the bigger practical hit): test isolation replaces termui.visible_prompt_func with a shim that echoes the prompt tail and the typed reply to sys.stdout. Because the WIN branch doesn't wrap the call in redirect_stdout, stdout becomes " n\n" + <payload>. Any test doing json.loads(result.stdout) on a prompt-then-JSON flow passes on POSIX and fails on Windows with Expecting value: line 1 column 2 (char 1).
Reproduction
Deterministic on a Windows runner; on POSIX you can force the branch to see the exact same behavior:
import json
import click, click.termui
from click.testing import CliRunner
@click.command()
def cmd():
if not click.confirm("Continue?", default=False, err=True):
click.echo(json.dumps({"ok": False}))
# click.termui.WIN = True # <- uncomment on POSIX to simulate
result = CliRunner().invoke(cmd, [], input="n\n")
print(repr(result.stdout))
# POSIX / main: '{"ok": false}\n'
# Windows 8.4.2: ' n\n{"ok": false}\n'
Environment: click 8.4.2, Python 3.12, observed on GitHub Actions windows-latest (and reproduced via the WIN = True simulation above on macOS/Linux).
Note on main
This is already fixed on main as a side effect of the colorama removal (74546476376b39d05186ec1085fd2faf9d86fa20) — _readline_prompt no longer has a WIN branch, so the err=True redirect applies on all platforms. So this report is effectively a request to consider backporting the redirect part to the 8.4.x stable line (the if WIN: branch could keep the separate echo for coloring but wrap the func call in redirect_stdout(sys.stderr) when err=True).
Downstream context, if useful: this surfaced as two Windows-only CI failures in memtomem (bisect + analysis in memtomem/memtomem#1640); we've since worked around it by bypassing the prompt machinery for our --json paths.
On the stable 8.4.x line,
prompt()/confirm()witherr=Truestill write part of the prompt interaction to stdout on Windows, because_readline_prompt'sWINbranch bypasses the stderr redirect that the POSIX branch applies:Consequences on Windows only:
input(text[-1:])writes the prompt's last character (the trailing space of"... [y/N]: ") to stdout. For CLIs that promise "stdout is a single machine-readable document" under a--jsonflag (prompt chrome on stderr viaerr=True), that's a stray byte in the payload stream — benign for JSON parsers since it's whitespace, but the contract is broken.CliRunner(the bigger practical hit): test isolation replacestermui.visible_prompt_funcwith a shim that echoes the prompt tail and the typed reply tosys.stdout. Because theWINbranch doesn't wrap the call inredirect_stdout, stdout becomes" n\n" + <payload>. Any test doingjson.loads(result.stdout)on a prompt-then-JSON flow passes on POSIX and fails on Windows withExpecting value: line 1 column 2 (char 1).Reproduction
Deterministic on a Windows runner; on POSIX you can force the branch to see the exact same behavior:
Environment: click 8.4.2, Python 3.12, observed on GitHub Actions
windows-latest(and reproduced via theWIN = Truesimulation above on macOS/Linux).Note on main
This is already fixed on
mainas a side effect of the colorama removal (74546476376b39d05186ec1085fd2faf9d86fa20) —_readline_promptno longer has aWINbranch, so theerr=Trueredirect applies on all platforms. So this report is effectively a request to consider backporting the redirect part to the 8.4.x stable line (theif WIN:branch could keep the separateechofor coloring but wrap thefunccall inredirect_stdout(sys.stderr)whenerr=True).Downstream context, if useful: this surfaced as two Windows-only CI failures in memtomem (bisect + analysis in memtomem/memtomem#1640); we've since worked around it by bypassing the prompt machinery for our
--jsonpaths.