Question: I plan to use subprocess.Popen (in Python 3.11.2) to implement git mktree < foo.txt. Now I face the question. In order to reproduce my situation, here is the script that creates the environment. #!/bin/bash export GIT_AUTHOR_NAME=foo export GIT_AUTHOR_EMAIL=foo@bar.com export GIT_AUTHOR_DATE="Tue Jun 4 21:40:15 2024 +0800" export GIT_COMMITTER_NAME=foo export GIT_COMMITTER_EMAIL=foo@bar.com export GIT_COMMITTER_DATE="Tue Jun 4 21:40:15 2024 +0800" rm -rf foo git init foo mkdir -p foo/hello echo hello > foo/hello/hello.txt echo hello > foo/hello.txt echo world > foo/world.txt git -C foo add . git -C foo commit -m 'hello world' git -C foo log --no-decorate git -C foo ls-tree HEAD hello.txt git -C foo ls-tree HEAD world.txt  It's expected to print the commit and 2 blob entries. commit d2b25fd15c1435f515dd6379eca8d691dde6abeb Author: foo <foo@bar.com> Date: Tue Jun 4 21:40:15 2024 +0800 hello world 100644 blob ce013625030ba8dba906f756967f9e9ca394464a hello.txt 100644 blob cc628ccd10742baea8241c5924df992b5c019f71 world.txt  I want to create a commit from the tree that has only hello.txt and world.txt. So first I need to create the new tree object. (Update: after getting the right solution, I find that the code has a fatal bug. It creates a tree object with wrong content due to str and bytes.) import subprocess # get the blob entry of hello.txt o, e = subprocess.Popen( ['git', 'ls-tree', 'HEAD', 'hello.txt'], stdout=subprocess.PIPE, env={'GIT_DIR': 'foo/.git'}, ).communicate() line1 = o.decode() # get the blob entry of world.txt o, e = subprocess.Popen( ['git', 'ls-tree', 'HEAD', 'world.txt'], stdout=subprocess.PIPE, env={'GIT_DIR': 'foo/.git'}, ).communicate() line2 = o.decode() # write the 2 lines to foo.txt with open('foo.txt', 'w') as f: f.write(line1) f.write(line2) # create a new tree object from foo.txt with open('foo.txt') as f: o, e = subprocess.Popen( ['git', 'mktree'], stdin=f, stdout=subprocess.PIPE, env={'GIT_DIR': 'foo/.git'}, ).communicate() tree = o.decode() print(f'created tree object {tree}')  I wonder if I can use an in-memory file object so that I don't have to create and remove foo.txt. As io.StringIO is recommended in many answers, I try the code. import subprocess import io line1 = '100644 blob ce013625030ba8dba906f756967f9e9ca394464a\thello.txt\n' line2 = '100644 blob cc628ccd10742baea8241c5924df992b5c019f71\tworld.txt\n' with io.StringIO(line1 + line2) as f: o, e = subprocess.Popen( ['git', 'mktree'], stdin=f, stdout=subprocess.PIPE, env={'GIT_DIR': 'foo/.git'}, ).communicate() tree = o.decode() print(f'created tree object {tree}')  It raises an exception. Traceback (most recent call last): File "<stdin>", line 2, in <module> File "C:\Python311\Lib\subprocess.py", line 892, in __init__ errread, errwrite) = self._get_handles(stdin, stdout, stderr) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "C:\Python311\Lib\subprocess.py", line 1339, in _get_handles p2cread = msvcrt.get_osfhandle(stdin.fileno()) ^^^^^^^^^^^^^^ io.UnsupportedOperation: fileno  According to the io doc, it seems io.StringIO does not have a file descriptor.  fileno() Return the underlying file descriptor (an integer) of the stream if it exists. An OSError is raised if the IO object does not use a file descriptor.  Is there any in-memory file object that has a file descriptor? Or is there any method to bypass the exception with io.StringIO in subprocess.Popen? Update: With the help of @Ture Pålsson 's answer, I get the expected solution. import subprocess # get the blob entry of hello.txt line1, _ = subprocess.Popen( ['git', 'ls-tree', 'HEAD', 'hello.txt'], stdout=subprocess.PIPE, env={'GIT_DIR': 'foo/.git'}, ).communicate() # get the blob entry of world.txt line2, _ = subprocess.Popen( ['git', 'ls-tree', 'HEAD', 'world.txt'], stdout=subprocess.PIPE, env={'GIT_DIR': 'foo/.git'}, ).communicate() # create a tree object from the lines # although Popen's text=True allows communicate's input to be str, # here it should be bytes. # str input creates a tree object with wrong content. o, e = subprocess.Popen( ['git', 'mktree'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, env={'GIT_DIR': 'foo/.git'}, ).communicate(input=line1+line2) tree = o.decode().strip() print(f'created tree object {tree}') 

Answer: Unless I am missing something, you can set stdin=PIPE when building the Popen object, and pass the input as the argument to communicate. Here’s what I used as a "smoke test": import subprocess with subprocess.Popen(['cat'], encoding='utf-8', stdin=subprocess.PIPE, stdout=subprocess.PIPE) as p: o, e = p.communicate('foo') print(o, e)