Thread-safe stopwatch for measuring elapsed time
Project description
ticko
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 nameis_running: bool- Whether the stopwatch is currently runningis_paused: bool- Whether the stopwatch is currently pausedtime_elapsed: float- Total elapsed timetime_since_last_lap: float- Elapsed time since the last lap markerlaps: tuple[float, ...]- Recorded lap durations in recording order (empty until the firstlap()orstop())
Methods:
start()- Start timing and returnNonerestart()- Discard any in-progress measurement and start a new one in a single atomic steppause()- Pause timing and exclude the paused interval from elapsed timeresume()- Resume a paused stopwatchstop()- Stop a running or paused stopwatch, callexit_callbackwhen configured, and return elapsed timelap()- Record and return lap durationreset()- Reset to initial state without callingexit_callback
Exceptions:
StopwatchError- Base class for stopwatch exceptionsAlreadyRunningError- Raised when starting an already running stopwatchNotRunningError- Raised when stopping, lapping, or pausing before start, after stop, or after resetPausedStateError- Raised when starting, pausing, or lapping while pausedNotPausedError- Raised when resuming a stopwatch that is not pausedNotStartedError- Raised when reading elapsed time before the first start or after a resetNoLapsRecordedError- 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
StopWatchtoStopwatch - Rename
StopWatchErrortoStopwatchError - Import from
ticko;ticko.stop_watchis no longer supported - Update
exit_callbackfunctions to accept elapsed seconds asfloat, not a stopwatch instance - Do not rely on
start()returning the start time; it now returnsNone - Rename
time_last_laptotime_since_last_lap - Remove uses of
time_last_lap_startandInvalidStateError - Pass
timer_funcandexit_callbacktoStopwatchas keyword arguments; positional arguments are no longer accepted - Update
except NotStartedErrorhandlers:stop()andlap()before start, after stop, or after reset now raiseNotRunningError, andtime_since_last_lapbefore any lap raisesNoLapsRecordedError - Remove uses of the
time_startandtime_stopproperties; usetime_elapsedfor measurements, or record your own timestamps next tostart()/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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5a23280665302a9ad6454f4684890a9105aae532d542d0ec9ffaee911ecae95d
|
|
| MD5 |
81e276f8bb9d960a6264180e25c6598b
|
|
| BLAKE2b-256 |
bfe1acf262f0db4c3a291e443fce294117cdcbe02c39608e5c8b975e72787de9
|
Provenance
The following attestation bundles were made for ticko-2.0.0.tar.gz:
Publisher:
publish.yml on NakuRei/ticko
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
ticko-2.0.0.tar.gz -
Subject digest:
5a23280665302a9ad6454f4684890a9105aae532d542d0ec9ffaee911ecae95d - Sigstore transparency entry: 1822830759
- Sigstore integration time:
-
Permalink:
NakuRei/ticko@166f177c5a856951a9789b0cadc9b3557f88a426 -
Branch / Tag:
refs/tags/v2.0.0 - Owner: https://github.com/NakuRei
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@166f177c5a856951a9789b0cadc9b3557f88a426 -
Trigger Event:
push
-
Statement type:
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
eb98bad8a68832bb637a07803f854cf7ed7076be691d5d1bd24d19c1320cd866
|
|
| MD5 |
0a646758f9d4e3a3ae207afb5313a668
|
|
| BLAKE2b-256 |
0e1bf81b1c6d5821913828758ce42e941176c141085341a386d01bf99a43d6ec
|
Provenance
The following attestation bundles were made for ticko-2.0.0-py3-none-any.whl:
Publisher:
publish.yml on NakuRei/ticko
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
ticko-2.0.0-py3-none-any.whl -
Subject digest:
eb98bad8a68832bb637a07803f854cf7ed7076be691d5d1bd24d19c1320cd866 - Sigstore transparency entry: 1822830797
- Sigstore integration time:
-
Permalink:
NakuRei/ticko@166f177c5a856951a9789b0cadc9b3557f88a426 -
Branch / Tag:
refs/tags/v2.0.0 - Owner: https://github.com/NakuRei
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@166f177c5a856951a9789b0cadc9b3557f88a426 -
Trigger Event:
push
-
Statement type: