Python process inspection

How to check the progress of a Python script that is already running

You can't attach a progress bar to a Python process that is already running, but you can see where it is. sudo py-spy dump --pid 12345 --locals prints the current line of every thread plus local variables such as the loop counter, and pv -d 12345 turns the byte offsets of its open files into a bar with percent done and ETA.

5 min readLogs and monitoring

At a glance

  • pgrep -af python finds the PID; tail -F follows the log if the script writes one.
  • py-spy dump --pid <pid> --locals prints 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.

Bash
pgrep -af python
tail -F nohup.out
ls -lht /data/output | head

See 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.

Bash
sudo py-spy dump --pid 12345 --locals
Terminal
Process 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:

  1. ls -l /proc/<pid>/fd shows which descriptor number points to your input file.
  2. /proc/<pid>/fdinfo/<fd> shows the current byte offset in the pos: field.
  3. Divide pos by 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.

Terminal
$ 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:00

Here the 300,000,000-byte file was 13.7% read at the first check (41,000,960 bytes). Each method answers a different question:

MethodWhat it tells youWhat it needs
tail -F on the logWhatever the script printsOutput that isn't stuck in Python's buffer
py-spy dump --localsCurrent line, call stack and loop variablessudo for a process py-spy did not start
/proc/<pid>/fdinfo/<fd>Byte offset in each open fileLinux, same user or root
pv -d <pid>Percent read, rate and ETA per fileLinux or macOS, a file read from start to end
ps -o etime,%cpu,statRun time, CPU use and process stateNothing extra
strace -p <pid>Live system calls such as reads and writesLinux, 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.

Bash
ps -o pid,etime,%cpu,stat -p 12345
sudo strace -p 12345 -e trace=read,write

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.

Get a live linkpip install tqdm-live

Sources