I brought our Django app up using Python 3 and it mostly just worked
I have been worrying for some time about the need to eventually get our Django web application running under Python 3; most recently I wrote about being realistic about our future plans, which mostly amounted to not doing anything until we had to. Well, guess what happened since then.
For reasons beyond the scope of this entry, last Friday I ended up working on moving our app from Django 1.10.7 to 1.11.x, which was enlivened by the usual problem. After I had it working under 1.11.22, I decided to try running it (in development mode, not in production) using Python 3 instead of Python 2, since Django 1.11.22 is itself fully compatible with Python 3. To my surprise, it took only a little bit of cleanup and additional changes beyond basic modernization to get it running, and the result is so far fully compatible with Python 2 as well (I committed the changes as part of the 1.11 move, and since Monday they're running in production).
I don't think this is particularly due to anything I've done in our app's code; instead, I think it's mostly due to the work that Django has done to make everything work more or less transparently. As the intermediate layer between your app and the web (and the database), Django is already the place that has to worry about character set conversion issues, so it can spare you from most of those. And generally that's the big difference between Python 2 and Python 3.
(The other difference is the
but you can make Python 2.7 work in the same way as Python 3 with
from __future__ import print_function', which is what I did.)
I haven't thoroughly tested our web app under Python 3, of course, but I did test a number of the basics and everything looks good. I'm fairly confident that there are no major issues left, only relatively small corner cases (and then the lurking issue of how well the Python 3 version of mod_wsgi works and if there are any traps there). I'm still planning to keep us on Python 2 and Django 1.11 through at least the end of this year, but if we needed to I could probably switch over to a current Django and Python 3 with not very much additional work (and most of the work would be updating to a new version of Django).
There was one interesting and amusing change I had to make, which
is that I had to add a bunch of
__str__ methods to various
Django models that previously only had
When building HTML for things like form <select> fields, Django
string-izes the names of model instances to determine what to put
in here, but in Python 2 it actually generates the Unicode version
and so ends up invoking
__unicode__, while in Python 3
Unicode already and so Django was using
__str__, which didn't
exist. This is an interesting little incompatibility.
Sidebar: The specific changes I needed to make
I'm going to write these down partly because I want a coherent record, and partly because some of them are interesting.
- When generating a random key to embed in a URL, read from
/dev/urandomusing binary mode instead of text mode and switch from an ad-hoc implementation of
base64.urlsafe_b64encodeto using the real thing. I don't know why I didn't use the base64 module in the first place; perhaps I just didn't look for it, since I already knew about Python 2's special purpose encodings.
__str__methods to various Django model classes that previously only had
- Switch from
print()as a function in some administrative tools the app has. The main app code doesn't use
- Fix mismatched tabs versus spaces indentation, which snuck in because
my usual editor for Python used to use all-tabs and now uses
all-spaces. At some point I should mass-convert all of the existing
code files to use all-spaces, perhaps with four-space indentation.
- Change a bunch of old style exception syntax, '
except Thing, e:', to '
except Thing as e:'. I wound up finding all of these with
- Fix one instance of sorting a dictionary's
.keys(), since Python 3 now returns an iterator here instead of a sortable object.
Many of these changes were good ideas in general, and none of them
are ones that I find objectionable. Certainly switching to just
base64.urlsafe_b64encode makes the code better (and it
makes me feel silly for not using it to start with).