Skip to content

Python subprocess: Python에서 외부 명령 실행하기 (완전 가이드)

Updated on

Python 스크립트는 종종 외부 프로그램을 호출해야 합니다. 파일을 압축하기 위해 셸 명령을 실행하거나, 버전 관리를 위해 git을 호출하거나, ffmpeg 같은 시스템 유틸리티로 비디오를 처리하거나, 데이터 파이프라인의 일부로 컴파일된 바이너리를 실행해야 할 수도 있습니다. 하지만 os.system()이나 백틱 스타일의 꼼수에 손대기 시작하면, 코드가 쉽게 깨지고 보안에 취약해지며 문제가 생겼을 때 디버깅이 불가능해집니다.

이 고통은 금방 커집니다. 출력을 캡처할 방법이 없어 결과가 허공으로 사라지고, 반환 코드를 무시해서 오류가 조용히 지나가 버립니다. 사용자 입력 파일명에 공백이나 세미콜론 하나만 들어가도 평범한 스크립트가 셸 인젝션 취약점으로 바뀔 수 있습니다. 그리고 subprocess가 멈춰버리면, 전체 Python 프로그램도 함께 멈춥니다 — 타임아웃도, 복구도, 설명도 없습니다.

Python의 subprocess 모듈은 표준 해결책입니다. os.system(), os.popen(), (deprecated된) commands 모듈 같은 오래된 함수들을 하나의 일관된 API로 대체하여, 프로세스 생성, 출력 캡처, 오류 처리, 타임아웃 설정, 파이프라인 구성까지 모두 지원합니다. 이 가이드는 subprocess를 효과적이고 안전하게 사용하는 데 필요한 모든 내용을 다룹니다.

📚

subprocess.run() 빠른 시작

subprocess.run() 함수는 Python 3.5에 도입되었고, 외부 명령을 실행하는 권장 방식입니다. 명령을 실행하고 종료될 때까지 기다린 뒤 CompletedProcess 객체를 반환합니다.

import subprocess
 
# Run a simple command
result = subprocess.run(["ls", "-la"], capture_output=True, text=True)
 
print(result.stdout)       # standard output as a string
print(result.stderr)       # standard error as a string
print(result.returncode)   # 0 means success

핵심 파라미터:

  • capture_output=True는 stdout과 stderr를 캡처합니다 (stdout=subprocess.PIPE, stderr=subprocess.PIPE와 동일)
  • text=True는 bytes 대신 문자열로 디코딩합니다
  • 명령은 문자열 리스트로 전달하며, 각 인자는 리스트의 별도 요소로 분리합니다
import subprocess
 
# Run a command with arguments
result = subprocess.run(
    ["python", "--version"],
    capture_output=True,
    text=True
)
print(result.stdout.strip())  # e.g., "Python 3.12.1"

명령을 리스트로 전달할지 문자열로 전달할지 이해하기

subprocess.run()의 첫 번째 인자는 리스트 또는 문자열이 될 수 있습니다. 이 차이는 정확성과 보안에 매우 중요합니다.

리스트 형태 (권장)

리스트의 각 요소는 별도의 인자입니다. Python은 이를 셸 해석 없이 운영체제에 직접 전달합니다.

import subprocess
 
# Each argument is a separate list element
result = subprocess.run(
    ["grep", "-r", "TODO", "/home/user/project"],
    capture_output=True,
    text=True
)
print(result.stdout)

공백, 따옴표, 특수 문자가 포함된 파일명도 각 인자가 그대로 전달되기 때문에 정상 동작합니다:

import subprocess
 
# Filename with spaces -- works correctly as a list element
result = subprocess.run(
    ["cat", "my file with spaces.txt"],
    capture_output=True,
    text=True
)

문자열 형태 (shell=True 필요)

문자열 하나로 전달하면 shell=True가 필요하며, 시스템 셸(Unix는 /bin/sh, Windows는 cmd.exe)이 명령을 해석합니다.

import subprocess
 
# String form requires shell=True
result = subprocess.run(
    "ls -la | grep '.py'",
    shell=True,
    capture_output=True,
    text=True
)
print(result.stdout)

이 방식은 파이프(|), 리다이렉트(>), 글로빙(*.py), 환경 변수 확장($HOME) 같은 셸 기능을 사용할 수 있게 해줍니다. 하지만 심각한 보안 위험도 함께 생기며, 이는 아래 Security 섹션에서 다룹니다.

출력 캡처하기

stdout과 stderr를 각각 캡처

import subprocess
 
result = subprocess.run(
    ["python", "-c", "import sys; print('out'); print('err', file=sys.stderr)"],
    capture_output=True,
    text=True
)
 
print(f"stdout: {result.stdout}")   # "out\n"
print(f"stderr: {result.stderr}")   # "err\n"

stderr를 stdout에 합치기

출력을 하나의 스트림으로 받고 싶을 때가 있습니다. stderr=subprocess.STDOUT를 사용하세요:

import subprocess
 
result = subprocess.run(
    ["python", "-c", "import sys; print('out'); print('err', file=sys.stderr)"],
    stdout=subprocess.PIPE,
    stderr=subprocess.STDOUT,
    text=True
)
 
print(result.stdout)  # Contains both "out\n" and "err\n"

출력 버리기

출력을 숨기려면 subprocess.DEVNULL로 보내면 됩니다:

import subprocess
 
# Run silently -- discard all output
result = subprocess.run(
    ["apt-get", "update"],
    stdout=subprocess.DEVNULL,
    stderr=subprocess.DEVNULL
)

바이너리 출력

text=True를 생략하면 raw bytes를 받습니다. 이미지나 압축 파일 같은 바이너리 데이터에 유용합니다:

import subprocess
 
# Capture binary output (e.g., from curl)
result = subprocess.run(
    ["curl", "-s", "https://example.com/image.png"],
    capture_output=True
)
 
image_bytes = result.stdout  # bytes object
print(f"Downloaded {len(image_bytes)} bytes")

오류 처리

반환 코드를 직접 확인하기

기본적으로 subprocess.run()은 명령이 실패해도 예외를 발생시키지 않습니다. returncode를 직접 확인해야 합니다:

import subprocess
 
result = subprocess.run(
    ["ls", "/nonexistent/path"],
    capture_output=True,
    text=True
)
 
if result.returncode != 0:
    print(f"Command failed with code {result.returncode}")
    print(f"Error: {result.stderr}")

check=True로 실패 시 자동 예외 처리

check=True는 반환 코드가 0이 아닐 때 subprocess.CalledProcessError를 발생시킵니다:

import subprocess
 
try:
    result = subprocess.run(
        ["ls", "/nonexistent/path"],
        capture_output=True,
        text=True,
        check=True
    )
except subprocess.CalledProcessError as e:
    print(f"Command failed with return code {e.returncode}")
    print(f"stderr: {e.stderr}")
    print(f"stdout: {e.stdout}")

이 패턴은 “항상 성공해야 하는” 명령에 권장됩니다. 실패를 조용히 무시하지 못하게 만들어, 반드시 명시적으로 처리하도록 강제합니다.

명령을 찾을 수 없는 경우 처리

실행 파일이 존재하지 않으면 Python은 FileNotFoundError를 발생시킵니다:

import subprocess
 
try:
    result = subprocess.run(
        ["nonexistent_command"],
        capture_output=True,
        text=True
    )
except FileNotFoundError:
    print("Command not found -- is it installed and in PATH?")

타임아웃

오래 실행되거나 멈춘 프로세스는 스크립트를 영원히 막을 수 있습니다. timeout(초) 파라미터는 제한 시간 내에 끝나지 않으면 프로세스를 종료하고 subprocess.TimeoutExpired를 발생시킵니다:

import subprocess
 
try:
    result = subprocess.run(
        ["sleep", "30"],
        timeout=5,
        capture_output=True,
        text=True
    )
except subprocess.TimeoutExpired:
    print("Process timed out after 5 seconds")

이는 네트워크 명령, 외부 API 호출, 또는 멈출 가능성이 있는 어떤 프로세스에도 필수입니다:

import subprocess
 
