Datetimes and Timezones

These examples show how to handle Python datetime.datetime objects correctly in PyMongo.

Basic Usage

PyMongo uses datetime.datetime objects for representing dates and times in MongoDB documents. Because MongoDB assumes that dates and times are in UTC, care should be taken to ensure that dates and times written to the database reflect UTC. For example, the following code stores the current UTC date and time into MongoDB:

>>> result = db.objects.insert_one(
...     {"last_modified": datetime.datetime.now(tz=datetime.timezone.utc)}
... )

Always use datetime.datetime.now(tz=datetime.timezone.utc)(), which explicitly returns the current time in UTC, instead of datetime.datetime.now(), with no arguments, which returns the current local time. Avoid doing this:

>>> result = db.objects.insert_one({"last_modified": datetime.datetime.now()})

The value for last_modified is very different between these two examples, even though both documents were stored at around the same local time. This will be confusing to the application that reads them:

>>> [doc["last_modified"] for doc in db.objects.find()]  
[datetime.datetime(2015, 7, 8, 18, 17, 28, 324000),
 datetime.datetime(2015, 7, 8, 11, 17, 42, 911000)]

bson.codec_options.CodecOptions has a tz_aware option that enables “aware” datetime.datetime objects, i.e., datetimes that know what timezone they’re in. By default, PyMongo retrieves naive datetimes:

>>> result = db.tzdemo.insert_one({"date": datetime.datetime(2002, 10, 27, 6, 0, 0)})
>>> db.tzdemo.find_one()["date"]
datetime.datetime(2002, 10, 27, 6, 0)
>>> options = CodecOptions(tz_aware=True)
>>> db.get_collection("tzdemo", codec_options=options).find_one()["date"]  
datetime.datetime(2002, 10, 27, 6, 0,
                  tzinfo=<bson.tz_util.FixedOffset object at 0x10583a050>)

Saving Datetimes with Timezones

When storing datetime.datetime objects that specify a timezone (i.e. they have a tzinfo property that isn’t None), PyMongo will convert those datetimes to UTC automatically:

>>> import pytz
>>> pacific = pytz.timezone("US/Pacific")
>>> aware_datetime = pacific.localize(datetime.datetime(2002, 10, 27, 6, 0, 0))
>>> result = db.times.insert_one({"date": aware_datetime})
>>> db.times.find_one()["date"]
datetime.datetime(2002, 10, 27, 14, 0)

Reading Time

As previously mentioned, by default all datetime.datetime objects returned by PyMongo will be naive but reflect UTC (i.e. the time as stored in MongoDB). By setting the tz_aware option on CodecOptions, datetime.datetime objects will be timezone-aware and have a tzinfo property that reflects the UTC timezone.

PyMongo 3.1 introduced a tzinfo property that can be set on CodecOptions to convert datetime.datetime objects to local time automatically. For example, if we wanted to read all times out of MongoDB in US/Pacific time:

>>> from bson.codec_options import CodecOptions
>>> db.times.find_one()['date']
datetime.datetime(2002, 10, 27, 14, 0)
>>> aware_times = db.times.with_options(codec_options=CodecOptions(
...     tz_aware=True,
...     tzinfo=pytz.timezone('US/Pacific')))
>>> result = aware_times.find_one()['date']
datetime.datetime(2002, 10, 27, 6, 0,  
                  tzinfo=<DstTzInfo 'US/Pacific' PST-1 day, 16:00:00 STD>)

Handling out of range datetimes

Python’s datetime can only represent datetimes within the range allowed by min and max, whereas the range of datetimes allowed in BSON can represent any 64-bit number of milliseconds from the Unix epoch. To deal with this, we can use the bson.datetime_ms.DatetimeMS object, which is a wrapper for the int built-in.

To decode UTC datetime values as DatetimeMS, CodecOptions should have its datetime_conversion parameter set to one of the options available in bson.datetime_ms.DatetimeConversion. These include DATETIME, DATETIME_MS, DATETIME_AUTO, DATETIME_CLAMP. DATETIME is the default option and has the behavior of raising an OverflowError upon attempting to decode an out-of-range date. DATETIME_MS will only return DatetimeMS objects, regardless of whether the represented datetime is in- or out-of-range:

>>> from datetime import datetime
>>> from bson import encode, decode
>>> from bson.datetime_ms import DatetimeMS
>>> from bson.codec_options import CodecOptions, DatetimeConversion
>>> x = encode({"x": datetime(1970, 1, 1)})
>>> codec_ms = CodecOptions(datetime_conversion=DatetimeConversion.DATETIME_MS)
>>> decode(x, codec_options=codec_ms)
{'x': DatetimeMS(0)}

DATETIME_AUTO will return datetime if the underlying UTC datetime is within range, or DatetimeMS if the underlying datetime cannot be represented using the builtin Python datetime:

>>> x = encode({"x": datetime(1970, 1, 1)})
>>> y = encode({"x": DatetimeMS(-(2**62))})
>>> codec_auto = CodecOptions(datetime_conversion=DatetimeConversion.DATETIME_AUTO)
>>> decode(x, codec_options=codec_auto)
{'x': datetime.datetime(1970, 1, 1, 0, 0)}
>>> decode(y, codec_options=codec_auto)
{'x': DatetimeMS(-4611686018427387904)}

DATETIME_CLAMP will clamp resulting datetime objects to be within min and max (trimmed to 999000 microseconds):

>>> x = encode({"x": DatetimeMS(2**62)})
>>> y = encode({"x": DatetimeMS(-(2**62))})
>>> codec_clamp = CodecOptions(datetime_conversion=DatetimeConversion.DATETIME_CLAMP)
>>> decode(x, codec_options=codec_clamp)
{'x': datetime.datetime(9999, 12, 31, 23, 59, 59, 999000)}
>>> decode(y, codec_options=codec_clamp)
{'x': datetime.datetime(1, 1, 1, 0, 0)}

DatetimeMS objects have support for rich comparison methods against other instances of DatetimeMS. They can also be converted to datetime objects with to_datetime().