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()
.