Skip to content
32 changes: 26 additions & 6 deletions Doc/library/datetime.rst
Original file line number Diff line number Diff line change
Expand Up @@ -619,11 +619,27 @@ Notes:
(4)
:class:`date` objects are equal if they represent the same date.

:class:`!date` objects that are not also :class:`.datetime` instances
are never equal to :class:`!datetime` objects, even if they represent
the same date.

(5)
*date1* is considered less than *date2* when *date1* precedes *date2* in time.
In other words, ``date1 < date2`` if and only if ``date1.toordinal() <
date2.toordinal()``.

Order comparison between a :class:`!date` object that is not also a
:class:`.datetime` instance and a :class:`!datetime` object raises
:exc:`TypeError`.

.. versionchanged:: 3.13
Comparison between :class:`.datetime` object and an instance of
the :class:`date` subclass that is not a :class:`!datetime` subclass
no longer coverts the latter to :class:`!date`, ignoring the time part
and the time zone.
The default behavior can be changed by overriding the special comparison
methods in subclasses.

In Boolean contexts, all :class:`date` objects are considered to be true.

Instance methods:
Expand Down Expand Up @@ -1192,9 +1208,6 @@ Supported operations:
and time, taking into account the time zone.

Naive and aware :class:`!datetime` objects are never equal.
:class:`!datetime` objects are never equal to :class:`date` objects
that are not also :class:`!datetime` instances, even if they represent
the same date.

If both comparands are aware and have different :attr:`~.datetime.tzinfo`
attributes, the comparison acts as comparands were first converted to UTC
Expand All @@ -1206,9 +1219,8 @@ Supported operations:
*datetime1* is considered less than *datetime2* when *datetime1* precedes
*datetime2* in time, taking into account the time zone.

Order comparison between naive and aware :class:`.datetime` objects,
as well as a :class:`!datetime` object and a :class:`!date` object
that is not also a :class:`!datetime` instance, raises :exc:`TypeError`.
Order comparison between naive and aware :class:`.datetime` objects
raises :exc:`TypeError`.

If both comparands are aware and have different :attr:`~.datetime.tzinfo`
attributes, the comparison acts as comparands were first converted to UTC
Expand All @@ -1218,6 +1230,14 @@ Supported operations:
Equality comparisons between aware and naive :class:`.datetime`
instances don't raise :exc:`TypeError`.

.. versionchanged:: 3.13
Comparison between :class:`.datetime` object and an instance of
the :class:`date` subclass that is not a :class:`!datetime` subclass
no longer coverts the latter to :class:`!date`, ignoring the time part
and the time zone.
The default behavior can be changed by overriding the special comparison
methods in subclasses.

Instance methods:

.. method:: datetime.date()
Expand Down
35 changes: 11 additions & 24 deletions Lib/_pydatetime.py
Original file line number Diff line number Diff line change
Expand Up @@ -556,10 +556,6 @@ def _check_tzinfo_arg(tz):
if tz is not None and not isinstance(tz, tzinfo):
raise TypeError("tzinfo argument must be None or of a tzinfo subclass")

def _cmperror(x, y):
raise TypeError("can't compare '%s' to '%s'" % (
type(x).__name__, type(y).__name__))

