221

I have a python method which accepts a date input as a string.

How do I add a validation to make sure the date string being passed to the method is in the ffg. format:

'YYYY-MM-DD'

if it's not, method should raise some sort of error

jamylak
  • 128,818
  • 30
  • 231
  • 230
codemickeycode
  • 2,555
  • 2
  • 18
  • 16
  • 3
    It might be more Pythonic (ask for forgiveness, not permission) not to check at all, and catch any resulting exceptions that occur. – Thomas Jun 01 '13 at 08:23
  • Related: [In python, how to check if a date is valid?](http://stackoverflow.com/q/9987818/55075) – kenorb Jul 27 '15 at 20:35

5 Answers5

338
>>> import datetime
>>> def validate(date_text):
        try:
            datetime.date.fromisoformat(date_text)
        except ValueError:
            raise ValueError("Incorrect data format, should be YYYY-MM-DD")

    
>>> validate('2003-12-23')
>>> validate('2003-12-32')

Traceback (most recent call last):
  File "<pyshell#20>", line 1, in <module>
    validate('2003-12-32')
  File "<pyshell#18>", line 5, in validate
    raise ValueError("Incorrect data format, should be YYYY-MM-DD")
ValueError: Incorrect data format, should be YYYY-MM-DD

Note that datetime.date.fromisoformat() obviously works only when date is in ISO format. If you need to check date in some other format, use datetime.datetime.strptime().

Mortasen
  • 117
  • 1
  • 8
jamylak
  • 128,818
  • 30
  • 231
  • 230
  • 13
    Is there a way of doing that without a try/except? Python tends to slow down significantly when an exception is raised and caught. – chiffa Sep 06 '16 at 01:26
  • 1
    @chiffa You could match a date format regex but its not recommended because it's less robust and exceptions are clearer. Are you sure date validation is your bottleneck? – jamylak Sep 06 '16 at 02:52
  • 2
    Not really, so in the end I will just wrap throw-except construct in a function. I am just surprised that there is no bool-returning validating function that would trigger the Exception throw in the datetime library. – chiffa Sep 12 '16 at 16:26
  • @chiffa Maybe they didnt include bool returning validating function on purpose, it might exist in external libraries – jamylak Sep 13 '16 at 02:48
  • I know its been over a year, but I was parsing some logs, and happen to be stripping out lines that did not start with a date. I just tested the suggested method against checking the timestring for "startswith()". ~590k lines of logs. Using dateutil parser and its exceptions : 129 sec. Using dumb as hell "startswith": 3 sec. Now I know it doesn't work for everyone, but if a method that works in most cases is 2 orders of magnitude faster, its 2 orders of magnitude faster. I got about 2000x more logs to work through for my script, guess what I'm gonna use? – Miao Liu Mar 23 '18 at 02:41
  • 3
    For those who want zero padding in dates, this solution won't work as strptime is not strict about zero padding. Implement a regex of your own or check the length of the resultant string after stripping whitespace and then make use of this solution . – รยקคгรђשค May 28 '18 at 19:55
  • @MiaoLiu In your use case you've found the method that is fastest in raw speed, you've done the right thing. this is a more generic solution for a different problem – jamylak Jul 01 '18 at 09:16
  • 1
    Yes, agree with @Suparshva For example this string will be assumed as correct date, without ValueError: "2018-10-1" – Vladimir Chub Oct 10 '18 at 13:09
102

The Python dateutil library is designed for this (and more). It will automatically convert this to a datetime object for you and raise a ValueError if it can't.

As an example:

>>> from dateutil.parser import parse
>>> parse("2003-09-25")
datetime.datetime(2003, 9, 25, 0, 0)

This raises a ValueError if the date is not formatted correctly:

>>> parse("2003-09-251")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/jacinda/envs/dod-backend-dev/lib/python2.7/site-packages/dateutil/parser.py", line 720, in parse
    return DEFAULTPARSER.parse(timestr, **kwargs)
  File "/Users/jacinda/envs/dod-backend-dev/lib/python2.7/site-packages/dateutil/parser.py", line 317, in parse
    ret = default.replace(**repl)
ValueError: day is out of range for month

dateutil is also extremely useful if you start needing to parse other formats in the future, as it can handle most known formats intelligently and allows you to modify your specification: dateutil parsing examples.

It also handles timezones if you need that.

Update based on comments: parse also accepts the keyword argument dayfirst which controls whether the day or month is expected to come first if a date is ambiguous. This defaults to False. E.g.

>>> parse('11/12/2001')
>>> datetime.datetime(2001, 11, 12, 0, 0) # Nov 12
>>> parse('11/12/2001', dayfirst=True)
>>> datetime.datetime(2001, 12, 11, 0, 0) # Dec 11
Jacinda
  • 4,932
  • 3
  • 26
  • 37
  • 2
    it may accept too much e.g., `parse('13/12/2001')` is "13 Dec" but `parse('11/12/2001')` is "12 Nov" (the first result would suggest "11 Dec" here). – jfs Nov 02 '15 at 12:41
  • 5
    `parse` actually takes a `dayfirst` keyword argument that allows you to control this. `parse('11/12/2001', dayfirst=True)` will return "11 Dec." dateutil's default is `dayfirst=False` – Jacinda Nov 02 '15 at 20:40
  • 2
    you are missing the point that `datetutil.parser.parse()` accepts too many time formats (you could find other examples with ambiguous input). If you want to *validate* that your input is in YYYY-MM-DD format then the `parse()` function is the wrong tool. – jfs Nov 04 '15 at 14:03
  • 2
    That's a completely valid point - if you really want to restrict to just that specific format this doesn't do that, and the accepted answer already does a great job of doing the right thing in that case. I think when I wrote the answer I was thinking more along the lines of pointing out how to validate whether it was a valid date as opposed to the particular format that the author requested, which when people come across this question is what they're often looking for. – Jacinda Nov 05 '15 at 16:14
  • Is there a way to get `.parse()` to return the format string in addition to the `datetime` object? – citynorman Oct 05 '17 at 17:13
  • I find this function problematic - parse('1') is valid. But, really, it is not. – Nabakamal Das Oct 01 '20 at 12:42
71

I think the full validate function should look like this:

from datetime import datetime

def validate(date_text):
    try:
        if date_text != datetime.strptime(date_text, "%Y-%m-%d").strftime('%Y-%m-%d'):
            raise ValueError
        return True
    except ValueError:
        return False

Executing just

datetime.strptime(date_text, "%Y-%m-%d") 

is not enough because strptime method doesn't check that month and day of the month are zero-padded decimal numbers. For example

datetime.strptime("2016-5-3", '%Y-%m-%d')

will be executed without errors.

Eduard Stepanov
  • 1,183
  • 8
  • 9
  • 6
    "You are technically correct - the best kind of correct." I needed to ensure this in my strings. – delrocco Oct 12 '17 at 21:49
  • This works fine against my tests, however I the documentation seems incorrect as it states: "%d -> Day of the month as a zero-padded decimal number -> 01, 02, …, 31" and the same for the %m -> Month as a zero-padded decimal number. -> 01, 02, …, 12 https://docs.python.org/2/library/datetime.html#strftime-strptime-behavior – thanos.a Jan 13 '20 at 19:58
  • 4
    If you need to check that month and day are zero-padded, wouldn't it suffice just to check the length of the string and `datetime.strptime(date_text, "%Y-%m-%d")`? – Kyle Barron Feb 09 '20 at 01:12
  • this is the only correct answer. especially when there are multiple datetime formats that need to be supported – Ubaid Qureshi Aug 21 '23 at 19:13
22
from datetime import datetime

datetime.strptime(date_string, "%Y-%m-%d")

..this raises a ValueError if it receives an incompatible format.

..if you're dealing with dates and times a lot (in the sense of datetime objects, as opposed to unix timestamp floats), it's a good idea to look into the pytz module, and for storage/db, store everything in UTC.

Mr. B
  • 2,536
  • 1
  • 26
  • 26
  • 2
    You were faster, I would have posted it myself (http://ideone.com/vuxDDf). Upvote. – Tadeck Jun 01 '13 at 08:25
  • ..just saw it right after it was posted, and happen to have been working with datetime objects today. – Mr. B Jun 01 '13 at 08:27
4

From mere curiosity, I timed the two rivalling answers posted above.
And I had the following results:

dateutil.parser (valid str): 4.6732222699938575
dateutil.parser (invalid str): 1.7270505399937974
datetime.strptime (valid): 0.7822393209935399
datetime.strptime (invalid): 0.4394566189876059

And here's the code I used (Python 3.6)


from dateutil import parser as date_parser
from datetime import datetime
from timeit import timeit


def is_date_parsing(date_str):
    try:
        return bool(date_parser.parse(date_str))
    except ValueError:
        return False


def is_date_matching(date_str):
    try:
        return bool(datetime.strptime(date_str, '%Y-%m-%d'))
    except ValueError:
        return False



if __name__ == '__main__':
    print("dateutil.parser (valid date):", end=' ')
    print(timeit("is_date_parsing('2021-01-26')",
                 setup="from __main__ import is_date_parsing",
                 number=100000))

    print("dateutil.parser (invalid date):", end=' ')
    print(timeit("is_date_parsing('meh')",
                 setup="from __main__ import is_date_parsing",
                 number=100000))

    print("datetime.strptime (valid date):", end=' ')
    print(timeit("is_date_matching('2021-01-26')",
                 setup="from __main__ import is_date_matching",
                 number=100000))

    print("datetime.strptime (invalid date):", end=' ')
    print(timeit("is_date_matching('meh')",
                 setup="from __main__ import is_date_matching",
                 number=100000))
Gergely M
  • 583
  • 4
  • 11