Python 2 to 3

How to Upgrade and What Features to Start Using

/ @treyhunner

On-site training for Python & Django teams
Weekly Python Chat live webcast host
Python Morsels
San Diego Python meetup co-organizer
Previous Django Girls San Diego co-organizer
Python Software Foundation director

Python 2 End of Life

January 1, 2020

1 year, 7 months, 19 days

Syntax improvements


>>> many_numbers = range(1_000_000, 10_000_000)
>>> first, *middle, last = many_numbers[:10_000]
>>> new_numbers = [last, *middle, first]
>>> print(*middle[:4], sep=', ', end='!\n')
1000001, 1000002, 1000003, 1000004!
>>> print(f"{first:,}, {last:,}, and {len(middle)} more")
1,000,000, 1,009,999, and 9998 more
>>> 5 / 2
2.5

class LatLong:
    def __init__(self, lat, long):
        self.lat, self.long = lat, long
    def __eq__(self, other):
        if isinstance(other, LatLong):
            return (self.lat, self.long) == (other.lat, other.long)
        return NotImplemented
    def __ne__(self, other):
        return not self.__eq__(other)
    def __repr__(self):
        return "{class_name}(lat={lat}, long={long})".format(
            class_name=type(self),
            lat=self.lat,
            long=self.long,
        )
          

>>> location = LatLong(32.5395, -117.0440)
>>> location
LatLong(lat=32.5395, long=-117.0440)
>>> location2 = LatLong(lat=32.5395, long=-117.0440)
>>> location == location2
True
          

class LatLong:
    def __init__(self, lat, long):
        self.lat, self.long = lat, long
    def __eq__(self, other):
        if isinstance(other, LatLong):
            return (self.lat, self.long) == (other.lat, other.long)
        return NotImplemented


    def __repr__(self):
        return "{class_name}(lat={lat}, long={long})".format(
            class_name=type(self),
            lat=self.lat,
            long=self.long,
        )
         

>>> location = LatLong(32.5395, -117.0440)
>>> location
LatLong(lat=32.5395, long=-117.0440)
>>> location2 = LatLong(lat=32.5395, long=-117.0440)
>>> location == location2
True
          
class LatLong:
    def __init__(self, lat, long):
        self.lat, self.long = lat, long
    def __eq__(self, other):
        if isinstance(other, LatLong):
            return (self.lat, self.long) == (other.lat, other.long)
        return NotImplemented


    def __repr__(self):
        return f"{type(self)}(lat={self.lat}, long={self.long})"



          

>>> location = LatLong(32.5395, -117.0440)
>>> location
LatLong(lat=32.5395, long=-117.0440)
>>> location2 = LatLong(lat=32.5395, long=-117.0440)
>>> location == location2
True
          
class LatLong:
    def __init__(self, lat, long):
        self.lat, self.long = lat, long
    def __eq__(self, other):
        if isinstance(other, LatLong):
            return (self.lat, self.long) == (other.lat, other.long)
        return NotImplemented


    def __repr__(self):
        return f"{type(self)}(lat={self.lat}, long={self.long})"



          

from dataclasses import dataclass

@dataclass
class LatLong:
    lat: float
    long: float

from collections import UserDict
from functools import total_ordering

@total_ordering
class ComparableDict(UserDict):
    """Implement less than and greater than on dictionary."""
    def __lt__(self, other):
        if isinstance(other, ComparableDict):
            return sorted(self.items()) < sorted(other.items())
        return super().__lt__(other)

Python 3

Brought to you by the makers of Python 2

Approaches to Upgrading

  • Gradual: add support for 3, drop 2 eventually
  • Sudden: upgrade code to 3 and drop 2 immediately
  • Extreme: rewrite the entire code base in Python 3

Sudden vs Gradual

  • Sudden migrations are great for stagnant projects
  • Gradual migrations are best if you need to keep shipping
  • We'll discuss gradual upgrades first

Gradual Migration Strategy

