Skip to main content

typed-time-provider

DI-friendly wall-clock and monotonic clock providers with typed time units.

typed-time-provider is a small Python package for applications that need explicit, typed, testable time handling.

It separates three different responsibilities:

  1. capturing wall-clock UNIX timestamps
  2. measuring monotonic elapsed time and deadlines
  3. formatting UNIX timestamps into human-readable strings

The package is designed for dependency injection, strict typing, deterministic tests, and domain models that should not pass raw int timestamps everywhere.

Status

Alpha.

The public API is intentionally small, but the package is already covered by strict linting, type checking, and tests.

Features

  • Typed time units:
    • Hours
    • Minutes
    • Seconds
    • Milliseconds
    • Microseconds
    • Nanoseconds
  • Semantic time unit subclasses, for example:
    • UnixTimestampMicroseconds
    • DurationMilliseconds
    • MonotonicDeadlineMilliseconds
  • Wall-clock provider for UNIX timestamps
  • Monotonic clock provider for elapsed time and deadline checks
  • Timestamp formatter with explicit timezone and precision handling
  • Protocol interfaces for dependency injection
  • Runtime contract validator for application composition boundaries
  • py.typed marker for typed package consumers
  • Strict mypy and pyright compatible public API

Installation

python -m pip install typed-time-provider

For local development:

python -m pip install -e ".[dev]"

Requirements

Python >=3.10.

Runtime dependencies:

  • base-typed-int
  • base-typed-string
  • tzdata

Quick start

Capture a UNIX timestamp

from __future__ import annotations

from typed_time_provider import Microseconds, Nanoseconds, Seconds, WallClock


def main() -> None:
    wall_clock: WallClock[Microseconds] = WallClock(
        preferred_time_unit_type=Microseconds,
    )

    current_unix_microseconds: Microseconds = wall_clock.now_unix()

    current_unix_seconds: Seconds = wall_clock.now_unix(
        return_type=Seconds,
    )

    current_unix_nanoseconds: Nanoseconds = wall_clock.now_unix(
        return_type=Nanoseconds,
    )

    print(current_unix_microseconds)
    print(current_unix_seconds)
    print(current_unix_nanoseconds)


if __name__ == "__main__":
    main()

Wall-clock time

WallClock is for UNIX timestamps only.

Use it when you need to store or send a real-world timestamp, for example:

  • database created_at
  • audit events
  • API response timestamps
  • domain event timestamps
from __future__ import annotations

from typed_time_provider import Microseconds, Seconds, WallClock


def fixed_unix_nanosecond_factory() -> int:
    return 1_775_000_000_123_456_789


def main() -> None:
    wall_clock: WallClock[Microseconds] = WallClock(
        preferred_time_unit_type=Microseconds,
        unix_nanosecond_factory=fixed_unix_nanosecond_factory,
    )

    current_unix_microseconds: Microseconds = wall_clock.now_unix()

    shifted_unix_seconds: Seconds = wall_clock.now_unix_with_delta(
        delta_time=Seconds(30),
        return_type=Seconds,
    )

    print(current_unix_microseconds)
    print(shifted_unix_seconds)


if __name__ == "__main__":
    main()

Monotonic time

MonotonicClock is for durations, elapsed time, and deadlines.

Monotonic values are not UNIX timestamps. Do not store them as real-world time and do not format them as calendar time.

from __future__ import annotations

from typed_time_provider import Milliseconds, MonotonicClock, Nanoseconds


def main() -> None:
    monotonic_clock: MonotonicClock[Milliseconds] = MonotonicClock(
        preferred_time_unit_type=Milliseconds,
    )

    started_at_nanoseconds: Nanoseconds = monotonic_clock.now_monotonic(
        return_type=Nanoseconds,
    )

    elapsed_milliseconds: Milliseconds = monotonic_clock.elapsed_since(
        started_at=started_at_nanoseconds,
    )

    has_elapsed: bool = monotonic_clock.has_elapsed_since(
        started_at=started_at_nanoseconds,
        elapsed_time=Milliseconds(500),
    )

    deadline_nanoseconds: Nanoseconds = monotonic_clock.now_monotonic_with_delta(
        delta_time=Milliseconds(250),
        return_type=Nanoseconds,
    )

    is_deadline_reached: bool = monotonic_clock.is_deadline_reached(
        deadline_time=deadline_nanoseconds,
    )

    print(elapsed_milliseconds)
    print(has_elapsed)
    print(deadline_nanoseconds)
    print(is_deadline_reached)


