KhueApps
Home/Python/Track app usage time on Windows with Python (foreground polling)

Track app usage time on Windows with Python (foreground polling)

Last updated: October 07, 2025

Overview

This guide shows how to track time spent in each app on Windows using Python. We poll the current foreground window via the Windows API and attribute elapsed time to its owning process. It’s simple, robust, and good enough for personal productivity tracking.

  • Platform: Windows 10/11
  • Language: Python 3.9+
  • Dependencies: psutil (Windows API accessed via ctypes; no pywin32 required)
  • Approach: Foreground window polling at a small interval (e.g., 0.5–1.0s)

Quickstart

  1. Install Python 3.9+.
  2. Install dependency:
    pip install psutil
    
  3. Save the minimal script below as track_app_time.py.
  4. Run it, work as usual, then press Ctrl+C to stop and write a CSV summary.

Minimal working example

import time
import csv
import collections
from datetime import datetime
import psutil
import ctypes
from ctypes import wintypes

# Windows API setup via ctypes
user32 = ctypes.WinDLL('user32', use_last_error=True)
GetForegroundWindow = user32.GetForegroundWindow
GetWindowThreadProcessId = user32.GetWindowThreadProcessId
GetWindowTextW = user32.GetWindowTextW
GetWindowTextLengthW = user32.GetWindowTextLengthW

# Types
DWORD = wintypes.DWORD
HWND = wintypes.HWND

def get_foreground_pid() -> int | None:
    hwnd = GetForegroundWindow()
    if not hwnd:
        return None
    pid = DWORD()
    tid = GetWindowThreadProcessId(HWND(hwnd), ctypes.byref(pid))  # noqa: F841
    if pid.value == 0:
        return None
    return pid.value

def get_window_title(hwnd) -> str:
    length = GetWindowTextLengthW(HWND(hwnd))
    if length == 0:
        return ''
    buf = ctypes.create_unicode_buffer(length + 1)
    GetWindowTextW(HWND(hwnd), buf, length + 1)
    return buf.value

def get_foreground_app() -> str:
    hwnd = GetForegroundWindow()
    if not hwnd:
        return 'NoActiveWindow'
    pid = DWORD()
    _ = GetWindowThreadProcessId(HWND(hwnd), ctypes.byref(pid))
    if pid.value == 0:
        return 'NoActiveWindow'
    try:
        p = psutil.Process(int(pid.value))
        name = p.name()  # e.g., chrome.exe, WINWORD.EXE
        # Optional: include title for per-window tracking
        # title = get_window_title(hwnd)
        return name or 'UnknownProcess'
    except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
        return 'UnknownProcess'

def format_duration(seconds: float) -> str:
    s = int(seconds)
    h, r = divmod(s, 3600)
    m, s = divmod(r, 60)
    return f"{h:02d}:{m:02d}:{s:02d}"

def save_csv(path: str, totals: dict[str, float]) -> None:
    rows = sorted(totals.items(), key=lambda kv: kv[1], reverse=True)
    with open(path, 'w', newline='', encoding='utf-8') as f:
        w = csv.writer(f)
        w.writerow(['app', 'seconds', 'hh:mm:ss'])
        for app, secs in rows:
            w.writerow([app, int(secs), format_duration(secs)])

def main(poll_interval: float = 0.5, summary_every: float = 60.0) -> None:
    totals = collections.defaultdict(float)
    last_app = get_foreground_app()
    last_t = time.monotonic()
    last_summary = last_t

    print('Tracking... Press Ctrl+C to stop.')
    try:
        while True:
            time.sleep(poll_interval)
            now = time.monotonic()
            elapsed = now - last_t
            totals[last_app] += elapsed

            current_app = get_foreground_app()
            if current_app != last_app:
                last_app = current_app
            last_t = now

            if now - last_summary >= summary_every:
                top = sorted(totals.items(), key=lambda kv: kv[1], reverse=True)[:5]
                pretty = ', '.join(f"{k}={format_duration(v)}" for k, v in top)
                print(f"Summary: {pretty}")
                last_summary = now
    except KeyboardInterrupt:
        pass

    ts = datetime.now().strftime('%Y%m%d-%H%M%S')
    out = f'app-usage-{ts}.csv'
    save_csv(out, totals)
    total_time = sum(totals.values())
    print(f"Saved {out}. Total tracked: {format_duration(total_time)}")

