← Home

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: psutil was 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:

  1. psutil.NoSuchProcess — processes die while you’re iterating. Always catch it.
  2. psutil.AccessDenied — system processes won’t let you read their memory. Always catch it.
  3. mem.available not mem.total - mem.used — the OS reclaims caches. available is 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 * 100

process_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):
        pass

v0.0 vs v0.1

v0.0 subprocess:   Linux only, fragile parsing, crashes on macOS
v0.1 psutil:       Linux + macOS + Windows, stable API, exception-safe

Solution

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)

Resources