ISO8601 without an offset is semantically different to one with an offset, it represents a time in local timezone (context-dependent). It isn't an "optional" offset in the sense that you can just omit it, it is a fundamentally different data type.
Without this distinction, there is no way to specify a local time in ISO8601, which would be highly inconvenient for certain applications. For example, how do you represent an event that occurs at 9am every day regardless of location? After all, dates and times are used for more than just storing absolute timestamps.
You are absolutely correct that offsets are also not timezones, which makes the ability to specify local "floating" times even more important (i.e. you can't just denormalize the above concept into a list of timestamps with offsets for each timezone you care about, as the offsets will change over time [edit: and tz->offset conversion is lossy and not reversible]).
Maybe there should be a "float" offset marker. Another thing I was reminded of is that +0000 offset should be Z (UTC), but it is often application dependent if -0000 offset is also Z as some applications use -0000 for "user local time, regardless of user". Which is related to "floating", but yet another semantic difference.
I suspect a side-effect of libraries lacking support for the full range of ISO8601 (for example, refusing to parse a value without an offset, forcing people to use such hacks). Wouldn't surprise me; iOS doesn't even have a consistent way to parse ISO8601 values with and without milliseconds.
[edit:
Apparently this is a RFC3339 thing, did not know that: https://tools.ietf.org/html/rfc3339#section-4.3
Negative zero offset is invalid in ISO8601, but valid and carries the meaning you describe in RFC3339. I misread what that meaning actually is from your comment - it is different to a "floating" or unqualified local time, it always represents a UTC time but with an unknown local time offset. ]
Is the distinction in representation too subtle?
We accept this subtlety elsewhere; we are used to 0 and "0" being different things, and expect them to behave differently under operators. Few would find the following surprising:
0 + 0 == 0
"0" + "0" == "00"
Then why would we expect these to behave the same? 20200710T010000Z + `3 hours`
20200710T010000 + `3 hours`
The first is a fixed timestamp in UTC, the result of the second depends on timezone.ECMAScript specifies "simplified extended ISO 8601", which is just that string above.
It took me quite a while staring at Stack Overflow answers and my code, wondering why the SO answers supposedly worked and my code did not, before I realized I had time.strptime and they were using datetime.strptime.
That also reminds me that .NET documentation likes to pedantically remind me that what I think of as ISO 8601 is probably more specifically IETF RFC 3339, which defines a formal BNF grammar as ISO didn't think to do that in the 80s. (Yay, standards.)
python-dateutil for incomplete but stdlibbish ISO 8601:2004.
python-edtf for ISO 8601-2:2019.
However, the ISO 8601-2:2019 spec is something of a clusterfuck born out of EDTF.
Most public discourse about it can be found in various confused crossover threads from people who need open-ended dates for their projects.[2-9]
Here's a haskell implementation of ISO 8601-1:2019 and ISO 8601-2:2019, for reference.[10]
[1]: https://github.com/metomi/isodatetime/issues/138
[2]: https://www.loc.gov/standards/datetime/
[3]: https://www.loc.gov/standards/datetime/background.html
[4]: https://www.loc.gov/standards/datetime/implementations.html
[5]: https://github.com/plk/biblatex/issues/656
[6]: https://github.com/ixc/python-edtf/issues/24
[7]: https://github.com/saw-leipzig/csv2cmi/issues/18
[8]: https://github.com/schemaorg/schemaorg/issues/242
[9]: https://github.com/JohnLukeBentley/open-datetime-standard-bo...
Why is it so hard to configure a Linux system (its locale settings) to get RFC 3339 formatted dates everywhere? Sure, I can `ls -l --time-style=full-iso`, `git log --date=iso`, `date --iso-8601=s`, `date --rfc-3339=s` and change the configuration of every single application by hand. Am I missing something?
Often, people recommend the hackish `LC_TIME=en_DK.UTF-8`. This however, doesn't work for Java applications and caused various other issues.