if __name__ == "__main__":
    main()

Timestamp formatting

TimeFormatter converts already captured UNIX timestamps into human-readable strings.

It does not read system time.

from __future__ import annotations

from typed_time_provider import (
    NanosecondPrecisionFormattedTimestamp,
    Nanoseconds,
    SecondPrecisionFormattedTimestamp,
    Seconds,
    TimeFormatter,
    TimePrecision,
)


def main() -> None:
    nanosecond_formatter: TimeFormatter[NanosecondPrecisionFormattedTimestamp]
    nanosecond_formatter = TimeFormatter(
        default_time_precision=TimePrecision.NANOSECOND,
    )

    second_formatter: TimeFormatter[SecondPrecisionFormattedTimestamp]
    second_formatter = TimeFormatter(
        default_time_precision=TimePrecision.SECOND,
    )

    berlin_timestamp: NanosecondPrecisionFormattedTimestamp
    berlin_timestamp = nanosecond_formatter.format_unix_to_human(
        unix_timestamp=Nanoseconds(1_775_000_000_123_456_789),
        user_timezone_name="Europe/Berlin",
    )

    utc_timestamp: SecondPrecisionFormattedTimestamp
    utc_timestamp = second_formatter.format_unix_to_human(
        unix_timestamp=Seconds(1_775_000_000),
        user_timezone_name=None,
    )

    print(berlin_timestamp)
    print(utc_timestamp)


if __name__ == "__main__":
    main()

user_timezone_name=None means UTC.

Timezone names must be valid IANA timezone names, for example:

"UTC"
"Europe/Berlin"
"Asia/Baku"
"America/New_York"

Formatting precision

Supported formatting precision values:

from typed_time_provider import TimePrecision

TimePrecision.HOUR
TimePrecision.MINUTE
TimePrecision.SECOND
TimePrecision.MILLISECOND
TimePrecision.MICROSECOND
TimePrecision.NANOSECOND

Example output formats:

2026-04-30 14 Europe/Berlin
2026-04-30 14:32 Europe/Berlin
2026-04-30 14:32:45 Europe/Berlin
2026-04-30 14:32:45.123 Europe/Berlin
2026-04-30 14:32:45.123456 Europe/Berlin
2026-04-30 14:32:45.123456789 Europe/Berlin

Dependency injection

Application code should usually depend on protocols, not concrete classes.

from __future__ import annotations

from dataclasses import dataclass

from typed_time_provider import (
    Microseconds,
    SecondPrecisionFormattedTimestamp,
    TimeFormatter,
    TimeFormatterInterface,
    TimePrecision,
    WallClock,
    WallClockInterface,
)


@dataclass(frozen=True, slots=True)
class AuditEvent:
    event_name: str
    created_at_unix_microseconds: Microseconds
    created_at_human: SecondPrecisionFormattedTimestamp


class AuditEventFactory:
    def __init__(
        self,
        wall_clock: WallClockInterface[Microseconds],
        time_formatter: TimeFormatterInterface[SecondPrecisionFormattedTimestamp],
        default_timezone_name: str | None,
    ) -> None:
        self._wall_clock: WallClockInterface[Microseconds] = wall_clock
        self._time_formatter: TimeFormatterInterface[
            SecondPrecisionFormattedTimestamp
        ] = time_formatter
        self._default_timezone_name: str | None = default_timezone_name

    def create_audit_event(
        self,
        event_name: str,
    ) -> AuditEvent:
        created_at_unix_microseconds: Microseconds = self._wall_clock.now_unix()

        created_at_human: SecondPrecisionFormattedTimestamp
        created_at_human = self._time_formatter.format_unix_to_human(
            unix_timestamp=created_at_unix_microseconds,
            user_timezone_name=self._default_timezone_name,
        )

        audit_event: AuditEvent = AuditEvent(
            event_name=event_name,
            created_at_unix_microseconds=created_at_unix_microseconds,
            created_at_human=created_at_human,
        )

        return audit_event


def build_audit_event_factory() -> AuditEventFactory:
    wall_clock: WallClock[Microseconds] = WallClock(
        preferred_time_unit_type=Microseconds,
    )

    time_formatter: TimeFormatter[SecondPrecisionFormattedTimestamp] = TimeFormatter(
        default_time_precision=TimePrecision.SECOND,
    )

    audit_event_factory: AuditEventFactory = AuditEventFactory(
        wall_clock=wall_clock,
        time_formatter=time_formatter,
        default_timezone_name="UTC",
    )

    return audit_event_factory

Semantic time units

You can create semantic subclasses for stronger domain meaning.

from __future__ import annotations

from typed_time_provider import Microseconds, Milliseconds, WallClock


class UnixTimestampMicroseconds(Microseconds):
    """UNIX timestamp stored with microsecond precision."""


class DurationMilliseconds(Milliseconds):
    """Duration stored with millisecond precision."""


def main() -> None:
    wall_clock: WallClock[UnixTimestampMicroseconds] = WallClock(
        preferred_time_unit_type=UnixTimestampMicroseconds,
    )

    created_at: UnixTimestampMicroseconds = wall_clock.now_unix()

    retry_delay: DurationMilliseconds = DurationMilliseconds(500)

    print(created_at)
    print(retry_delay)


if __name__ == "__main__":
    main()

Semantic subclasses are preserved when possible.

Runtime contract validation

Constructors keep injected factories lightweight and side-effect free.

Use TypedTimeProviderRuntimeContractValidator at composition boundaries when invalid wiring should fail fast during startup.

from __future__ import annotations

from typed_time_provider import (
    Microseconds,
    TimePrecision,
    TypedTimeProviderRuntimeContractValidator,
    TimeFormatter,
    WallClock,
)


def unix_nanosecond_factory() -> int:
    return 1_775_000_000_123_456_789


def main() -> None:
    wall_clock: WallClock[Microseconds] = WallClock(
        preferred_time_unit_type=Microseconds,
        unix_nanosecond_factory=unix_nanosecond_factory,
    )

    time_formatter: TimeFormatter = TimeFormatter(
        default_time_precision=TimePrecision.SECOND,
    )

    runtime_contract_validator: TypedTimeProviderRuntimeContractValidator
    runtime_contract_validator = TypedTimeProviderRuntimeContractValidator()

    runtime_contract_validator.validate_wall_clock(
        wall_clock=wall_clock,
    )

    runtime_contract_validator.validate_time_formatter(
        time_formatter=time_formatter,
    )


if __name__ == "__main__":
    main()

Public API

Main imports are available from the root package:

from typed_time_provider import (
    BaseFormattedTimestamp,
    BaseTimeUnit,
    HourPrecisionFormattedTimestamp,
    Hours,
    MicrosecondPrecisionFormattedTimestamp,
    Microseconds,
    MillisecondPrecisionFormattedTimestamp,
    Milliseconds,
    MinutePrecisionFormattedTimestamp,
    Minutes,
    MICROSECONDS_IN_SECOND,
    MILLISECONDS_IN_SECOND,
    MonotonicClock,
    MonotonicClockInterface,
    NanosecondPrecisionFormattedTimestamp,
    Nanoseconds,
    NANOSECONDS_IN_HOUR,
    NANOSECONDS_IN_MICROSECOND,
    NANOSECONDS_IN_MILLISECOND,
    NANOSECONDS_IN_MINUTE,
    NANOSECONDS_IN_SECOND,
    SecondPrecisionFormattedTimestamp,
    Seconds,
    SECONDS_IN_HOUR,
    SECONDS_IN_MINUTE,
    SUPPORTED_TIME_UNIT_TYPES,
    SupportedTimeUnitTypes,
    TimeFormatter,
    TimeFormatterInterface,
    TimePrecision,
    TypedTimeProviderError,
    TypedTimeProviderInvalidInputTypeError,
    TypedTimeProviderInvalidInputValueError,
    TypedTimeProviderInvariantViolationError,
    TypedTimeProviderRuntimeContractValidator,
    TimeUnitType,
    TimeUnitValue,
    WallClock,
    WallClockInterface,
)

