import sys
stdin_input = sys.stdin.read()
print(f"Info loaded from stdin: {stdin_input}")
user_input = input("User input goes here: ")
Error received:
C:>echo "hello" | python winput.py
Info loaded from stdin: "hello"
User input goes here: Traceback (most recent call last):
File "C:winput.py", line 6, in <module>
user_input = input("User input goes here: ")
EOFError: EOF when reading a line
I’ve recently learned this is because sys.stdin
is being used for FIFO, which leaves it closed after reading.
I can make it work on CentOS by adding sys.stdin = open("/dev/tty")
after stdin_input = sys.stdin.read()
based on this question, but this doesn’t work for Windows.
Preferably rather than identifying the OS and assigning a new value to sys.stdin
accordingly, I’d rather approach it dynamically. Is there a way to identify what the equivalent of /dev/tty
would be in every case, without necessarily having to know /dev/tty
or the equivalent is for the specific OS?
Edit:
The reason for the sys.stdin.read()
is to take in JSON input piped from another application. I also have an option to read the JSON data from a file, but being able to used the piped data is very convenient. Once the data is received, I’d like to get user input separately.
I’m currently working around my problem with the following:
if os.name == "posix":
sys.stdin = open("/dev/tty")
elif os.name == "nt":
sys.stdin = open("con")
else:
raise RunTimeError(
f"Error trying to assign to sys.stdin due to unknown os {os.name}"
)
This may very well work in all cases but it would still be preferable to know what /dev/tty
or con
or whatever the equivalent is for the OS is dynamically. If it’s not possible and my workaround is the best solution, I’m okay with that.
2
Answers
So your real issue is that
sys.stdin
can be only one of two things:It doesn’t matter that you consumed all of
sys.stdin
by doingsys.stdin.read()
, whensys.stdin
was redirected to some file-system object, you lost the ability to read from the terminal viasys.stdin
.In practice, I’d strongly suggest not trying to do this. Use
argparse
and accept whatever you were considering accepting viainput
from the command line and avoid the whole problem (in practice, I basically never see real production code that’s not a REPL of some sort dynamically interacting with the user viastdin
/stdout
interactions; for non-REPL cases,sys.stdin
is basically always either unused or piped from a file/program, because writing clean user-interaction code like this is a pain, and it’s a pain for the user to have to type their responses without making mistakes). The input that might come for a file orstdin
can be handled by passingtype=argparse.FileType()
to theadd_argument
call in question, and the user can then opt to pass either a file name or-
(where-
means "Read from stdin"), leaving your code looking like:The user can then do:
or:
or (on shells with process substitution, like
bash
, as an alternative to the first use case):and it works either way.
If you must do this, your existing solution is really the only reasonable solution, but you can make it a little cleaner factoring it out and only making the path dynamic, not the whole code path:
This will probably die with an
OSError
subclass on theopen
call if run without a connected terminal, e.g. when launched withpythonw
on Windows (another reason not to use this design), or launched in non-terminal ways on UNIX-likes, but that’s better than silently misbehaving.You’d use it with just:
and it would restore the original
sys.stdin
automatically when thewith
block is exited.Since you’re using Bash, you can avoid this problem by using process substitution, which is like a pipe, but delivered via a temporary filename argument instead of via stdin.
That would look like:
Then in your Python script, receive the argument and handle it accordingly:
(
sys.argv
is just used for demo. In a real script I’d useargparse
.)Example run:
The other massive advantage of this is that it works seamlessly with actual filenames, for example: