137. Psutil (App V0.1)
RAM Manager v0.1 — cross-platform with psutil
137. Psutil (App V0.1)
He Googles “python cross-platform memory”. Every answer says the same thing: use psutil.
🤔 Socratic question: Why do we need a library for something the OS already knows? Shouldn’t Python have this built in? Here’s the thing — every OS speaks a different language. psutil is the translator.
🏛️ History:
psutilwas created in 2009 by Giampaolo Rodolà. It’s now in the top 100 most downloaded Python packages with 100+ million downloads per month. Instagram, Netflix, and Dropbox all use it to monitor their servers. When you use psutil, you’re using the same tool that keeps Instagram online for 2 billion users.
import psutil
mem = psutil.virtual_memory()
print(f"Total: {mem.total / 1e9:.1f} GB")
print(f"Used: {mem.used / 1e9:.1f} GB ({mem.percent}%)")
print(f"Available: {mem.available / 1e9:.1f} GB")That’s four lines that work on macOS, Linux, and Windows. He installs it and runs it. The numbers match Activity Monitor exactly.
Per-process is just as clean:
processes = []
for proc in psutil.process_iter(['pid', 'name', 'memory_info']):
try:
rss_mb = proc.info['memory_info'].rss / 1e6
processes.append({'name': proc.info['name'], 'pid': proc.info['pid'], 'rss_mb': rss_mb})
except (psutil.NoSuchProcess, psutil.AccessDenied):
pass # process died or we can't read it — skip silently
top10 = sorted(processes, key=lambda p: p['rss_mb'], reverse=True)[:10]
for p in top10:
print(f"{p['rss_mb']:>8.1f} MB {p['name']} (PID {p['pid']})")Two things catch him out:
psutil.NoSuchProcess— processes die while you’re iterating. Always catch it.psutil.AccessDenied— system processes won’t let you read their memory. Always catch it.mem.availablenotmem.total - mem.used— the OS reclaims caches.availableis the honest number.
🤯 Mind-blown moment: psutil replaces 12+ different Unix commands (
ps,top,free,vmstat,netstat,lsof,ifconfig,df,du,uptime,who,iostat) with one cross-platform Python API. One library. All platforms. That’s why everyone uses it.
💡 Fun fact: mem.available is not the same as mem.total - mem.used. Linux aggressively uses spare RAM as filesystem cache to speed up disk reads. That cached memory is instantly reclaimable when a new process needs it. mem.free is strictly unused; mem.available is free + reclaimable cache. On a healthy Linux system, mem.free might show 200 MB while mem.available shows 6 GB. Always use mem.available.
⚠️ Watch out: Dividing bytes by 1024 gives kilobytes, not gigabytes. To get gigabytes, divide by 1e9 (or 1024**3 for binary gigabytes). A common mistake is seeing mem.used / 1024 and thinking it’s GB — it’s actually KB, which will show values like 8000000 instead of 8.0.
🤔 Think about it: sorted(processes, reverse=True) would fail on a list of dicts — Python doesn’t know how to compare two dicts. Why do you need key=lambda p: p['rss_mb']? What does the key parameter actually do under the hood?
🐍 Aryan discovers psutil and rewrites the RAM monitor. No more shell commands — pure Python, works on Linux, macOS, and Windows. But he uses the wrong psutil attributes, forgets to handle NoSuchProcess, and prints bytes instead of GB.
Learning objectives
- Use psutil.virtual_memory() for RAM stats
- Use mem.available (not mem.free) for usable RAM
- Convert bytes to GB with / 1e9
- Catch NoSuchProcess and AccessDenied in process_iter loops
- Sort processes descending by RSS with reverse=True
Key concepts
- psutil.virtual_memory() — RAM stats
- mem.available — usable RAM (free + reclaimable cache)
- process_iter([‘attrs’]) — iterate processes with attributes
- NoSuchProcess / AccessDenied — expected psutil exceptions
- sorted(list, key=…, reverse=True) — descending sort
Try it
Concept detail
App v0.1 — psutil cross-platform RAM monitor
This replaces the subprocess version. Same functionality, works on Linux, macOS, and Windows.
virtual_memory() fields
mem = psutil.virtual_memory()
mem.total # total physical RAM (bytes)
mem.used # used RAM (bytes)
mem.available # available for new processes (free + cache) ← use this
mem.free # strictly free (no cache) ← usually too low
mem.percent # used / total * 100process_iter() with attributes
for proc in psutil.process_iter(['pid', 'name', 'memory_info']):
try:
rss_mb = proc.info['memory_info'].rss / 1e6
print(f"{proc.info['name']}: {rss_mb:.0f} MB")
except (psutil.NoSuchProcess, psutil.AccessDenied):
passv0.0 vs v0.1
v0.0 subprocess: Linux only, fragile parsing, crashes on macOS
v0.1 psutil: Linux + macOS + Windows, stable API, exception-safeSolution
import psutil
def get_ram_stats() -> dict:
mem = psutil.virtual_memory()
return {
'percent': mem.percent,
'used_gb': mem.used / 1e9,
'total_gb': mem.total / 1e9,
'available_gb': mem.available / 1e9, # available, not free
}
def get_top_processes(n: int = 5) -> list[dict]:
processes = []
for proc in psutil.process_iter(['pid', 'name', 'memory_info']):
try:
rss = proc.info['memory_info'].rss / 1e6 # bytes → MB
processes.append({
'name': proc.info['name'],
'pid': proc.info['pid'],
'rss_mb': rss,
})
except (psutil.NoSuchProcess, psutil.AccessDenied):
pass # process exited or no permission — skip it
return sorted(processes, key=lambda p: p['rss_mb'], reverse=True)[:n]
def main():
stats = get_ram_stats()
print(f"RAM: {stats['percent']:.1f}% used")
print(f"Used: {stats['used_gb']:.1f} GB / {stats['total_gb']:.1f} GB")
print(f"Available: {stats['available_gb']:.1f} GB")
if stats['percent'] > 80:
print('WARNING: High RAM usage!')
print('\nTop processes:')
for p in get_top_processes(5):
print(f" {p['name']:<20} PID {p['pid']:<8} {p['rss_mb']:.0f} MB")
if __name__ == '__main__':
main()Tests
import psutil
import inspect
import pytest
from unittest.mock import patch, MagicMock
def _make_mock_mem(total=17_179_869_184, used=8_589_934_592, available=7_516_192_768, percent=50.0):
mem = MagicMock()
mem.total = total
mem.used = used
mem.available = available
mem.free = available - 536_870_912 # strictly free (less than available)
mem.percent = percent
return mem
def _make_mock_proc(name, pid, rss_bytes):
proc = MagicMock()
mem_info = MagicMock()
mem_info.rss = rss_bytes
proc.info = {'name': name, 'pid': pid, 'memory_info': mem_info}
return proc
def test_get_ram_stats_returns_expected_keys():
with patch('psutil.virtual_memory', return_value=_make_mock_mem()):
stats = get_ram_stats()
assert 'percent' in stats
assert 'used_gb' in stats
assert 'total_gb' in stats
assert 'available_gb' in stats
def test_get_ram_stats_percent_in_range():
with patch('psutil.virtual_memory', return_value=_make_mock_mem(percent=72.5)):
stats = get_ram_stats()
assert 0 <= stats['percent'] <= 100
def test_get_ram_stats_uses_available_not_free():
src = inspect.getsource(get_ram_stats)
assert 'mem.available' in src, 'Use mem.available, not mem.free'
def test_get_ram_stats_gb_conversion():
with patch('psutil.virtual_memory', return_value=_make_mock_mem(total=16_000_000_000)):
stats = get_ram_stats()
assert stats['total_gb'] >= 1.0, 'total_gb seems too small — check division by 1e9'
def test_get_top_processes_returns_descending():
mock_procs = [
_make_mock_proc('chrome', 812, 2_000_000_000),
_make_mock_proc('python', 421, 500_000_000),
_make_mock_proc('vim', 300, 50_000_000),
]
with patch('psutil.process_iter', return_value=mock_procs):
procs = get_top_processes(3)
rsses = [p['rss_mb'] for p in procs]
assert rsses == sorted(rsses, reverse=True), 'Processes must be sorted descending by RSS'
def test_get_top_processes_rss_in_mb():
mock_procs = [_make_mock_proc('chrome', 812, 1_800_000_000)]
with patch('psutil.process_iter', return_value=mock_procs):
procs = get_top_processes(5)
if procs:
assert procs[0]['rss_mb'] < 100_000, 'rss_mb looks like bytes, not MB (forgot / 1e6?)'
assert procs[0]['rss_mb'] > 1, 'rss_mb looks too small — check conversion'
def test_get_top_processes_handles_no_such_process():
def bad_proc_iter(attrs):
proc = MagicMock()
proc.info = {'name': 'zombie', 'pid': 999, 'memory_info': MagicMock()}
proc.info['memory_info'].rss # accessing causes exception
raise psutil.NoSuchProcess(999)
# Should not raise; returns empty list gracefully
with patch('psutil.process_iter', side_effect=lambda *a: iter([])):
procs = get_top_processes(5)
assert isinstance(procs, list)