Kristian Glass - Do I Smell Burning?

Python's 'Surprise' Imports

There was some code, it looked a bit like this:

from django.utils import timezone

start_date = timezone.datetime(2015, 1, 1)

Django recommends that you use, for example, django.utils.timezone.now to ensure you always get “the right now” (i.e. timezone-aware). So you might, as with the code example above, extrapolate that timezone.datetime(2015, 1, 1) will give you a timezone-aware “1st of January 2015” datetime object.

This is not what happens. You get a naïve datetime object. Why?

Hint: django.utils.timezone doesn’t provide datetime as part of its API.

So how does that code work, I hear you ask?

Line 9 of django/utils/timezone.py:

from datetime import datetime, timedelta, tzinfo

That’s it. django.utils.timezone.datetime is effectively just another binding for the vanilla datetime.datetime:

>>> from django.utils import timezone
>>> timezone.datetime
<type 'datetime.datetime'>
>>> import datetime
>>> timezone.datetime == datetime.datetime
True

This is useful behaviour if you’re looking to move your code around while providing compatibility support for old imports.

This is not so useful behaviour when you suddenly find that, surprise, that thing you thought was a library feature turns out to be the stdlib in disguise.

Does this fall foul of “explicit is better than implicit”? It certainly gets me on the Principle of Least Astonishment; I understand why it happens, but it’s still surprising from a user’s perspective!

Incidentally, we found this when we added warnings.simplefilter("error") to our tests so that any generated warnings would cause test failure. You probably want to do that too.

Comments