This issue tracker has been migrated to GitHub, and is currently read-only.
For more information, see the GitHub FAQs in the Python's Developer Guide.

classification
Title: Add option to output UTC datetimes as "Z" in `.isoformat()`
Type: enhancement Stage: patch review
Components: Library (Lib) Versions: Python 3.11
process
Status: open Resolution:
Dependencies: Superseder:
Assigned To: p-ganssle Nosy List: belopolsky, brett.cannon, eric.araujo, godlygeek, p-ganssle
Priority: normal Keywords: patch

Created on 2022-02-02 17:31 by p-ganssle, last changed 2022-04-11 14:59 by admin.

Pull Requests
URL Status Linked Edit
PR 32041 open godlygeek, 2022-03-22 03:53
Messages (6)
msg412384 - (view) Author: Paul Ganssle (p-ganssle) * (Python committer) Date: 2022-02-02 17:31
As part of bpo-35829, it was suggested that we add the ability to output the "Z" suffix in `isoformat()`, so that `fromisoformat()` can both be the exact functional inverse of `isoformat()` and parse datetimes with "Z" outputs. I think that that's not a particularly compelling motivation for this, but I also see plenty of examples of `datetime.utcnow().isoformat() + "Z"` out there, so it seems like this is a feature that we would want to have *anyway*, particularly if we want to deprecate and remove `utcnow`.

I've spun this off into its own issue so that we can discuss how to implement the feature. The two obvious questions I see are:

1. What do we call the option? `use_utc_designator`, `allow_Z`, `utc_as_Z`?
2. What do we consider as "UTC"? Is it anything with +00:00? Just `timezone.utc`? Anything that seems like a fixed-offset zone with 0 offset?

For example, do we want this?

>>> LON = zoneinfo.ZoneInfo("Europe/London")
>>> datetime(2022, 3, 1, tzinfo=LON).isoformat(utc_as_z=True)
2022-03-01T00:00:00Z
>>> datetime(2022, 6, 1, tzinfo=LON).isoformat(utc_as_z=True)
2022-06-01T00:00:00+01:00

Another possible definition might be if the `tzinfo` is a fixed-offset zone with offset 0:

>>> datetime.timezone.utc.utcoffset(None)
timedelta(0)
>>> zoneinfo.ZoneInfo("UTC").utcoffset(None)
timedelta(0)
>>> dateutil.tz.UTC.utcoffset(None)
timedelta(0)
>>> pytz.UTC.utcoffset(None)
timedelta(0)

The only "odd man out" is `dateutil.tz.tzfile` objects representing fixed offsets, since all `dateutil.tz.tzfile` objects return `None` when `utcoffset` or `dst` are passed `None`. This can and will be changed in future versions.

I feel like "If the offset is 00:00, use Z" is the wrong rule to use conceptually, but considering that people will be opting into this behavior, it is more likely that they will be surprised by `datetime(2022, 3, 1, tzinfo=ZoneInfo("Europe/London").isoformat(utc_as_z=True)` returning `2022-03-01T00:00:00+00:00` than alternation between `Z` and `+00:00`.

Yet another option might be to add a completely separate function, `utc_isoformat(*args, **kwargs)`, which is equivalent to (in the parlance of the other proposal) `dt.astimezone(timezone.utc).isoformat(*args, **kwargs, utc_as_z=True)`.  Basically, convert any datetime to UTC and append a Z to it. The biggest footgun there would be people using it on naïve datetimes and not realizing that it would interpret them as system local times.
msg412876 - (view) Author: Éric Araujo (eric.araujo) * (Python committer) Date: 2022-02-08 22:53
Would it be horrible to have the timezone instance control this?
msg413102 - (view) Author: Matt Wozniski (godlygeek) * Date: 2022-02-11 22:16
> I feel like "If the offset is 00:00, use Z" is the wrong rule to use conceptually

This is a really good point that I hadn't considered: `+00:00` and `Z` are semantically different, and just because a datetime has a UTC offset of 0 doesn't mean it should get a `Z`; `Z` is reserved specifically for UTC.

It seems like the most semantically correct thing would be to only use `Z` if `tzname()` returns exactly "UTC". That would do the right thing for your London example for every major timezone library I'm aware of:

>>> datetime.datetime.now(zoneinfo.ZoneInfo("Europe/London")).tzname()
'GMT'
>>> datetime.datetime.now(zoneinfo.ZoneInfo("UTC")).tzname()
'UTC'
>>> datetime.datetime.now(datetime.timezone.utc).tzname()
'UTC'

>>> datetime.datetime.now(dateutil.tz.gettz("Europe/London")).tzname()
'GMT'
>>> datetime.datetime.now(dateutil.tz.UTC).tzname()
'UTC'

>>> datetime.datetime.now(pytz.timezone("Europe/London")).tzname()
'GMT'
>>> datetime.datetime.now(pytz.UTC).tzname()
'UTC'

I think the right rule to use conceptually is "if `use_utc_designator` is true and the timezone name is 'UTC' then use Z". We could also check the offset, but I'm not convinced we need to.
msg416622 - (view) Author: Paul Ganssle (p-ganssle) * (Python committer) Date: 2022-04-03 14:59
I think this approach is probably the best we can do, but I could also imagine that users might find it to be confusing behavior. I wonder if there's any informal user testing we can do?

I guess the ISO 8601 spec does call "Z" the "UTC designator", so `use_utc_designator` seems like approximately the right name. My main hesitation with this name is that I suspect users may think that `use_utc_designator` means that they *unconditionally* want to use `Z` — without reading the documentation (which we can assume 99% of users won't do) — you might assume that `dt.isoformat(use_utc_designator=True)` would translate to `dt.astimezone(timezone.utc).replace(tzinfo=None).isoformat() + "Z"`.

A name like `utc_as_z` is definitely less... elegant, but conveys the concept a bit more clearly. Would be worth throwing it to a poll or something before merging.
msg416638 - (view) Author: Éric Araujo (eric.araujo) * (Python committer) Date: 2022-04-03 18:30
Bad idea: pass `zulu=True`

It is short, memorable if you know about it, otherwise obscure enough to push people to read the docs and be clear about what it does.

Also strange and far from obvious, so a bad idea.  Unless… ?
msg416648 - (view) Author: Matt Wozniski (godlygeek) * Date: 2022-04-04 01:30
> My main hesitation with this name is that I suspect users may think that `use_utc_designator` means that they *unconditionally* want to use `Z` — without reading the documentation (which we can assume 99% of users won't do)

I was thinking along similar lines when I used `use_utc_designator` in the PR, but I drew a different conclusion. I was thinking that the name `use_utc_designator` is sufficiently abstruse that no one would even be able to guess that it's referring to "Z" without actually reading the documentation for the parameter. In particular, I worry that `zulu=True` or `allow_Z=True` might lead people to make the mistake of thinking that they'll always get "Z" instead of "+00:00".

> A name like `utc_as_z` is definitely less... elegant, but conveys the concept a bit more clearly.

This would definitely be more memorable and more approachable. If we stick with making it conditional on `tzname() == "UTC"`, I definitely think we want to have "utc" in the name of the parameter, and `utc_as_z` satisfies that.

`utc_as_z` seems reasonable to me. Let me know if you'd like me to update the PR.
History
Date User Action Args
2022-04-11 14:59:55adminsetgithub: 90772
2022-04-04 01:30:42godlygeeksetmessages: + msg416648
2022-04-03 18:30:05eric.araujosetmessages: + msg416638
2022-04-03 14:59:11p-gansslesetmessages: + msg416622
2022-03-22 03:53:35godlygeeksetkeywords: + patch
stage: needs patch -> patch review
pull_requests: + pull_request30131
2022-02-11 22:16:24godlygeeksetnosy: + godlygeek
messages: + msg413102
2022-02-08 22:53:39eric.araujosetnosy: + eric.araujo
messages: + msg412876
2022-02-02 17:31:46p-gansslecreate