-
-
Notifications
You must be signed in to change notification settings - Fork 30.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
datetime.strptime without a year fails on Feb 29 #70647
Comments
$ python
Python 3.5.1 (default, Dec 7 2015, 12:58:09)
[GCC 5.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>>
>>>
>>>
>>> import time
>>>
>>> time.strptime("Feb 29", "%b %d")
time.struct_time(tm_year=1900, tm_mon=2, tm_mday=29, tm_hour=0, tm_min=0, tm_sec=0, tm_wday=0, tm_yday=60, tm_isdst=-1)
>>>
>>>
>>> import datetime
>>>
>>> datetime.datetime.strptime("Feb 29", "%b %d")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/usr/lib/python3.5/_strptime.py", line 511, in _strptime_datetime
return cls(*args)
ValueError: day is out of range for month The same issue is seen in all versions of Python |
Python's time.strptime() behavior is consistent with that of glibc 2.19: ======= strptime_c.c ======= #define _XOPEN_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
int
main(void)
{
struct tm tm;
char buf[255];
memset(&tm, 0, sizeof(struct tm));
strptime("Feb 29", "%b %d", &tm);
strftime(buf, sizeof(buf), "%d %b %Y %H:%M", &tm);
puts(buf);
exit(EXIT_SUCCESS);
} ======= $ gcc strptime_c.c
$ ./a.out
29 Feb 1900 00:00 I'm not saying that the behavior is a good API, but given the unfortunate API at hand, parsing a date without specifying what year it is using strptime is a bad idea. |
This is not no more bug than >>> from datetime import *
>>> datetime.strptime('0228', '%m%d')
datetime.datetime(1900, 2, 28, 0, 0) Naturally, as long as datetime.strptime('0228', '%m%d') is the same as datetime.strptime('19000228', '%Y%m%d'), datetime.strptime('0229', '%m%d') should raise a ValueError as long as datetime.strptime('19000229', '%Y%m%d') does. The only improvement, I can think of in this situation is to point the user to time.strptime() in the error message. The time.strptime method works just fine in the recent versions (see bpo-14157.) >>> time.strptime('0229', '%m%d')[1:3]
(2, 29) |
time.strptime() is "working" (not raising an exception) as it appears not to validate the day of the month when a year is not specified, yet the return value from either of these APIs is a date which has no concept of an ambiguous year. ## Via the admittedly old Python 2.7.6 from Ubuntu 14.04: ##
# 1900 was not a leap year as it is not divisible by 400.
>>> time.strptime("1900 Feb 29", "%Y %b %d")
ValueError: day is out of range for month
>>> time.strptime("Feb 29", "%b %d")
time.struct_time(tm_year=1900, tm_mon=2, tm_mday=29, tm_hour=0, tm_min=0, tm_sec=0, tm_wday=0, tm_yday=60, tm_isdst=-1) So what should the validation behavior be? >>> datetime.datetime.strptime("Feb 29", "%b %d")
ValueError: day is out of range for month
>>> datetime.datetime.strptime("2016 Feb 29", "%Y %b %d")
datetime.datetime(2016, 2, 29, 0, 0)
>>> datetime.datetime.strptime("1900 Feb 29", "%Y %b %d")
ValueError: day is out of range for month
>>> datetime.datetime(year=1900, month=2, day=29)
ValueError: day is out of range for month datetime objects cannot be constructed with the invalid date (as the time.strptime return value allows). Changing the API to assume the current year or a +/- 6 months from "now" when no year is parsed is likely to break existing code. |
I suspect this is going to come up about this time of every leap year :-/ The workaround is prepending "%Y " to the pattern and eg: "2020 " to the date string, but that's not very nice. Would adding a kwarg "default_year" be an acceptable solution? In the weird case where you want to do date maths involving the month as well, you can always use a safe choice like "default_year=2020" and then fix the year up afterwards:
|
I don't think adding a default_year parameter is the right solution here. The actual problem is that Since there is no concept of a partial datetime, I think our best option would be to throw an exception, except that this has been baked into the library for ages and would start to throw exceptions even when the person has correctly handled the Feb 29th case. I think one possible "solution" to this would be to raise a warning any time someone tries to use |
Not disagreeing with you that "%b %d" timestamps with no "%Y" are excerable, but they're fairly common in the *nix world unfortunately. People need to parse them, and the simple and obvious way to do this breaks every four years. I like the idea of having a warning for not including %Y *and* not setting a default_year kwarg though. |
I _doubt_ there is code expecting the default year when unspecified to actually be 1900. Change that default to any old year with a leap year (1904?) and it'll still (a) stand out as a special year that can be looked up should it wind up being _used_ as the year in code somewhere and (b) not fail every four years for people just parsing to extract Month + Day values. |
In the 21st century, the year 2000 default makes much more sense than 1900. Luckily 2000 is also a leap year. |
Yes, code that has been working for my organization the past two years just broke this weekend. Meaning depends on context. The straightforward solution is that if no year is specified, the return value should default to the current year. |
It's kind of funny that there's already consideration of this in _strptime._strptime(), which returns a tuple used by datetime.datetime.strptime() to construct the new datetime. I think the concern though is if we changed the default year that might possibly break someone's existing code: thus my suggestion to allow the programmer to explicitly change the default. However, I can also see that if their code is parsing dates in this way it is already wrong, and that if we're causing users pain now when they upgrade Python we're at least saving them pain at 2024-02-29 00:00:01. Taking that approach, perhaps parsing dates with no year should just throw an exception, forcing the programmer to do it right the first time. In this case though, I'd rather have a "year" kwarg to prevent the programmer having to do horrible string hacks like my code currently does. I'm not sure: is it useful for me to produce a PR so we have something specific to consider? |
Unfortunately, we encountered this problem again recently on 2024-02-29. It seems that the source code has considered the issue that 1900 is not a leap year and change the year to 1904. Lines 490 to 496 in 2e94a66
But it was eventually modified to return to 1900 Lines 535 to 539 in 2e94a66
Then an exception occurs when we execute datetime.strptime("02-29", "%m-%d") , so what's the point of doing this?
This hidden problem will occur once every four years. For those who encounter it once, it will be fixed directly. I still hope to fix it in later versions, for example, change it to 2000, or only return 1904 when the date is 2.29 and explain it in the documentation. |
I would be in favor of either raising an exception if the year is unspecified or changing the default to some leap year. 2000 is fine. I think I don't think it's a terrible string hack to have people prepend a default year to their strings, considering they know the format, so I'm not really in favor of the added complexity of something like "default_year" (or a whole host of default_x parameters). I would say raising an exception is the "correct" thing to do, but also the most annoying fix here, so I'm on the fence about it. Either way, we should probably get this one fixed by ~2026-2027 😛 |
…datetime. Every four years people encounter this because it just isn't obvious. This moves the footnote up to a note with a code example. We'd love to change the default year value for datetime but doing that could have other consequences for existing code. This documented workaround *always* works.
If nothing else, more clearly calling this out with the recommended workaround in the docs is a good idea. Draft PR up to do that. While changing the default Users clearly want a library to parse partial values. Otherwise this bug wouldn't keep coming up like clockwork. So if we want to raise an exception, we also need to offer an actual API intended for partial value parsing... The Perhaps a The third party |
… month. The presence of the values in the error message gives a stronger hint as to what went wrong. ``` >>> datetime.strptime("2.29", "%m.%d") Traceback (most recent call last): File "<stdin>", line 1, in <module> datetime.strptime("2.29", "%m.%d") ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^ File ".../Lib/_strptime.py", line 565, in _strptime_datetime return cls(*args) ~~~^^^^^^^ ValueError: day 29 is out of range for month 2 in year 1900 ```
…ent to avoid leap-year bugs (GH-117107)
…r present to avoid leap-year bugs (pythonGH-117107)
…rning (pythonGH-117668) * Fix `test_strptime` raises a DeprecationWarning * Ignore deprecation warnings where appropriate. * Update Lib/test/datetimetester.py This is follow on work to silence unnecessary warnings from the test suite that changes for python#70647 added. (cherry picked from commit abead54) Co-authored-by: Nice Zombies <[email protected]>
…arning (GH-117668) (GH-118956) gh-117655: Prevent `test_strptime` from raising a DeprecationWarning (GH-117668) * Fix `test_strptime` raises a DeprecationWarning * Ignore deprecation warnings where appropriate. * Update Lib/test/datetimetester.py This is follow on work to silence unnecessary warnings from the test suite that changes for #70647 added. (cherry picked from commit abead54) Co-authored-by: Nice Zombies <[email protected]>
... /usr/lib/python3.13/site-packages/jupyter_client/jsonutil.py:31: in <module> datetime.strptime("1", "%d") # noqa /usr/lib64/python3.13/_strptime.py:573: in _strptime_datetime tt, fraction, gmtoff_fraction = _strptime(data_string, format) /usr/lib64/python3.13/_strptime.py:336: in _strptime format_regex = _TimeRE_cache.compile(format) /usr/lib64/python3.13/_strptime.py:282: in compile return re_compile(self.pattern(format), IGNORECASE) /usr/lib64/python3.13/_strptime.py:270: in pattern warnings.warn("""\ E DeprecationWarning: Parsing dates involving a day of month without a year specified is ambiguious E and fails to parse leap day. The default behavior will change in Python 3.15 E to either always raise an exception or to use a different default year (TBD). E To avoid trouble, add a specific year to the input & format. E See python/cpython#70647. Fixes jupyter#1020
... /usr/lib/python3.13/site-packages/ipykernel/jsonutil.py:29: in <module> datetime.strptime("1", "%d") /usr/lib64/python3.13/_strptime.py:573: in _strptime_datetime tt, fraction, gmtoff_fraction = _strptime(data_string, format) /usr/lib64/python3.13/_strptime.py:336: in _strptime format_regex = _TimeRE_cache.compile(format) /usr/lib64/python3.13/_strptime.py:282: in compile return re_compile(self.pattern(format), IGNORECASE) /usr/lib64/python3.13/_strptime.py:270: in pattern warnings.warn("""\ E DeprecationWarning: Parsing dates involving a day of month without a year specified is ambiguious E and fails to parse leap day. The default behavior will change in Python 3.15 E to either always raise an exception or to use a different default year (TBD). E To avoid trouble, add a specific year to the input & format. E See python/cpython#70647. See also jupyter/jupyter_client#1020
... /usr/lib/python3.13/site-packages/nbclient/jsonutil.py:29: in <module> datetime.strptime("1", "%d") /usr/lib64/python3.13/_strptime.py:573: in _strptime_datetime tt, fraction, gmtoff_fraction = _strptime(data_string, format) /usr/lib64/python3.13/_strptime.py:336: in _strptime format_regex = _TimeRE_cache.compile(format) /usr/lib64/python3.13/_strptime.py:282: in compile return re_compile(self.pattern(format), IGNORECASE) /usr/lib64/python3.13/_strptime.py:270: in pattern warnings.warn("""\ E DeprecationWarning: Parsing dates involving a day of month without a year specified is ambiguious E and fails to parse leap day. The default behavior will change in Python 3.15 E to either always raise an exception or to use a different default year (TBD). E To avoid trouble, add a specific year to the input & format. E See python/cpython#70647. See also jupyter/jupyter_client#1020
…rning (pythonGH-117668) * Fix `test_strptime` raises a DeprecationWarning * Ignore deprecation warnings where appropriate. * Update Lib/test/datetimetester.py This is follow on work to silence unnecessary warnings from the test suite that changes for python#70647 added.
... /usr/lib/python3.13/site-packages/jupyter_client/jsonutil.py:31: in <module> datetime.strptime("1", "%d") # noqa /usr/lib64/python3.13/_strptime.py:573: in _strptime_datetime tt, fraction, gmtoff_fraction = _strptime(data_string, format) /usr/lib64/python3.13/_strptime.py:336: in _strptime format_regex = _TimeRE_cache.compile(format) /usr/lib64/python3.13/_strptime.py:282: in compile return re_compile(self.pattern(format), IGNORECASE) /usr/lib64/python3.13/_strptime.py:270: in pattern warnings.warn("""\ E DeprecationWarning: Parsing dates involving a day of month without a year specified is ambiguious E and fails to parse leap day. The default behavior will change in Python 3.15 E to either always raise an exception or to use a different default year (TBD). E To avoid trouble, add a specific year to the input & format. E See python/cpython#70647. Fixes #1020
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:
bugs.python.org fields:
Linked PRs
The text was updated successfully, but these errors were encountered: