Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion Doc/library/subprocess.rst
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,7 @@ functions.
start_new_session=False, pass_fds=(), *, group=None, \
extra_groups=None, user=None, umask=-1, \
encoding=None, errors=None, text=None, pipesize=-1, \
process_group=None)
process_group=None, force_hide=False)

Execute a child program in a new process. On POSIX, the class uses
:meth:`os.execvpe`-like behavior to execute the child program. On Windows,
Expand Down Expand Up @@ -457,6 +457,10 @@ functions.
into the shell (e.g. :command:`dir` or :command:`copy`). You do not need
``shell=True`` to run a batch file or console-based executable.

On Windows, ``force_hide=True`` attempts to start the application without creating
or showing any windows. Some applications may ignore this request, and applications
that are hidden often cannot be used or exited by users.

.. note::

Read the `Security Considerations`_ section before using ``shell=True``.
Expand Down
35 changes: 30 additions & 5 deletions Lib/subprocess.py
Original file line number Diff line number Diff line change
Expand Up @@ -843,6 +843,8 @@ class Popen:

startupinfo and creationflags (Windows only)

force_hide (Windows only)

restore_signals (POSIX only)

start_new_session (POSIX only)
Expand Down Expand Up @@ -875,7 +877,8 @@ def __init__(self, args, bufsize=-1, executable=None,
restore_signals=True, start_new_session=False,
pass_fds=(), *, user=None, group=None, extra_groups=None,
encoding=None, errors=None, text=None, umask=-1, pipesize=-1,
process_group=None):
process_group=None,
force_hide=False):
"""Create new Popen instance."""
if not _can_fork_exec:
raise OSError(
Expand Down Expand Up @@ -920,6 +923,9 @@ def __init__(self, args, bufsize=-1, executable=None,
if creationflags != 0:
raise ValueError("creationflags is only supported on Windows "
"platforms")
if force_hide:
raise ValueError("force_hide is only supported on Windows "
"platforms")

self.args = args
self.stdin = None
Expand Down Expand Up @@ -1097,7 +1103,9 @@ def __init__(self, args, bufsize=-1, executable=None,
errread, errwrite,
restore_signals,
gid, gids, uid, umask,
start_new_session, process_group)
start_new_session,
process_group,
force_hide)
except:
# Cleanup if the child failed starting.
for f in filter(None, (self.stdin, self.stdout, self.stderr)):
Expand Down Expand Up @@ -1510,7 +1518,8 @@ def _execute_child(self, args, executable, preexec_fn, close_fds,
unused_restore_signals,
unused_gid, unused_gids, unused_uid,
unused_umask,
unused_start_new_session, unused_process_group):
unused_start_new_session, unused_process_group,
force_hide):
"""Execute program (MS Windows version)"""

assert not pass_fds, "pass_fds not supported on Windows."
Expand Down Expand Up @@ -1574,9 +1583,24 @@ def _execute_child(self, args, executable, preexec_fn, close_fds,
# the ones in the handle_list
close_fds = False

if shell:
if force_hide or shell:
# We pass SW_HIDE to the process so that it will not display any
# window even if it normally would.
startupinfo.dwFlags |= _winapi.STARTF_USESHOWWINDOW
startupinfo.wShowWindow = _winapi.SW_HIDE

if force_hide and not (creationflags & _winapi.DETACHED_PROCESS):
# If the child process is flagged as SUBSYSTEM_CONSOLE, then
# it either inherits the console of its parent, if the parent
# has one, or allocates a new console. An inherited console may
# be visible even if SW_HIDE is set. By setting the creation
# flag CREATE_NEW_CONSOLE, the child is forced to allocate a
# new console that will have a hidden window.
# Note: CREATE_NEW_CONSOLE cannot be used with DETACHED_PROCESS.
creationflags |= _winapi.CREATE_NEW_CONSOLE

if shell:
comspec = os.environ.get("COMSPEC", "cmd.exe")
if not executable:
# gh-101283: without a fully-qualified path, before Windows
# checks the system directories, it first looks in the
Expand Down Expand Up @@ -1884,7 +1908,8 @@ def _execute_child(self, args, executable, preexec_fn, close_fds,
errread, errwrite,
restore_signals,
gid, gids, uid, umask,
start_new_session, process_group):
start_new_session, process_group,
unused_force_hide):
"""Execute program (POSIX version)"""

if isinstance(args, (str, bytes)):
Expand Down
26 changes: 23 additions & 3 deletions Lib/test/test_subprocess.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@
raise unittest.SkipTest("test module requires subprocess")

mswindows = (sys.platform == "win32")
if mswindows:
try:
import ctypes
except ImportError:
ctypes = None

#
# Depends on the following external programs: Python
Expand Down Expand Up @@ -2509,6 +2514,10 @@ def test_invalid_args(self):
[sys.executable, "-c",
"import sys; sys.exit(47)"],
creationflags=47)
self.assertRaises(ValueError, subprocess.call,
[sys.executable, "-c",
"import sys; sys.exit(47)"],
force_hide=47)

def test_shell_sequence(self):
# Run command through the shell (sequence)
Expand Down Expand Up @@ -3693,12 +3702,23 @@ def test_startupinfo_copy(self):
self.assertEqual(startupinfo.lpAttributeList, {"handle_list": []})

def test_creationflags(self):
# creationflags argument
CREATE_NEW_CONSOLE = 16
sys.stderr.write(" a DOS box should flash briefly ...\n")
subprocess.call(sys.executable +
' -c "import time; time.sleep(0.25)"',
creationflags=CREATE_NEW_CONSOLE)
creationflags=subprocess.CREATE_NEW_CONSOLE)

def test_force_hide(self):
if ctypes:
script = textwrap.dedent(r'''
import sys, ctypes
GetConsoleWindow = ctypes.WinDLL('kernel32').GetConsoleWindow
IsWindowVisible = ctypes.WinDLL('user32').IsWindowVisible
sys.exit(IsWindowVisible(GetConsoleWindow()))
''')
else:
script = 'import sys; sys.exit(0)'
rc = subprocess.call([sys.executable, '-c', script], force_hide=True)
self.assertEqual(rc, 0)

def test_invalid_args(self):
# invalid arguments should raise ValueError
Expand Down
Copy link
Copy Markdown
Author

@ammgws ammgws Apr 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if something is meant to be done about the date in the filename since it has been years since it was first generated...

Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Adds new ``force_hide`` argument to :mod:`subprocess` functions.
This passes ``SW_HIDE`` to the new process, which most applications
will use to not display any window even if they normally would.
Loading