Skip to main content

pydura

Parse, format, and apply human-readable durations with nanosecond precision.

from pydura import Duration, format_duration, parse

delay = parse('1 day, 3h & 15m')

str(delay)  # '1 day 3 hours 15 minutes'
format_duration(delay, max_units=2)  # '1 day 3 hours'
delay.total_seconds()  # 98100.0

Getting started

With a Python 3.14+ virtual environment active:

python -m pip install pydura

There are three things to get familiar with:

  • parse(text) reads a duration from text.
  • Duration(...) builds one from integer amounts, like Duration(minutes=5).
  • format_duration(value) writes a duration or timedelta as words.

Reading durations

Compact strings, full words, and a bit of extra text all work:

parse('1h30m')
parse('1.5 hours')
parse('wait 1 hour and another 30 minutes')
# All three give you the same duration.

Units are case-insensitive, and whitespace between a number and its unit is fine. Here are the supported spellings:

Unit Spellings
Year y, yr, yrs, year, years
Month mo, mon, mons, month, months
Week w, wk, wks, week, weeks
Day d, day, days
Hour h, hr, hrs, hour, hours
Minute m, min, mins, minute, minutes
Second s, sec, secs, second, seconds
Millisecond ms, msec, msecs, millisecond, milliseconds
Microsecond us, µs, μs, usec, microsecond, microseconds
Nanosecond ns, nsec, nanosecond, nanoseconds

A year is always 365 days, a month 30 days, a week 7 days, and a day 24 hours. These are elapsed durations, so adding a month won't adjust for which calendar month you're in.

Extra text and errors

The parser picks out the number-and-unit pairs it knows and skips everything else. 1h 999 elephants gives you one hour. If nothing matches, it raises ValueError. Non-string input raises TypeError.

That makes it forgiving, but also means it won't catch every typo. A bare 0 needs a unit (0s works), and scientific notation, HH:MM times, and ISO 8601 durations aren't supported. For example, 1e3s becomes three seconds because 1e is skipped. Keep that in mind if you're validating user input.

Negative durations

A sign on the first recognized component becomes the default for later unsigned components. A later explicit sign affects just that component:

str(parse('-1h30m'))  # '-1 hour 30 minutes'
str(parse('-1h+30m15m'))  # '-45 minutes': -60 + 30 - 15
str(parse('1h-30m15m'))  # '45 minutes': 60 - 30 + 15

Working with a duration

Build a duration with any mix of integer weeks, days, hours, minutes, seconds, milliseconds, microseconds, and nanoseconds:

timeout = Duration(seconds=30, milliseconds=500)

timeout * 2  # Duration(nanoseconds=61000000000)
timeout + Duration(seconds=5)  # Duration(nanoseconds=35500000000)
timeout - Duration(milliseconds=500)  # Duration(nanoseconds=30000000000)

Values are immutable, so arithmetic gives you a new duration. You can add and subtract durations or timedelta objects, multiply by an integer in either order, negate a duration, or take its abs().

Durations can also be sorted, compared with each other, and used as dictionary keys. Only a zero duration is false in a boolean check. To compare with a timedelta, convert it with Duration.from_timedelta() first.

The constructor accepts integers, including negative ones. Use parse('1.5h') for fractions; passing a float or boolean to the constructor raises TypeError.

Precision

Every duration stores an exact integer .nanoseconds value:

parse('1.000000001s').nanoseconds  # 1000000001

Parsing uses exact decimal arithmetic. Each component drops any fraction of a nanosecond toward zero before the components are added, so 0.5ns 0.5ns is zero. Use .total_seconds() when you want a float and don't need every nanosecond.

Values can grow beyond a signed 64-bit integer. Python's integer-string digit limit still applies to exceptionally long whole-number inputs and formatted output.

Dates and timedeltas

Add a duration to a datetime, or convert it to and from a timedelta:

from datetime import UTC, datetime, timedelta

delay = parse('1h30m')
start = datetime(2026, 1, 1, 12, tzinfo=UTC)

start + delay  # 2026-01-01 13:30:00+00:00
start - delay  # 2026-01-01 10:30:00+00:00
delay.to_timedelta()  # timedelta(seconds=5400)
Duration.from_timedelta(timedelta(minutes=90)) == delay  # True

Timezone-aware datetimes keep their timezone. Adding 1d advances exactly 24 elapsed hours, including across daylight saving changes, so the local clock hour may change. Naive datetimes stay naive.

Python's timedelta stores microseconds. If conversion would lose nanoseconds, pydura raises ValueError unless you explicitly allow truncation:

parse('1501ns').to_timedelta(truncate=True)  # timedelta(microseconds=1)
parse('-1501ns').to_timedelta(truncate=True)  # timedelta(microseconds=-1)
parse('1501ns').add_to(start, truncate=True)

Datetime arithmetic uses exact conversion by default. Use add_to() with truncate=True when dropping the remainder is okay.

Conversions and datetime arithmetic can raise OverflowError outside the standard library's supported range. Timezones with changing offsets also need the resulting UTC datetime to fit that range.

Turning durations into words

str(duration) includes every nonzero component, down to nanoseconds. format_duration() does the same and also accepts a timedelta:

format_duration(timedelta(hours=2, minutes=15))  # '2 hours 15 minutes'
format_duration(parse('2h15m3s'), max_units=2)  # '2 hours 15 minutes'
str(Duration())  # '0 milliseconds'

max_units limits the number of nonzero components. Smaller units are left off without rounding. Use a positive integer, or None for everything. Full output can be passed back to parse() without losing precision.

Development

With the project's virtual environment active:

python -m pip install -e ".[dev]"
python -m pytest --cov=pydura --cov-branch
python -m ruff check .
python -m mypy
python -m build
python -m twine check --strict dist/*
python benchmarks/benchmark.py

The benchmark runner covers parsing, formatting, arithmetic, and large inputs. Pass --baseline /path/to/saved_duration.py to compare with a saved copy of pydura/_duration.py. Add --json /path/to/results.json to keep the measurements.

Metadata

Release files for pydura 0.1.1

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

Source distribution (sdist)

Source distribution for pydura 0.1.1
File Size Uploaded Provenance Actions
pydura-0.1.1.tar.gz 18.4 kB With provenance Details for pydura-0.1.1.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for pydura 0.1.1
File Interpreter ABI Platform Actions
pydura-0.1.1-py3-none-any.whl Python 3 none any Details for pydura-0.1.1-py3-none-any.whl

Total release size: 27.7 kB

Release files / pydura-0.1.1.tar.gz

Download URL pydura-0.1.1.tar.gz
Size 18.4 kB
Tags Source
SHA-256 checksum
How to use checksums
fc2195782deff1a6c4238958d8dfd4bac633a196c902d2af445492d28f671e62
BLAKE2b-256 checksum
How to use checksums
3de4749e62ca2e0a6ff50c6352e61bfa92d0fe29aa0186071d88754921b9604b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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 Oct 5, 2026.

Transparency log

Release files / pydura-0.1.1-py3-none-any.whl

Download URL pydura-0.1.1-py3-none-any.whl
Size 9.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
42b097c9e54e9d822b514100134517fe128388e78211ef4ffeeff69ee2221151
BLAKE2b-256 checksum
How to use checksums
1dd8d5817455f2a334b471a61fb75910ca9e3fae5a88662c2d2aaf4fe8a7a2a4
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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 Oct 5, 2026.

Transparency log

Security for pydura 0.1.1

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.

Full provenance

Both files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for pydura-0.1.1.tar.gz

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 Oct 5, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.1.1 This release

2 release files Full provenance

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