def run_with_timeout(cmd, timeout_seconds=30):
    """Run a command with timeout and error handling."""
    try:
        result = subprocess.run(
            cmd,
            capture_output=True,
            text=True,
            timeout=timeout_seconds,
            check=True
        )
        return result.stdout
    except subprocess.TimeoutExpired:
        print(f"Command timed out after {timeout_seconds}s: {' '.join(cmd)}")
        return None
    except subprocess.CalledProcessError as e:
        print(f"Command failed (code {e.returncode}): {e.stderr}")
        return None
    except FileNotFoundError:
        print(f"Command not found: {cmd[0]}")
        return None
 
# Usage
output = run_with_timeout(["ping", "-c", "4", "example.com"], timeout_seconds=10)
if output:
    print(output)

프로세스에 입력(stdin) 전달하기

input 파라미터로 프로세스의 stdin에 데이터를 보낼 수 있습니다:

import subprocess
 
# Send text to stdin
result = subprocess.run(
    ["grep", "error"],
    input="line 1\nerror on line 2\nline 3\nerror on line 4\n",
    capture_output=True,
    text=True
)
 
print(result.stdout)
# "error on line 2\nerror on line 4\n"

이는 셸 명령으로 데이터를 파이프로 흘려보내는 흔한 패턴을 대체합니다:

import subprocess
import json
 
# Send JSON to a processing command
data = {"name": "Alice", "score": 95}
json_string = json.dumps(data)
 
result = subprocess.run(
    ["python", "-c", "import sys, json; d = json.load(sys.stdin); print(d['name'])"],
    input=json_string,
    capture_output=True,
    text=True
)
 
print(result.stdout.strip())  # "Alice"

환경 변수

기본적으로 subprocess는 현재 환경 변수를 상속합니다. 이를 수정할 수도 있습니다:

import subprocess
import os
 
# Add or override environment variables
custom_env = os.environ.copy()
custom_env["API_KEY"] = "secret123"
custom_env["DEBUG"] = "true"
 
result = subprocess.run(
    ["python", "-c", "import os; print(os.environ.get('API_KEY'))"],
    env=custom_env,
    capture_output=True,
    text=True
)
 
print(result.stdout.strip())  # "secret123"

반드시 os.environ.copy()를 베이스로 사용하세요. 기존 환경 없이 dict를 그대로 넘기면 상속되는 변수가 모두 제거되어, PATH, HOME 등 시스템 변수에 의존하는 명령이 깨질 수 있습니다.

작업 디렉터리

cwd 파라미터는 subprocess의 작업 디렉터리를 설정합니다:

import subprocess
 
# Run git status in a specific repository
result = subprocess.run(
    ["git", "status", "--short"],
    cwd="/home/user/my-project",
    capture_output=True,
    text=True
)
 
print(result.stdout)

subprocess.run() vs Popen: 언제 무엇을 쓸까

subprocess.run()subprocess.Popen을 감싼 편의 래퍼입니다. 대부분의 경우 run()이면 충분합니다. 다음이 필요할 때 Popen을 사용하세요:

  • 출력의 실시간 스트리밍(생성되는 즉시 한 줄씩)
  • 실행 중인 프로세스와 상호작용(입력 보내기, 루프로 출력 읽기)
  • 여러 프로세스를 연결하는 멀티 스텝 파이프라인 구성
  • 프로세스 라이프사이클을 직접 제어하는 논블로킹 실행

비교 표

Featuresubprocess.run()subprocess.Popenos.system()
RecommendedYes (Python 3.5+)Yes (advanced)No (deprecated pattern)
Capture outputYes (capture_output=True)Yes (via PIPE)No
Return valueCompletedProcess objectPopen process objectExit code (int)
Timeout supportYes (timeout param)Manual (via wait/communicate)No
Error checkingcheck=True raises exceptionManualMust parse exit code
Input to stdininput parametercommunicate() or stdin.write()No
Real-time outputNo (waits for completion)Yes (stream line by line)Output goes to terminal
PipelinesLimited (single command)Yes (chain multiple Popen)Yes (via shell string)
SecuritySafe with list argsSafe with list argsShell injection risk
Shell featuresOnly with shell=TrueOnly with shell=TrueAlways uses shell

고급: subprocess.Popen

Popen은 프로세스 라이프사이클을 완전히 제어할 수 있게 해줍니다. 생성자 호출 즉시 프로세스가 시작되며, 상호작용 가능한 Popen 객체가 반환됩니다.

기본 Popen 사용법

import subprocess
 
proc = subprocess.Popen(
    ["ls", "-la"],
    stdout=subprocess.PIPE,
    stderr=subprocess.PIPE,
    text=True
)
 
stdout, stderr = proc.communicate()  # Wait for completion and get output
print(f"Return code: {proc.returncode}")
print(stdout)

출력 실시간 스트리밍

run()과 달리, Popen은 출력이 생성되는 즉시 한 줄씩 읽을 수 있습니다:

import subprocess
 
proc = subprocess.Popen(
    ["ping", "-c", "5", "example.com"],
    stdout=subprocess.PIPE,
    text=True
)
 
# Read output line by line as it arrives
for line in proc.stdout:
    print(f"[LIVE] {line.strip()}")
 
proc.wait()  # Wait for process to finish
print(f"Exit code: {proc.returncode}")

이는 진행 상황을 보여주거나 로그를 실시간으로 남기고 싶은 장시간 실행 명령에서 필수입니다:

import subprocess
import sys
 
def run_with_live_output(cmd):
    """Run a command and stream its output in real time."""
    proc = subprocess.Popen(
        cmd,
        stdout=subprocess.PIPE,
        stderr=subprocess.STDOUT,
        text=True,
        bufsize=1  # Line-buffered
    )
 
    output_lines = []
    for line in proc.stdout:
        line = line.rstrip()
        print(line)
        output_lines.append(line)
 
    proc.wait()
    return proc.returncode, "\n".join(output_lines)
 
# Usage
code, output = run_with_live_output(["pip", "install", "requests"])
print(f"\nFinished with exit code: {code}")

파이프라인 구성하기

한 프로세스의 stdout을 다른 프로세스의 stdin에 연결해 여러 명령을 체인으로 구성할 수 있습니다:

import subprocess
 
# Equivalent to: cat /var/log/syslog | grep "error" | wc -l
p1 = subprocess.Popen(
    ["cat", "/var/log/syslog"],
    stdout=subprocess.PIPE
)
 
p2 = subprocess.Popen(
    ["grep", "error"],
    stdin=p1.stdout,
    stdout=subprocess.PIPE
)
 
# Allow p1 to receive SIGPIPE if p2 exits early
p1.stdout.close()
 
p3 = subprocess.Popen(
    ["wc", "-l"],
    stdin=p2.stdout,
    stdout=subprocess.PIPE,
    text=True
)
 
p2.stdout.close()
 
output, _ = p3.communicate()
print(f"Error count: {output.strip()}")

p1.stdout.close()를 p2에 연결한 뒤 호출하는 것은 중요합니다. p2가 먼저 종료되면 p1이 SIGPIPE를 받을 수 있게 해서, 데드락을 방지합니다.

인터랙티브 프로세스 통신

import subprocess
 
# Start a Python REPL as a subprocess
proc = subprocess.Popen(
    ["python", "-i"],
    stdin=subprocess.PIPE,
    stdout=subprocess.PIPE,
    stderr=subprocess.PIPE,
    text=True
)
 
# Send commands and get results
stdout, stderr = proc.communicate(input="print(2 + 2)\nprint('hello')\n")
print(f"stdout: {stdout}")
print(f"stderr: {stderr}")

shell=True: 강력하지만 위험함

shell=True를 설정하면 명령을 시스템 셸을 통해 실행하여 셸 기능을 사용할 수 있지만, 보안 위험이 생깁니다.

shell=True가 유용한 경우

import subprocess
 
# Shell features: pipes, redirects, globbing, env vars
result = subprocess.run(
    "ls *.py | wc -l",
    shell=True,
    capture_output=True,
    text=True
)
print(f"Python files: {result.stdout.strip()}")
 
# Environment variable expansion
result = subprocess.run(
    "echo $HOME",
    shell=True,
    capture_output=True,
    text=True
)
print(result.stdout.strip())

셸 인젝션 문제

정제되지 않은 사용자 입력을 shell=True에 절대 전달하지 마세요:

import subprocess
 
# DANGEROUS -- shell injection vulnerability
user_input = "file.txt; rm -rf /"  # malicious input
subprocess.run(f"cat {user_input}", shell=True)  # Executes "rm -rf /"!
 
# SAFE -- use list form without shell=True
subprocess.run(["cat", user_input])  # Treats entire string as filename

동적 값을 포함한 채로 shell=True가 꼭 필요하다면 shlex.quote()를 사용하세요:

import subprocess
import shlex
 
user_input = "file with spaces.txt; rm -rf /"
safe_input = shlex.quote(user_input)
 
# shlex.quote wraps in single quotes, neutralizing shell metacharacters
result = subprocess.run(
    f"cat {safe_input}",
    shell=True,
    capture_output=True,
    text=True
)

하지만 가장 안전한 접근은 shell=True를 완전히 피하고, 셸 기능을 Python으로 대체하는 것입니다:

import subprocess
import glob
 
# Instead of: subprocess.run("ls *.py | wc -l", shell=True)
py_files = glob.glob("*.py")
print(f"Python files: {len(py_files)}")
 
# Instead of: subprocess.run("cat file1.txt file2.txt > combined.txt", shell=True)
with open("combined.txt", "w") as outfile:
    result = subprocess.run(
        ["cat", "file1.txt", "file2.txt"],
        stdout=outfile
    )

보안 모범 사례

PracticeDoDon't
Command format["cmd", "arg1", "arg2"]f"cmd {user_input}" with shell=True
User inputshlex.quote() if shell is neededString concatenation into commands
Shell modeshell=False (default)shell=True with untrusted input
Executable pathFull paths like /usr/bin/gitRely on PATH for security-critical code
Input validationValidate and sanitize before passingPass raw user input to commands
import subprocess
import shlex
from pathlib import Path
 
def safe_file_operation(filename):
    """Safely run a command with user-supplied filename."""
    # Validate input
    path = Path(filename)
    if not path.exists():
        raise FileNotFoundError(f"File not found: {filename}")
 
    # Check for path traversal
    resolved = path.resolve()
    allowed_dir = Path("/home/user/uploads").resolve()
    if not str(resolved).startswith(str(allowed_dir)):
        raise PermissionError("Access denied: file outside allowed directory")
 
    # Use list form -- no shell injection possible
    result = subprocess.run(
        ["wc", "-l", str(resolved)],
        capture_output=True,
        text=True,
        check=True
    )
    return result.stdout.strip()

실전 예제

git 명령 실행하기

import subprocess
 
def git_status(repo_path):
    """Get git status for a repository."""
    result = subprocess.run(
        ["git", "status", "--porcelain"],
        cwd=repo_path,
        capture_output=True,
        text=True,
        check=True
    )
    return result.stdout.strip()
 
def git_log(repo_path, n=5):
    """Get last n commit messages."""
    result = subprocess.run(
        ["git", "log", f"--oneline", f"-{n}"],
        cwd=repo_path,
        capture_output=True,
        text=True,
        check=True
    )
    return result.stdout.strip()
 
status = git_status("/home/user/my-project")
if status:
    print("Uncommitted changes:")
    print(status)
else:
    print("Working directory clean")

파일 압축/해제

import subprocess
 
def compress_directory(source_dir, output_file):
    """Create a tar.gz archive of a directory."""
    subprocess.run(
        ["tar", "-czf", output_file, "-C", source_dir, "."],
        check=True
    )
    print(f"Created archive: {output_file}")
 
def extract_archive(archive_file, dest_dir):
    """Extract a tar.gz archive."""
    subprocess.run(
        ["tar", "-xzf", archive_file, "-C", dest_dir],
        check=True
    )
    print(f"Extracted to: {dest_dir}")
 
compress_directory("/home/user/data", "/tmp/data_backup.tar.gz")

시스템 정보 확인

import subprocess
 
def get_disk_usage():
    """Get disk usage summary."""
    result = subprocess.run(
        ["df", "-h", "/"],
        capture_output=True,
        text=True,
        check=True
    )
    return result.stdout
 
def get_memory_info():
    """Get memory usage on Linux."""
    result = subprocess.run(
        ["free", "-h"],
        capture_output=True,
        text=True,
        check=True
    )
    return result.stdout
 
def get_process_list(filter_name=None):
    """List running processes, optionally filtered."""
    cmd = ["ps", "aux"]
    result = subprocess.run(cmd, capture_output=True, text=True, check=True)
 
    if filter_name:
        lines = result.stdout.strip().split("\n")
        header = lines[0]
        matching = [line for line in lines[1:] if filter_name in line]
        return header + "\n" + "\n".join(matching)
 
    return result.stdout
 
print(get_disk_usage())

외부 도구로 데이터 파일 처리하기

import subprocess
import csv
import io
 
def sort_csv_by_column(input_file, column_index=1):
    """Sort a CSV file using the system sort command (fast for large files)."""
    result = subprocess.run(
        ["sort", "-t,", f"-k{column_index}", input_file],
        capture_output=True,
        text=True,
        check=True
    )
    return result.stdout
 
def count_lines(filepath):
    """Count lines in a file using wc (faster than Python for huge files)."""
    result = subprocess.run(
        ["wc", "-l", filepath],
        capture_output=True,
        text=True,
        check=True
    )
    return int(result.stdout.strip().split()[0])
 
def search_in_files(directory, pattern, file_type="*.py"):
    """Search for a pattern in files using grep."""
    result = subprocess.run(
        ["grep", "-rn", "--include", file_type, pattern, directory],
        capture_output=True,
        text=True
    )
    # grep returns exit code 1 if no matches found (not an error)
    if result.returncode == 0:
        return result.stdout
    elif result.returncode == 1:
        return ""  # No matches
    else:
        raise subprocess.CalledProcessError(result.returncode, result.args)
 
matches = search_in_files("/home/user/project", "TODO")
if matches:
    print(matches)
else:
    print("No TODOs found")

자동 배포 스크립트

import subprocess
import sys
 
def deploy(repo_path, branch="main"):
    """Simple deployment script using subprocess."""
    steps = [
        (["git", "fetch", "origin"], "Fetching latest changes"),
        (["git", "checkout", branch], f"Switching to {branch}"),
        (["git", "pull", "origin", branch], "Pulling latest code"),
        (["pip", "install", "-r", "requirements.txt"], "Installing dependencies"),
        (["python", "manage.py", "migrate"], "Running migrations"),
        (["python", "manage.py", "collectstatic", "--noinput"], "Collecting static files"),
    ]
 
    for cmd, description in steps:
        print(f"\n--- {description} ---")
        try:
            result = subprocess.run(
                cmd,
                cwd=repo_path,
                capture_output=True,
                text=True,
                check=True,
                timeout=120
            )
            if result.stdout:
                print(result.stdout)
        except subprocess.CalledProcessError as e:
            print(f"FAILED: {e.stderr}")
            sys.exit(1)
        except subprocess.TimeoutExpired:
            print(f"TIMEOUT: {description} took too long")
            sys.exit(1)
 
    print("\nDeployment complete")

크로스 플랫폼 고려사항

Windows와 Unix는 명령 동작이 다릅니다. 이식 가능한 코드를 작성하세요:

import subprocess
import platform
 
def run_command(cmd_unix, cmd_windows=None):
    """Run a command with platform awareness."""
    if platform.system() == "Windows":
        cmd = cmd_windows or cmd_unix
        # Windows often needs shell=True for built-in commands
        return subprocess.run(cmd, shell=True, capture_output=True, text=True)
    else:
        return subprocess.run(cmd, capture_output=True, text=True)
 
# List directory contents
result = run_command(
    cmd_unix=["ls", "-la"],
    cmd_windows="dir"
)
print(result.stdout)

주요 플랫폼 차이:

FeatureUnix/macOSWindows
Shell/bin/shcmd.exe
Path separator/\\
Built-in commands (dir, copy)Not availableRequire shell=True
Executable extensionNot needed.exe required sometimes
Signal handlingFull POSIX signalsLimited
shlex.quote()WorksUse subprocess.list2cmdline()

Jupyter Notebooks에서 subprocess 실행하기

Jupyter notebook에서 셸 명령을 실행하는 것은 데이터 사이언티스트에게 흔한 워크플로우입니다. Jupyter는 빠른 셸 호출을 위한 !command 문법을 지원하지만, subprocess를 사용하면 Python 코드 안에서 제대로 된 오류 처리와 출력 캡처를 할 수 있습니다.

