Add a built-in equivalent of C's `getch()`

I was making a Python program, and I had to get if the user typed a certain character, but without for the user to have to hit Enter, because my program only had to use one character. I didn’t want to use a third-party packages because for most they use either platform-specific code, that I want to avoid for compatibility, or obscure solutions, and I also had the constraint to only use the standard library (+ platform-specific library in last resort).

Finally I used platform-specific libraries to access to the C library and use the getch() function.

But, I wondered: “CPython is made mostly in C, so that’s in fact possible to add in it an equivalent of C’s getch() that, when called, act the same as getch()?”. So I propose this feature, probably add this built-in function in a built-in module like io because it’s related to inputs/outputs, or os because it’s where there’s most of built-in functions made in C.

Here’s an example of what it will look like in Python:

import io # or whatever it will be put it in

prompt = "Press any key: "
key = io.read_char(promp) # read_char is an example name

print(f"You pressed the {key} key")
11 Likes

In Windows, there’s already msvcrt.getch. In Linux/POSIX, there’s this recipe. But yeah I agree that a unified built-in function would be nice.

1 Like

Yes, these exist, but for example, on another type of OS (I don’t know which, but imagining), there will probably the getch() implemented in C with the standard C library, but there won’t be an actual real thing equivalent in Python.

But good point.

Until your imagined OS is in reality I don’t think we need to worry about it.

On second thought, we can move the getch code in getpass.unix_getpass to a standalone function in os so both getpass.unix_getpass and getpass.win_getpass, which currently calls msvcrt.getwch, can call os.getch instead.

This seems to be a good idea.

Previous discussions on various lists:
https://mail.python.org/pipermail/python-list/2022-December/908427.html
https://mail.python.org/pipermail/python-list/2019-May/891423.html
https://mail.python.org/pipermail/tutor/2020-April/116317.html
https://mail.python.org/pipermail/tutor/2020-September/117232.html
CPython issue:

This is a fairly common thing to want and I have long thought that Python should have it.

The OP in this thread should have mentioned this:

Immediate feedback to that issue was:

This feature is not a minor feature. We will need to make it platform-agnostic on our side so the workload might be non-trivial.

I think that this is precisely why it should be in the stdlib. Generally Python provides platform agnostic ways of doing common things. That makes it possible for users of Python to write portable scripts without needing to know how to make it work on different platforms.

To move this forwards though what is needed is a good implementation and a review of options and usecases. I think there are some good implementations on PyPI although a stdlib solution might want to be simpler. Someone should test them out, review the options, see whether they are suitable for the stdlib etc.

There are different potential ways to implement this like blocking vs non-blocking. I would say that for the simple Python script case blocking is the right behaviour.

I think (can’t check) that the Windows getch function returns bytes but a proper solution here should return str. There is also getwch and I’m not sure of exact differences there.

There is also the issue of e.g. arrow keys and modifiers like Ctrl etc. For a stdlib solution I would probably just declare those out of scope: the function returns a single character string.

Some options for getch work in “proper” terminals but not in e.g. IDLE or other IDEs. This kind of feature is likely to be used by beginner programmers who may want to run their code in something like IDLE so I think it is important that a stdlib solution should work in the console pane of IDEs as well.

2 Likes

I forgot to mention these issues, yes. The last one was made by me and I opened this topic in reaction to the answer to my issue.

Your point of view about this is pretty close to mine.

Correct. It does

But it also copes with, eg, arrow keys, backspace &c.

Yes, I thought that too. So to have a platform-independent experience, it will be needed to adapt the behavior on Windows.

Also Windows’ getch() handle special characters like arrow keys differently from *nix one:

Key Windows *nix
Up \xe0H \x1b[A
Dow \xe0P \x1b[B
Left \xe0K \x1b[D
Right \xe0M \x1b[C

I would say two apis:

getch() : Converts input chars into the (as much as it can) same format cross os.
and
getch_native(): Calls the native C getch implementation and passes back the result without trying to coerce.

Most of the time getch() is what folks would want, but having the native version may be nice for some cases.

1 Like

It’s a good idea. Just I think for the cross os version, a name a little bit more pythonic, but the idea itself seems good.

1 Like

I would really like to have a reliable and useful getch(), I just want to remind that it is not just a binding to a C-function. A good one is already implemented in the Python by the good old curses library. However the library handles also terminal output. It needs to be initialized before use and interleaving normal terminal I/O with curses I/O leads to problems, so it cannot be used just to get the getch.

If a small subset giving us a getch()-ish function would be implemented, there are some details to be considered:

  • interaction with line buffering, mixing input() with getch() should not lead to surprises.
  • should getch() support nodelay mode (returns immediately if no key is pressed)?
  • should raw mode be supported as well? (keys like ^C, ^S, ^Q are returned unprocessed, i.e. no interrupt, no flow control, etc.)
  • to enable processing like if ch == KEY_UP: ... all the curses.KEY_xxxx symbolic constants needs to be exported too. At least on Unix/Linux, the translation from keystrokes to function keys involves special timing when the <ESC> key is pressed and a lookup in the terminfo database (of course, the curses library does this for us)
2 Likes

I’m a fan of this, and it’s not often that I put a +1 on these kinds of ideas! Only supporting getch under msvcrt has been a bummer for me when I’ve needed it previously.

Generally, it’s too easy to say “put it on PyPI and see if it gets usage,” but this is, in fact, on PyPI under the getch package, which hasn’t needed any patches since its release over 10 years ago – I would argue that’s a good indicator that little maintenance is needed (and likewise, makes a convenient backport for those that need to support <3.14)

Regarding these points, I think we’re heading the wrong direction with trying to make this higher level. getch, in my experience, needs to be very simple – adding extra abstractions should be done by a third party.

The only extra work that Python should be doing, apart from calling the C-level function, is adding some sort of mapping to make the results from Windows and other systems equivalent (e.g., pressing up arrow returns the same thing for both systems.) Something like getch_native doesn’t seem necessary, because you could just use the existing methods for calling getch in place of it (such as msvcrt.getch, which should be native.)

3 Likes

Cool. Would the author of that package be willing to contribute it to the stdlib? That sounds like a better approach than reimplementing the code from scratch…

4 Likes

No idea. Hopefully yes, but anyone could do it – I’m not sure it’s worth trying to contact someone for a package they wrote 11 years ago. The Linux implementation looks like this on that package:

/* reads from keypress, doesn't echo */
int getch(void)
{
    struct termios oldattr, newattr;
    int ch;
    tcgetattr( STDIN_FILENO, &oldattr );
    newattr = oldattr;
    newattr.c_lflag &= ~( ICANON | ECHO );
    tcsetattr( STDIN_FILENO, TCSANOW, &newattr );
    ch = getchar();
    tcsetattr( STDIN_FILENO, TCSANOW, &oldattr );
    return ch;
}

/* reads from keypress, echoes */
int getche(void)
{
    struct termios oldattr, newattr;
    int ch;
    tcgetattr( STDIN_FILENO, &oldattr );
    newattr = oldattr;
    newattr.c_lflag &= ~( ICANON );
    tcsetattr( STDIN_FILENO, TCSANOW, &newattr );
    ch = getchar();
    tcsetattr( STDIN_FILENO, TCSANOW, &oldattr );
    return ch;
}

Granted, this needs to be modernized a little bit (namely, PEP 7 compliance, and fix some missing edge-case error checks).

If we want to use the code then someone should contact them for license/copyright reasons.

The getch package is under the public domain! :smile: