I had this on the someday section of my todo list, but was prompted into action by a pull request. In the end, getting Python 2 and 3 compatible code was easier than I thought.

I started with the pull request code and got it so the tests were mostly passing in 3 and then worked to make it compatible with 2 again. I guess this is all old news now for most (then again, maybe not as Python 2 shows no signs of dying), but if you’ve only got something small I wouldn’t bother with things like 2to3 (which produced worse results than the pull request) and instead just port things manually. Then make everything work for both by simply using sys.version conditionals instead of using compatibility libraries like six or future.

Some examples:

Import conditionally and use Python 2 names

So I could continue to use urllib2 throughout the code, for Python 3, urllib.request gets imported as urllib2 and urllib.parse as urllib:

import sys
if sys.version_info > (3, 0):
	import urllib.request as urllib2
	import urllib.error
	from urllib.error import HTTPError
	import urllib.parse as urllib
else:
	import urllib2
	from urllib2 import HTTPError
	import urllib

Dummy Monkey Patching / Subclassing

Python 2 still requires (will always) monkey patching to add HTTP DELETE to urllib2. But Python 3 doesn’t. How to do for one, but not the other? Simply pass through. This might be obvious to some, but, for me, I thought this was a great little trick.

class Request(urllib2.Request):

	if sys.version_info < (3, 0):
		#Code here
		return urllib2.Request.get_method(self)
	else:
		pass

Yes, there are better HTTP libraries that would make all this easier on Python 2 and 3, but simplenote.py needs to work with the base Python install.

Use Exceptions as an alternative to version checking

Yeah, as I write this, I guess I should have just done this one way (sys.version), but you can do things like this:

try:
	#For Python 2
	values = base64.b64encode(bytes(auth_params,'utf-8'))
except TypeError:
	#For Python 3
	values = base64.encodestring(auth_params)

and this:

try:
	#For Python 2
	return str(self.token,'utf-8')
except TypeError:
	#For Python 3
	return self.token

One really odd thing I found was that under Python 3.4 expectedFailures passing would lead to the test run failing.

Don’t Expect Failures if you don’t expect failures?

This has surely got to be one of those programming oddities as opposed to a proper bug* in Python 3, but I had to remove the use of @unittest.expectedFailure as when the test passed it caused the whole test run to fail in Python 3 whereas it would work as intended in Python 2. Fortunately another pull request had much improved the testing making this particular test much more stable.

* - Sometimes you just need to get something working and move on, rather than try to get to the bottom of a rabbit hole that’s probably still being dug by a hundred rabbits in all directions.