alec_djinn
alec_djinn

Reputation: 10819

Best way to write Python 2 and 3 compatible code using nothing but the standard library

I am trying to make some of my code Python 2 and 3 compatible.

At the moment I am struggling with functions like range/xrange and methods like dict.items/dict.iteritems. Ideally I would like my code to be able to use the former in Python 3.x and the latter in Python 2.x.

Using if/else seems to me to be the easiest way to implement this:

if py >= 3:
    for item in array.items()
    ...
else:
    for item in array.iteritems()

However, doing like that results in lots of repeated and ugly code. Is there a better way to do that using only the standard library? Can I just state somewhere at the beginning of the code to always use range/dict.items if py >= 3 and xrange/dict.iteritems if not?

Is it possible to do something like this?

if py < 3:
    use xrange as range

I have looked around and I know that several libraries, like six o futurize) are used to solve this issue. However I am working on a server that run only python 2.7 and I am not allowed to install any extra libraries on it. I have some python3 code I would like to use but I also want to maintain only one version of the code.

Upvotes: 15

Views: 14657

Answers (5)

Zudhin
Zudhin

Reputation: 326

import sys
VERSION = float("{}.{}".format(sys.version_info.major, sys.version_info.minor))

And by using this we can write conditional code for desired versions.

if VERSION >= 3.5:
     from subprocess import run as _run
else:
     from subprocess import call as _run

Upvotes: 0

NeuronQ
NeuronQ

Reputation: 8215

The simple, "Don't Make Me Think!" solution I use is to start simple scripts with:

#!/usr/bin/env python
# just make sure that Python 3 code runs fine with 2.7+ too ~98% of the time :)
from __future__ import (division, print_function, absolute_import,
                        unicode_literals)
from builtins import int
try:
    from future_builtins import ascii, filter, hex, map, oct, zip
except:
    pass
import sys
if sys.version_info.major > 2:
    xrange = range

(Extra tip to stop most pep8 linters for unnecessarily yelling at you for this: move last 3 lines inside and at the top of the try block above)

But the only case I use this is basically "shell scripts that were too large and hairy so I quickly rewrote them to Python and I just want them to run under both Python 2 and 3 with 0 dependencies". Please do NOT use this in real application/library code until you know exactly what are the consequences of all the lines above, and if they are enough for your use case.

Also, the "solution" in this case for .iteritems is "just don't use it", ignore memory use optimizations and just always use .items instead - if this matters, it means you're not writing a "0 dependencies simple script" anymore, so just pick Python 3 and code for it (or Python 2 if you need to pretend we're in 2008).

Also, check these resources to get a proper understanding:


(NOTE: I'm answering this already answered question mainly because the accepted answers roughly translates to "you are stupid and this is dumb" and I find this very rude for an SO answer: no matter how dumb the question, and how "wrong" to actually answer it, a question deserves a real answer._

Upvotes: 17

Joe
Joe

Reputation: 2564

I would recommend writing for py2 or py3 in your projects's modules, but not mix them together and not include any sort of 2/3 checks at all. Your program's logic shouldn't have to care about its version of python, except maybe for avoiding functions on builtin objects that conflict.

Instead, import * from your own compatiblity layer that fixes the differences between your framework and use shadowing to make it transparent to your actual project's module.

For instance, in the compatibility module, you can write Roland Smith's substition for range/xrange, and in your other modules you add "from compatibility import *". Doing this, every module can use "xrange" and the compatibility layer will manage the 2/3 differences.

Unfortunately it won't solve existing objects functions such as dict.iteritems; typically you would monkey-patch the dict methods, but it is not possible on builtin types (see https://stackoverflow.com/a/192857/1741414). I can imagine some workarounds:

  • Function wrappers (essentially sobolevn's answer)
  • Don't use .items() functions at all; use simple loops on keys and then access the dictionary with those keys:
    for key in my_dict:
        value = my_dict[key]
        # rest of code goes here

Upvotes: 3

sobolevn
sobolevn

Reputation: 18090

I guess you are mixing up array and dict in this case.

If you are restricted in using 3-d party libraries for any reason, so why not like this:

def iterate_items(to_iterate):
    if py >= 3:
        return to_iterate.items()
    else:
        return to_iterate.iteritems()

And then use it:

for item in iterate_items(your_dict):
    ...

Upvotes: 2

Roland Smith
Roland Smith

Reputation: 43563

import sys

if sys.version_info.major > 2:
    xrange = range

But as Wim implies, this is basically rewriting six yourself.

And as you can see, six does a lot more that handling range. Just e.g. look at the _moved_attributes list in the six source code.

And while Python comes with "batteries included", its standard library is not and cannot be all-encompassing. Nor is it devoid of flaws.

Sometimes there are better batteries out there, and it would be a waste not to use them. Just compare urllib2 with requests. The latter is much nicer to work with.

Upvotes: 11

Related Questions