Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to write code that works in both Python 2 and Python 3?

A Django website I maintain currently uses Python 2.7 but I know that I'll have to upgrade it to Python 3 in a couple of months. If I'm writing code right now that has to work in Python 2, is there a Pythonic way to write it such that it would also work in Python 3 without any changes if I know what the syntax is going to be in Python 3? Ideally I'd like the code to continue to work even after the upgrade without changing it but it would be easy for me to spot where I've done this in the codebase so that I can change the code when I have time. Here's an example of what I'm talking about:

# Python 2 uses 'iteritems'
def log_dict(**kwargs):
    for key, value in kwargs.iteritems():
        log.info("{0}: {1}".format(key, value))

# Python 3 uses 'items'
def log_dict(**kwargs):
    for key, value in kwargs.items():
        log.info("{0}: {1}".format(key, value))
like image 983
Jim Avatar asked Apr 28 '18 17:04

Jim


1 Answers

There is official documentation suggesting ways to do this. That documentation has changed over time as the situation has changed, so it's worth going directly to the source (especially if you're reading this answer a year or two after it was written).

It's also worth reading the Conservative Python 3 Porting Guide and skimming Nick Coghlan's Python 3 Q&A, especially this section.

Going back in time from the early 2018:

futurize

The current official suggestions are:

  • Only worry about supporting Python 2.7
  • Make sure you have good test coverage (coverage.py can help; pip install coverage)
  • Learn the differences between Python 2 & 3
  • Use Futurize (or Modernize) to update your code (e.g. pip install future)
  • Use Pylint to help make sure you don’t regress on your Python 3 support (pip install pylint)
  • Use caniusepython3 to find out which of your dependencies are blocking your use of Python 3 (pip install caniusepython3)
  • Once your dependencies are no longer blocking you, use continuous integration to make sure you stay compatible with Python 2 & 3 (tox can help test against multiple versions of Python; pip install tox)
  • Consider using optional static type checking to make sure your type usage works in both Python 2 & 3 (e.g. use mypy to check your typing under both Python 2 & Python 3).

Notice the last suggestion. Guido and another of the core devs have both been heavily involved in leading large teams to port large 2.7 codebases to 3.x, and found mypy to be very helpful (especially in dealing with bytes-vs.-unicode issues). In fact, that's a large part of the reason gradual static typing is now an official part of the language.

You also almost certainly want to use all of the future statements available in 2.7. This is so obvious that they seem to have forgotten to leave it out of the docs, but, besides making your life easier (e.g., you can write print function calls), futurize and modernize (and six and sixer) require it.

six

The documentation is aimed at people making an irreversible transition to Python 3 in the near future. If you're planning to stick with dual-version code for a long time, you might be better off following the previous recommendations, which largely revolved around using six instead of futurize. Six covers more of the differences between the two languages, and also makes you write code that's explicit about being dual-version instead of being as close to Python 3 as possible while still running in 2.7. But the downside is that you're effectively doing two ports—one from 2.7 to six-based dual-version code, and then, later, from 3.x-only six code to 3.x-only "native" code.

2to3

The original recommended answer was to use 2to3, a tool that can automatically convert Python 2 code to Python 3 code, or guide you in doing so. If you want your code to work in both, you need to deliver Python 2 code, then run 2to3 at installation time to port it to Python 3. Which means you need to test your code both ways, and usually modify it so that it still works in 2.7 but also works in 3.x after 2to3, which isn't always easy to work out. This turns out to not be feasible for most non-trivial projects, so it's no longer recommended by the core devs—but it is still built in with Python 2.7 and 3.x, and getting updates.

There are also two variations on 2to3: sixer auto-ports your Python 2.7 code to dual-version code that uses six, and 3to2 lets you write your code for Python 3 and auto-port back to 2.7 at install time. Both of these were popular for a time, but don't seem to be used much anymore; modernize and futurize, respectively, are their main successors.


For your specific question,

  • kwargs.items() will work on both, if you don't mind a minor performance cost in 2.7.
  • 2to3 can automatically change that iteritems to items at install time on 3.x.
  • futurize can be used to do either of the above.
  • six will allow you to write six.iteritems(kwargs), which will do iteritems in 2.7 and items in 3.x.
  • six will also allow you to write six.viewitems(kwargs), which will do viewitems in 2.7 (which is identical to what items does in 3.x, rather than just similar).
  • modernize and sixer will automatically change that kwargs.iteritems() to six.iteritems(kwargs).
  • 3to2 will let you write kwargs.items() and autmatically convert it to viewitems at install time on 2.x.
  • mypy can verify that you're just using the result as a general iterable (rather than specifically as an iterator), so changing to viewitems or items leaves your code still correctly typed.
like image 84
abarnert Avatar answered Nov 15 '22 05:11

abarnert