Stable time unit conversion constants are also available from the root package:

from typed_time_provider import (
    MICROSECONDS_IN_SECOND,
    MILLISECONDS_IN_SECOND,
    NANOSECONDS_IN_HOUR,
    NANOSECONDS_IN_MICROSECOND,
    NANOSECONDS_IN_MILLISECOND,
    NANOSECONDS_IN_MINUTE,
    NANOSECONDS_IN_SECOND,
    SECONDS_IN_HOUR,
    SECONDS_IN_MINUTE,
)

Design rules

Wall-clock and monotonic time are separate

Use WallClock for real-world UNIX timestamps.

Use MonotonicClock for elapsed time, deadlines, and duration checks.

This avoids mixing two different time domains.

Formatting is separate from capturing time

TimeFormatter does not call time.time_ns().

This keeps formatting deterministic and easy to test.

Nanoseconds are the internal canonical unit

Conversions use nanoseconds internally and return the requested typed unit at API boundaries.

Lower precision conversion truncates

When converting to a lower precision unit, fractional lower-level units are truncated by integer division.

Example:

from typed_time_provider import Microseconds, Nanoseconds
from typed_time_provider.conversion import convert_nanoseconds_to_time_unit

converted_microseconds: Microseconds = convert_nanoseconds_to_time_unit(
    nanoseconds=Nanoseconds(2_500),
    time_unit_type=Microseconds,
)

assert converted_microseconds == Microseconds(2)

Error hierarchy

All package-specific exceptions inherit from TypedTimeProviderError.

TypedTimeProviderError
├── TypedTimeProviderInvalidInputTypeError
├── TypedTimeProviderInvalidInputValueError
└── TypedTimeProviderInvariantViolationError

Development

Install development dependencies:

python -m pip install -e ".[dev]"

Run linting:

ruff check .

Run tests:

pytest

Run mypy:

mypy

Run pyright:

pyright

Build package:

python -m build

Check package metadata:

python -m twine check dist/*

Upload to PyPI:

python -m twine upload dist/*

Examples

Example scripts are available in the examples package.

Run from project root after installing the package in editable mode:

python -m examples.basic_wall_clock_usage
python -m examples.basic_time_formatter_usage
python -m examples.basic_monotonic_clock_usage
python -m examples.dependency_injection_usage
python -m examples.runtime_contract_validation_usage
python -m examples.semantic_time_units_usage

License

MIT License.

See LICENSE for details.

Metadata

Release files for typed-time-provider 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for typed-time-provider 0.2.0
File Size Uploaded
typed_time_provider-0.2.0.tar.gz 28.5 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for typed-time-provider 0.2.0
File Interpreter ABI Platform
typed_time_provider-0.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 58.0 kB

Release files / typed_time_provider-0.2.0.tar.gz

Download URL typed_time_provider-0.2.0.tar.gz
Size 28.5 kB
Tags Source
SHA-256 checksum
How to use checksums
b0a67bb83510a36496e5317951e5b24984a59987f0346b8f55f6ae36903c6c0f
BLAKE2b-256 checksum
How to use checksums
67ea09ae3fed86276b90eae3c495a00501897a3a320a3a4393fb203fff007cb6
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 8, 2026.

Transparency log

Release files / typed_time_provider-0.2.0-py3-none-any.whl

Download URL typed_time_provider-0.2.0-py3-none-any.whl
Size 29.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
51d160811aa0de082209220c0788f48876c74880fd36c25a0fa0a30ef7732aa1
BLAKE2b-256 checksum
How to use checksums
9af1fdc98f0895e52c32bc9c8663ec9388879c4e96f0647c9bc1cbb8956ad98d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 8, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page