Skip to main content

Thread-safe stopwatch for measuring elapsed time

Project description

ticko

CI codecov Python 3.10+ License: MIT

A modern, thread-safe stopwatch library for Python.

Why ticko?

  • Thread-safe by design - Use confidently in concurrent applications
  • Type-safe - Full type hints for excellent IDE support
  • Zero dependencies - Pure Python, no external requirements
  • Flexible API - Context managers, decorators, or manual control
  • Well-tested - Comprehensive test coverage

Installation

pip install ticko

Quick Start

from ticko import Stopwatch

# Basic usage
with Stopwatch() as sw:
    # Your code here
    pass

print(f"Elapsed: {sw.time_elapsed:.2f}s")
from ticko import stopwatch

# Decorator for function timing
@stopwatch
def process_data():
    # Your code here
    pass

process_data()  # Prints execution time to stdout by default

Core Features

Context Managers

with Stopwatch() as sw:
    # ... your code ...
    pass

print(f"Elapsed: {sw.time_elapsed:.3f}s")

The context manager starts timing on entry and stops on exit. If the block raises an exception, the original exception is preserved.

Manual Control

sw = Stopwatch()
sw.start()
# ... your code ...
elapsed = sw.stop()

Pause and Resume

sw = Stopwatch()
sw.start()

# ... active work ...
sw.pause()
# ... waiting or setup that should not count ...
sw.resume()
# ... more active work ...

elapsed = sw.stop()

Paused intervals are excluded from time_elapsed. Use is_paused to check for the paused state. Calling stop() while paused finalizes the stopwatch with the elapsed time frozen at pause().

Lap Timing

sw = Stopwatch()
sw.start()

# Record multiple laps
lap1 = sw.lap()
lap2 = sw.lap()

elapsed = sw.stop()

The full history of recorded lap durations is available through the laps property, which returns an immutable tuple in recording order. Each lap() appends the duration since the previous lap (or since start() for the first one), and stop() appends the final segment from the last lap (or from start() if none) to the stop time. The durations therefore sum to time_elapsed. The tuple is empty until the first lap() or stop() records a duration, so a start() -> stop() with no lap() in between yields a single duration equal to time_elapsed. start() and reset() clear the history.

sw = Stopwatch()
sw.start()
sw.lap()
sw.lap()
sw.stop()

print(sw.laps)  # Recorded lap durations, including the final stop segment

Decorator Timing

@stopwatch
def load_data() -> list[str]:
    return ["alpha", "beta", "gamma"]


load_data()

The decorator prints elapsed time to stdout by default. Pass exit_callback to send elapsed seconds to logging, metrics, stderr, or another destination.

Generator and async generator functions are timed during consumption, not object creation, and time spent between yielded values is excluded. For partially consumed async generators, fully consume the generator or call await generator.aclose() when elapsed time must be reported.

Custom Callbacks

Stopwatch and @stopwatch accept an exit_callback with the signature Callable[[float], None]. The callback receives elapsed seconds when timing finishes.

If the program uses stdout for structured data or piped output, pass exit_callback to route timing information elsewhere.

import sys


def report_time(elapsed: float) -> None:
    print(f"Execution took {elapsed:.3f}s", file=sys.stderr)


sw = Stopwatch(exit_callback=report_time)
sw.start()
# ... your code ...
sw.stop()

The same callback form works with the decorator:

@stopwatch(exit_callback=report_time)
def my_function():
    pass

Or route timing to the logging system:

import logging


logger = logging.getLogger(__name__)


def log_time(elapsed: float) -> None:
    logger.info("Execution took %.3fs", elapsed)


@stopwatch(exit_callback=log_time)
def my_function():
    pass

Thread Safety

from concurrent.futures import ThreadPoolExecutor

sw = Stopwatch()
sw.start()

# Multiple threads can safely share one Stopwatch
with ThreadPoolExecutor(max_workers=5) as executor:
    for _ in range(10):
        executor.submit(sw.lap)

elapsed = sw.stop()

For more examples, see the examples/ directory.

API Overview

Stopwatch

Constructor:

  • Stopwatch(*, name=None, timer_func=time.perf_counter, exit_callback=None) - Create a stopwatch with optional naming, custom timing, and stop callback

When passing a custom timer_func, keep it fast and side-effect-light. The stopwatch may call it while holding its internal lock, so the timer function must not call methods or properties on the same Stopwatch instance.

Properties:

  • name: str | None - Optional stopwatch name
  • is_running: bool - Whether the stopwatch is currently running
  • is_paused: bool - Whether the stopwatch is currently paused
  • time_elapsed: float - Total elapsed time
  • time_since_last_lap: float - Elapsed time since the last lap marker
  • laps: tuple[float, ...] - Recorded lap durations in recording order (empty until the first lap() or stop())

Methods:

  • start() - Start timing and return None
  • restart() - Discard any in-progress measurement and start a new one in a single atomic step
  • pause() - Pause timing and exclude the paused interval from elapsed time
  • resume() - Resume a paused stopwatch
  • stop() - Stop a running or paused stopwatch, call exit_callback when configured, and return elapsed time
  • lap() - Record and return lap duration
  • reset() - Reset to initial state without calling exit_callback

Exceptions:

  • StopwatchError - Base class for stopwatch exceptions
  • AlreadyRunningError - Raised when starting an already running stopwatch
  • NotRunningError - Raised when stopping, lapping, or pausing before start, after stop, or after reset
  • PausedStateError - Raised when starting, pausing, or lapping while paused
  • NotPausedError - Raised when resuming a stopwatch that is not paused
  • NotStartedError - Raised when reading elapsed time before the first start or after a reset
  • NoLapsRecordedError - Raised when reading lap elapsed time before any lap

@stopwatch

Decorator for quick, visible function timing.

By default, the decorator prints a human-readable timing message to stdout every time the decorated function exits. The output includes the callable name and elapsed seconds. Use exit_callback when timing information should go to stderr, logging, metrics, or another destination.

Works on regular functions, callable objects, async def functions, generator functions, async generator functions, and callable objects whose __call__ uses one of those forms. For async callables, timing covers the awaited body until it returns or raises.

For generator and async generator callables, timing starts when the returned generator is first consumed. Timing is reported when consumption completes, when an exception leaves the wrapper, or when the generator is explicitly closed. For partially consumed async generators, fully consume the generator or call await generator.aclose() when elapsed time must be reported.

Time spent between yielded values is excluded. Generator timing is the sum of time spent executing the generator body, including cleanup run by close() or aclose(). To measure the wall-clock duration of an entire consumption session, wrap the consumer loop with Stopwatch instead.

Decorated generator and async generator callables return protocol-compatible wrapper objects that support the full generator protocol and satisfy collections.abc.Generator / collections.abc.AsyncGenerator checks, but they are not native generator objects. Code relying on concrete-type introspection (inspect.isgenerator(), inspect.isgeneratorfunction(), and the async equivalents) may not recognize them.

When a synchronous function returns an awaitable object, timing ends when that object is returned. Awaiting or consuming that object is not measured.

Migrating from 1.x

  • Rename StopWatch to Stopwatch
  • Rename StopWatchError to StopwatchError
  • Import from ticko; ticko.stop_watch is no longer supported
  • Update exit_callback functions to accept elapsed seconds as float, not a stopwatch instance
  • Do not rely on start() returning the start time; it now returns None
  • Rename time_last_lap to time_since_last_lap
  • Remove uses of time_last_lap_start and InvalidStateError
  • Pass timer_func and exit_callback to Stopwatch as keyword arguments; positional arguments are no longer accepted
  • Update except NotStartedError handlers: stop() and lap() before start, after stop, or after reset now raise NotRunningError, and time_since_last_lap before any lap raises NoLapsRecordedError
  • Remove uses of the time_start and time_stop properties; use time_elapsed for measurements, or record your own timestamps next to start()/stop() if you need absolute times

Development

# Install with dev dependencies
uv sync --dev

# Install pre-commit hooks
uv run pre-commit install

# Run tests
uv run pytest tests/

# Run tests with coverage report
uv run pytest tests -v --cov=src --cov-report=term-missing --cov-report=xml:cov.xml

# Type checking
uv run mypy .

# Lint checking
uv run ruff check

# Format checking
uv run ruff format --check --diff

License

MIT License - Copyright (c) 2025 NakuRei

Contributing

Contributions welcome! Feel free to open issues or submit pull requests.

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

ticko-2.0.0.tar.gz (16.0 kB view details)

Uploaded Source

Built Distribution

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

ticko-2.0.0-py3-none-any.whl (17.0 kB view details)

Uploaded Python 3

File details

Details for the file ticko-2.0.0.tar.gz.

File metadata

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

File hashes

Hashes for ticko-2.0.0.tar.gz
Algorithm Hash digest
SHA256 5a23280665302a9ad6454f4684890a9105aae532d542d0ec9ffaee911ecae95d
MD5 81e276f8bb9d960a6264180e25c6598b
BLAKE2b-256 bfe1acf262f0db4c3a291e443fce294117cdcbe02c39608e5c8b975e72787de9

See more details on using hashes here.

Provenance

The following attestation bundles were made for ticko-2.0.0.tar.gz:

Publisher: publish.yml on NakuRei/ticko

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

File details

Details for the file ticko-2.0.0-py3-none-any.whl.

File metadata

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

File hashes

Hashes for ticko-2.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 eb98bad8a68832bb637a07803f854cf7ed7076be691d5d1bd24d19c1320cd866
MD5 0a646758f9d4e3a3ae207afb5313a668
BLAKE2b-256 0e1bf81b1c6d5821913828758ce42e941176c141085341a386d01bf99a43d6ec

See more details on using hashes here.

Provenance

The following attestation bundles were made for ticko-2.0.0-py3-none-any.whl:

Publisher: publish.yml on NakuRei/ticko

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