Skip to content

datetime: add ability to parse RFC 3339 dates and times #60077

Description

@nagle
mannequin
BPO 15873
Nosy @warsaw, @jcea, @cben, @ncoghlan, @abalkin, @vstinner, @jwilk, @mcepl, @merwok, @bitdancer, @karlcow, @elprans, @flying-sheep, @mihaic, @Fak3, @berkerpeksag, @vadmium, @boxed, @jstasiak, @offby1, @deronnax, @pbryan, @pganssle, @sirex, @jaitaiwan
PRs
  • bpo-15873: Implement [date][time].fromisoformat #4699
  • bpo-15873: add '.fromisoformat' for date, time and datetime #4841
  • bpo-15873Add What's new entry for datetime.fromisoformat #5559
  • bpo-15873Add What's new entry for datetime.fromisoformat #5559
  • [3.7] bpo-15873 Add What's new entry for datetime.fromisoformat (GH-5559) #5939
  • Files
  • issue15873-proto.diff
  • test-cases.py: Test cases
  • fromisoformat.patch
  • fromisoformat2.patch: slightly improved version, better use of timedelta
  • fromisoformat3.patch
  • fromisoformat4.patch
  • simplerfromisoformat.patch: simpler, stricter version of fromisoformat
  • fromisoformat_new.patch
  • fromisoformat_new2.patch
  • fromisoformat_new3.patch
  • fromisoformat_singledispatch.patch
  • fromisoformat_regexinclasses.patch: regex as class attributes
  • fromisoformat_strptimesingledispatch.patch: regex looked up by class type in _strptime.py
  • fromisoformat_regexinclasses2.patch: regex as class attributes, 2nd version
  • Note: these values reflect the state of the issue at the time it was migrated and might not reflect the current state.

    Show more details

    GitHub fields:

    assignee = 'https://github.com/abalkin'
    closed_at = <Date 2018-03-01.18:50:17.237>
    created_at = <Date 2012-09-06.21:08:49.605>
    labels = ['3.7', 'type-feature', 'library']
    title = 'datetime: add ability to parse RFC 3339 dates and times'
    updated_at = <Date 2018-03-01.18:50:17.236>
    user = 'https://bugs.python.org/nagle'

    bugs.python.org fields:

    activity = <Date 2018-03-01.18:50:17.236>
    actor = 'belopolsky'
    assignee = 'belopolsky'
    closed = True
    closed_date = <Date 2018-03-01.18:50:17.237>
    closer = 'belopolsky'
    components = ['Library (Lib)']
    creation = <Date 2012-09-06.21:08:49.605>
    creator = 'nagle'
    dependencies = []
    files = ['27141', '27165', '41922', '41923', '41926', '41927', '41934', '41935', '41940', '41945', '41951', '44015', '44016', '44019']
    hgrepos = []
    issue_num = 15873
    keywords = ['patch']
    message_count = 90.0
    messages = ['169941', '169952', '169966', '169968', '169970', '170098', '170104', '170109', '170112', '170114', '170116', '170180', '170181', '174339', '183672', '183743', '183809', '183921', '183931', '221829', '221830', '221831', '221903', '260099', '260100', '260150', '260266', '260276', '260280', '260282', '260292', '260293', '260294', '260295', '260298', '260303', '260309', '260318', '260337', '260342', '260343', '260344', '260345', '260347', '260350', '260356', '260382', '260420', '260426', '260427', '260440', '260441', '260442', '260445', '260449', '260989', '260990', '260991', '263867', '269714', '269722', '270529', '270828', '270829', '270831', '270899', '272021', '272026', '273609', '291822', '291831', '304950', '307603', '307604', '307605', '307606', '307607', '307610', '307616', '308214', '308505', '308507', '308510', '308569', '308637', '308851', '309168', '309175', '311703', '313105']
    nosy_count = 36.0
    nosy_names = ['barry', 'jcea', 'cben', 'roysmith', 'ncoghlan', 'belopolsky', 'nagle', 'vstinner', 'jwilk', 'mcepl', 'eric.araujo', 'Arfrever', 'r.david.murray', 'davydov', 'cvrebert', 'karlcow', 'SilentGhost', 'Elvis.Pranskevichus', 'perey', 'flying sheep', 'mihaic', 'aymeric.augustin', 'Roman.Evstifeev', 'berker.peksag', 'martin.panter', 'piotr.dobrogost', 'kirpit', 'Anders.Hovm\xc3\xb6ller', 'jstasiak', 'Eric.Hanchrow', 'deronnax', 'pbryan', 'p-ganssle', 'sirex', 'larsonreever', 'jaitaiwan']
    pr_nums = ['4699', '4841', '5559', '5559', '5939']
    priority = 'normal'
    resolution = 'fixed'
    stage = 'resolved'
    status = 'closed'
    superseder = None
    type = 'enhancement'
    url = 'https://bugs.python.org/issue15873'
    versions = ['Python 3.7']

    Activity

    1. nagle commented on Sep 6, 2012

      naglemannequin
      MannequinAuthor

      The datetime module has support for output to a string of dates and times in ISO 8601 format ("2012-09-09T18:00:00-07:00"), with the object method "isoformat([sep])". But there's no support for parsing such strings. A string to datetime class method should be provided, one capable of parsing at least the RFC 3339 subset of ISO 8601.

      The problem is parsing time zone information correctly.
      The allowed formats for time zone are
      empty - no TZ, date/time is "naive" in the datetime sense
      Z - zero, or Zulu time, i.e. UTC.
      [+-]nn.nn - offset from UTC

      "strptime" does not understand timezone offsets. The "datetime" documentation suggests that the "z" format directive handles time zone info, but that's not actually implemented for input.

      Pypi has four modules for parsing ISO 8601 dates. Each has least one major
      problem in time zone handling:

      iso8601 0.1.4
      Abandonware. Mishandles time zone when time zone is "Z" and
      the default time zone is specified.
      iso8601.py 0.1dev
      Always returns a "naive" datetime object, even if zone specified.
      iso8601plus 0.1.6
      Fork of abandonware version above. Same bug.
      zc.iso8601 0.2.0
      Zope version. Imports the pytz module with the full Olsen time zone
      database, but doesn't actually use that database.

      Thus, nothing in Pypi provides a good alternative.

      It would be appropriate to handle this in the datetime module. One small, correct, tested function would be better than the existing five bad alternatives.

    2. added
      stdlibStandard Library Python modules in the Lib/ directory
      type-featureA feature request or enhancement
      on Sep 6, 2012
    3. AlexanderBelopolsky commented on Sep 6, 2012

      AlexanderBelopolskymannequin
      Mannequin

      %z format is supported, but it cannot accept colon in TZ offset. It can parse offsets like -0600 just fine. What OP is looking for is the GNU date %:z format which datetime does not support.

      For ISO 8601 compliance, however I think we need a way to specify a parser that will accept any valid 8601 format: with T or space separator and with or without : in time and timezone and with or without dashes in date.

      I would very much like such promiscuous parser to be implemented in datetime.__new__. So that we can create datetime objects from strings the way we do it with numbers.

    4. nagle commented on Sep 7, 2012

      naglemannequin
      MannequinAuthor

      Re: "%z format is supported".

      That's platform-specific; the actual parsing is delegated to the C library. It's not in Python 2.7 / Win32:

      ValueError: 'z' is a bad directive in format '%Y-%m-%dT%H:%M:%S%z'

      It really shouldn't be platform-specific; the underlying platform is irrelevant to this task. That's more of a documentation error; the features not common to all supported Python platforms should not be mentioned in the documentation.

      Re: "I would very much like such promiscuous parser to be implemented in datetime.__new__. "

      For string input, it's probably better to do this conversion in a specific class-level function. Full ISO 8601 dates/times generally come from computer-generated data via a file or API. If invalid text shows up, it should be detected as an error, not be heuristically interpreted as a date. There's already "fromtimestamp" and "fromordinal",
      and "isoformat" as an instance method, so "fromisoformat" seems reasonable.

      I'd also suggest providing a standard subclass of tzinfo in datetime for fixed offsets. That's needed to express the time zone information in an ISO 8601 date. The new "fromisoformat" would convert an ISO 8601 date/time would be convertible to a time-zone "aware" datetime object. If converted back to an ISO 8601 string with .isoformat(), the round trip should preserve the original data, including time zone offset.

      (Several more implementations of this conversion have turned up. In addition to the four already mentioned, there was one in xml.util, and one in feedparser. There are probably more yet to be found.)

    5. AlexanderBelopolsky commented on Sep 7, 2012

      AlexanderBelopolskymannequin
      Mannequin

      On Thu, Sep 6, 2012 at 9:51 PM, John Nagle <report@bugs.python.org> wrote:

      It's not in Python 2.7 / Win32.

      Python 2.x series is closed and cannot accept new features. Both %z
      and fixed offset tzinfo subclass are implemented in 3.2.

    6. abalkin commented on Sep 7, 2012

      @abalkin
      Member

      I am attaching a quick python only prototype for the proposed feature. My goal is to make date/time objects behave like numeric types for which constructors accept strings produced by str(). Since str() format is ISO 8601, it is natural to accept ISO 8601 formats in constructors.

    7. roysmith commented on Sep 9, 2012

      roysmithmannequin
      Mannequin

      We need to define the scope of what input strings will be accepted. ISO-8601 defines a lot of stuff which we may not wish to accept.

      Do we want to accept both basic format (YYYYMMDD) and extended format (YYYY-MM-DD)?

      Do we want to accept things like "1985-W15-5", which is (if I understand this correctly(), the 5th day of the 15th week of 1985 [section 4.1.4.2].

      Do we want to accept [section 4.2.2.4], "23:20,8", which is 23 hours, 20 minutes, 8 tenths of a minute.

      I suspect most people who have been following the recent thread (https://groups.google.com/d/topic/comp.lang.python/Q2w4R89Nq1w/discussion) would say none of the above are needed. All that's needed is if you have an existing datetime object, d1, you can do:

      s = str(d1)
      d2 = datetime.datetime(s)
      assert d1 == d2

      for all values of d1.

      But, let's at least agree on that. Or, in the alternative, agree on something else. Then we know what we're shooting for.

    8. AlexanderBelopolsky commented on Sep 9, 2012

      AlexanderBelopolskymannequin
      Mannequin

      On Sep 9, 2012, at 8:15 AM, Roy Smith <report@bugs.python.org> wrote:

      We need to define the scope of what input strings will be accepted.

      Since it is easier to widen the domain of acceptable arguments than to narrow it in the future, I would say let's start by accepting str(x) only where x is date, time, timezone or datetime. I would leave out timedelta for now because it's str(x) does not resemble ISO at all.

      Either that or full ISO 8601. Anything in between is just too hard to explain.

    9. roysmith commented on Sep 9, 2012

      roysmithmannequin
      Mannequin

      I see I mis-stated my example. When I wrote:

      s = str(d1)
      d2 = datetime.datetime(s)
      assert d1 == d2

      what I really meant was:

      s = d1.isoformat()
      d2 = datetime.datetime(s)
      assert d1 == d2

      But, now I realize that while that is certainly an absolute lower bound, it's almost certainly not sufficient. The most common use case I see on a daily basis is parsing strings that look like "2012-09-07T23:59:59+00:00". This is also John Nagle's original use case from the cited mailing list thread:

      I want to parse standard ISO date/time strings such as
      2012-09-09T18:00:00-07:00

      Datetime.isoformat() returns something that matches the beginning of that, but doesn't have the time zone offset. And it's the offset that makes strptime() not usable as a soluation, because "%z" isn't portable.

      If we don't satisfy the "2012-09-07T23:59:59+00:00" case, then we won't have really done anything useful.

    10. nagle commented on Sep 9, 2012

      naglemannequin
      MannequinAuthor

      For what parts of ISO 8601 to accept, there's a standard: RFC3339, "Date and Time on the Internet: Timestamps". See section 5.6:

      date-fullyear = 4DIGIT
      date-month = 2DIGIT ; 01-12
      date-mday = 2DIGIT ; 01-28, 01-29, 01-30, 01-31 based on
      ; month/year
      time-hour = 2DIGIT ; 00-23
      time-minute = 2DIGIT ; 00-59
      time-second = 2DIGIT ; 00-58, 00-59, 00-60 based on leap second
      ; rules
      time-secfrac = "." 1*DIGIT
      time-numoffset = ("+" / "-") time-hour ":" time-minute
      time-offset = "Z" / time-numoffset

      partial-time = time-hour ":" time-minute ":" time-second
      [time-secfrac]
      full-date = date-fullyear "-" date-month "-" date-mday
      full-time = partial-time time-offset

      date-time = full-date "T" full-time

      NOTE: Per [ABNF] and ISO8601, the "T" and "Z" characters in this
      syntax may alternatively be lower case "t" or "z" respectively.

        ISO 8601 defines date and time separated by "T".
        Applications using this syntax may choose, for the sake of
        readability, to specify a full-date and full-time separated by
        (say) a space character.
      

      That's straightforward, and can be expressed as a regular expression.

    11. abalkin commented on Sep 9, 2012

      @abalkin
      Member

      I realize that while that is certainly an absolute lower bound,
      it's almost certainly not sufficient. The most common use case
      I see on a daily basis is parsing strings that look like
      "2012-09-07T23:59:59+00:00".

      This is exactly what isoformat() of an aware datetime looks like:

      >>> datetime.now(timezone.utc).isoformat()
      '2012-09-09T16:09:46.165886+00:00'

      str() is the same up to T replaced by space:

      >>> print(datetime.now(timezone.utc))
      2012-09-09 15:19:12.567692+00:00
    12. abalkin commented on Sep 9, 2012

      @abalkin
      Member

      For what parts of ISO 8601 to accept, there's a standard: RFC3339

      This is almost indistinguishable from the idea of accepting .isoformat() and str() results. From what I see the only difference is that 't' is accepted for date/time separator and 'z' is accepted as a timezone.

      Let's start with this.

      As an ultimate solution, I would like to see something like codec registry so that we can do things like datetime(.., format='rfc3339') or date(.., format='gnu') for GNU parse_datetime. I think this will look more pythonic than strptime(). Of course, strptime format can also be accepted as the value for the format keyword.

    13. self-assigned this
      on Sep 10, 2012
    14. roysmith commented on Sep 10, 2012

      roysmithmannequin
      Mannequin

      I've started collecting some test cases. I'll keep adding to the collection. I'm going to start trolling ISO 8601:2004(E) for more. Let me know if there are other sources I should be considering.

    15. 67 remaining items

    16. abalkin commented on Dec 5, 2017

      @abalkin
      Member

      +1 on what Paul said.

      Mathieu, the goal for 3.7 will be to get Paul's PR merged. It will be great if you could help in reviewing it. We can return to the features in your PR during the 3.8 development cycle.

    17. pganssle commented on Dec 5, 2017

      @pganssle
      Member

      The other difference is Mattieu guarantees ValueError for invalid input strings, which I think is good.

      I forgot to address this - but I don't think this is a difference in approaches. If you pass None or an int or something, the problem is with the type, not the value, so at a minimum you're looking at TypeError and ValueError - and those are the only exceptions raised in my patch.

      (I'll note that my patch does not accept bytes, though this is something of an artificial limitation, since the patch makes use of the fact that all valid isoformat() strings will contain at most exactly 1 non-ascii character in position 10, so we could easily work around this, but I think the trend for CPython is to avoid blurring the lines between bytes and str rather than encouraging their interchangeable use.)

    18. deronnax commented on Dec 13, 2017

      deronnaxmannequin
      Mannequin

      I finally released my work. It looks like Paul's work is more comprehensive, but if you want to pick one thing or two in mine, feel free.

    19. vadmium commented on Dec 18, 2017

      @vadmium
      Member

      Regarding Matthieu’s RFC 3339 parser, Victor wanted to use the round-half-to-even rule to get a whole number of microseconds. But considering the “time” class cannot represent 24:00, how do you round up in the extreme case past 23:59?

      time.fromisoformat("23:59:59.9999995")

      Perhaps it is better to always truncate to zero, only support 6 digits (rejecting fractions of a microsecond), or add Anders’s truncate_microseconds=True option.

    20. pganssle commented on Dec 18, 2017

      @pganssle
      Member

      @martin.panter I don't see the problem here? Wouldn't 23:59.9999995 round up to 00:00?

    21. vadmium commented on Dec 18, 2017

      @vadmium
      Member

      Not if the time is associated with a particular day. Imagine implementing datetime.fromisoformat by separately calling date.fromisoformat and time.fromisoformat. The date will be off by one day if you naively rounded 2017-12-18 23:59 “up” to 2017-12-18 00:00.

    22. pganssle commented on Dec 18, 2017

      @pganssle
      Member

      Not if the time is associated with a particular day. Imagine implementing datetime.fromisoformat by separately calling date.fromisoformat and time.fromisoformat. The date will be off by one day if you naively rounded 2017-12-18 23:59 “up” to 2017-12-18 00:00.

      Yes, I suppose this is a problem if you implement it that way. Seems like a somewhat moot point, but I think any decision about rounding should probably be driven by what people are expecting more than by how it is implemented.

      That said, I can see a good case for truncation *and* rounding up for something like '2016-12-31T23:59:59.999999999'. Rounding up to '2017-01-01' is certainly the closest whole millisecond to round to, *but* often people expressing a "23:59:59.9999999" are trying to actually express "the last possible moment *before* 00:00".

    23. jaitaiwan commented on Dec 19, 2017

      jaitaiwanmannequin
      Mannequin

      I wanted to note here... I've been trying to get strptime to work with the types of dates specified in this request and came across a documentation bug here: https://docs.python.org/3.5/library/time.html#time.strptime

      You can see that the %z attribute's examples given have colons in them while the format specified is +HHMM rather than +HH:MM which the examples illude to.

    24. abalkin commented on Dec 21, 2017

      @abalkin
      Member

      New changeset 09dc2f5 by Alexander Belopolsky (Paul Ganssle) in branch 'master':
      bpo-15873: Implement [date][time].fromisoformat (bpo-4699)
      09dc2f5

    25. deronnax commented on Dec 29, 2017

      deronnaxmannequin
      Mannequin

      maybe it's worth adding an entry in python 3.7 "what's new" ? I think it was a very long awaited issue.
      The opposite of isoformat() is a very frequent question from python newcomers

    26. bitdancer commented on Dec 29, 2017

      @bitdancer
      Member

      Correct, a new feature should always get a what's new entry. You could submit a PR for it :)

    27. abalkin commented on Feb 6, 2018

      @abalkin
      Member

      New changeset 22864bc by Alexander Belopolsky (Paul Ganssle) in branch 'master':
      Add What's new entry for datetime.fromisoformat (bpo-5559)
      22864bc

    28. abalkin commented on Mar 1, 2018

      @abalkin
      Member

      New changeset 0e06be8 by Alexander Belopolsky (Miss Islington (bot)) in branch '3.7':
      Add What's new entry for datetime.fromisoformat (GH-5559) (GH-5939)
      0e06be8

    29. transferred this issue fromon Apr 10, 2022
    Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

    Metadata

    Metadata

    Assignees

    Labels

    3.7 (EOL)end of lifestdlibStandard Library Python modules in the Lib/ directorytype-featureA feature request or enhancement

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions