Cross-Platform Compatibility
Click’s compatibility layer presents consistent text, binary, and terminal behavior across Windows consoles and POSIX-style environments. It detects the environment, repairs incompatible streams, and selects Unicode-aware console adapters when needed.
It exists because operating systems disagree about argument encoding, console APIs, and the relationship between text streams and their binary buffers. The Windows console requires special handling, while POSIX environments can still expose ASCII or otherwise misconfigured streams.
Sources: src/click/_compat.py:241-284, src/click/_compat.py:340-344, src/click/_compat.py:347-351, src/click/_compat.py:212-218
Core concepts
Platform detection
Platform detection identifies Windows and Cygwin environments so compatibility behavior can be selected without changing higher-level Click code. WIN and CYGWIN derive from sys.platform.
Sources: src/click/_compat.py:13, src/click/_compat.py:14
Dispatching text stream
A dispatching text stream is a text interface that chooses between a native Windows console adapter and a repaired ordinary stream. get_text_stdin, get_text_stdout, and get_text_stderr first ask _get_windows_console_stream; if it returns nothing, they use the generic repair path.
Sources: src/click/_compat.py:340-344, src/click/_compat.py:347-351, src/click/_compat.py:354-358
Stream repair
Stream repair converts or wraps streams whose encoding, error policy, or binary interface is unsuitable. _force_correct_text_stream preserves compatible streams, finds an underlying binary stream when necessary, and otherwise creates a text wrapper with replacement errors by default.
Sources: src/click/_compat.py:241-284, src/click/_compat.py:22-40
Unicode console adapter
A Unicode console adapter translates UTF-16-LE text streams into Windows console API calls. ConsoleStream accepts both text and bytes, sending text through the emulated text stream and bytes through its binary buffer.
Sources: src/click/_winconsole.py:190-219, src/click/_winconsole.py:222-229, src/click/_winconsole.py:232-239, src/click/_winconsole.py:242-249
How the compatibility boundary works
The main boundary is get_text_stdout and its stdin/stderr counterparts. Each function first requests a platform-specific console stream, then falls back to _force_correct_text_reader or _force_correct_text_writer when no Windows console adapter is available.
The Windows-specific dispatcher is deliberately selective: it returns no adapter unless buffer access exists, the requested encoding is UTF-16-LE or unspecified, errors are strict or unspecified, and _is_console confirms that the stream is attached to a console.
The console test checks for fileno, obtains the operating-system handle, and calls GetConsoleMode; streams without a usable file descriptor or console handle are rejected. The Windows module therefore exists because this detection and I/O path depends on Windows-specific handles and APIs rather than ordinary Python stream behavior.
This diagram shows how a text-output request crosses from the portable compatibility API into either the Windows console path or generic stream repair.
Evidence
- stdout-entrysrc/click/_compat.py:347
- win-dispatchsrc/click/_winconsole.py:272
- console-detectsrc/click/_winconsole.py:259
- windows-factorysrc/click/_winconsole.py:232
- windows-writersrc/click/_winconsole.py:158
- console-streamsrc/click/_winconsole.py:190
- generic-repairsrc/click/_compat.py:303
Sources: src/click/_compat.py:340-344, src/click/_compat.py:347-351, src/click/_compat.py:354-358, src/click/_winconsole.py:272-292, src/click/_winconsole.py:259-269
How Unicode input and output avoid common failures
On POSIX systems, command-line input is traditionally bytes, while sys.argv is text in Python. Click therefore treats Unicode as the native parameter value, but misconfigured locales can introduce surrogate escapes or prevent reliable round-tripping.
On Windows, Click extracts Unicode parameters from sys.argv instead of relying on the initially wrong encoding. If sys.argv was modified before invocation, Click falls back to byte input, which exposes only the code-page subset.
For stream encoding, get_best_encoding uses the stream’s declared encoding or Python’s default encoding, but changes ASCII to UTF-8. _stream_is_misconfigured treats an absent encoding as ASCII and identifies that stream as unsafe.
| Surface | Windows behavior | Portable fallback |
|---|---|---|
| Console input | _get_text_stdin uses _WindowsConsoleReader with UTF-16-LE. | _force_correct_text_reader repairs or wraps the input stream. |
| Console output | _get_text_stdout and _get_text_stderr use _WindowsConsoleWriter with UTF-16-LE. | _force_correct_text_writer repairs or wraps the output stream. |
| Binary access | get_binary_stdin, get_binary_stdout, and get_binary_stderr discover existing binary streams or raise RuntimeError. | The discovery checks the stream itself and then its .buffer. |
The Windows reader requires an even byte count because it reads UTF-16-LE code units, and it calls ReadConsoleW; aborted reads briefly sleep before the operation is retried by the surrounding flow. The writer limits each call to MAX_BYTES_WRITTEN, passes the buffer to WriteConsoleW, and raises an OSError when bytes were requested but none were written.
Older Windows consoles also impose a 64K-character limitation for binary-mode writes, so Click replaces sys.stdout and sys.stderr with wrappers that work around the limit. The console’s default fonts may still lack emoji and other special characters even when the encoding path succeeds.
The compatibility layer does not make every stream interchangeable. _find_binary_reader and _find_binary_writer inspect the stream and its .buffer, while _FixupStream supplies missing methods such as read1, readable, writable, and seekable for badly patched stream objects.
Sources: docs/unicode-support.md:8-12, docs/unicode-support.md:41-45, docs/wincmd.md:17-23, src/click/_compat.py:51-56, src/click/_compat.py:212-218, src/click/_winconsole.py:222-229, src/click/_winconsole.py:232-239, src/click/_winconsole.py:242-249, src/click/_compat.py:287-300, src/click/_compat.py:303-316, src/click/_compat.py:319-323, src/click/_compat.py:326-330, src/click/_compat.py:333-337, src/click/_compat.py:176-191, src/click/_compat.py:194-209, src/click/_winconsole.py:123-155, src/click/_winconsole.py:158-187, docs/wincmd.md:43-49, src/click/_compat.py:85-151
How it connects
User-facing output should go through click.echo, because that path supports Windows Unicode output, binary destinations, bytes written to text outputs, ANSI stripping for non-interactive streams, and flushing. Direct print or sys.stdout.write does not receive the same Windows console treatment.
The low-level stream maps expose the compatibility boundary to other Click code: binary_streams maps standard-stream names to binary getters, while text_streams maps them to text getters. The default text getters are cached through _make_cached_stream_func, so repeated access can reuse a wrapper for the same source stream.
File handling also uses this layer. open_stream routes "-" to Click’s standard-stream getters, uses _wrap_io_open for ordinary files, and can create an _AtomicFile whose close operation replaces the destination with the temporary file.
This behavior is consumed by Echo and Output Handling, while command-line argument behavior connects to Commands and Groups and The Low-Level Parser. Testing constraints are documented in Testing with CliRunner, including the fact that file-descriptor capture is unsupported on Windows.
Sources: src/click/utils.py:247-259, src/click/utils.py:273-276, src/click/_compat.py:580-584, src/click/_compat.py:586-590, src/click/_compat.py:547-572, src/click/_compat.py:374-452, src/click/_compat.py:361-371, src/click/_compat.py:455-488, src/click/testing.py:368-375
Key takeaways
- Click separates portable stream repair from the Windows console adapter.
- Windows console text is routed through UTF-16-LE wrappers and
ReadConsoleW/WriteConsoleW. - Unicode arguments normally come from sys.argv; modified sys.argv can force a less complete byte fallback.
- ASCII or incompatible streams are repaired through binary-stream discovery and text wrappers.
- click.echo is the intended output path because it applies these compatibility behaviors.