psutil
psutil is the standard Python library for reading what the operating system knows: running processes, CPU load, memory, disks, network counters, sensors, and logged-in users. It reimplements what tools like ps, top, free, lsof, and netstat show, behind one Python API that works the same on Linux, Windows, macOS, and the BSDs. Under the hood it is C extensions talking to each platform's native interfaces (procfs, sysctl, Windows APIs), so calls are fast and do not shell out. Monitoring agents, task runners, and test frameworks use it to watch, limit, and kill processes.
If Python code needs to know about processes or system resources, psutil is the answer and has been for fifteen years; roughly 91M weekly downloads agree. Just read the platform notes in the docs before trusting any number inside a container.
Use it if
- You are building a monitoring agent, health endpoint, or autoscaler that needs CPU, memory, disk, and network numbers without parsing top output
- You manage child processes and need to find, inspect, terminate, or wait on whole process trees reliably across platforms
- You need per-process detail (open files, connections, memory maps, CPU affinity) that subprocess and os in the stdlib simply do not expose
- You ship one codebase to Linux, Windows, and macOS and want the same call to mean roughly the same thing on each
- You are inside a container and expect container-aware numbers: psutil reports host-level CPU and memory unless you read cgroup limits yourself, which routinely produces wrong autoscaling decisions
- You only need disk usage or CPU count: shutil.disk_usage and os.cpu_count in the stdlib cover that with zero dependencies
- You need GPU metrics: psutil does not touch GPUs at all, so NVIDIA users need nvidia-ml-py or vendor tooling
- You run on unusual platforms or need every field everywhere: plenty of APIs are platform-specific (sensors are mostly Linux, net_connections needs root on macOS), so cross-platform code fills up with AccessDenied handling and hasattr checks
Setup reality
pip install psutil is usually one wheel and done, since prebuilt wheels cover Linux, macOS, and Windows on common architectures. On Alpine, unusual CPU targets, or brand-new Python versions you fall back to compiling the C extension, which means gcc plus python3-dev headers or the install fails with a compiler error. The API has sharp edges you learn the hard way: cpu_percent(interval=None) returns a meaningless 0.0 on first call, many process methods raise NoSuchProcess or AccessDenied mid-iteration, and version 7 dropped Python 2 leftovers and some deprecated aliases, so old snippets from Stack Overflow may not run as pasted.
Patterns
Measure CPU utilization correctlycpu-percent
import psutil
# Blocking sample over 1 second, per core:
psutil.cpu_percent(interval=1, percpu=True) # [4.0, 6.9, 3.7, 9.2]
# Non-blocking: compares against the previous call
psutil.cpu_percent(interval=None)The very first interval=None call returns a meaningless 0.0 because there is no previous sample; call it once, wait, then read.
Read system memory and swapmemory-usage
import psutil
mem = psutil.virtual_memory()
print(mem.percent, mem.available)
swap = psutil.swap_memory()
print(swap.percent)Use mem.available (not mem.free) to judge headroom; free excludes cache and buffers and looks alarmingly low on healthy Linux boxes.
List partitions and check disk spacedisk-usage
import psutil
for part in psutil.disk_partitions():
usage = psutil.disk_usage(part.mountpoint)
print(part.device, part.mountpoint, f"{usage.percent}%")disk_partitions(all=False) skips pseudo filesystems like proc and tmpfs; disk_usage on an unmounted or permission-blocked path raises OSError.
Read network I/O per interfacenetwork-counters
import psutil
counters = psutil.net_io_counters(pernic=True)
for nic, io in counters.items():
print(nic, io.bytes_sent, io.bytes_recv)These are cumulative since boot; to get a rate, sample twice and divide the delta by the elapsed time yourself.
List open TCP connectionslist-connections
import psutil
for conn in psutil.net_connections(kind="tcp"):
if conn.status == psutil.CONN_LISTEN:
print(conn.laddr, conn.pid)On macOS this requires root and on Linux you may get pid=None for other users' sockets; expect AccessDenied in unprivileged code.
Inspect one process efficientlyinspect-process
import psutil
p = psutil.Process(7055)
with p.oneshot():
print(p.name())
print(p.exe())
print(p.memory_info().rss)
print(p.cpu_percent(interval=1.0))oneshot() caches process data so grouped reads hit the OS once; per-process cpu_percent also needs an interval or a prior call to mean anything.
Iterate all processes without racingiterate-processes
import psutil
for proc in psutil.process_iter(attrs=["pid", "name", "username"]):
info = proc.info
if info["name"] == "python3":
print(info["pid"], info["username"])Passing attrs makes process_iter swallow NoSuchProcess and AccessDenied per process and hand you a plain dict, which is what you want in a loop.
Terminate a process and its childrenkill-process-tree
import psutil
def kill_tree(pid, timeout=3):
parent = psutil.Process(pid)
procs = parent.children(recursive=True) + [parent]
for p in procs:
p.terminate()
gone, alive = psutil.wait_procs(procs, timeout=timeout)
for p in alive:
p.kill()terminate() sends SIGTERM for a graceful stop and kill() sends SIGKILL; wait_procs gives stragglers a chance before you escalate.
Handle vanished and protected processeshandle-process-errors
import psutil
try:
p = psutil.Process(pid)
cmdline = p.cmdline()
except psutil.NoSuchProcess:
cmdline = None # exited between lookup and call
except psutil.AccessDenied:
cmdline = "<protected>"
except psutil.ZombieProcess:
cmdline = "<zombie>"Every Process method can raise these at call time, not construction time, because the process can die at any moment.
Read temperatures and batterysensors-battery
import psutil
if hasattr(psutil, "sensors_temperatures"):
temps = psutil.sensors_temperatures()
for name, entries in temps.items():
for e in entries:
print(name, e.label, e.current)
batt = psutil.sensors_battery()
if batt:
print(batt.percent, batt.power_plugged)sensors_temperatures is mostly Linux-only and does not exist on some platforms, hence the hasattr guard; sensors_battery returns None on desktops.
Get boot time and logged-in usersboot-time-users
import datetime
import psutil
boot = datetime.datetime.fromtimestamp(psutil.boot_time())
print("up since", boot)
for user in psutil.users():
print(user.name, user.host, user.started)boot_time() is a plain epoch float and does not change across calls; cache it rather than re-reading in a loop.
Spawn a subprocess you can monitorspawn-and-monitor
import psutil
p = psutil.Popen(["python3", "worker.py"])
print(p.pid, p.status())
print(p.cpu_times())
returncode = p.wait(timeout=30)psutil.Popen wraps subprocess.Popen and adds all Process methods; wait(timeout=...) raises TimeoutExpired instead of blocking forever.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| distro | PyPI | You only need to identify the Linux distribution and version, not live system metrics |
| py-cpuinfo | PyPI | You need CPU model, flags, and identification details rather than utilization numbers |
| nvidia-ml-py | PyPI | You need GPU utilization and memory from NVIDIA cards, which psutil does not cover |