Skip to main content

Terminal progress bars via decorators

Project description

⏱ timeo

Terminal progress bars for Python functions — just add a decorator.

PyPI Python License: MIT CI GitHub

✓ process_files   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━  100%  12.4s
  download_data   ━━━━━━━━━━━━━━━━━━━━━━━          72%   0:00:04
  compress_output ━━━━━━                           20%   0:00:31

Why timeo?

Most progress bar libraries ask you to wrap your loops manually and manage the display yourself. timeo gets out of the way — decorate a function, iterate normally, done.

The other thing that sets timeo apart is that it can actually learn how long your functions take. After the first run, it remembers the timing and uses it to drive the progress bar on every subsequent run — so instead of a generic spinner, you get a bar that fills at a pace that reflects reality. The more times a function runs, the more accurate the estimate gets. And if your code changes in a way that affects runtime, timeo detects the drift and resets automatically rather than showing you a bar based on stale data.

# before
for item in items:
    process(item)

# after
@timeo.track
def run(items):
    for item in timeo.iter(items):
        process(item)

Installation

pip install timeo

Usage

Basic progress bar

@timeo.track wraps any function with a live progress bar. The total is inferred automatically from the first argument with a len(). Use timeo.iter() to advance the bar on each iteration — no manual bookkeeping needed.

import timeo

@timeo.track
def process_files(files):
    for f in timeo.iter(files):
        do_work(f)

process_files(my_files)

Prefer manual control? Use timeo.advance() instead:

@timeo.track
def process_files(files):
    for f in files:
        do_work(f)
        timeo.advance()

Multiple concurrent functions

Every decorated function gets its own bar. They render together in a single live display. Finished bars collapse to a compact summary line so the output stays clean.

@timeo.track
def process_files(files):
    for f in timeo.iter(files):
        do_work(f)

@timeo.track
def download_data(urls):
    for url in timeo.iter(urls):
        fetch(url)

process_files(my_files)
download_data(my_urls)
✓ process_files   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━  100%  12.4s
  download_data   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━   72%  0:00:04

Concurrent execution (threads) is fully supported — each bar tracks its own function independently.


Learn mode

Add learn=True and timeo will remember how long your function takes across runs, building an EMA (exponential moving average) of its runtime. Instead of counting steps, the bar fills over the expected duration.

@timeo.track(learn=True)
def run_pipeline(data):
    heavy_computation(data)
    more_work(data)
Run Behaviour
First Indeterminate spinner with run_pipeline (learning...) label
Subsequent Determinate bar filling over the expected duration
After code change Cache invalidates automatically — learning restarts

The cache key is a hash of the function's bytecode — not its name — so renaming a function preserves its history, and changing its implementation resets it.

Estimate accuracy

timeo uses several strategies to keep timing estimates accurate over time:

Decaying alpha — Early runs use a higher smoothing factor so the estimate converges quickly from a cold start (alpha = max(0.2, 1 / run_count)), giving a true running average for the first few runs before settling at the steady-state weight.

Drift detection — If the last 3 actual runtimes collectively deviate from the stored estimate by more than 25%, the entry resets automatically and learning restarts. This handles cases where a function you call internally changed — even if your decorated function's own code didn't.

Explicit dependencies — For finer control, declare which helper functions affect your runtime with depends_on. If any listed function's bytecode changes, the cache key changes and learning restarts:

@timeo.track(learn=True, depends_on=[helper_fn, another_fn])
def run_pipeline(data):
    helper_fn(data)
    another_fn(data)

By default timing data is stored in your platform's user cache directory (e.g. ~/Library/Caches/timeo/timings.json on macOS). Use cache="project" to store it in .timeo/timings.json relative to the current directory instead — useful for per-project isolation or sharing timings with a team via version control:

@timeo.track(learn=True, cache="project")
def run_pipeline(data):
    ...

Note: If using cache="project", add .timeo/ to your .gitignore unless you intentionally want to commit timing data.

cache= Location Best for
"user" (default) ~/.cache/timeo/timings.json Personal scripts, cross-project reuse
"project" .timeo/timings.json Per-project isolation, shared team timings

Explicit display control

The live display starts and stops automatically in most cases. For complex scripts with branching logic or long-running setup, use timeo.live() to pin the display lifetime explicitly:

with timeo.live():
    process_files(my_files)
    download_data(my_urls)
    # display stays open until the with-block exits

CLI

timeo ships a command-line tool for inspecting and managing the learn-mode cache.

View cache contents

timeo cache info                  # user cache (default)
timeo cache info --cache project  # project cache

Output:

Cache location: /Users/you/Library/Caches/timeo/timings.json
Entries: 2

╭─────────────────────┬──────────────┬──────┬─────────────────────╮
│ Function            │ EMA Duration │ Runs │ Last Updated        │
├─────────────────────┼──────────────┼──────┼─────────────────────┤
│ module.run_pipeline │       12.40s │    7 │ 2026-04-10 09:21:00 │
│ module.process_data │        3.20s │    3 │ 2026-04-09 14:05:12 │
╰─────────────────────┴──────────────┴──────┴─────────────────────╯

Reset the cache

timeo cache reset                  # delete entire user cache (default)
timeo cache reset --cache project  # delete entire project cache

You will be prompted to confirm. Pass --yes to skip the prompt:

timeo cache reset --yes

To remove only entries last updated before a specific date, use --before:

timeo cache reset --before 2025-01-01        # remove entries older than Jan 1 2025
timeo cache reset --before 2025-01-01 --yes  # skip confirmation

This leaves newer entries intact — useful for pruning stale data without losing recent timing history.


How it works

Piece Role
@timeo.track Wraps the function, infers total from Sized args, manages the task lifecycle
ProgressManager Singleton owning the rich live display; reference-counted so it tears down only when all tasks finish
TrackedTask Dataclass holding per-function progress state
timeo.iter() Thin generator wrapper that calls advance() on each item
ContextVar Ensures timeo.advance() always updates the right bar, even across threads

The display is built on rich.progress and rich.live.


Built with rich · MIT License

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

timeo-0.5.2.tar.gz (34.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

timeo-0.5.2-py3-none-any.whl (16.9 kB view details)

Uploaded Python 3

File details

Details for the file timeo-0.5.2.tar.gz.

File metadata

  • Download URL: timeo-0.5.2.tar.gz
  • Upload date:
  • Size: 34.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for timeo-0.5.2.tar.gz
Algorithm Hash digest
SHA256 d2b6f4459449cfcd68c523512cc9f911b2c05a5d4459dfbd1e3d6d0301aca51a
MD5 ce544c0a0d610fc74035915a29da4c70
BLAKE2b-256 e7e97acc4044d163dca9651fca6c229ce4a6ed7612eb045eca9dd75f302b05f1

See more details on using hashes here.

Provenance

The following attestation bundles were made for timeo-0.5.2.tar.gz:

Publisher: publish.yml on wtewalt/timeo

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file timeo-0.5.2-py3-none-any.whl.

File metadata

  • Download URL: timeo-0.5.2-py3-none-any.whl
  • Upload date:
  • Size: 16.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for timeo-0.5.2-py3-none-any.whl
Algorithm Hash digest
SHA256 f4bb9ee033fba718c8a665ad44fab3247f186c77dab06b946c658484d08d6b2e
MD5 371120427bb3446e37c48e54d1883f3a
BLAKE2b-256 4c81d076fad52b241df35a10bcf1cda0668f24e976aedc09112596b7776d5515

See more details on using hashes here.

Provenance

The following attestation bundles were made for timeo-0.5.2-py3-none-any.whl:

Publisher: publish.yml on wtewalt/timeo

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page