On a wide (ncursesw) build, window.inch() and window.mvinch() return the low 8 bits of a character's code point instead of its locale-encoded byte, so they disagree with instr() for the same cell. It is invisible for ASCII/Latin-1 (where the byte equals the low byte of the code point) and only shows for other single-byte characters.
Under an ISO-8859-15 locale (€ U+20AC encodes to 0xA4):
importcurses, localelocale.setlocale(locale.LC_ALL, '') # run under e.g. LANG=en_US.ISO-8859-15defmain(stdscr):
stdscr.addstr(0, 0, '€')
print(hex(stdscr.instr(0, 0, 1)[0])) # 0xa4 — correctprint(hex(stdscr.inch(0, 0) &curses.A_CHARTEXT)) # 0xac — wrongcurses.wrapper(main)
Cause: ncurses' winch() returns the raw cell character with no locale conversion, unlike instr() and getbkgd(). getbkgd() is not affected. Reported upstream (bug-ncurses), where the 8-bit behavior is considered documented, so this should be worked around on the CPython side by re-encoding the cell to its locale byte when the character has a single-byte form.
The previous fix was based on the ncurses documentation, which explicitly says that inch() returns only the low-order eight bits of the character code from the cell. In fact it returns the whole code point, ORed with the color pair and attributes. It could "work", even for non-encodable characters and even in a UTF-8 locale, but only when there are no attributes and the color pair is 0. Otherwise they pollute each other, and you can reconstruct neither the character nor its color and attributes. The previous fix kept the polluted high bits. The new fix rebuilds the result entirely from win_wch(). It returns 0 as the character for non-encodable characters.
This can break code that interpreted the result of inch() as the code point. But it is the lesser evil — better than keeping the old, broken behavior — because there is another way to read characters (instr()), while there is no other way to read the color and attributes in older Python versions.
In 3.16, the new in_wch() method returns the whole character together with its attributes and color pair, so it gives a lossless way to read a cell there. On older versions there is no in_wch(), and inch() is the only reader of a cell's attributes and color — which is exactly why fixing it correctly matters.
Activity
gh-153862: Fix curses window.inch() for non-ASCII characters on a wid…
[3.15] gh-153862: Fix curses window.inch() for non-ASCII characters o…
[3.13] gh-153862: Fix curses window.inch() for non-ASCII characters o…
[3.14] gh-153862: Fix curses window.inch() for non-ASCII characters o…
pythongh-153862: Rebuild curses window.inch() from the wide cell
serhiy-storchaka commented on Jul 26, 2026
It turned out to be worse than I anticipated.
The previous fix was based on the ncurses documentation, which explicitly says that inch() returns only the low-order eight bits of the character code from the cell. In fact it returns the whole code point, ORed with the color pair and attributes. It could "work", even for non-encodable characters and even in a UTF-8 locale, but only when there are no attributes and the color pair is 0. Otherwise they pollute each other, and you can reconstruct neither the character nor its color and attributes. The previous fix kept the polluted high bits. The new fix rebuilds the result entirely from win_wch(). It returns 0 as the character for non-encodable characters.
This can break code that interpreted the result of inch() as the code point. But it is the lesser evil — better than keeping the old, broken behavior — because there is another way to read characters (instr()), while there is no other way to read the color and attributes in older Python versions.
In 3.16, the new in_wch() method returns the whole character together with its attributes and color pair, so it gives a lossless way to read a cell there. On older versions there is no in_wch(), and inch() is the only reader of a cell's attributes and color — which is exactly why fixing it correctly matters.
gh-153862: Fix spurious color pair in curses window.inch() on a wide …
[3.15] gh-153862: Fix spurious color pair in curses window.inch() on …
[3.13] gh-153862: Fix spurious color pair in curses window.inch() on …
[3.14] gh-153862: Fix spurious color pair in curses window.inch() on …
pythongh-153862: Fix spurious color pair in curses window.inch() on a…