Gradual Migration Strategy

  1. Have a very good testing process in place
  2. Drop support for older versions (move to Python 2.7)
  3. Write "straddling code" in the "common subset" of 2/3
  4. Opt-in to back-ported Python 3 features
  5. Remove uses of deprecated libraries and syntax
  6. Rely on your tests to get your code working again
  7. Start running your code on Python 3 in production
  8. Drop Python 2 compatibility code and clean up your code

from urllib2 import urlopen

response = urlopen('http://pseudorandom.name')
name = response.read().rstrip()
print " ".join(name)
          

from __future__ import print_function
try:
    from urllib.request import urlopen
except ImportError:
    from urllib2 import urlopen

response = urlopen('http://pseudorandom.name')
name = response.read().decode('utf-8').rstrip()
print(" ".join(name))
          

Migrating isn't easy

But there is help

Compatibility Libraries

six

future

Automated Conversion

modernize

futurize

Compatibility libraries


try:
    from urllib.request import urlopen  # Python 3
except ImportError:
    from urllib2 import urlopen         # Python 2
          

from six.moves.urllib.request import urlopen  # six
          

from future.moves.urllib.request import urlopen  # future
          

from future.standard_library import install_aliases
install_aliases()
from urllib.request import urlopen
          

The future library encourages Python 3 idioms

Automated code conversion tools

  • These tools transform code to a common 2/3 subset
  • modernize relies on the six compatibility library
  • futurize relies on on the future library
  • 2to3 is for sudden upgrades (support 3, drop 2)

class Circle(object):
    def __init__(self, radius=1):
        self.radius = radius

    def __nonzero__(self):
        return self.radius != 0

for n in range(3):
    circle = Circle(n)
    truthiness = "truthy" if circle else "falsey"
    print "Circle with radius", circle.radius, "is", truthiness
          

from __future__ import print_function
from builtins import range
from builtins import object

class Circle(object):
    def __init__(self, radius=1):
        self.radius = radius

    def __bool__(self):
        return self.radius != 0

for n in range(3):
    circle = Circle(n)
    truthiness = "truthy" if circle else "falsey"
    print("Circle with radius", circle.radius, "is", truthiness)

          

You'll still need to fix things manually



import csv
from urllib2 import urlopen
from StringIO import StringIO

def save_tabbed_data_as_csv(url, filename):
    response = StringIO(urlopen(url).read())
    reader = csv.reader(response, delimiter='\t')
    csv_file = open(filename, mode='wb')
    csv.writer(csv_file).writerows(reader)

from future import standard_library
standard_library.install_aliases()
import csv
from urllib.request import urlopen
from io import StringIO

def save_tabbed_data_as_csv(url, filename):
    response = StringIO(urlopen(url).read())
    reader = csv.reader(response, delimiter='\t')
    csv_file = open(filename, mode='wb')
    csv.writer(csv_file).writerows(reader)
          

from future import standard_library
standard_library.install_aliases()
import csv
from urllib.request import urlopen
from io import StringIO, open

def save_tabbed_data_as_csv(url, filename):
    response = StringIO(urlopen(url).read().decode('utf-8'))
    reader = csv.reader(response, delimiter='\t')
    csv_file = open(filename, mode='wt', newline='')
    csv.writer(csv_file).writerows(reader)
          

The automated conversion tools won't solve all your problems

You'll still need to fix code manually

The Unicode Situation

  • Python 3.0 uses str for text and bytes for bytes
  • Python 2.0 uses str for both bytes and text
  • The automated tools try to guess (often incorrectly) which strings should be text and which should be bytes
  • Embrace the "unicode sandwich" from Pragmatic Unicode

Useful Resources

  1. The Conservative Python 3 Porting Guide
  2. Porting Python 2 Code to Python 3
  3. Supporting Python 3: An in-depth guide
  4. Migrating Python 2 to Python 3
  5. Pragmatic Unicode

What about sudden migrations?

  1. Fork your Python 2 version and don't change it anymore
  2. You could use 2to3 (I've never used it, good luck!)
  3. You still need to manually fix all the same things (like unicode/bytes issues and division issues)
  4. You still need a solid testing process

Good Luck!

Need help getting your team up to speed on Python?

Trey Hunner
Python & Django Team Trainer

Contact me: trey@truthful.technology