Which PyPI JSON API timestamp field should a client trust when converting release times to regional zones?
0 reputation · 01 Jan 2022, 02:12 UTC
0 reputation · 01 Jan 2022, 02:12 UTC
I'm building a small client that reads release metadata from PyPI's JSON API (/pypi/<project>/json) and displays each file's upload time converted to the viewer's regional time zone. The API exposes two timestamp fields per release file: the legacy upload_time string and upload_time_iso_8601, which is documented as the preferred field and carries a Z suffix indicating UTC.
The ambiguity I'm unsure how to handle: the legacy field has no explicit timezone designator, so parsing it as a naive datetime risks silently treating UTC as local time. The ISO field avoids that, but the two fields can differ in format and fractional-second precision, and I need to decide whether older tooling compatibility justifies keeping the legacy value as a fallback. I'm assuming Python 3.9+ with zoneinfo for the conversion itself.
My questions:
upload_time_iso_8601 guaranteed to always be present for all releases, including very old ones, or must clients handle its absence?35275 reputation · 01 Jan 2022, 06:56 UTC
Use upload_time_iso_8601 as your primary field. It carries an explicit Z (UTC) designator, so parsing it produces an aware datetime with no guessing. Keep upload_time only as a defensive fallback, and always interpret it as UTC — never as local time.
In the current JSON API, upload_time_iso_8601 is populated for all release files, including very old ones; for releases that predate the field, PyPI backfills it from the legacy value. That said, treating it as unconditionally guaranteed is brittle API design. A one-line fallback costs nothing:
from datetime import datetime, timezone
from zoneinfo import ZoneInfo
def upload_dt(file_meta: dict) -> datetime:
iso = file_meta.get("upload_time_iso_8601")
if iso:
# Python 3.9/3.10: fromisoformat doesn't accept 'Z'
return datetime.fromisoformat(iso.replace("Z", "+00:00"))
legacy = file_meta["upload_time"] # e.g. "2013-06-18T19:53:34"
return datetime.strptime(legacy, "%Y-%m-%dT%H:%M:%S").replace(tzinfo=timezone.utc)
local = upload_dt(f).astimezone(ZoneInfo("Europe/Berlin"))
Note the .replace("Z", "+00:00") step: datetime.fromisoformat only accepts Z directly starting in Python 3.11, so the substitution keeps you compatible with your stated 3.9+ floor.
They represent the same instant. Differences you may observe are formatting-only: the ISO field includes fractional seconds and the Z suffix, while the legacy string is a naive YYYY-MM-DDTHH:MM:SS. Two practical consequences:
They answer different questions. Sorting by upload_time_iso_8601 gives you true chronological order of uploads, which is what you want for a "recently published" view — and it correctly handles a maintainer uploading a 1.2.1 bugfix after 2.0. Version metadata is authoritative for logical ordering ("what's the latest version?"), which you should compute with packaging.version.parse rather than timestamps, since upload order and version order routinely diverge on mature projects.
You can confirm the behavior in a minute:
curl -s https://pypi.org/pypi/six/json | python -c "
import json,sys
d=json.load(sys.stdin)
old=d['releases']['0.9.0'][0]
print(old['upload_time'], '|', old['upload_time_iso_8601'])"
For a pre-2015 release you should see the ISO field present and matching the legacy value once the latter is read as UTC. Sorting a package's files by the ISO field should also yield monotonically non-decreasing instants.
One caveat worth flagging: the backfill guarantee reflects the API's current documented behavior, not a contractual promise. The graceful-fallback pattern above keeps your client correct even if that assumption ever breaks.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.