노트북에서 subprocess 호출을 디버깅할 때 — 특히 명령이 조용히 실패하거나 예상치 못한 출력을 낼 때 — RunCell (opens in a new tab)이 도움이 될 수 있습니다. RunCell은 노트북 컨텍스트를 이해하는 Jupyter용 AI agent입니다. subprocess 명령이 실패하는 이유를 진단하고, 올바른 인자를 제안하며, 플랫폼별 특이점도 처리해줍니다. 터미널과 노트북을 왔다 갔다 하며 셸 명령을 디버깅하는 대신, 셀 안에서 직접 문제를 추적할 수 있습니다.

import subprocess
 
# In a Jupyter notebook: capture and display command output
result = subprocess.run(
    ["pip", "list", "--format=columns"],
    capture_output=True,
    text=True
)
 
# Display as formatted output in the notebook
print(result.stdout)

자주 하는 실수와 해결 방법

실수 1: 출력 캡처를 깜빡함

import subprocess
 
# Output goes to terminal, not captured
result = subprocess.run(["ls", "-la"])
print(result.stdout)  # None!
 
# Fix: add capture_output=True
result = subprocess.run(["ls", "-la"], capture_output=True, text=True)
print(result.stdout)  # Actual output

실수 2: shell=True 없이 문자열을 사용함

import subprocess
 
# Fails: string passed without shell=True
# subprocess.run("ls -la")  # FileNotFoundError: "ls -la" is not a program
 
# Fix option 1: use a list
subprocess.run(["ls", "-la"])
 
# Fix option 2: use shell=True (less safe)
subprocess.run("ls -la", shell=True)

실수 3: 오류를 무시함

import subprocess
 
# Bad: silently continues on failure
result = subprocess.run(["rm", "/important/file"], capture_output=True, text=True)
# ... continues even if rm failed
 
# Good: check=True raises exception on failure
try:
    result = subprocess.run(
        ["rm", "/important/file"],
        capture_output=True,
        text=True,
        check=True
    )
except subprocess.CalledProcessError as e:
    print(f"Failed to delete: {e.stderr}")

실수 4: Popen에서 데드락 발생

import subprocess
 
# DEADLOCK: stdout buffer fills up, process blocks, .wait() waits forever
proc = subprocess.Popen(["command_with_lots_of_output"], stdout=subprocess.PIPE)
proc.wait()  # Deadlock!
 
# Fix: use communicate() which handles buffering
proc = subprocess.Popen(["command_with_lots_of_output"], stdout=subprocess.PIPE, text=True)
stdout, stderr = proc.communicate()  # Safe

실수 5: 인코딩을 처리하지 않음

import subprocess
 
# Bytes output can cause issues
result = subprocess.run(["cat", "data.txt"], capture_output=True)
# result.stdout is bytes, not str
 
# Fix: use text=True or encoding parameter
result = subprocess.run(["cat", "data.txt"], capture_output=True, text=True)
 
# For specific encodings:
result = subprocess.run(
    ["cat", "data.txt"],
    capture_output=True,
    encoding="utf-8",
    errors="replace"  # Handle invalid bytes
)

subprocess.run() 전체 파라미터 레퍼런스

import subprocess
 
result = subprocess.run(
    args,                    # Command as list or string
    stdin=None,              # Input source (PIPE, DEVNULL, file object, or None)
    stdout=None,             # Output destination
    stderr=None,             # Error destination
    capture_output=False,    # Shorthand for stdout=PIPE, stderr=PIPE
    text=False,              # Decode output as strings (alias: universal_newlines)
    shell=False,             # Run through system shell
    cwd=None,                # Working directory
    timeout=None,            # Seconds before TimeoutExpired
    check=False,             # Raise CalledProcessError on non-zero exit
    env=None,                # Environment variables dict
    encoding=None,           # Output encoding (alternative to text=True)
    errors=None,             # Encoding error handling ('strict', 'replace', 'ignore')
    input=None,              # String/bytes to send to stdin
)

FAQ

Python에서 subprocess 모듈이란 무엇인가요?

subprocess 모듈은 Python 스크립트 내부에서 외부 명령과 프로그램을 실행하기 위한 Python 표준 라이브러리입니다. os.system(), os.popen(), commands 모듈 같은 이전 방식들을 대체했습니다. 새 프로세스를 생성하고, 입력/출력/에러 파이프에 연결하며, 반환 코드 확인과 타임아웃 처리까지 제공합니다. 주요 인터페이스는 간단한 명령 실행을 위한 subprocess.run()이며, 실시간 I/O나 파이프라인 같은 고급 사용 사례에는 subprocess.Popen을 사용합니다.

subprocess.run()과 subprocess.Popen의 차이는 무엇인가요?

subprocess.run()은 고수준 편의 함수로, 명령을 실행하고 완료될 때까지 기다린 뒤 출력이 담긴 CompletedProcess 객체를 반환합니다. 대부분의 작업에 적합합니다. subprocess.Popen은 저수준 클래스로 프로세스를 직접 제어합니다. 출력 실시간 스트리밍, 인터랙티브 입력 전송, 멀티 프로세스 파이프라인 구성, 프로세스 라이프사이클 수동 관리가 가능합니다. 실시간 출력이 필요하거나 여러 프로세스를 연결해야 할 때 Popen을 사용하세요.

subprocess에서 shell=True는 위험한가요?

네. shell=True를 신뢰할 수 없는 입력과 함께 사용하면 셸 인젝션 취약점이 생깁니다. shell=True가 설정되면 명령 문자열이 시스템 셸로 전달되어 해석되며, ;, |, &&, $() 같은 셸 메타문자가 실행됩니다. 공격자가 임의 명령을 주입할 수 있습니다. 안전한 기본값은 shell=False + 리스트 형태 인자 전달입니다. 꼭 shell=True가 필요하다면 shlex.quote()로 입력을 정제하고, 사용자 원본 입력을 그대로 넘기지 마세요.

subprocess 명령의 출력을 어떻게 캡처하나요?

subprocess.run()capture_output=Truetext=True를 사용하세요. 출력은 result.stdout(문자열), 오류는 result.stderr에 저장됩니다. 예: result = subprocess.run(["ls", "-la"], capture_output=True, text=True)result.stdout를 읽으면 됩니다. text=True가 없으면 출력은 bytes로 반환됩니다.

Python에서 subprocess 타임아웃은 어떻게 처리하나요?

subprocess.run()timeout(초)을 전달하세요. 제한 시간을 넘기면 Python이 프로세스를 종료하고 subprocess.TimeoutExpired 예외를 발생시킵니다. 예: subprocess.run(["slow_command"], timeout=30). Popen에서는 proc.communicate(timeout=30) 또는 proc.wait(timeout=30)를 사용합니다. 타임아웃이 중요한 코드는 항상 try/except로 감싸세요.

subprocess가 권장되는데도 os.system()이 여전히 동작하는 이유는 무엇인가요?

os.system()은 공식적으로 deprecated된 것은 아니지만, 레거시 인터페이스로 간주됩니다. 셸을 통해 명령을 실행(shell=True 같은 방식)하며, 출력을 캡처할 수 없고, 타임아웃 메커니즘이 없으며, 종료 상태만 반환합니다. subprocess.run()os.system()이 할 수 있는 일을 모두 하면서, 출력 캡처, 오류 처리, 타임아웃, 안전한 인자 전달까지 제공합니다. 새로운 코드는 모두 subprocess를 사용해야 합니다.

결론

subprocess 모듈은 외부 명령을 실행하기 위한 Python의 결정판 도구입니다. 단순한 명령 실행에는 subprocess.run()을 사용하세요 — 출력 캡처, 오류 확인, 타임아웃, 입력 전달을 한 번의 함수 호출로 처리할 수 있습니다. 실시간 출력 스트리밍, 인터랙티브한 프로세스 통신, 멀티 스텝 파이프라인이 필요할 때만 subprocess.Popen을 선택하면 됩니다.

가장 중요한 습관은 사용자 입력과 함께 shell=True를 피하는 것입니다. 명령을 리스트로 전달하면 셸 인젝션 위험을 원천적으로 제거할 수 있습니다. check=True로 실패를 조기에 잡고, timeout으로 멈춘 프로세스를 방지하고, text=True로 bytes 대신 문자열을 다루세요.

git 자동화부터 데이터 파이프라인 오케스트레이션까지, subprocessos.system()이 제공하지 못했던 제어력과 안전성을 제공합니다. 이 패턴들을 익히면 어떤 외부 도구든 자신 있게 Python 워크플로우에 통합할 수 있습니다.

📚