136. Subprocess (App V0.0)
RAM Manager v0.0 — subprocess shell script
136. Subprocess (App V0.0)
Aryan’s first Python attempt reads shell output directly.
🤔 Socratic question: You know
ps auxworks in the terminal. Can’t you just… run that from Python? Yes. But should you? What happens on Windows? What if the output format changes? This chapter is about learning why the obvious approach fails.
He knows ps aux works. His first instinct: run it from Python.
import subprocess
result = subprocess.run(
['ps', 'aux'],
capture_output=True, text=True
)
lines = result.stdout.splitlines()
# Sort by RSS (column index 5 on Linux)
processes = sorted(lines[1:], key=lambda l: int(l.split()[5] or 0), reverse=True)
for line in processes[:10]:
parts = line.split(None, 10)
print(f"PID {parts[1]:>6} RSS {int(parts[5])//1024:>5} MB {parts[10][:30]}")It works. On his Mac. On Linux the column indices are slightly different. On Windows, ps doesn’t exist at all. And when a process exits mid-run, he gets a ValueError trying to parse the garbled row.
He also notices that subprocess.run() without check=True silently returns exit code 1 on failure. Without capture_output=True the output goes straight to the terminal instead of into result.stdout.
He uses it enough to understand it. But he knows there’s a better way.
💡 Real-world: DevOps engineers still use
subprocessall the time — for running git commands, calling system utilities, orchestrating shell scripts. It’s not wrong, it’s just the wrong tool for this job. Every tool in your belt has its place.
💡 Fun fact: The free command on Linux reads /proc/meminfo — a virtual file that the kernel keeps updated in real-time. When you run free -m, you’re not running a program that inspects memory; you’re reading a file that the kernel writes. psutil (which you’ll use in v0.1) reads /proc/meminfo directly in C, skipping the subprocess entirely and working on all platforms.
⚠️ Watch out: bare except: pass is one of the worst patterns in Python. It catches everything — KeyboardInterrupt, SystemExit, MemoryError — and silently swallows it. Your program appears to run fine while something is deeply wrong. At minimum, catch specific exceptions like subprocess.CalledProcessError and print a message with sys.exit(1).
🤔 Think about it: v0.0 only works on Linux. v0.1 (psutil) works on Linux, macOS, and Windows. What does that mean for a developer who writes a tool on a Mac and deploys it on a Linux server? Why is cross-platform compatibility a hidden cost of using subprocess for system monitoring?
🐚 Aryan writes his first RAM monitor using subprocess to call shell commands. It works on Linux but the output parsing is fragile, errors are silently swallowed, and it crashes on macOS. Fix the bugs so it at least works reliably on Linux.
Learning objectives
- Use capture_output=True + text=True for shell command output
- Parse free -m and ps aux output
- Handle FileNotFoundError for missing commands
- Use sys.exit(1) on unrecoverable errors
- Understand why subprocess is fragile across OSes
Key concepts
- subprocess.run(capture_output=True, text=True) — capture output as str
- result.stdout.split(‘\n’) — parse command output
- FileNotFoundError — command not found
- sys.exit(1) — exit with error code
Try it
Concept detail
App v0.0 — The subprocess approach
This is the “naive” version. It works on Linux but breaks on macOS (no free command) and Windows. We’ll fix this in v0.1 with psutil.
# What free -m outputs:
total used free shared buff/cache available
Mem: 15987 8234 2156 512 5597 6841
Swap: 2047 0 2047# What ps aux --sort=-%mem outputs:
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
aryan 812 2.1 12.4 6.2G 2.0G ? Sl 10:23 0:45 chromeThe problem: These commands only exist on Linux. On macOS: vm_stat, top -l 1 — completely different format. On Windows: entirely different tools.
The fix: psutil (v0.1) reads /proc and OS APIs directly.
Solution
import subprocess
import sys
def get_ram_percent() -> float:
result = subprocess.run(
['free', '-m'],
capture_output=True,
text=True,
check=True
)
lines = result.stdout.strip().split('\n')
mem_line = lines[1].split() # Mem: total used free ...
total = int(mem_line[1])
used = int(mem_line[2])
return (used / total) * 100
def get_top_processes(n: int = 5) -> list[str]:
result = subprocess.run(
['ps', 'aux', '--sort=-%mem'],
capture_output=True,
text=True,
check=True
)
lines = result.stdout.strip().split('\n')
return lines[1:n + 1] # skip header row
def main():
try:
percent = get_ram_percent()
print(f'RAM usage: {percent:.1f}%')
if percent > 80:
print('WARNING: High RAM usage!')
procs = get_top_processes(5)
print('\nTop processes:')
for p in procs:
print(' ', p)
except subprocess.CalledProcessError as e:
print(f'Command failed: {e}', file=sys.stderr)
sys.exit(1)
except FileNotFoundError as e:
print(f'Command not found — this script requires Linux: {e}', file=sys.stderr)
sys.exit(1)
if __name__ == '__main__':
main()Tests
import subprocess
import inspect
import pytest
from unittest.mock import patch, MagicMock
FREE_M_OUTPUT = (
' total used free shared buff/cache available\n'
'Mem: 15987 8234 2156 512 5597 6841\n'
'Swap: 2047 0 2047\n'
)
PS_OUTPUT = (
'USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND\n'
'aryan 812 2.1 12.4 6291456 2048000 ? Sl 10:23 0:45 /usr/bin/chrome\n'
'aryan 421 0.5 4.2 1048576 671000 ? S 09:10 0:12 python3\n'
'root 1 0.0 0.1 12345 1234 ? Ss 08:00 0:01 systemd\n'
)
def _fake_run(cmd, **kwargs):
result = MagicMock()
result.returncode = 0
if 'free' in cmd:
result.stdout = FREE_M_OUTPUT
else:
result.stdout = PS_OUTPUT
result.stderr = ''
return result
def test_get_ram_percent_uses_capture_output():
src = inspect.getsource(get_ram_percent)
assert 'capture_output' in src, 'Must use capture_output=True'
def test_get_ram_percent_uses_text_mode():
src = inspect.getsource(get_ram_percent)
assert 'text=True' in src, 'Must use text=True to get str not bytes'
def test_get_ram_percent_returns_float():
with patch('subprocess.run', side_effect=_fake_run):
result = get_ram_percent()
assert isinstance(result, float)
assert 0.0 <= result <= 100.0
def test_get_top_processes_returns_list():
with patch('subprocess.run', side_effect=_fake_run):
result = get_top_processes(2)
assert isinstance(result, list)
assert len(result) <= 2
def test_main_handles_file_not_found():
with patch('subprocess.run', side_effect=FileNotFoundError('free not found')):
with pytest.raises(SystemExit):
main()
def test_exception_not_silenced():
src = inspect.getsource(main)
assert 'except Exception:\n pass' not in src and \
'except:\n pass' not in src, (
'Do not silently swallow exceptions with bare except/pass'
)