At a glance
pgrep -af pythonfinds the PID;tail -Ffollows the log if the script writes one.py-spy dump --pid <pid> --localsprints the current line and the loop counter.pv -d <pid>shows a live bar for every file the process is reading, on Linux and macOS.- The same stack twice, near 0% CPU and no new output usually means it is waiting on something.
Find the process and its output
Start by finding the process ID (PID) of your job. pgrep -af python lists all running Python processes with their full command lines.
If you started the script inside tmux or screen, reattach to see its terminal. If you redirected stdout to a file or used nohup, follow the file with tail -F nohup.out (see how to follow a log with tail).
Sometimes the log sits at zero bytes even though your script calls print(). Python block-buffers stdout when it is not a terminal, so output stays in memory until the buffer fills or the script ends, and you can't change that on a running process. Check whether output files are still growing instead.
pgrep -af python
tail -F nohup.out
ls -lht /data/output | headSee where it is with py-spy dump
When there is no log to read, py-spy looks inside the Python process from the outside. It reads the target's memory from a separate process and does not run code inside your job, though by default it pauses the process briefly while sampling (--nonblocking avoids the pause, at the cost of occasional partial stacks).
py-spy dump --pid <pid> prints the current call stack of every Python thread without stopping the job. Run it twice a few seconds apart: if the line numbers change, the script is moving. --locals also prints the local variables of each frame, which often includes the loop counter.
On Linux, attaching to a process that py-spy did not start usually needs sudo, and macOS always needs root.
sudo py-spy dump --pid 12345 --localsProcess 12345: python process_rows.py
Python v3.13.14 (/usr/local/bin/python3.13)
Thread 12345 (idle): "MainThread"
handle (process_rows.py:5)
Arguments:
row: 295
main (process_rows.py:10)
Locals:
rows: <range at 0x7ffa3fed03f0>
i: 295
row: 295
<module> (process_rows.py:12)The i: 295 line under main is the iteration the loop is on right now. If you know how many items the loop has, you know how far along the job is. The py-spy guide covers top, record and permissions in depth.
Measure progress through the input file
If your script reads a big file from start to end, the Linux kernel already tracks how many bytes it has read:
ls -l /proc/<pid>/fdshows which descriptor number points to your input file./proc/<pid>/fdinfo/<fd>shows the current byte offset in thepos:field.- Divide
posby the file size to get the share that is done.
pv -d <pid> does the math for you: it draws a bar with rate and ETA for every regular file the process has open, and pv -d <pid>:<fd> watches a single descriptor. lsof -o -p <pid> shows the same offsets with a leading 0t.
$ ls -l /proc/12345/fd
lr-x------ 1 user user 64 Oct 30 00:27 3 -> /tmp/big.bin
$ grep pos /proc/12345/fdinfo/3
pos: 41000960
$ lsof -o -p 12345 | grep big.bin
python3 12345 user 3r REG 0,2055 0t83001344 31233773 /tmp/big.bin
$ pv -d 12345
3:/tmp/big.bin: 185MiB 0:00:01 [ 184MiB/s] [========> ] 64% ETA 0:00:00Here the 300,000,000-byte file was 13.7% read at the first check (41,000,960 bytes). Each method answers a different question:
| Method | What it tells you | What it needs |
|---|---|---|
tail -F on the log | Whatever the script prints | Output that isn't stuck in Python's buffer |
py-spy dump --locals | Current line, call stack and loop variables | sudo for a process py-spy did not start |
/proc/<pid>/fdinfo/<fd> | Byte offset in each open file | Linux, same user or root |
pv -d <pid> | Percent read, rate and ETA per file | Linux or macOS, a file read from start to end |
ps -o etime,%cpu,stat | Run time, CPU use and process state | Nothing extra |
strace -p <pid> | Live system calls such as reads and writes | Linux, same permissions as py-spy |
Check whether it is stuck
A quiet script might be working through a slow step, or it might be hung. Check its elapsed time, CPU use and state with ps.
If CPU use is near 0%, no new output appears, and two py-spy dump runs a few seconds apart show the same stack, the job is usually waiting on something external such as a network response, a lock or input. strace -p <pid> shows the system calls it makes in real time, and -e trace=read,write narrows that down to reads and writes so you can see whether data is still moving.
ps -o pid,etime,%cpu,stat -p 12345
sudo strace -p 12345 -e trace=read,writeNext time, start it with a progress link
You can't attach a progress bar to a process that is already running; a real bar takes a restart. Before the next long run, wrap the main loop in tqdm, and start Python with python -u so output isn't stuck in a buffer when it goes to a file.
FAQ
Can I add a progress bar to a Python script that is already running?
No. Adding a progress bar means changing the code, which takes a restart. Without one, read the loop counter with py-spy dump --pid <pid> --locals or, on Linux and macOS, watch the file offsets with pv -d <pid>.
Is it safe to run py-spy on a long job?
Yes. py-spy reads the process's memory from a separate process and never runs code inside your job. By default it pauses the process briefly to sample the stack; add --nonblocking to avoid the pause, which can give occasional errors or partial stacks.
Why is nohup.out empty even though my script prints?
When stdout goes to a file, Python block-buffers it, so prints sit in memory until the buffer fills or the process exits. You can't change that on a running process, so start the next run with python -u or PYTHONUNBUFFERED=1.
How do I tell if my Python script is frozen or just slow?
Check CPU use with ps and run py-spy dump --pid <pid> twice a few seconds apart. Near 0% CPU, no new output and the same stack both times usually means it is waiting on the network, a lock or input; strace -p <pid> confirms which system call it is blocked in.
Does any of this work on macOS or Windows?
py-spy runs on Linux, macOS, Windows and FreeBSD, and macOS always needs root to attach. pv -d works on Linux and macOS. The /proc files and strace are Linux only.
Check on long runs from your phone
Change one import and every tqdm bar prints a link you can open on any device. Get a Discord or Slack message when the run finishes, fails or stalls.
pip install tqdm-liveSources
- py-spy README github.com
- proc_pid_fdinfo(5) man page man7.org
- pv(1) man page man7.org
- lsof(8) man page man7.org
- strace(1) man page man7.org
- Python docs: sys.stdout buffering docs.python.org