def _divide_and_round(a, b):
"""divide a by b and round result to the nearest integer

Expand Down Expand Up @@ -1113,32 +1109,33 @@ def replace(self, year=None, month=None, day=None):
# Comparisons of date objects with other.

def __eq__(self, other):
if isinstance(other, date):
if isinstance(other, date) and not isinstance(other, datetime):
return self._cmp(other) == 0
return NotImplemented

def __le__(self, other):
if isinstance(other, date):
if isinstance(other, date) and not isinstance(other, datetime):
return self._cmp(other) <= 0
return NotImplemented

def __lt__(self, other):
if isinstance(other, date):
if isinstance(other, date) and not isinstance(other, datetime):
return self._cmp(other) < 0
return NotImplemented

def __ge__(self, other):
if isinstance(other, date):
if isinstance(other, date) and not isinstance(other, datetime):
return self._cmp(other) >= 0
return NotImplemented

def __gt__(self, other):
if isinstance(other, date):
if isinstance(other, date) and not isinstance(other, datetime):
return self._cmp(other) > 0
return NotImplemented

def _cmp(self, other):
assert isinstance(other, date)
assert not isinstance(other, datetime)
y, m, d = self._year, self._month, self._day
y2, m2, d2 = other._year, other._month, other._day
return _cmp((y, m, d), (y2, m2, d2))
Expand Down Expand Up @@ -2137,42 +2134,32 @@ def dst(self):
def __eq__(self, other):
if isinstance(other, datetime):
return self._cmp(other, allow_mixed=True) == 0
elif not isinstance(other, date):
return NotImplemented
else:
return False
return NotImplemented

def __le__(self, other):
if isinstance(other, datetime):
return self._cmp(other) <= 0
elif not isinstance(other, date):
return NotImplemented
else:
_cmperror(self, other)
return NotImplemented

def __lt__(self, other):
if isinstance(other, datetime):
return self._cmp(other) < 0
elif not isinstance(other, date):
return NotImplemented
else:
_cmperror(self, other)
return NotImplemented

def __ge__(self, other):
if isinstance(other, datetime):
return self._cmp(other) >= 0
elif not isinstance(other, date):
return NotImplemented
else:
_cmperror(self, other)
return NotImplemented

def __gt__(self, other):
if isinstance(other, datetime):
return self._cmp(other) > 0
elif not isinstance(other, date):
return NotImplemented
else:
_cmperror(self, other)
return NotImplemented

def _cmp(self, other, allow_mixed=False):
assert isinstance(other, datetime)
Expand Down
64 changes: 36 additions & 28 deletions Lib/test/datetimetester.py
Original file line number Diff line number Diff line change
Expand Up @@ -5435,42 +5435,50 @@ def fromutc(self, dt):

class Oddballs(unittest.TestCase):

def test_bug_1028306(self):
def test_date_datetime_comparison(self):
# bpo-1028306, bpo-5516 (gh-49766)
# Trying to compare a date to a datetime should act like a mixed-
# type comparison, despite that datetime is a subclass of date.
as_date = date.today()
as_datetime = datetime.combine(as_date, time())
self.assertTrue(as_date != as_datetime)
self.assertTrue(as_datetime != as_date)
self.assertFalse(as_date == as_datetime)
self.assertFalse(as_datetime == as_date)
self.assertRaises(TypeError, lambda: as_date < as_datetime)
self.assertRaises(TypeError, lambda: as_datetime < as_date)
self.assertRaises(TypeError, lambda: as_date <= as_datetime)
self.assertRaises(TypeError, lambda: as_datetime <= as_date)
self.assertRaises(TypeError, lambda: as_date > as_datetime)
self.assertRaises(TypeError, lambda: as_datetime > as_date)
self.assertRaises(TypeError, lambda: as_date >= as_datetime)
self.assertRaises(TypeError, lambda: as_datetime >= as_date)

# Nevertheless, comparison should work with the base-class (date)
# projection if use of a date method is forced.
self.assertEqual(as_date.__eq__(as_datetime), True)
different_day = (as_date.day + 1) % 20 + 1
as_different = as_datetime.replace(day= different_day)
self.assertEqual(as_date.__eq__(as_different), False)
date_sc = SubclassDate(as_date.year, as_date.month, as_date.day)
datetime_sc = SubclassDatetime(as_date.year, as_date.month,
as_date.day, 0, 0, 0)
for d in (as_date, date_sc):
for dt in (as_datetime, datetime_sc):
for x, y in (d, dt), (dt, d):
self.assertTrue(x != y)
self.assertFalse(x == y)
self.assertRaises(TypeError, lambda: x < y)
self.assertRaises(TypeError, lambda: x <= y)
self.assertRaises(TypeError, lambda: x > y)
self.assertRaises(TypeError, lambda: x >= y)

# And date should compare with other subclasses of date. If a
# subclass wants to stop this, it's up to the subclass to do so.
date_sc = SubclassDate(as_date.year, as_date.month, as_date.day)
self.assertEqual(as_date, date_sc)
self.assertEqual(date_sc, as_date)

# Ditto for datetimes.
datetime_sc = SubclassDatetime(as_datetime.year, as_datetime.month,
as_date.day, 0, 0, 0)
self.assertEqual(as_datetime, datetime_sc)
self.assertEqual(datetime_sc, as_datetime)
for x, y in ((as_date, date_sc),
(date_sc, as_date),
(as_datetime, datetime_sc),
(datetime_sc, as_datetime)):
self.assertTrue(x == y)
self.assertFalse(x != y)
self.assertFalse(x < y)
self.assertFalse(x > y)
self.assertTrue(x <= y)
self.assertTrue(x >= y)

# Nevertheless, comparison should work if other object is an instance
# of date or datetime class with overridden comparison operators.
# So special methods should return NotImplemented, as if
# date and datetime were independent classes.
for x, y in (as_date, as_datetime), (as_datetime, as_date):
self.assertEqual(x.__eq__(y), NotImplemented)
self.assertEqual(x.__ne__(y), NotImplemented)
self.assertEqual(x.__lt__(y), NotImplemented)
self.assertEqual(x.__gt__(y), NotImplemented)
self.assertEqual(x.__gt__(y), NotImplemented)
self.assertEqual(x.__ge__(y), NotImplemented)

def test_extra_attributes(self):
with self.assertWarns(DeprecationWarning):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Fix :class:`~datetime.date`-:class:`~datetime.datetime` comparison. Now the
special comparison methods like ``__eq__`` and ``__lt__`` return
:data:`NotImplemented` if one of comparands is :class:`!date` and other is
:class:`!datetime` instead of ignoring the time part and the time zone or
forcefully return "not equal" or raise :exc:`TypeError`. It makes comparison
of :class:`!date` and :class:`!datetime` subclasses more symmetric and
allows to change the default behavior by overriding the special comparison
methods in subclasses.
36 changes: 10 additions & 26 deletions Modules/_datetimemodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -1816,16 +1816,6 @@ diff_to_bool(int diff, int op)
Py_RETURN_RICHCOMPARE(diff, 0, op);
}

/* Raises a "can't compare" TypeError and returns NULL. */
static PyObject *
cmperror(PyObject *a, PyObject *b)
{
PyErr_Format(PyExc_TypeError,
"can't compare %s to %s",
Py_TYPE(a)->tp_name, Py_TYPE(b)->tp_name);
return NULL;
}

/* ---------------------------------------------------------------------------
* Class implementations.
*/
Expand Down Expand Up @@ -3448,7 +3438,15 @@ date_isocalendar(PyDateTime_Date *self, PyObject *Py_UNUSED(ignored))
static PyObject *
date_richcompare(PyObject *self, PyObject *other, int op)
{
if (PyDate_Check(other)) {
/* Since DateTime is a subclass of Date, if the other object is
* a DateTime, it would compute an equality testing or an ordering
* based on the date part alone, and we don't want that.
* So return NotImplemented here in that case.
* If a subclass wants to change this, it's up to the subclass to do so.
* The behavior is the same as if Date and DateTime were independent
* classes.
*/
if (PyDate_Check(other) && !PyDateTime_Check(other)) {
int diff = memcmp(((PyDateTime_Date *)self)->data,
((PyDateTime_Date *)other)->data,
_PyDateTime_DATE_DATASIZE);
Expand Down Expand Up @@ -5880,21 +5878,7 @@ datetime_richcompare(PyObject *self, PyObject *other, int op)
PyObject *offset1, *offset2;
int diff;

if (! PyDateTime_Check(other)) {
if (PyDate_Check(other)) {
/* Prevent invocation of date_richcompare. We want to
return NotImplemented here to give the other object
a chance. But since DateTime is a subclass of
Date, if the other object is a Date, it would
compute an ordering based on the date part alone,
and we don't want that. So force unequal or
uncomparable here in that case. */
if (op == Py_EQ)
Py_RETURN_FALSE;
if (op == Py_NE)
Py_RETURN_TRUE;
return cmperror(self, other);
}
if (!PyDateTime_Check(other)) {
Py_RETURN_NOTIMPLEMENTED;
}

Expand Down