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:
- capturing wall-clock UNIX timestamps
- measuring monotonic elapsed time and deadlines
- 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:
HoursMinutesSecondsMillisecondsMicrosecondsNanoseconds
- Semantic time unit subclasses, for example:
UnixTimestampMicrosecondsDurationMillisecondsMonotonicDeadlineMilliseconds
- 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.typedmarker for typed package consumers- Strict
mypyandpyrightcompatible 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-intbase-typed-stringtzdata
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)
| File | Size | Uploaded | |
|---|---|---|---|
| typed_time_provider-0.2.0.tar.gz | 28.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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 logRelease 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