if __name__ == '__main__':
    main()

Notes:

  • The tracker attributes each polling interval to the app that was foreground at the check. Short switches between polls may be missed; use a smaller interval for higher fidelity.
  • The script writes a CSV on exit with per-app totals.

How it works

  • GetForegroundWindow returns the handle of the active window.
  • GetWindowThreadProcessId yields the process ID owning that window.
  • psutil.Process(pid).name() retrieves the process executable name used as the app key.
  • Time is accumulated using time.monotonic() to avoid clock adjustments.

Step-by-step: run as a background tracker

  1. Create a dedicated folder for logs (e.g., C:\Users\you\AppUsageLogs).
  2. Copy the script there and run it with:
    python track_app_time.py
    
  3. Let it run while you work; press Ctrl+C to stop.
  4. Open the generated CSV in a spreadsheet to analyze.

Optional: Start on login

  • Use Task Scheduler → Create Basic Task → Trigger “When I log on” → Action “Start a program” → Program/script: python, Add arguments: path\to\track_app_time.py → Configure for Windows 10 or later.

Customization ideas

  • Per-window tracking: include window title in the key (e.g., f"{name} | {title}").
  • Category rollups: map process names to categories (Work, Social, etc.).
  • Persistent logging: append a row every minute to a daily CSV instead of only on exit.
  • Idle detection: if no foreground window for N seconds, attribute to Idle.

Pitfalls and caveats

  • UWP apps: Many store apps run under ApplicationFrameHost.exe, hiding the true app identity. Distinguishing them requires UI Automation or package inspection.
  • Admin context: AccessDenied can occur for some processes; the code falls back to UnknownProcess.
  • Multi-user sessions: You only see your session’s foreground window.
  • Accuracy vs. overhead: Smaller poll intervals improve accuracy but increase CPU wakeups.
  • Sleep/lock: When the screen is locked, the foreground may be None; those intervals become NoActiveWindow (treat as Idle).
  • Time changes: Use monotonic time (already handled) to avoid issues with clock skew/DST.

Performance notes

  • CPU usage: At a 0.5s interval, CPU impact is typically <1% on modern machines. Increase interval if needed.
  • Memory: The totals dict grows with unique app names; in practice it remains small.
  • I/O: CSV is written once on exit in the MWE. For long runs, flush periodic snapshots (e.g., every 5–10 minutes) to avoid data loss on crashes.
  • Event-driven alternative: A WinEvent hook on EVENT_SYSTEM_FOREGROUND provides exact switch times with near-zero polling. It’s more complex but reduces missed short switches.

Testing tips

  • Open two apps and rapidly Alt+Tab; decrease poll_interval to 0.2s and verify totals.
  • Lock the screen for 30s; confirm NoActiveWindow accrues time.
  • Kill a tracked process; ensure the script continues without crashing.

Tiny FAQ

  • Q: Can I track minimized or background apps? A: This tracker only counts the foreground app.
  • Q: Can I get per-document time (e.g., each Word file)? A: Include window titles in the key; titles often contain filenames.
  • Q: Does this run on macOS/Linux? A: No. This uses the Windows user32 API.
  • Q: How precise is it? A: With 0.5s polling, expect sub-second average error per switch; very fast switches may be missed.

Next steps

  • Add daily rollover files and periodic autosave.
  • Implement an event-driven foreground change hook for perfect switch timing.
  • Build a small dashboard to visualize trends by day and category.

Series: Automate boring tasks with Python

Python