From c5a30df3d09812d2496294cdc4502cdfc17297db Mon Sep 17 00:00:00 2001
From: Hugo <hugovk@users.noreply.github.com>
Date: Thu, 28 Feb 2019 21:23:37 +0200
Subject: [PATCH 01/10] Add python_requires to help pip

---
 python/setup.py | 1 +
 1 file changed, 1 insertion(+)

diff --git a/python/setup.py b/python/setup.py
index 9db138ff71..3ba766f808 100644
--- a/python/setup.py
+++ b/python/setup.py
@@ -77,6 +77,7 @@ def finalize_options(self):
     version=tskit_version,
     # TODO setup a tskit developers email address.
     author_email='jerome.kelleher@well.ox.ac.uk',
+    python_requires='>=3.5',
     classifiers=[
         'Development Status :: 4 - Beta',
         'Intended Audience :: Developers',

From 6ddc9bd3b25f896f1445f9c407fd00bf4da25584 Mon Sep 17 00:00:00 2001
From: Hugo <hugovk@users.noreply.github.com>
Date: Thu, 28 Feb 2019 21:25:23 +0200
Subject: [PATCH 02/10] Drop support for legacy Python 2.7

---
 .travis.yml                    |  4 ----
 python/setup.py                |  9 ++-------
 python/tests/test_drawing.py   | 15 ++++-----------
 python/tests/test_highlevel.py |  5 -----
 python/tests/test_lowlevel.py  |  2 --
 python/tests/test_stats.py     | 14 ++------------
 python/tests/test_tables.py    |  4 ----
 python/tests/test_threads.py   |  7 ++-----
 python/tskit/drawing.py        |  9 +--------
 python/tskit/trees.py          |  9 +--------
 10 files changed, 12 insertions(+), 66 deletions(-)

diff --git a/.travis.yml b/.travis.yml
index d9936c96f8..3565a3c748 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -8,8 +8,6 @@ language: python
 
 matrix:
   include:
-    - os: linux
-      python: 2.7
     - os: linux
       python: 3.6
     - os: osx
@@ -21,8 +19,6 @@ install:
   - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then
       curl https://repo.continuum.io/miniconda/Miniconda3-latest-MacOSX-x86_64.sh > miniconda.sh;
       export TRAVIS_PYTHON_VERSION="3.6";
-    elif [[ "$TRAVIS_PYTHON_VERSION" == "2.7" ]]; then
-      wget https://repo.continuum.io/miniconda/Miniconda2-latest-Linux-x86_64.sh -O miniconda.sh;
     else
       wget https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh -O miniconda.sh;
     fi
diff --git a/python/setup.py b/python/setup.py
index 3ba766f808..165f9e6cd8 100644
--- a/python/setup.py
+++ b/python/setup.py
@@ -1,4 +1,3 @@
-import sys
 import os.path
 import codecs
 import platform
@@ -14,10 +13,7 @@
 class local_build_ext(build_ext):
     def finalize_options(self):
         build_ext.finalize_options(self)
-        if sys.version_info[0] >= 3:
-            import builtins
-        else:
-            import __builtin__ as builtins
+        import builtins
         # Prevent numpy from thinking it is still in its setup process:
         builtins.__NUMPY_SETUP__ = False
         import numpy
@@ -83,12 +79,11 @@ def finalize_options(self):
         'Intended Audience :: Developers',
         'Topic :: Scientific/Engineering :: Bio-Informatics',
         'License :: OSI Approved :: MIT License',
-        'Programming Language :: Python :: 2',
-        'Programming Language :: Python :: 2.7',
         'Programming Language :: Python :: 3',
         'Programming Language :: Python :: 3.5',
         'Programming Language :: Python :: 3.6',
         'Programming Language :: Python :: 3.7',
+        'Programming Language :: Python :: 3 :: Only',
     ],
     keywords='tree sequence',
     packages=['tskit'],
diff --git a/python/tests/test_drawing.py b/python/tests/test_drawing.py
index 5b6e13a5ab..d506733be8 100644
--- a/python/tests/test_drawing.py
+++ b/python/tests/test_drawing.py
@@ -28,7 +28,6 @@
 from __future__ import division
 
 import os
-import sys
 import tempfile
 import unittest
 import xml.etree
@@ -38,8 +37,6 @@
 import tskit
 import tests.tsutil as tsutil
 
-IS_PY2 = sys.version_info[0] < 3
-
 
 class TestTreeDraw(unittest.TestCase):
     """
@@ -169,13 +166,10 @@ def test_ascii_variants(self):
     def test_unicode_variants(self):
         t = self.get_binary_tree()
         for fmt in ["unicode", "UNICODE", "uniCODE"]:
-            if IS_PY2:
-                self.assertRaises(ValueError, t.draw, format=fmt)
-            else:
-                output = t.draw(format=fmt)
-                self.assertRaises(
-                    xml.etree.ElementTree.ParseError, xml.etree.ElementTree.fromstring,
-                    output)
+            output = t.draw(format=fmt)
+            self.assertRaises(
+                xml.etree.ElementTree.ParseError, xml.etree.ElementTree.fromstring,
+                output)
 
     def test_bad_formats(self):
         t = self.get_binary_tree()
@@ -302,7 +296,6 @@ def test_no_node_labels(self):
             self.assertEqual(text.find(str(u)), -1)
 
 
-@unittest.skipIf(IS_PY2, "Unicode tree drawing not supported on Python 2")
 class TestDrawUnicode(TestDrawText):
     """
     Tests the Unicode tree drawing method
diff --git a/python/tests/test_highlevel.py b/python/tests/test_highlevel.py
index 2d96a661ad..f7911be302 100644
--- a/python/tests/test_highlevel.py
+++ b/python/tests/test_highlevel.py
@@ -45,7 +45,6 @@
 import unittest
 import warnings
 import uuid as _uuid
-import sys
 
 import numpy as np
 import msprime
@@ -56,8 +55,6 @@
 import tests.tsutil as tsutil
 import tests.simplify as simplify
 
-IS_PY2 = sys.version_info[0] < 3
-
 
 def insert_uniform_mutations(tables, num_mutations, nodes):
     """
@@ -2414,7 +2411,6 @@ def get_instances(self, n):
             tskit.Edge(left=j, right=j, parent=j, child=j) for j in range(n)]
 
 
-@unittest.skipIf("py2", IS_PY2)
 class TestSiteContainer(unittest.TestCase, SimpleContainersMixin):
     def get_instances(self, n):
         return [
@@ -2458,7 +2454,6 @@ def get_instances(self, n):
             tskit.Edgeset(left=j, right=j, parent=j, children=j) for j in range(n)]
 
 
-@unittest.skipIf("py2", IS_PY2)
 class TestVariantContainer(unittest.TestCase, SimpleContainersMixin):
     def get_instances(self, n):
         return [
diff --git a/python/tests/test_lowlevel.py b/python/tests/test_lowlevel.py
index 49ab96bb61..31906486d3 100644
--- a/python/tests/test_lowlevel.py
+++ b/python/tests/test_lowlevel.py
@@ -32,7 +32,6 @@
 import os
 import platform
 import random
-import sys
 import tempfile
 import unittest
 
@@ -40,7 +39,6 @@
 
 import _tskit
 
-IS_PY2 = sys.version_info[0] < 3
 IS_WINDOWS = platform.system() == "Windows"
 
 
diff --git a/python/tests/test_stats.py b/python/tests/test_stats.py
index 709ef6f917..da2fc2c2f3 100644
--- a/python/tests/test_stats.py
+++ b/python/tests/test_stats.py
@@ -27,7 +27,6 @@
 from __future__ import division
 
 import unittest
-import sys
 
 import numpy as np
 import msprime
@@ -38,9 +37,6 @@
 import tests.test_wright_fisher as wf
 
 
-IS_PY2 = sys.version_info[0] < 3
-
-
 def get_r2_matrix(ts):
     """
     Returns the matrix for the specified tree sequence. This is computed
@@ -377,14 +373,8 @@ def verify(self, ts, reference_sets, focal=None):
         A1 = naive_genealogical_nearest_neighbours(ts, focal, reference_sets)
         A2 = tsutil.genealogical_nearest_neighbours(ts, focal, reference_sets)
         A3 = ts.genealogical_nearest_neighbours(focal, reference_sets)
-        if IS_PY2:
-            # Threads not supported on PY2
-            self.assertRaises(
-                ValueError, ts.genealogical_nearest_neighbours, focal,
-                reference_sets, num_threads=3)
-        else:
-            A4 = ts.genealogical_nearest_neighbours(focal, reference_sets, num_threads=3)
-            self.assertTrue(np.array_equal(A3, A4))
+        A4 = ts.genealogical_nearest_neighbours(focal, reference_sets, num_threads=3)
+        self.assertTrue(np.array_equal(A3, A4))
         self.assertEqual(A1.shape, A2.shape)
         self.assertEqual(A1.shape, A3.shape)
         self.assertTrue(np.allclose(A1, A2))
diff --git a/python/tests/test_tables.py b/python/tests/test_tables.py
index 17f76a939e..019b02c8f6 100644
--- a/python/tests/test_tables.py
+++ b/python/tests/test_tables.py
@@ -33,7 +33,6 @@
 import string
 import unittest
 import warnings
-import sys
 
 import numpy as np
 import six
@@ -44,8 +43,6 @@
 
 import tests.tsutil as tsutil
 
-IS_PY2 = sys.version_info[0] < 3
-
 
 def random_bytes(max_length):
     """
@@ -1238,7 +1235,6 @@ class TestSimplifyTables(unittest.TestCase):
     """
     random_seed = 42
 
-    @unittest.skipIf(IS_PY2, "Warnings different in Py2")
     def test_deprecated_zero_mutation_sites(self):
         ts = msprime.simulate(10,  mutation_rate=1, random_seed=self.random_seed)
         tables = ts.dump_tables()
diff --git a/python/tests/test_threads.py b/python/tests/test_threads.py
index d7b8f9779b..f5085e0924 100644
--- a/python/tests/test_threads.py
+++ b/python/tests/test_threads.py
@@ -26,7 +26,6 @@
 from __future__ import print_function
 from __future__ import division
 
-import sys
 import threading
 import unittest
 import platform
@@ -37,7 +36,6 @@
 import tskit
 import tests.tsutil as tsutil
 
-IS_PY2 = sys.version_info[0] < 3
 IS_WINDOWS = platform.system() == "Windows"
 
 
@@ -141,10 +139,9 @@ def worker(thread_index, results):
             self.assertEqual(results[j][0], m - j - 1)
 
 
-# @unittest.skipIf(IS_PY2, "Cannot test thread support on Py2.")
-# Temporarily skipping these on windows too. See
+# Temporarily skipping these on Windows. See
 # https://github.com/jeromekelleher/tskit/issues/344
-@unittest.skipIf(IS_PY2 or IS_WINDOWS, "Cannot test thread support on Py2.")
+@unittest.skipIf(IS_WINDOWS, "Cannot test thread support on Windows.")
 class TestTables(unittest.TestCase):
     """
     Tests to ensure that attempts to access tables in threads correctly
diff --git a/python/tskit/drawing.py b/python/tskit/drawing.py
index 43343c029d..4f1a67ccf4 100644
--- a/python/tskit/drawing.py
+++ b/python/tskit/drawing.py
@@ -28,7 +28,6 @@
 
 import array
 import collections
-import sys
 
 try:
     import svgwrite
@@ -36,8 +35,6 @@
 except ImportError:  # pragma: no cover
     _svgwrite_imported = False
 
-IS_PY2 = sys.version_info[0] < 3
-
 NULL_NODE = -1
 
 
@@ -64,8 +61,6 @@ def draw_tree(
     elif fmt == "ascii":
         cls = AsciiTreeDrawer
     elif fmt == "unicode":
-        if IS_PY2:
-            raise ValueError("Unicode tree drawing not supported on Python 2")
         cls = UnicodeTreeDrawer
 
     # We can't draw trees with zero roots.
@@ -387,9 +382,7 @@ def _convert_text(self, text):
         return array.array(self.array_type, text.encode())
 
     def draw(self):
-        s = self._draw().tostring()
-        if not IS_PY2:
-            s = s.decode()
+        s = self._draw().tostring().decode()
         return s
 
 
diff --git a/python/tskit/trees.py b/python/tskit/trees.py
index f5aa0e00a5..ac266b4ca8 100644
--- a/python/tskit/trees.py
+++ b/python/tskit/trees.py
@@ -31,7 +31,6 @@
 import collections
 import itertools
 import json
-import sys
 import base64
 import warnings
 import functools
@@ -54,9 +53,6 @@
 from _tskit import NULL
 
 
-IS_PY2 = sys.version_info[0] < 3
-
-
 CoalescenceRecord = collections.namedtuple(
     "CoalescenceRecord",
     ["left", "right", "node", "children", "time", "population"])
@@ -1350,8 +1346,7 @@ def newick(self, precision=14, root=None, node_labels=None):
             root = self.root
         if node_labels is None:
             s = self._ll_tree.get_newick(precision=precision, root=root)
-            if not IS_PY2:
-                s = s.decode()
+            s = s.decode()
         else:
             return self.__build_newick(root, precision, node_labels) + ";"
         return s
@@ -2664,8 +2659,6 @@ def genealogical_nearest_neighbours(self, focal, reference_sets, num_threads=0):
             return self._ll_tree_sequence.genealogical_nearest_neighbours(
                 focal, reference_sets)
         else:
-            if IS_PY2:  # pragma: no cover
-                raise ValueError("Threads not supported on Python 2.")
             worker = functools.partial(
                 self._ll_tree_sequence.genealogical_nearest_neighbours,
                 reference_sets=reference_sets)

From 9c80ffa43c7b76e0be26921b561de7d44b8a09ea Mon Sep 17 00:00:00 2001
From: Hugo <hugovk@users.noreply.github.com>
Date: Thu, 28 Feb 2019 21:27:07 +0200
Subject: [PATCH 03/10] Upgrade Python syntax with pyupgrade

---
 python/tests/simplify.py           |  2 +-
 python/tests/test_dict_encoding.py |  8 ++++----
 python/tests/test_highlevel.py     |  4 ++--
 python/tests/test_tree_stats.py    | 24 ++++++++++++------------
 python/tskit/drawing.py            |  2 +-
 python/tskit/trees.py              |  2 +-
 6 files changed, 21 insertions(+), 21 deletions(-)

diff --git a/python/tests/simplify.py b/python/tests/simplify.py
index 2d5759258c..0839fbe8c4 100644
--- a/python/tests/simplify.py
+++ b/python/tests/simplify.py
@@ -288,7 +288,7 @@ def process_parent_edges(self, edges):
         """
         Process all of the edges for a given parent.
         """
-        assert len(set(e.parent for e in edges)) == 1
+        assert len({e.parent for e in edges}) == 1
         parent = edges[0].parent
         S = []
         for edge in edges:
diff --git a/python/tests/test_dict_encoding.py b/python/tests/test_dict_encoding.py
index f3a7a10d62..2b339f2a63 100644
--- a/python/tests/test_dict_encoding.py
+++ b/python/tests/test_dict_encoding.py
@@ -138,7 +138,7 @@ def test_missing_sequence_length(self):
     def test_missing_tables(self):
         tables = get_example_tables()
         d = tables.asdict()
-        table_names = set(d.keys()) - set(["sequence_length"])
+        table_names = set(d.keys()) - {"sequence_length"}
         for table_name in table_names:
             d = tables.asdict()
             del d[table_name]
@@ -149,7 +149,7 @@ def test_missing_tables(self):
     def test_missing_columns(self):
         tables = get_example_tables()
         d = tables.asdict()
-        table_names = set(d.keys()) - set(["sequence_length"])
+        table_names = set(d.keys()) - {"sequence_length"}
         for table_name in table_names:
             table_dict = d[table_name]
             for colname in table_dict.keys():
@@ -170,7 +170,7 @@ class TestBadTypes(unittest.TestCase):
     def verify_columns(self, value):
         tables = get_example_tables()
         d = tables.asdict()
-        table_names = set(d.keys()) - set(["sequence_length"])
+        table_names = set(d.keys()) - {"sequence_length"}
         for table_name in table_names:
             table_dict = d[table_name]
             for colname in table_dict.keys():
@@ -208,7 +208,7 @@ def verify(self, num_rows):
 
         tables = get_example_tables()
         d = tables.asdict()
-        table_names = set(d.keys()) - set(["sequence_length"])
+        table_names = set(d.keys()) - {"sequence_length"}
         for table_name in sorted(table_names):
             table_dict = d[table_name]
             for colname in sorted(table_dict.keys()):
diff --git a/python/tests/test_highlevel.py b/python/tests/test_highlevel.py
index f7911be302..1ceece9b49 100644
--- a/python/tests/test_highlevel.py
+++ b/python/tests/test_highlevel.py
@@ -1102,7 +1102,7 @@ def verify_samples(self, ts):
     def test_samples(self):
         for ts in get_example_tree_sequences():
             self.verify_samples(ts)
-            pops = set(node.population for node in ts.nodes())
+            pops = {node.population for node in ts.nodes()}
             for pop in pops:
                 subsample = ts.samples(pop)
                 self.assertTrue(np.array_equal(subsample, ts.samples(population=pop)))
@@ -1358,7 +1358,7 @@ def test_simplify(self):
             num_mutations += ts.get_num_mutations()
             sample_sizes = {0, 1}
             if n > 2:
-                sample_sizes |= set([2, max(2, n // 2), n - 1])
+                sample_sizes |= {2, max(2, n // 2), n - 1}
             for k in sample_sizes:
                 subset = random.sample(list(ts.samples()), k)
                 self.verify_simplify_topology(ts, subset)
diff --git a/python/tests/test_tree_stats.py b/python/tests/test_tree_stats.py
index f37269304f..9cb3757e30 100644
--- a/python/tests/test_tree_stats.py
+++ b/python/tests/test_tree_stats.py
@@ -144,7 +144,7 @@ def Y2(self, X, Y, begin=0.0, end=None):
             this_length = min(end, tr.interval[1]) - max(begin, tr.interval[0])
             for x in X:
                 for y in Y:
-                    for z in set(Y) - set([y]):
+                    for z in set(Y) - {y}:
                         xy_mrca = tr.mrca(x, y)
                         xz_mrca = tr.mrca(x, z)
                         yz_mrca = tr.mrca(y, z)
@@ -176,8 +176,8 @@ def Y1(self, X, begin=0.0, end=None):
                 break
             this_length = min(end, tr.interval[1]) - max(begin, tr.interval[0])
             for x in X:
-                for y in set(X) - set([x]):
-                    for z in set(X) - set([x, y]):
+                for y in set(X) - {x}:
+                    for z in set(X) - {x, y}:
                         xy_mrca = tr.mrca(x, y)
                         xz_mrca = tr.mrca(x, z)
                         yz_mrca = tr.mrca(y, z)
@@ -239,7 +239,7 @@ def f3(self, A, B, C, begin=0.0, end=None):
             SS = 0
             for a in A:
                 for b in B:
-                    for c in set(A) - set([a]):
+                    for c in set(A) - {a}:
                         for d in C:
                             SS += path_length(tr, tr.mrca(a, c), tr.mrca(b, d))
                             SS -= path_length(tr, tr.mrca(a, d), tr.mrca(b, c))
@@ -264,8 +264,8 @@ def f2(self, A, B, begin=0.0, end=None):
             SS = 0
             for a in A:
                 for b in B:
-                    for c in set(A) - set([a]):
-                        for d in set(B) - set([b]):
+                    for c in set(A) - {a}:
+                        for d in set(B) - {b}:
                             SS += path_length(tr, tr.mrca(a, c), tr.mrca(b, d))
                             SS -= path_length(tr, tr.mrca(a, d), tr.mrca(b, c))
             S += SS * this_length
@@ -400,7 +400,7 @@ def Y2(self, X, Y, begin=0.0, end=None):
             if (site_positions[k] >= begin) and (site_positions[k] < end):
                 for x in X:
                     for y in Y:
-                        for z in set(Y) - set([y]):
+                        for z in set(Y) - {y}:
                             if ((haps[x][k] != haps[y][k])
                                and (haps[x][k] != haps[z][k])):
                                 # x|yz
@@ -416,8 +416,8 @@ def Y1(self, X, begin=0.0, end=None):
         for k in range(self.tree_sequence.num_sites):
             if (site_positions[k] >= begin) and (site_positions[k] < end):
                 for x in X:
-                    for y in set(X) - set([x]):
-                        for z in set(X) - set([x, y]):
+                    for y in set(X) - {x}:
+                        for z in set(X) - {x, y}:
                             if ((haps[x][k] != haps[y][k])
                                and (haps[x][k] != haps[z][k])):
                                 # x|yz
@@ -464,7 +464,7 @@ def f3(self, A, B, C, begin=0.0, end=None):
             if (site_positions[k] >= begin) and (site_positions[k] < end):
                 for a in A:
                     for b in B:
-                        for c in set(A) - set([a]):
+                        for c in set(A) - {a}:
                             for d in C:
                                 if ((haps[a][k] == haps[c][k])
                                    and (haps[a][k] != haps[d][k])
@@ -491,8 +491,8 @@ def f2(self, A, B, begin=0.0, end=None):
             if (site_positions[k] >= begin) and (site_positions[k] < end):
                 for a in A:
                     for b in B:
-                        for c in set(A) - set([a]):
-                            for d in set(B) - set([b]):
+                        for c in set(A) - {a}:
+                            for d in set(B) - {b}:
                                 if ((haps[a][k] == haps[c][k])
                                    and (haps[a][k] != haps[d][k])
                                    and (haps[a][k] != haps[b][k])):
diff --git a/python/tskit/drawing.py b/python/tskit/drawing.py
index 4f1a67ccf4..ecc70ddc41 100644
--- a/python/tskit/drawing.py
+++ b/python/tskit/drawing.py
@@ -289,7 +289,7 @@ def _convert_text(self, text):
 
     def _assign_coordinates(self):
         # Get the age of each node and rank them.
-        times = set(self._tree.time(u) for u in self._tree.nodes())
+        times = {self._tree.time(u) for u in self._tree.nodes()}
         depth = {t: 2 * j for j, t in enumerate(sorted(times, reverse=True))}
         for u in self._tree.nodes():
             self._y_coords[u] = depth[self._tree.time(u)]
diff --git a/python/tskit/trees.py b/python/tskit/trees.py
index ac266b4ca8..cd8a5a3f6c 100644
--- a/python/tskit/trees.py
+++ b/python/tskit/trees.py
@@ -1302,7 +1302,7 @@ def __build_newick(self, node, precision, node_labels):
         """
         label = node_labels.get(node, "")
         if self.is_leaf(node):
-            s = "{0}".format(label)
+            s = "{}".format(label)
         else:
             s = "("
             for child in self.children(node):

From 61cd4d633cbd7fe14d8518b1edda6200aa52d128 Mon Sep 17 00:00:00 2001
From: Hugo <hugovk@users.noreply.github.com>
Date: Thu, 28 Feb 2019 21:59:55 +0200
Subject: [PATCH 04/10] Remove six dependency

---
 docs/rtd_requirements.txt             |   1 -
 python/requirements/conda-minimal.txt |   1 -
 python/requirements/development.txt   |   1 -
 python/setup.py                       |   2 +-
 python/tests/test_drawing.py          |  26 +--
 python/tests/test_highlevel.py        |  42 ++---
 python/tests/test_metadata.py         |  12 +-
 python/tests/test_tables.py           |  26 +--
 python/tests/test_topology.py         | 248 +++++++++++++-------------
 python/tests/test_tree_stats.py       |  34 ++--
 python/tskit/tables.py                |   2 +-
 11 files changed, 196 insertions(+), 199 deletions(-)

diff --git a/docs/rtd_requirements.txt b/docs/rtd_requirements.txt
index 4f9895802f..faa3f1b40b 100644
--- a/docs/rtd_requirements.txt
+++ b/docs/rtd_requirements.txt
@@ -1,5 +1,4 @@
 numpy
-six
 svgwrite
 jsonschema
 h5py
diff --git a/python/requirements/conda-minimal.txt b/python/requirements/conda-minimal.txt
index 9ed290c21e..6f4570efc1 100644
--- a/python/requirements/conda-minimal.txt
+++ b/python/requirements/conda-minimal.txt
@@ -1,7 +1,6 @@
 numpy
 nose
 h5py
-six
 jsonschema
 svgwrite
 msprime
diff --git a/python/requirements/development.txt b/python/requirements/development.txt
index 6674bd2c25..c70786ea05 100644
--- a/python/requirements/development.txt
+++ b/python/requirements/development.txt
@@ -6,7 +6,6 @@ mock
 newick
 nose
 numpy
-six
 kastore
 jsonschema
 sphinx
diff --git a/python/setup.py b/python/setup.py
index 165f9e6cd8..ccbdbc13a7 100644
--- a/python/setup.py
+++ b/python/setup.py
@@ -89,7 +89,7 @@ def finalize_options(self):
     packages=['tskit'],
     include_package_data=True,
     ext_modules=[_tskit_module],
-    install_requires=[numpy_ver, "h5py", "svgwrite", "six", "jsonschema"],
+    install_requires=[numpy_ver, "h5py", "svgwrite", "jsonschema"],
     entry_points={
         'console_scripts': [
             'tskit=tskit.__main__:main',
diff --git a/python/tests/test_drawing.py b/python/tests/test_drawing.py
index d506733be8..64253b9bba 100644
--- a/python/tests/test_drawing.py
+++ b/python/tests/test_drawing.py
@@ -27,13 +27,13 @@
 from __future__ import print_function
 from __future__ import division
 
+import io
 import os
 import tempfile
 import unittest
 import xml.etree
 
 import msprime
-import six
 import tskit
 import tests.tsutil as tsutil
 
@@ -230,7 +230,7 @@ def test_draw_zero_edge_tree(self):
         self.verify_basic_text(text)
 
     def test_even_num_children_tree(self):
-        nodes = six.StringIO("""\
+        nodes = io.StringIO("""\
         id  is_sample   time
         0   1           0
         1   1           1
@@ -240,7 +240,7 @@ def test_even_num_children_tree(self):
         5   1           5
         6   1           7
         """)
-        edges = six.StringIO("""\
+        edges = io.StringIO("""\
         left    right   parent  child
         0       1       6       0
         0       1       6       1
@@ -255,7 +255,7 @@ def test_even_num_children_tree(self):
         self.verify_basic_text(text)
 
     def test_odd_num_children_tree(self):
-        nodes = six.StringIO("""\
+        nodes = io.StringIO("""\
         id  is_sample   time
         0   1           0
         1   1           1
@@ -264,7 +264,7 @@ def test_odd_num_children_tree(self):
         4   1           4
         5   1           5
         """)
-        edges = six.StringIO("""\
+        edges = io.StringIO("""\
         left    right   parent  child
         0       1       5       0
         0       1       5       1
@@ -317,13 +317,13 @@ def verify_text_rendering(self, drawn, drawn_tree, debug=False):
             self.assertEqual(l1.rstrip(), l2.rstrip())
 
     def test_simple_tree(self):
-        nodes = six.StringIO("""\
+        nodes = io.StringIO("""\
         id  is_sample   time
         0   1           0
         1   1           0
         2   1           2
         """)
-        edges = six.StringIO("""\
+        edges = io.StringIO("""\
         left    right   parent  child
         0       1       2       0
         0       1       2       1
@@ -338,14 +338,14 @@ def test_simple_tree(self):
         self.verify_text_rendering(drawn, tree)
 
     def test_trident_tree(self):
-        nodes = six.StringIO("""\
+        nodes = io.StringIO("""\
         id  is_sample   time
         0   1           0
         1   1           0
         2   1           0
         3   1           2
         """)
-        edges = six.StringIO("""\
+        edges = io.StringIO("""\
         left    right   parent  child
         0       1       3       0
         0       1       3       1
@@ -361,7 +361,7 @@ def test_trident_tree(self):
         self.verify_text_rendering(drawn, tree)
 
     def test_pitchfork_tree(self):
-        nodes = six.StringIO("""\
+        nodes = io.StringIO("""\
         id  is_sample   time
         0   1           0
         1   1           0
@@ -369,7 +369,7 @@ def test_pitchfork_tree(self):
         3   1           0
         4   1           2
         """)
-        edges = six.StringIO("""\
+        edges = io.StringIO("""\
         left    right   parent  child
         0       1       4       0
         0       1       4       1
@@ -398,13 +398,13 @@ def test_pitchfork_tree(self):
         self.verify_text_rendering(drawn, tree)
 
     def test_stick_tree(self):
-        nodes = six.StringIO("""\
+        nodes = io.StringIO("""\
         id  is_sample   time
         0   1           0
         1   1           1
         2   1           2
         """)
-        edges = six.StringIO("""\
+        edges = io.StringIO("""\
         left    right   parent  child
         0       1       1       0
         0       1       2       1
diff --git a/python/tests/test_highlevel.py b/python/tests/test_highlevel.py
index 1ceece9b49..9fada93929 100644
--- a/python/tests/test_highlevel.py
+++ b/python/tests/test_highlevel.py
@@ -35,12 +35,12 @@
 
 import collections
 import itertools
+import io
 import json
 import math
 import os
 import random
 import shutil
-import six
 import tempfile
 import unittest
 import warnings
@@ -1715,10 +1715,10 @@ def convert(v):
     def test_output_format(self):
         for ts in get_example_tree_sequences():
             for precision in [2, 7]:
-                nodes_file = six.StringIO()
-                edges_file = six.StringIO()
-                sites_file = six.StringIO()
-                mutations_file = six.StringIO()
+                nodes_file = io.StringIO()
+                edges_file = io.StringIO()
+                sites_file = io.StringIO()
+                mutations_file = io.StringIO()
                 ts.dump_text(
                     nodes=nodes_file, edges=edges_file, sites=sites_file,
                     mutations=mutations_file, precision=precision)
@@ -1778,12 +1778,12 @@ def verify_approximate_equality(self, ts1, ts2):
 
     def test_text_record_round_trip(self):
         for ts1 in get_example_tree_sequences():
-            nodes_file = six.StringIO()
-            edges_file = six.StringIO()
-            sites_file = six.StringIO()
-            mutations_file = six.StringIO()
-            individuals_file = six.StringIO()
-            populations_file = six.StringIO()
+            nodes_file = io.StringIO()
+            edges_file = io.StringIO()
+            sites_file = io.StringIO()
+            mutations_file = io.StringIO()
+            individuals_file = io.StringIO()
+            populations_file = io.StringIO()
             ts1.dump_text(
                 nodes=nodes_file, edges=edges_file, sites=sites_file,
                 mutations=mutations_file, individuals=individuals_file,
@@ -1803,20 +1803,20 @@ def test_text_record_round_trip(self):
             self.verify_approximate_equality(ts1, ts2)
 
     def test_empty_files(self):
-        nodes_file = six.StringIO("is_sample\ttime\n")
-        edges_file = six.StringIO("left\tright\tparent\tchild\n")
-        sites_file = six.StringIO("position\tancestral_state\n")
-        mutations_file = six.StringIO("site\tnode\tderived_state\n")
+        nodes_file = io.StringIO("is_sample\ttime\n")
+        edges_file = io.StringIO("left\tright\tparent\tchild\n")
+        sites_file = io.StringIO("position\tancestral_state\n")
+        mutations_file = io.StringIO("site\tnode\tderived_state\n")
         self.assertRaises(
             _tskit.LibraryError, tskit.load_text,
             nodes=nodes_file, edges=edges_file, sites=sites_file,
             mutations=mutations_file)
 
     def test_empty_files_sequence_length(self):
-        nodes_file = six.StringIO("is_sample\ttime\n")
-        edges_file = six.StringIO("left\tright\tparent\tchild\n")
-        sites_file = six.StringIO("position\tancestral_state\n")
-        mutations_file = six.StringIO("site\tnode\tderived_state\n")
+        nodes_file = io.StringIO("is_sample\ttime\n")
+        edges_file = io.StringIO("left\tright\tparent\tchild\n")
+        sites_file = io.StringIO("position\tancestral_state\n")
+        mutations_file = io.StringIO("site\tnode\tderived_state\n")
         ts = tskit.load_text(
             nodes=nodes_file, edges=edges_file, sites=sites_file,
             mutations=mutations_file, sequence_length=100)
@@ -2333,8 +2333,8 @@ def verify_random_permutation(self, ts):
         other_ts.dump(self.temp_file)
         ts3 = tskit.load(self.temp_file)
         self.verify_tree_sequences_equal(other_ts, ts3)
-        nodes_file = six.StringIO()
-        edges_file = six.StringIO()
+        nodes_file = io.StringIO()
+        edges_file = io.StringIO()
         # Also verify we can read the text version.
         other_ts.dump_text(nodes=nodes_file, edges=edges_file, precision=14)
         nodes_file.seek(0)
diff --git a/python/tests/test_metadata.py b/python/tests/test_metadata.py
index 69c1c1b820..904f9ff941 100644
--- a/python/tests/test_metadata.py
+++ b/python/tests/test_metadata.py
@@ -27,6 +27,7 @@
 from __future__ import print_function
 from __future__ import division
 
+import io
 import json
 import os
 import tempfile
@@ -35,7 +36,6 @@
 
 import numpy as np
 import python_jsonschema_objects as pjs
-import six
 import msprime
 
 import tskit
@@ -192,7 +192,7 @@ class TestLoadTextMetadata(unittest.TestCase):
     """
 
     def test_individuals(self):
-        individuals = six.StringIO("""\
+        individuals = io.StringIO("""\
         id  flags location     metadata
         0   1     0.0,1.0,0.0  abc
         1   1     1.0,2.0      XYZ+
@@ -212,7 +212,7 @@ def test_individuals(self):
                              b.metadata)
 
     def test_nodes(self):
-        nodes = six.StringIO("""\
+        nodes = io.StringIO("""\
         id  is_sample   time    metadata
         0   1           0   abc
         1   1           0   XYZ+
@@ -226,7 +226,7 @@ def test_nodes(self):
                              b.metadata)
 
     def test_sites(self):
-        sites = six.StringIO("""\
+        sites = io.StringIO("""\
         position    ancestral_state metadata
         0.1 A   abc
         0.5 C   XYZ+
@@ -240,7 +240,7 @@ def test_sites(self):
                              b.metadata)
 
     def test_mutations(self):
-        mutations = six.StringIO("""\
+        mutations = io.StringIO("""\
         site    node    derived_state   metadata
         0   2   C   mno
         0   3   G   )(*&^%$#@!
@@ -253,7 +253,7 @@ def test_mutations(self):
                              b.metadata)
 
     def test_populations(self):
-        populations = six.StringIO("""\
+        populations = io.StringIO("""\
         id    metadata
         0     mno
         1     )(*&^%$#@!
diff --git a/python/tests/test_tables.py b/python/tests/test_tables.py
index 019b02c8f6..f9c11f1f3b 100644
--- a/python/tests/test_tables.py
+++ b/python/tests/test_tables.py
@@ -28,6 +28,7 @@
 from __future__ import print_function
 from __future__ import division
 
+import io
 import pickle
 import random
 import string
@@ -35,7 +36,6 @@
 import warnings
 
 import numpy as np
-import six
 
 import tskit
 import _tskit
@@ -1140,20 +1140,20 @@ class TestSortMutations(unittest.TestCase):
     """
 
     def test_sort_mutations_stability(self):
-        nodes = six.StringIO("""\
+        nodes = io.StringIO("""\
         id      is_sample   time
         0       1           0
         1       1           0
         """)
-        edges = six.StringIO("""\
+        edges = io.StringIO("""\
         left    right   parent  child
         """)
-        sites = six.StringIO("""\
+        sites = io.StringIO("""\
         position    ancestral_state
         0.1     0
         0.2     0
         """)
-        mutations = six.StringIO("""\
+        mutations = io.StringIO("""\
         site    node    derived_state   parent
         1       0       1               -1
         1       1       1               -1
@@ -1173,19 +1173,19 @@ def test_sort_mutations_stability(self):
         self.assertEqual(list(mutations.node), [1, 0, 0, 1])
 
     def test_sort_mutations_remap_parent_id(self):
-        nodes = six.StringIO("""\
+        nodes = io.StringIO("""\
         id      is_sample   time
         0       1           0
         """)
-        edges = six.StringIO("""\
+        edges = io.StringIO("""\
         left    right   parent  child
         """)
-        sites = six.StringIO("""\
+        sites = io.StringIO("""\
         position    ancestral_state
         0.1     0
         0.2     0
         """)
-        mutations = six.StringIO("""\
+        mutations = io.StringIO("""\
         site    node    derived_state   parent
         1       0       1               -1
         1       0       0               0
@@ -1208,18 +1208,18 @@ def test_sort_mutations_remap_parent_id(self):
         self.assertEqual(list(mutations.parent), [-1, 0, 1, -1, 3, 4])
 
     def test_sort_mutations_bad_parent_id(self):
-        nodes = six.StringIO("""\
+        nodes = io.StringIO("""\
         id      is_sample   time
         0       1           0
         """)
-        edges = six.StringIO("""\
+        edges = io.StringIO("""\
         left    right   parent  child
         """)
-        sites = six.StringIO("""\
+        sites = io.StringIO("""\
         position    ancestral_state
         0.1     0
         """)
-        mutations = six.StringIO("""\
+        mutations = io.StringIO("""\
         site    node    derived_state   parent
         1       0       1               -2
         """)
diff --git a/python/tests/test_topology.py b/python/tests/test_topology.py
index 57a64f778c..2579408a0f 100644
--- a/python/tests/test_topology.py
+++ b/python/tests/test_topology.py
@@ -33,11 +33,11 @@
     # This fails for Python 3.x, but that's fine.
     pass
 
+import io
 import unittest
 import itertools
 import random
 
-import six
 import numpy as np
 import msprime
 
@@ -442,13 +442,13 @@ def verify_zero_roots(self, ts):
             self.assertEqual(tree.roots, [])
 
     def test_simple_hole(self):
-        nodes = six.StringIO("""\
+        nodes = io.StringIO("""\
         id  is_sample   time
         0   1           0
         1   1           0
         2   0           1
         """)
-        edges = six.StringIO("""\
+        edges = io.StringIO("""\
         left    right   parent  child
         0       1       2       0
         2       3       2       0
@@ -463,13 +463,13 @@ def test_simple_hole(self):
         self.verify_trees(ts, expected)
 
     def test_simple_hole_zero_roots(self):
-        nodes = six.StringIO("""\
+        nodes = io.StringIO("""\
         id  is_sample   time
         0   0           0
         1   0           0
         2   0           1
         """)
-        edges = six.StringIO("""\
+        edges = io.StringIO("""\
         left    right   parent  child
         0       1       2       0
         2       3       2       0
@@ -485,13 +485,13 @@ def test_simple_hole_zero_roots(self):
         self.verify_zero_roots(ts)
 
     def test_initial_gap(self):
-        nodes = six.StringIO("""\
+        nodes = io.StringIO("""\
         id  is_sample   time
         0   1           0
         1   1           0
         2   0           1
         """)
-        edges = six.StringIO("""\
+        edges = io.StringIO("""\
         left    right   parent  child
         1       2       2       0,1
         """)
@@ -502,13 +502,13 @@ def test_initial_gap(self):
         self.verify_trees(ts, expected)
 
     def test_initial_gap_zero_roots(self):
-        nodes = six.StringIO("""\
+        nodes = io.StringIO("""\
         id  is_sample   time
         0   0           0
         1   0           0
         2   0           1
         """)
-        edges = six.StringIO("""\
+        edges = io.StringIO("""\
         left    right   parent  child
         1       2       2       0,1
         """)
@@ -520,13 +520,13 @@ def test_initial_gap_zero_roots(self):
         self.verify_zero_roots(ts)
 
     def test_final_gap(self):
-        nodes = six.StringIO("""\
+        nodes = io.StringIO("""\
         id  is_sample   time
         0   1           0
         1   1           0
         2   0           1
         """)
-        edges = six.StringIO("""\
+        edges = io.StringIO("""\
         left    right   parent  child
         0       2       2       0,1
         """)
@@ -537,13 +537,13 @@ def test_final_gap(self):
         self.verify_trees(ts, expected)
 
     def test_final_gap_zero_roots(self):
-        nodes = six.StringIO("""\
+        nodes = io.StringIO("""\
         id  is_sample   time
         0   0           0
         1   0           0
         2   0           1
         """)
-        edges = six.StringIO("""\
+        edges = io.StringIO("""\
         left    right   parent  child
         0       2       2       0,1
         """)
@@ -555,13 +555,13 @@ def test_final_gap_zero_roots(self):
         self.verify_zero_roots(ts)
 
     def test_initial_and_final_gap(self):
-        nodes = six.StringIO("""\
+        nodes = io.StringIO("""\
         id  is_sample   time
         0   1           0
         1   1           0
         2   0           1
         """)
-        edges = six.StringIO("""\
+        edges = io.StringIO("""\
         left    right   parent  child
         1       2       2       0,1
         """)
@@ -573,13 +573,13 @@ def test_initial_and_final_gap(self):
         self.verify_trees(ts, expected)
 
     def test_initial_and_final_gap_zero_roots(self):
-        nodes = six.StringIO("""\
+        nodes = io.StringIO("""\
         id  is_sample   time
         0   0           0
         1   0           0
         2   0           1
         """)
-        edges = six.StringIO("""\
+        edges = io.StringIO("""\
         left    right   parent  child
         1       2       2       0,1
         """)
@@ -599,7 +599,7 @@ class TestTsinferExamples(TopologyTestCase):
     def test_no_last_tree(self):
         # The last tree was not being generated here because of a bug in
         # the low-level tree generation code.
-        nodes = six.StringIO("""\
+        nodes = io.StringIO("""\
         id      is_sample   population      time
         0       1       -1              3.00000000000000
         1       1       -1              2.00000000000000
@@ -613,7 +613,7 @@ def test_no_last_tree(self):
         9       1       -1              1.00000000000000
         10      1       -1              1.00000000000000
         """)
-        edges = six.StringIO("""\
+        edges = io.StringIO("""\
         id      left            right           parent  child
         0       62291.41659631  79679.17408763  1       5
         1       62291.41659631  62374.60889677  1       6
@@ -672,12 +672,12 @@ class TestRecordSquashing(TopologyTestCase):
     Tests that we correctly squash adjacent equal records together.
     """
     def test_single_record(self):
-        nodes = six.StringIO("""\
+        nodes = io.StringIO("""\
         id  is_sample   time
         0   1           0
         1   1           1
         """)
-        edges = six.StringIO("""\
+        edges = io.StringIO("""\
         left    right   parent  child
         0       1       1       0
         1       2       1       0
@@ -751,7 +751,7 @@ class TestUnaryNodes(TopologyTestCase):
     """
     def test_simple_case(self):
         # Simple case where we have n = 2 and some unary nodes.
-        nodes = six.StringIO("""\
+        nodes = io.StringIO("""\
         id      is_sample   time
         0       1           0
         1       1           0
@@ -760,7 +760,7 @@ def test_simple_case(self):
         4       0           2
         5       0           3
         """)
-        edges = six.StringIO("""\
+        edges = io.StringIO("""\
         left    right   parent  child
         0       1       2       0
         0       1       3       1
@@ -774,8 +774,8 @@ def test_simple_case(self):
             sites += "{} 0\n".format(position)
             mutations += "{} {} 1\n".format(j, j)
         ts = tskit.load_text(
-            nodes=nodes, edges=edges, sites=six.StringIO(sites),
-            mutations=six.StringIO(mutations), strict=False)
+            nodes=nodes, edges=edges, sites=io.StringIO(sites),
+            mutations=io.StringIO(mutations), strict=False)
 
         self.assertEqual(ts.sample_size, 2)
         self.assertEqual(ts.num_nodes, 6)
@@ -815,7 +815,7 @@ def test_ladder_tree(self):
         root = num_unary_nodes + 3
         root_time = num_unary_nodes + 3
         edges += "0    1     {}      1,{}\n".format(root, num_unary_nodes + 2)
-        ts = tskit.load_text(six.StringIO(nodes), six.StringIO(edges), strict=False)
+        ts = tskit.load_text(io.StringIO(nodes), io.StringIO(edges), strict=False)
         t = next(ts.trees())
         self.assertEqual(t.mrca(0, 1), root)
         self.assertEqual(t.tmrca(0, 1), root_time)
@@ -892,7 +892,7 @@ class TestGeneralSamples(TopologyTestCase):
     """
     def test_simple_case(self):
         # Simple case where we have n = 3 and samples starting at n.
-        nodes = six.StringIO("""\
+        nodes = io.StringIO("""\
         id      is_sample   time
         0       0           2
         1       0           1
@@ -900,19 +900,19 @@ def test_simple_case(self):
         3       1           0
         4       1           0
         """)
-        edges = six.StringIO("""\
+        edges = io.StringIO("""\
         left    right   parent  child
         0       1       1       2,3
         0       1       0       1,4
         """)
-        sites = six.StringIO("""\
+        sites = io.StringIO("""\
         position    ancestral_state
         0.1     0
         0.2     0
         0.3     0
         0.4     0
         """)
-        mutations = six.StringIO("""\
+        mutations = io.StringIO("""\
         site    node    derived_state
         0       2       1
         1       3       1
@@ -1060,21 +1060,21 @@ def verify_simplify(
         required output.
         """
         ts = tskit.load_text(
-            nodes=six.StringIO(nodes_before),
-            edges=six.StringIO(edges_before),
-            sites=six.StringIO(sites_before) if sites_before is not None else None,
+            nodes=io.StringIO(nodes_before),
+            edges=io.StringIO(edges_before),
+            sites=io.StringIO(sites_before) if sites_before is not None else None,
             mutations=(
-                six.StringIO(mutations_before)
+                io.StringIO(mutations_before)
                 if mutations_before is not None else None),
             strict=False)
         before = ts.dump_tables()
 
         ts = tskit.load_text(
-            nodes=six.StringIO(nodes_after),
-            edges=six.StringIO(edges_after),
-            sites=six.StringIO(sites_after) if sites_after is not None else None,
+            nodes=io.StringIO(nodes_after),
+            edges=io.StringIO(edges_after),
+            sites=io.StringIO(sites_after) if sites_after is not None else None,
             mutations=(
-                six.StringIO(mutations_after)
+                io.StringIO(mutations_after)
                 if mutations_after is not None else None),
             strict=False,
             sequence_length=before.sequence_length)
@@ -1112,8 +1112,8 @@ def test_unsorted_edges(self):
         1       2       2       0,1
         1       2       3       0,1
         """
-        nodes = tskit.parse_nodes(six.StringIO(nodes_before), strict=False)
-        edges = tskit.parse_edges(six.StringIO(edges_before), strict=False)
+        nodes = tskit.parse_nodes(io.StringIO(nodes_before), strict=False)
+        edges = tskit.parse_edges(io.StringIO(edges_before), strict=False)
         # Cannot use load_text here because it calls sort()
         tables = tskit.TableCollection(sequence_length=2)
         tables.nodes.set_columns(**nodes.asdict())
@@ -1431,7 +1431,7 @@ class TestNonSampleExternalNodes(TopologyTestCase):
     """
     def test_simple_case(self):
         # Simplest case where we have n = 2 and external non-sample nodes.
-        nodes = six.StringIO("""\
+        nodes = io.StringIO("""\
         id      is_sample   time
         0       1           0
         1       1           0
@@ -1439,18 +1439,18 @@ def test_simple_case(self):
         3       0           0
         4       0           0
         """)
-        edges = six.StringIO("""\
+        edges = io.StringIO("""\
         left    right   parent  child
         0       1       2       0,1,3,4
         """)
-        sites = six.StringIO("""\
+        sites = io.StringIO("""\
         id  position    ancestral_state
         0   0.1         0
         1   0.2         0
         2   0.3         0
         3   0.4         0
         """)
-        mutations = six.StringIO("""\
+        mutations = io.StringIO("""\
         site    node    derived_state
         0       0       1
         1       1       1
@@ -1513,20 +1513,20 @@ class TestMultipleRoots(TopologyTestCase):
 
     def test_simplest_degenerate_case(self):
         # Simplest case where we have n = 2 and no edges.
-        nodes = six.StringIO("""\
+        nodes = io.StringIO("""\
         id      is_sample   time
         0       1           0
         1       1           0
         """)
-        edges = six.StringIO("""\
+        edges = io.StringIO("""\
         left    right   parent  child
         """)
-        sites = six.StringIO("""\
+        sites = io.StringIO("""\
         id  position    ancestral_state
         0   0.1         0
         1   0.2         0
         """)
-        mutations = six.StringIO("""\
+        mutations = io.StringIO("""\
         site    node    derived_state
         0       0         1
         1       1         1
@@ -1552,7 +1552,7 @@ def test_simplest_degenerate_case(self):
 
     def test_simplest_non_degenerate_case(self):
         # Simplest case where we have n = 4 and two trees.
-        nodes = six.StringIO("""\
+        nodes = io.StringIO("""\
         id      is_sample   time
         0       1           0
         1       1           0
@@ -1561,19 +1561,19 @@ def test_simplest_non_degenerate_case(self):
         4       0           1
         5       0           2
         """)
-        edges = six.StringIO("""\
+        edges = io.StringIO("""\
         left    right   parent  child
         0       1       4       0,1
         0       1       5       2,3
         """)
-        sites = six.StringIO("""\
+        sites = io.StringIO("""\
         id  position    ancestral_state
         0   0.1         0
         1   0.2         0
         2   0.3         0
         3   0.4         0
         """)
-        mutations = six.StringIO("""\
+        mutations = io.StringIO("""\
         site    node    derived_state
         0       0       1
         1       1       1
@@ -1610,7 +1610,7 @@ def test_simplest_non_degenerate_case(self):
 
     def test_two_reducable_trees(self):
         # We have n = 4 and two trees, with some unary nodes and non-sample leaves
-        nodes = six.StringIO("""\
+        nodes = io.StringIO("""\
         id      is_sample   time
         0       1           0
         1       1           0
@@ -1622,14 +1622,14 @@ def test_two_reducable_trees(self):
         7       0           3
         8       0           0   # Non sample leaf
         """)
-        edges = six.StringIO("""\
+        edges = io.StringIO("""\
         left    right   parent  child
         0       1      4         0
         0       1      5         1
         0       1      6         4,5
         0       1      7         2,3,8
         """)
-        sites = six.StringIO("""\
+        sites = io.StringIO("""\
         id  position    ancestral_state
         0   0.1         0
         1   0.2         0
@@ -1637,7 +1637,7 @@ def test_two_reducable_trees(self):
         3   0.4         0
         4   0.5         0
         """)
-        mutations = six.StringIO("""\
+        mutations = io.StringIO("""\
         site    node    derived_state
         0       0       1
         1       1       1
@@ -1682,7 +1682,7 @@ def test_two_reducable_trees(self):
 
     def test_one_reducable_tree(self):
         # We have n = 4 and two trees. One tree is reducable and the other isn't.
-        nodes = six.StringIO("""\
+        nodes = io.StringIO("""\
         id      is_sample   time
         0       1           0
         1       1           0
@@ -1694,7 +1694,7 @@ def test_one_reducable_tree(self):
         7       0           3
         8       0           0   # Non sample leaf
         """)
-        edges = six.StringIO("""\
+        edges = io.StringIO("""\
         left    right   parent  child
         0       1      4         0
         0       1      5         1
@@ -1722,7 +1722,7 @@ def test_one_reducable_tree(self):
     # so there might be other problems with it.
     def test_mutations_over_roots(self):
         # Mutations over root nodes should be ok when we have multiple roots.
-        nodes = six.StringIO("""\
+        nodes = io.StringIO("""\
         id      is_sample   time
         0       1           0
         1       1           0
@@ -1731,13 +1731,13 @@ def test_mutations_over_roots(self):
         4       0           2
         5       0           2
         """)
-        edges = six.StringIO("""\
+        edges = io.StringIO("""\
         left    right   parent  child
         0       1       3       0,1
         0       1       4       3
         0       1       5       2
         """)
-        sites = six.StringIO("""\
+        sites = io.StringIO("""\
         id  position    ancestral_state
         0   0.1         0
         1   0.2         0
@@ -1746,7 +1746,7 @@ def test_mutations_over_roots(self):
         4   0.5         0
         5   0.6         0
         """)
-        mutations = six.StringIO("""\
+        mutations = io.StringIO("""\
         site    node    derived_state
         0       0       1
         1       1       1
@@ -1850,7 +1850,7 @@ def test_partial_non_sample_external_nodes(self):
         #
         #          (0.0, 0.2),                 (0.2, 0.8),         (0.8, 1.0)
 
-        nodes = six.StringIO("""\
+        nodes = io.StringIO("""\
         id      is_sample   time
         0       1           0
         1       1           0
@@ -1861,7 +1861,7 @@ def test_partial_non_sample_external_nodes(self):
         6       0           0.7
         7       0           1.0
         """)
-        edges = six.StringIO("""\
+        edges = io.StringIO("""\
         left    right   parent  child
         0.0     0.2     4       2,3
         0.2     0.8     4       0,2
@@ -1904,7 +1904,7 @@ def test_partial_non_sample_external_nodes_2(self):
         # 0.0 0     1           2     1   0       2     0   1           2
         #
         #          (0.0, 0.2),         (0.2, 0.8),         (0.8, 1.0)
-        nodes = six.StringIO("""\
+        nodes = io.StringIO("""\
         id      is_sample   time
         0       1           0
         1       1           0
@@ -1915,7 +1915,7 @@ def test_partial_non_sample_external_nodes_2(self):
         6       0           1.0
         7       0           0    # Non sample leaf
         """)
-        edges = six.StringIO("""\
+        edges = io.StringIO("""\
         left    right   parent  child
         0.0     0.2     3       2,7
         0.2     0.8     3       0,2
@@ -1961,7 +1961,7 @@ def test_single_offspring_records(self):
         # 0.0    0     1           2           1   0       2          0   1           2
         #
         #          (0.0, 0.2),               (0.2, 0.8),              (0.8, 1.0)
-        nodes = six.StringIO("""\
+        nodes = io.StringIO("""\
         id  is_sample   time
         0   1           0
         1   1           0
@@ -1972,7 +1972,7 @@ def test_single_offspring_records(self):
         6   0           0.7
         7   0           1.0
         """)
-        edges = six.StringIO("""\
+        edges = io.StringIO("""\
         left    right   parent  child
         0.0     0.2     4       2,3
         0.2     0.8     4       0,2
@@ -2056,7 +2056,7 @@ def test_many_single_offspring(self):
             {0: 9, 1: 10, 2: 5, 3: -1, 4: 3, 5: 3, 6: 3, 7: 5, 8: 7, 9: 6, 10: 8}
         ]
         true_haplotypes = ['0100', '0001', '1110']
-        nodes = six.StringIO("""\
+        nodes = io.StringIO("""\
         id      is_sample   time
         0       1           0
         1       1           0
@@ -2070,7 +2070,7 @@ def test_many_single_offspring(self):
         9       0           1
         10      0           1
         """)
-        edges = six.StringIO("""\
+        edges = io.StringIO("""\
         left    right   parent  child
         0.5     1.0     10      1
         0.0     0.4     10      2
@@ -2091,14 +2091,14 @@ def test_many_single_offspring(self):
         0.1     0.9     3       4,5
         0.0     0.1     3       4,5,7
         """)
-        sites = six.StringIO("""\
+        sites = io.StringIO("""\
         position    ancestral_state
         0.05        0
         0.15        0
         0.25        0
         0.4         0
         """)
-        mutations = six.StringIO("""\
+        mutations = io.StringIO("""\
         site    node    derived_state   parent
         0       7       1               -1
         0      10       0               0
@@ -2151,7 +2151,7 @@ def test_tricky_switches(self):
         # 0   1 2   3 4  5  . 0   1 2   3 4  5  . 4   5 2   3 0  1  .
         #                   .                   .                   .
         # 0.0              0.4                 0.5                 1.0
-        nodes = six.StringIO("""\
+        nodes = io.StringIO("""\
         id      is_sample   time
         0       1           0
         1       1           0
@@ -2167,7 +2167,7 @@ def test_tricky_switches(self):
         11      0           3
         12      0           4
         """)
-        edges = six.StringIO("""\
+        edges = io.StringIO("""\
         left right parent child
         0.0  0.5   6      0
         0.0  0.5   6      1
@@ -2236,7 +2236,7 @@ def test_tricky_simplify(self):
         #  . 0   1 2   3 4  5  . 4   5 2   3 0  1  .
         #  .                   .                   .
         # 0.4                 0.5                 1.0
-        nodes = six.StringIO("""\
+        nodes = io.StringIO("""\
         id      is_sample   time
         0       1           0
         1       1           0
@@ -2255,7 +2255,7 @@ def test_tricky_simplify(self):
         14      0           1
         15      0           2
         """)
-        edges = six.StringIO("""\
+        edges = io.StringIO("""\
         left right parent child
         0.0  0.5   6      0,1
         0.5  1.0   6      4,5
@@ -2318,7 +2318,7 @@ def test_ancestral_samples(self):
         #              *           *          *           *         *           *
         #          (0.0, 0.2),                 (0.2, 0.8),         (0.8, 1.0)
 
-        nodes = six.StringIO("""\
+        nodes = io.StringIO("""\
         id      is_sample   time
         0       0           0
         1       1           0
@@ -2330,7 +2330,7 @@ def test_ancestral_samples(self):
         7       0           1.0
         8       0           0.8
         """)
-        edges = six.StringIO("""\
+        edges = io.StringIO("""\
         left    right   parent  child
         0.0     0.2     4       2,3
         0.2     0.8     4       0,2
@@ -2396,7 +2396,7 @@ def test_all_ancestral_samples(self):
         #
         #          (0.0, 0.2),                 (0.2, 0.8),         (0.8, 1.0)
 
-        nodes = six.StringIO("""\
+        nodes = io.StringIO("""\
         id      is_sample   time
         0       0           0
         1       1           0.1
@@ -2408,7 +2408,7 @@ def test_all_ancestral_samples(self):
         7       0           1.0
         8       0           0.8
         """)
-        edges = six.StringIO("""\
+        edges = io.StringIO("""\
         left    right   parent  child
         0.0     0.2     4       2,3
         0.2     0.8     4       0,2
@@ -2454,7 +2454,7 @@ def test_internal_sampled_node(self):
         # 0.0    0      *         *            *  0      *      0    *         *
         #
         #          (0.0, 0.2),                 (0.2, 0.8),         (0.8, 1.0)
-        nodes = six.StringIO("""\
+        nodes = io.StringIO("""\
         id      is_sample   time
         0       0           0
         1       1           0.1
@@ -2466,7 +2466,7 @@ def test_internal_sampled_node(self):
         7       0           1.0
         8       0           0.8
         """)
-        edges = six.StringIO("""\
+        edges = io.StringIO("""\
         left    right   parent  child
         0.0     0.2     4       2,3
         0.2     0.8     4       0,2
@@ -2540,14 +2540,14 @@ class TestBadTrees(unittest.TestCase):
     try to create trees.
     """
     def test_simplest_contradictory_children(self):
-        nodes = six.StringIO("""\
+        nodes = io.StringIO("""\
         id      is_sample   time
         0       1           0
         1       1           0
         2       0           1
         3       0           2
         """)
-        edges = six.StringIO("""\
+        edges = io.StringIO("""\
         left    right   parent  child
         0.0     1.0     2       0
         0.0     1.0     3       0
@@ -2556,14 +2556,14 @@ def test_simplest_contradictory_children(self):
         self.assertRaises(_tskit.LibraryError, list, ts.trees())
 
     def test_partial_overlap_contradictory_children(self):
-        nodes = six.StringIO("""\
+        nodes = io.StringIO("""\
         id      is_sample   time
         0       1           0
         1       1           0
         2       0           1
         3       0           2
         """)
-        edges = six.StringIO("""\
+        edges = io.StringIO("""\
         left    right   parent  child
         0.0     1.0     2       0,1
         0.5     1.0     3       0
@@ -2744,8 +2744,8 @@ def test_many_trees(self):
 
     def test_small_tree_internal_samples(self):
         ts = tskit.load_text(
-            nodes=six.StringIO(self.small_tree_ex_nodes),
-            edges=six.StringIO(self.small_tree_ex_edges), strict=False)
+            nodes=io.StringIO(self.small_tree_ex_nodes),
+            edges=io.StringIO(self.small_tree_ex_edges), strict=False)
         tables = ts.dump_tables()
         nodes = tables.nodes
         flags = nodes.flags
@@ -2766,8 +2766,8 @@ def test_small_tree_internal_samples(self):
 
     def test_small_tree_linear_samples(self):
         ts = tskit.load_text(
-            nodes=six.StringIO(self.small_tree_ex_nodes),
-            edges=six.StringIO(self.small_tree_ex_edges), strict=False)
+            nodes=io.StringIO(self.small_tree_ex_nodes),
+            edges=io.StringIO(self.small_tree_ex_edges), strict=False)
         tables = ts.dump_tables()
         nodes = tables.nodes
         flags = nodes.flags
@@ -2788,8 +2788,8 @@ def test_small_tree_linear_samples(self):
 
     def test_small_tree_internal_and_external_samples(self):
         ts = tskit.load_text(
-            nodes=six.StringIO(self.small_tree_ex_nodes),
-            edges=six.StringIO(self.small_tree_ex_edges), strict=False)
+            nodes=io.StringIO(self.small_tree_ex_nodes),
+            edges=io.StringIO(self.small_tree_ex_edges), strict=False)
         tables = ts.dump_tables()
         nodes = tables.nodes
         flags = nodes.flags
@@ -2812,8 +2812,8 @@ def test_small_tree_internal_and_external_samples(self):
 
     def test_small_tree_mutations(self):
         ts = tskit.load_text(
-            nodes=six.StringIO(self.small_tree_ex_nodes),
-            edges=six.StringIO(self.small_tree_ex_edges), strict=False)
+            nodes=io.StringIO(self.small_tree_ex_nodes),
+            edges=io.StringIO(self.small_tree_ex_edges), strict=False)
         tables = ts.dump_tables()
         # Add some simple mutations here above the nodes we're keeping.
         tables.sites.add_row(position=0.25, ancestral_state="0")
@@ -2834,8 +2834,8 @@ def test_small_tree_mutations(self):
 
     def test_small_tree_filter_zero_mutations(self):
         ts = tskit.load_text(
-            nodes=six.StringIO(self.small_tree_ex_nodes),
-            edges=six.StringIO(self.small_tree_ex_edges), strict=False)
+            nodes=io.StringIO(self.small_tree_ex_nodes),
+            edges=io.StringIO(self.small_tree_ex_edges), strict=False)
         ts = tsutil.insert_branch_sites(ts)
         self.assertEqual(ts.num_sites, 8)
         self.assertEqual(ts.num_mutations, 8)
@@ -2848,8 +2848,8 @@ def test_small_tree_filter_zero_mutations(self):
 
     def test_small_tree_fixed_sites(self):
         ts = tskit.load_text(
-            nodes=six.StringIO(self.small_tree_ex_nodes),
-            edges=six.StringIO(self.small_tree_ex_edges), strict=False)
+            nodes=io.StringIO(self.small_tree_ex_nodes),
+            edges=io.StringIO(self.small_tree_ex_edges), strict=False)
         tables = ts.dump_tables()
         # Add some simple mutations that will be fixed after simplify
         tables.sites.add_row(position=0.25, ancestral_state="0")
@@ -2868,8 +2868,8 @@ def test_small_tree_fixed_sites(self):
 
     def test_small_tree_mutations_over_root(self):
         ts = tskit.load_text(
-            nodes=six.StringIO(self.small_tree_ex_nodes),
-            edges=six.StringIO(self.small_tree_ex_edges), strict=False)
+            nodes=io.StringIO(self.small_tree_ex_nodes),
+            edges=io.StringIO(self.small_tree_ex_edges), strict=False)
         tables = ts.dump_tables()
         tables.sites.add_row(position=0.25, ancestral_state="0")
         tables.mutations.add_row(site=0, node=8, derived_state="1")
@@ -2883,8 +2883,8 @@ def test_small_tree_mutations_over_root(self):
 
     def test_small_tree_recurrent_mutations(self):
         ts = tskit.load_text(
-            nodes=six.StringIO(self.small_tree_ex_nodes),
-            edges=six.StringIO(self.small_tree_ex_edges), strict=False)
+            nodes=io.StringIO(self.small_tree_ex_nodes),
+            edges=io.StringIO(self.small_tree_ex_edges), strict=False)
         tables = ts.dump_tables()
         # Add recurrent mutation on the root branches
         tables.sites.add_row(position=0.25, ancestral_state="0")
@@ -2901,8 +2901,8 @@ def test_small_tree_recurrent_mutations(self):
 
     def best_small_tree_back_mutations(self):
         ts = tskit.load_text(
-            nodes=six.StringIO(self.small_tree_ex_nodes),
-            edges=six.StringIO(self.small_tree_ex_edges), strict=False)
+            nodes=io.StringIO(self.small_tree_ex_nodes),
+            edges=io.StringIO(self.small_tree_ex_edges), strict=False)
         tables = ts.dump_tables()
         # Add a chain of mutations
         tables.sites.add_row(position=0.25, ancestral_state="0")
@@ -2935,13 +2935,13 @@ def best_small_tree_back_mutations(self):
         self.assertEqual(list(tss.haplotypes()), ["1", "0", "1"])
 
     def test_overlapping_unary_edges(self):
-        nodes = six.StringIO("""\
+        nodes = io.StringIO("""\
         id      is_sample   time
         0       1           0
         1       1           0
         2       0           1
         """)
-        edges = six.StringIO("""\
+        edges = io.StringIO("""\
         left    right   parent  child
         0       2       2       0
         1       3       2       1
@@ -2957,13 +2957,13 @@ def test_overlapping_unary_edges(self):
             self.assertEqual(t.parent_dict, trees[t.index])
 
     def test_overlapping_unary_edges_internal_samples(self):
-        nodes = six.StringIO("""\
+        nodes = io.StringIO("""\
         id      is_sample   time
         0       1           0
         1       1           0
         2       1           1
         """)
-        edges = six.StringIO("""\
+        edges = io.StringIO("""\
         left    right   parent  child
         0       2       2       0
         1       3       2       1
@@ -2978,13 +2978,13 @@ def test_overlapping_unary_edges_internal_samples(self):
         self.assertEqual(list(node_map), [0, 1, 2])
 
     def test_isolated_samples(self):
-        nodes = six.StringIO("""\
+        nodes = io.StringIO("""\
         id      is_sample   time
         0       1           0
         1       1           1
         2       1           2
         """)
-        edges = six.StringIO("""\
+        edges = io.StringIO("""\
         left    right   parent  child
         """)
         ts = tskit.load_text(nodes, edges, sequence_length=1, strict=False)
@@ -2997,7 +2997,7 @@ def test_isolated_samples(self):
         self.assertEqual(list(node_map), [0, 1, 2])
 
     def test_internal_samples(self):
-        nodes = six.StringIO("""\
+        nodes = io.StringIO("""\
         id      is_sample   population      time
         0       1       -1              1.00000000000000
         1       0       -1              1.00000000000000
@@ -3007,7 +3007,7 @@ def test_internal_samples(self):
         5       1       -1              0.00000000000000
 
         """)
-        edges = six.StringIO("""\
+        edges = io.StringIO("""\
         id      left            right           parent  child
         0       0.62185118      1.00000000      1       5
         1       0.00000000      0.62185118      2       5
@@ -3030,20 +3030,20 @@ def test_internal_samples(self):
             self.assertEqual(t.parent_dict, trees[t.index])
 
     def test_many_mutations_over_single_sample_ancestral_state(self):
-        nodes = six.StringIO("""\
+        nodes = io.StringIO("""\
         id      is_sample   time
         0       1           0
         1       0           1
         """)
-        edges = six.StringIO("""\
+        edges = io.StringIO("""\
         left    right   parent  child
         0       1       1       0
         """)
-        sites = six.StringIO("""\
+        sites = io.StringIO("""\
         position    ancestral_state
         0           0
         """)
-        mutations = six.StringIO("""\
+        mutations = io.StringIO("""\
         site    node    derived_state   parent
         0       0       1               -1
         0       0       0               0
@@ -3060,20 +3060,20 @@ def test_many_mutations_over_single_sample_ancestral_state(self):
         self.assertEqual(list(tss.haplotypes()), ["0"])
 
     def test_many_mutations_over_single_sample_derived_state(self):
-        nodes = six.StringIO("""\
+        nodes = io.StringIO("""\
         id      is_sample   time
         0       1           0
         1       0           1
         """)
-        edges = six.StringIO("""\
+        edges = io.StringIO("""\
         left    right   parent  child
         0       1       1       0
         """)
-        sites = six.StringIO("""\
+        sites = io.StringIO("""\
         position    ancestral_state
         0           0
         """)
-        mutations = six.StringIO("""\
+        mutations = io.StringIO("""\
         site    node    derived_state   parent
         0       0       1               -1
         0       0       0               0
@@ -3311,7 +3311,7 @@ def verify_parents(self, ts):
         self.assertTrue(np.array_equal(parent, tables.mutations.parent))
 
     def test_example(self):
-        nodes = six.StringIO("""\
+        nodes = io.StringIO("""\
         id      is_sample   time
         0       0           2.0
         1       0           1.0
@@ -3319,7 +3319,7 @@ def test_example(self):
         3       1           0
         4       1           0
         """)
-        edges = six.StringIO("""\
+        edges = io.StringIO("""\
         left    right   parent  child
         0.0    0.5   2  3
         0.0    0.8   2  4
@@ -3328,13 +3328,13 @@ def test_example(self):
         0.0    1.0   0  2
         0.8    1.0   0  4
         """)
-        sites = six.StringIO("""\
+        sites = io.StringIO("""\
         position    ancestral_state
         0.1     0
         0.5     0
         0.9     0
         """)
-        mutations = six.StringIO("""\
+        mutations = io.StringIO("""\
         site    node    derived_state   parent
         0       1       1               -1
         0       2       1               -1
diff --git a/python/tests/test_tree_stats.py b/python/tests/test_tree_stats.py
index 9cb3757e30..385b4a1e81 100644
--- a/python/tests/test_tree_stats.py
+++ b/python/tests/test_tree_stats.py
@@ -27,13 +27,13 @@
 from __future__ import division
 
 
+import io
 import unittest
 import random
 
 import numpy as np
 import numpy.testing as nt
 
-import six
 import msprime
 
 import tskit
@@ -836,7 +836,7 @@ def test_case_1(self):
         branch_true_Y = 0.2*(1 + 0.5) + 0.6*(0.4) + 0.2*(0.7+0.2)
         site_true_Y = 3 + 0 + 1
 
-        nodes = six.StringIO("""\
+        nodes = io.StringIO("""\
         id      is_sample   time
         0       1           0
         1       1           0
@@ -846,7 +846,7 @@ def test_case_1(self):
         5       0           0.7
         6       0           1.0
         """)
-        edges = six.StringIO("""\
+        edges = io.StringIO("""\
         left    right   parent  child
         0.2     0.8     3       0,2
         0.0     0.2     4       1,2
@@ -855,7 +855,7 @@ def test_case_1(self):
         0.8     1.0     5       0,4
         0.0     0.2     6       0,4
         """)
-        sites = six.StringIO("""\
+        sites = io.StringIO("""\
         id  position    ancestral_state
         0   0.05        0
         1   0.1         0
@@ -868,7 +868,7 @@ def test_case_1(self):
         8   0.95        0
         9   0.951       0
         """)
-        mutations = six.StringIO("""\
+        mutations = io.StringIO("""\
         site    node    derived_state
         0       4       1
         1       0       1
@@ -947,23 +947,23 @@ def f(x):
     def test_case_odds_and_ends(self):
         # Tests having (a) the first site after the first window, and
         # (b) no samples having the ancestral state.
-        nodes = six.StringIO("""\
+        nodes = io.StringIO("""\
         id      is_sample   time
         0       1           0
         1       1           0
         2       0           0.5
         3       0           1.0
         """)
-        edges = six.StringIO("""\
+        edges = io.StringIO("""\
         left    right   parent  child
         0.0     0.5     2       0,1
         0.5     1.0     3       0,1
         """)
-        sites = six.StringIO("""\
+        sites = io.StringIO("""\
         id  position    ancestral_state
         0   0.65        0
         """)
-        mutations = six.StringIO("""\
+        mutations = io.StringIO("""\
         site    node    derived_state   parent
         0       0       1               -1
         0       1       2               -1
@@ -997,7 +997,7 @@ def test_case_recurrent_muts(self):
         #       0     2       0        1   0   1       0   2       3
         site_true_Y = 0 + 1 + 1
 
-        nodes = six.StringIO("""\
+        nodes = io.StringIO("""\
         id      is_sample   time
         0       1           0
         1       1           0
@@ -1007,7 +1007,7 @@ def test_case_recurrent_muts(self):
         5       0           0.7
         6       0           1.0
         """)
-        edges = six.StringIO("""\
+        edges = io.StringIO("""\
         left    right   parent  child
         0.2     0.8     3       0,2
         0.0     0.2     4       1,2
@@ -1016,13 +1016,13 @@ def test_case_recurrent_muts(self):
         0.8     1.0     5       0,4
         0.0     0.2     6       0,4
         """)
-        sites = six.StringIO("""\
+        sites = io.StringIO("""\
         id  position    ancestral_state
         0   0.05        0
         1   0.3         0
         2   0.9         0
         """)
-        mutations = six.StringIO("""\
+        mutations = io.StringIO("""\
         site    node    derived_state   parent
         0       0       1               -1
         0       0       2               0
@@ -1098,7 +1098,7 @@ def test_case_2(self):
         # Y(0;1, 2)
         site_true_Y = 1
 
-        nodes = six.StringIO("""\
+        nodes = io.StringIO("""\
         is_sample       time    population
         1       0.000000        0
         1       0.000000        0
@@ -1112,7 +1112,7 @@ def test_case_2(self):
         0       1.000000        0
         0       1.000000        0
         """)
-        edges = six.StringIO("""\
+        edges = io.StringIO("""\
         left    right   parent  child
         0.500000        1.000000        10      1
         0.000000        0.400000        10      2
@@ -1133,14 +1133,14 @@ def test_case_2(self):
         0.100000        0.900000        3       4,5
         0.000000        0.100000        3       4,5,7
         """)
-        sites = six.StringIO("""\
+        sites = io.StringIO("""\
         id  position    ancestral_state
         0   0.0         0
         1   0.55        0
         2   0.75        0
         3   0.85        0
         """)
-        mutations = six.StringIO("""\
+        mutations = io.StringIO("""\
         site    node    derived_state   parent
         0       0       1               -1
         0       10      1               -1
diff --git a/python/tskit/tables.py b/python/tskit/tables.py
index 9d82660b78..8864440cf6 100644
--- a/python/tskit/tables.py
+++ b/python/tskit/tables.py
@@ -28,11 +28,11 @@
 
 import base64
 import collections
+import copyreg
 import datetime
 import warnings
 
 import numpy as np
-from six.moves import copyreg
 
 import _tskit
 # This circular import is ugly but it seems hard to avoid it since table collection

From 572fff3f7428e9b8a52d2f3b4474b211a7c18f25 Mon Sep 17 00:00:00 2001
From: Hugo <hugovk@users.noreply.github.com>
Date: Thu, 28 Feb 2019 22:05:27 +0200
Subject: [PATCH 05/10] Drop support for legacy Python 2.7

---
 docs/development.rst           |  3 +--
 python/Makefile                |  3 ---
 python/_tskitmodule.c          | 20 +-------------------
 python/tests/test_highlevel.py |  7 -------
 python/tests/test_topology.py  |  7 -------
 python/tskit/exceptions.py     | 22 +++++++++-------------
 python/tskit/trees.py          | 10 +---------
 7 files changed, 12 insertions(+), 60 deletions(-)

diff --git a/docs/development.rst b/docs/development.rst
index ad5f502c7b..6445c4baf3 100644
--- a/docs/development.rst
+++ b/docs/development.rst
@@ -25,8 +25,7 @@ Quickstart
 
 - Install the Python development requirements using
   ``pip install -r python/requirements/development.txt``.
-- Build the low level module by running ``make`` in the ``python`` directory. Python 3.x
-  is the default for developement (Python 2.x is discouraged).
+- Build the low level module by running ``make`` in the ``python`` directory.
 - Run the tests to ensure everything has worked: ``python -m nose -vs``. These should
   all pass.
 - Make your changes in a local branch, and open a pull request on GitHub when you
diff --git a/python/Makefile b/python/Makefile
index 3b9e7e3f5f..32ec4b43a8 100644
--- a/python/Makefile
+++ b/python/Makefile
@@ -8,9 +8,6 @@ allchecks: _tskitmodule.c
 ext3: _tskitmodule.c 
 	python3 setup.py build_ext --inplace
 
-ext2: _tskitmodule.c 
-	python2 setup.py build_ext --inplace
-
 ctags:
 	ctags lib/*.c lib/*.h tskit/*.py
 
diff --git a/python/_tskitmodule.c b/python/_tskitmodule.c
index b0bf126e1f..1ad59902de 100644
--- a/python/_tskitmodule.c
+++ b/python/_tskitmodule.c
@@ -28,10 +28,6 @@
 #include "kastore.h"
 #include "tskit.h"
 
-#if PY_MAJOR_VERSION >= 3
-#define IS_PY3K
-#endif
-
 #define MODULE_DOC \
 "Low level interface for tskit"
 
@@ -8452,8 +8448,6 @@ static PyMethodDef tskit_methods[] = {
  * from the Python documentation still use this idiom.
  */
 
-#if PY_MAJOR_VERSION >= 3
-
 static struct PyModuleDef tskitmodule = {
     PyModuleDef_HEAD_INIT,
     "_tskit",   /* name of module */
@@ -8468,18 +8462,8 @@ static struct PyModuleDef tskitmodule = {
 PyObject *
 PyInit__tskit(void)
 
-#else
-#define INITERROR return
-
-void
-init_tskit(void)
-#endif
 {
-#if PY_MAJOR_VERSION >= 3
-    PyObject *module = PyModule_Create(&tskitmodule);
-#else
-    PyObject *module = Py_InitModule3("_tskit", tskit_methods, MODULE_DOC);
-#endif
+PyObject *module = PyModule_Create(&tskitmodule);
     if (module == NULL) {
         INITERROR;
     }
@@ -8652,7 +8636,5 @@ init_tskit(void)
     PyModule_AddIntConstant(module, "FORWARD", TSK_DIR_FORWARD);
     PyModule_AddIntConstant(module, "REVERSE", TSK_DIR_REVERSE);
 
-#if PY_MAJOR_VERSION >= 3
     return module;
-#endif
 }
diff --git a/python/tests/test_highlevel.py b/python/tests/test_highlevel.py
index 9fada93929..1ed77728a5 100644
--- a/python/tests/test_highlevel.py
+++ b/python/tests/test_highlevel.py
@@ -26,13 +26,6 @@
 from __future__ import print_function
 from __future__ import division
 
-try:
-    # We use the zip as iterator functionality here.
-    from future_builtins import zip
-except ImportError:
-    # This fails for Python 3.x, but that's fine.
-    pass
-
 import collections
 import itertools
 import io
diff --git a/python/tests/test_topology.py b/python/tests/test_topology.py
index 2579408a0f..df41b511da 100644
--- a/python/tests/test_topology.py
+++ b/python/tests/test_topology.py
@@ -26,13 +26,6 @@
 from __future__ import print_function
 from __future__ import division
 
-try:
-    # We use the zip as iterator functionality here.
-    from future_builtins import zip
-except ImportError:
-    # This fails for Python 3.x, but that's fine.
-    pass
-
 import io
 import unittest
 import itertools
diff --git a/python/tskit/exceptions.py b/python/tskit/exceptions.py
index a4b74dfaac..80b957ffb6 100644
--- a/python/tskit/exceptions.py
+++ b/python/tskit/exceptions.py
@@ -36,19 +36,15 @@
 
 # TODO finalise this when working out the docs structure for tskit on rtd.
 
-try:
-    TskitException.__doc__ = "Superclass of all exceptions defined in tskit."
-    LibraryError.__doc__ = "Generic low-level error raised by the C library."
-    FileFormatError.__doc__ = "An error was detected in the file format."
-    VersionTooNewError.__doc__ = """
-    The version of the file is too new and cannot be read by the library.
-    """
-    VersionTooOldError.__doc__ = """
-    The version of the file is too old and cannot be read by the library.
-    """
-except AttributeError:  # pragma: no cover
-    # Python2 throws attribute error. Ignore.
-    pass
+TskitException.__doc__ = "Superclass of all exceptions defined in tskit."
+LibraryError.__doc__ = "Generic low-level error raised by the C library."
+FileFormatError.__doc__ = "An error was detected in the file format."
+VersionTooNewError.__doc__ = """
+The version of the file is too new and cannot be read by the library.
+"""
+VersionTooOldError.__doc__ = """
+The version of the file is too old and cannot be read by the library.
+"""
 
 
 class DuplicatePositionsError(TskitException):
diff --git a/python/tskit/trees.py b/python/tskit/trees.py
index cd8a5a3f6c..59f68ed73e 100644
--- a/python/tskit/trees.py
+++ b/python/tskit/trees.py
@@ -34,11 +34,7 @@
 import base64
 import warnings
 import functools
-try:
-    import concurrent.futures
-except ImportError:  # pragma: no cover
-    # We're on Python2; any attempts to use futures are dealt with below.
-    pass
+import concurrent.futures
 
 import numpy as np
 
@@ -1828,10 +1824,6 @@ def __reversed__(self):
         self.forward = False
         return self
 
-    def next(self):
-        # This is for Python2 support only.
-        return self.__next__()
-
     def __next__(self):
         if self.forward:
             self.more_trees = self.more_trees and self.tree.next()

From 037d461c7c15b580d4032700b32356f5f8bde966 Mon Sep 17 00:00:00 2001
From: Hugo <hugovk@users.noreply.github.com>
Date: Thu, 28 Feb 2019 22:10:20 +0200
Subject: [PATCH 06/10] The future is now

---
 python/stress_lowlevel.py          | 3 ---
 python/tests/__init__.py           | 4 ----
 python/tests/simplify.py           | 4 ----
 python/tests/test_dict_encoding.py | 3 ---
 python/tests/test_drawing.py       | 3 ---
 python/tests/test_file_format.py   | 3 ---
 python/tests/test_highlevel.py     | 3 ---
 python/tests/test_lowlevel.py      | 4 ----
 python/tests/test_metadata.py      | 3 ---
 python/tests/test_newick.py        | 3 ---
 python/tests/test_provenance.py    | 4 ----
 python/tests/test_stats.py         | 3 ---
 python/tests/test_tables.py        | 3 ---
 python/tests/test_threads.py       | 3 ---
 python/tests/test_topology.py      | 3 ---
 python/tests/test_tree_stats.py    | 4 ----
 python/tests/test_vcf.py           | 3 ---
 python/tests/test_wright_fisher.py | 3 ---
 python/tests/tsutil.py             | 4 ----
 python/tskit/__init__.py           | 3 ---
 python/tskit/__main__.py           | 4 ----
 python/tskit/drawing.py            | 3 ---
 python/tskit/formats.py            | 3 ---
 python/tskit/provenance.py         | 3 ---
 python/tskit/stats.py              | 3 ---
 python/tskit/tables.py             | 3 ---
 python/tskit/trees.py              | 3 ---
 27 files changed, 88 deletions(-)

diff --git a/python/stress_lowlevel.py b/python/stress_lowlevel.py
index ce5b25d40b..0f931c90c6 100644
--- a/python/stress_lowlevel.py
+++ b/python/stress_lowlevel.py
@@ -2,9 +2,6 @@
 Code to stress the low-level API as much as possible to expose
 any memory leaks or error handling issues.
 """
-from __future__ import print_function
-from __future__ import division
-
 import argparse
 import unittest
 import random
diff --git a/python/tests/__init__.py b/python/tests/__init__.py
index ca66d87f3c..6bced163ba 100644
--- a/python/tests/__init__.py
+++ b/python/tests/__init__.py
@@ -20,10 +20,6 @@
 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 # SOFTWARE.
 
-from __future__ import print_function
-from __future__ import unicode_literals
-from __future__ import division
-
 import base64
 
 # TODO remove this code and refactor elsewhere.
diff --git a/python/tests/simplify.py b/python/tests/simplify.py
index 0839fbe8c4..0c8df77a85 100644
--- a/python/tests/simplify.py
+++ b/python/tests/simplify.py
@@ -23,10 +23,6 @@
 """
 Python implementation of the simplify algorithm.
 """
-from __future__ import print_function
-from __future__ import unicode_literals
-from __future__ import division
-
 import sys
 
 import numpy as np
diff --git a/python/tests/test_dict_encoding.py b/python/tests/test_dict_encoding.py
index 2b339f2a63..774c81834b 100644
--- a/python/tests/test_dict_encoding.py
+++ b/python/tests/test_dict_encoding.py
@@ -23,9 +23,6 @@
 Test cases for the low-level dictionary encoding used to move
 data around in C.
 """
-from __future__ import print_function
-from __future__ import division
-
 import unittest
 
 import msprime
diff --git a/python/tests/test_drawing.py b/python/tests/test_drawing.py
index 64253b9bba..c8099b6ff7 100644
--- a/python/tests/test_drawing.py
+++ b/python/tests/test_drawing.py
@@ -24,9 +24,6 @@
 """
 Test cases for visualisation in tskit.
 """
-from __future__ import print_function
-from __future__ import division
-
 import io
 import os
 import tempfile
diff --git a/python/tests/test_file_format.py b/python/tests/test_file_format.py
index 018de57450..cd9775e436 100644
--- a/python/tests/test_file_format.py
+++ b/python/tests/test_file_format.py
@@ -23,9 +23,6 @@
 """
 Test cases for tskit's file format.
 """
-from __future__ import print_function
-from __future__ import division
-
 import os
 import tempfile
 import unittest
diff --git a/python/tests/test_highlevel.py b/python/tests/test_highlevel.py
index 1ed77728a5..fd6d41b20e 100644
--- a/python/tests/test_highlevel.py
+++ b/python/tests/test_highlevel.py
@@ -23,9 +23,6 @@
 """
 Test cases for the high level interface to msprime.
 """
-from __future__ import print_function
-from __future__ import division
-
 import collections
 import itertools
 import io
diff --git a/python/tests/test_lowlevel.py b/python/tests/test_lowlevel.py
index 31906486d3..64276483a0 100644
--- a/python/tests/test_lowlevel.py
+++ b/python/tests/test_lowlevel.py
@@ -23,10 +23,6 @@
 """
 Test cases for the low level C interface to tskit.
 """
-from __future__ import print_function
-from __future__ import division
-from __future__ import unicode_literals
-
 import collections
 import itertools
 import os
diff --git a/python/tests/test_metadata.py b/python/tests/test_metadata.py
index 904f9ff941..f72fa0af1e 100644
--- a/python/tests/test_metadata.py
+++ b/python/tests/test_metadata.py
@@ -24,9 +24,6 @@
 """
 Tests for metadata handling.
 """
-from __future__ import print_function
-from __future__ import division
-
 import io
 import json
 import os
diff --git a/python/tests/test_newick.py b/python/tests/test_newick.py
index e5d3e47487..a6bbfc8c5c 100644
--- a/python/tests/test_newick.py
+++ b/python/tests/test_newick.py
@@ -23,9 +23,6 @@
 """
 Tests for the newick output feature.
 """
-from __future__ import print_function
-from __future__ import division
-
 import unittest
 
 import msprime
diff --git a/python/tests/test_provenance.py b/python/tests/test_provenance.py
index faaf0078d8..3da9d675d8 100644
--- a/python/tests/test_provenance.py
+++ b/python/tests/test_provenance.py
@@ -23,10 +23,6 @@
 """
 Tests for the provenance information attached to tree sequences.
 """
-from __future__ import print_function
-from __future__ import unicode_literals
-from __future__ import division
-
 import unittest
 import json
 import platform
diff --git a/python/tests/test_stats.py b/python/tests/test_stats.py
index da2fc2c2f3..8a33d4db9d 100644
--- a/python/tests/test_stats.py
+++ b/python/tests/test_stats.py
@@ -23,9 +23,6 @@
 """
 Test cases for stats calculations in tskit.
 """
-from __future__ import print_function
-from __future__ import division
-
 import unittest
 
 import numpy as np
diff --git a/python/tests/test_tables.py b/python/tests/test_tables.py
index f9c11f1f3b..61c3bcc5db 100644
--- a/python/tests/test_tables.py
+++ b/python/tests/test_tables.py
@@ -25,9 +25,6 @@
 Test cases for the low-level tables used to transfer information
 between simulations and the tree sequence.
 """
-from __future__ import print_function
-from __future__ import division
-
 import io
 import pickle
 import random
diff --git a/python/tests/test_threads.py b/python/tests/test_threads.py
index f5085e0924..46a4f8443f 100644
--- a/python/tests/test_threads.py
+++ b/python/tests/test_threads.py
@@ -23,9 +23,6 @@
 """
 Test cases for threading enabled aspects of the API.
 """
-from __future__ import print_function
-from __future__ import division
-
 import threading
 import unittest
 import platform
diff --git a/python/tests/test_topology.py b/python/tests/test_topology.py
index df41b511da..7501b23cb3 100644
--- a/python/tests/test_topology.py
+++ b/python/tests/test_topology.py
@@ -23,9 +23,6 @@
 """
 Test cases for the supported topological variations and operations.
 """
-from __future__ import print_function
-from __future__ import division
-
 import io
 import unittest
 import itertools
diff --git a/python/tests/test_tree_stats.py b/python/tests/test_tree_stats.py
index 385b4a1e81..7c94ef0c6c 100644
--- a/python/tests/test_tree_stats.py
+++ b/python/tests/test_tree_stats.py
@@ -23,10 +23,6 @@
 """
 Test cases for generalized statistic computation.
 """
-from __future__ import print_function
-from __future__ import division
-
-
 import io
 import unittest
 import random
diff --git a/python/tests/test_vcf.py b/python/tests/test_vcf.py
index e391651b61..b62bbb7e94 100644
--- a/python/tests/test_vcf.py
+++ b/python/tests/test_vcf.py
@@ -23,9 +23,6 @@
 """
 Test cases for VCF output in tskit.
 """
-from __future__ import print_function
-from __future__ import division
-
 import collections
 import math
 import os
diff --git a/python/tests/test_wright_fisher.py b/python/tests/test_wright_fisher.py
index 8f079695b2..d2f5698377 100644
--- a/python/tests/test_wright_fisher.py
+++ b/python/tests/test_wright_fisher.py
@@ -23,9 +23,6 @@
 """
 Test various functions using messy tables output by a forwards-time simulator.
 """
-from __future__ import print_function
-from __future__ import division
-
 import itertools
 import random
 import unittest
diff --git a/python/tests/tsutil.py b/python/tests/tsutil.py
index 33d1ed1cd5..2c2faf15c2 100644
--- a/python/tests/tsutil.py
+++ b/python/tests/tsutil.py
@@ -23,10 +23,6 @@
 """
 A collection of utilities to edit and construct tree sequences.
 """
-from __future__ import print_function
-from __future__ import unicode_literals
-from __future__ import division
-
 import json
 import random
 
diff --git a/python/tskit/__init__.py b/python/tskit/__init__.py
index 4a188ab54f..41d94b2ab9 100644
--- a/python/tskit/__init__.py
+++ b/python/tskit/__init__.py
@@ -20,9 +20,6 @@
 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 # SOFTWARE.
 
-from __future__ import print_function
-from __future__ import division
-
 import _tskit
 FORWARD = _tskit.FORWARD
 REVERSE = _tskit.REVERSE
diff --git a/python/tskit/__main__.py b/python/tskit/__main__.py
index a0f7cb8179..b07a50eba2 100644
--- a/python/tskit/__main__.py
+++ b/python/tskit/__main__.py
@@ -1,7 +1,3 @@
-from __future__ import print_function
-from __future__ import division
-
-
 def main():
     print("Not implemented yet; please be patient!!")
 
diff --git a/python/tskit/drawing.py b/python/tskit/drawing.py
index ecc70ddc41..1b7b98d427 100644
--- a/python/tskit/drawing.py
+++ b/python/tskit/drawing.py
@@ -23,9 +23,6 @@
 """
 Module responsible for visualisations.
 """
-from __future__ import division
-from __future__ import print_function
-
 import array
 import collections
 
diff --git a/python/tskit/formats.py b/python/tskit/formats.py
index 6d8403fae5..81f0e42f54 100644
--- a/python/tskit/formats.py
+++ b/python/tskit/formats.py
@@ -24,9 +24,6 @@
 Module responsible for converting tree sequence files from older
 formats.
 """
-from __future__ import division
-from __future__ import print_function
-
 import datetime
 import json
 import logging
diff --git a/python/tskit/provenance.py b/python/tskit/provenance.py
index a99ebb9f3a..9dbab6bc65 100644
--- a/python/tskit/provenance.py
+++ b/python/tskit/provenance.py
@@ -24,9 +24,6 @@
 Common provenance methods used to determine the state and versions
 of various dependencies and the OS.
 """
-from __future__ import print_function
-from __future__ import division
-
 import platform
 import json
 import os.path
diff --git a/python/tskit/stats.py b/python/tskit/stats.py
index 63acb153c8..5d415b323d 100644
--- a/python/tskit/stats.py
+++ b/python/tskit/stats.py
@@ -22,9 +22,6 @@
 """
 Module responsible for computing various statistics on tree sequences.
 """
-from __future__ import division
-from __future__ import print_function
-
 import threading
 import struct
 import sys
diff --git a/python/tskit/tables.py b/python/tskit/tables.py
index 8864440cf6..b535eb82dd 100644
--- a/python/tskit/tables.py
+++ b/python/tskit/tables.py
@@ -23,9 +23,6 @@
 """
 Tree sequence IO via the tables API.
 """
-from __future__ import division
-from __future__ import print_function
-
 import base64
 import collections
 import copyreg
diff --git a/python/tskit/trees.py b/python/tskit/trees.py
index 59f68ed73e..59b88e9196 100644
--- a/python/tskit/trees.py
+++ b/python/tskit/trees.py
@@ -25,9 +25,6 @@
 """
 Module responsible for managing trees and tree sequences.
 """
-from __future__ import division
-from __future__ import print_function
-
 import collections
 import itertools
 import json

From 0fa07a9e3f08810e48b81cdfba8ed91f337c54dd Mon Sep 17 00:00:00 2001
From: Jerome Kelleher <jk@well.ox.ac.uk>
Date: Mon, 4 Mar 2019 15:22:57 +0000
Subject: [PATCH 07/10] Manually add wanted changes from pyupgrade --py3-plus

Using pyupgrade --py3-plus results removing explicit inherits from
object class declarations, which we prefer stylistically.
---
 docs/conf.py                |  2 +-
 python/tests/test_tables.py |  3 +--
 python/tskit/tables.py      | 16 ++++++++--------
 3 files changed, 10 insertions(+), 11 deletions(-)

diff --git a/docs/conf.py b/docs/conf.py
index b68ac878be..1eb16ef346 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -76,7 +76,7 @@ def handle_item(fieldarg, content):
             # inconsistencies later when references are resolved
             fieldtype = types.pop(fieldarg)
             if len(fieldtype) == 1 and isinstance(fieldtype[0], nodes.Text):
-                typename = u''.join(n.astext() for n in fieldtype)
+                typename = ''.join(n.astext() for n in fieldtype)
                 par.extend(self.make_xrefs(self.typerolename, domain, typename,
                                            addnodes.literal_emphasis))
             else:
diff --git a/python/tests/test_tables.py b/python/tests/test_tables.py
index 61c3bcc5db..28b83ff5d8 100644
--- a/python/tests/test_tables.py
+++ b/python/tests/test_tables.py
@@ -1,4 +1,3 @@
-# -*- coding: utf-8 -*-
 # MIT License
 #
 # Copyright (c) 2018-2019 Tskit Developers
@@ -883,7 +882,7 @@ def test_random_cases(self):
             self.verify_packing(strings)
 
     def test_unicode(self):
-        self.verify_packing([u'abcdé', u'€'])
+        self.verify_packing(['abcdé', '€'])
 
 
 class TestBytePacking(unittest.TestCase):
diff --git a/python/tskit/tables.py b/python/tskit/tables.py
index b535eb82dd..2e1156e9eb 100644
--- a/python/tskit/tables.py
+++ b/python/tskit/tables.py
@@ -190,7 +190,7 @@ class IndividualTable(BaseTable):
     def __init__(self, max_rows_increment=0, ll_table=None):
         if ll_table is None:
             ll_table = _tskit.IndividualTable(max_rows_increment=max_rows_increment)
-        super(IndividualTable, self).__init__(ll_table, IndividualTableRow)
+        super().__init__(ll_table, IndividualTableRow)
 
     @property
     def flags(self):
@@ -370,7 +370,7 @@ class NodeTable(BaseTable):
     def __init__(self, max_rows_increment=0, ll_table=None):
         if ll_table is None:
             ll_table = _tskit.NodeTable(max_rows_increment=max_rows_increment)
-        super(NodeTable, self).__init__(ll_table, NodeTableRow)
+        super().__init__(ll_table, NodeTableRow)
 
     @property
     def time(self):
@@ -560,7 +560,7 @@ class EdgeTable(BaseTable):
     def __init__(self, max_rows_increment=0, ll_table=None):
         if ll_table is None:
             ll_table = _tskit.EdgeTable(max_rows_increment=max_rows_increment)
-        super(EdgeTable, self).__init__(ll_table, EdgeTableRow)
+        super().__init__(ll_table, EdgeTableRow)
 
     @property
     def left(self):
@@ -699,7 +699,7 @@ class MigrationTable(BaseTable):
     def __init__(self, max_rows_increment=0, ll_table=None):
         if ll_table is None:
             ll_table = _tskit.MigrationTable(max_rows_increment=max_rows_increment)
-        super(MigrationTable, self).__init__(ll_table, MigrationTableRow)
+        super().__init__(ll_table, MigrationTableRow)
 
     @property
     def left(self):
@@ -865,7 +865,7 @@ class SiteTable(BaseTable):
     def __init__(self, max_rows_increment=0, ll_table=None):
         if ll_table is None:
             ll_table = _tskit.SiteTable(max_rows_increment=max_rows_increment)
-        super(SiteTable, self).__init__(ll_table, SiteTableRow)
+        super().__init__(ll_table, SiteTableRow)
 
     @property
     def position(self):
@@ -1056,7 +1056,7 @@ class MutationTable(BaseTable):
     def __init__(self, max_rows_increment=0, ll_table=None):
         if ll_table is None:
             ll_table = _tskit.MutationTable(max_rows_increment=max_rows_increment)
-        super(MutationTable, self).__init__(ll_table, MutationTableRow)
+        super().__init__(ll_table, MutationTableRow)
 
     @property
     def site(self):
@@ -1259,7 +1259,7 @@ class PopulationTable(BaseTable):
     def __init__(self, max_rows_increment=0, ll_table=None):
         if ll_table is None:
             ll_table = _tskit.PopulationTable(max_rows_increment=max_rows_increment)
-        super(PopulationTable, self).__init__(ll_table, PopulationTableRow)
+        super().__init__(ll_table, PopulationTableRow)
 
     @property
     def metadata(self):
@@ -1347,7 +1347,7 @@ class ProvenanceTable(BaseTable):
     def __init__(self, max_rows_increment=0, ll_table=None):
         if ll_table is None:
             ll_table = _tskit.ProvenanceTable(max_rows_increment=max_rows_increment)
-        super(ProvenanceTable, self).__init__(ll_table, ProvenanceTableRow)
+        super().__init__(ll_table, ProvenanceTableRow)
 
     @property
     def record(self):

From 4c3e6611b0fb05ec3348c53f1da53678544e8ca9 Mon Sep 17 00:00:00 2001
From: Jerome Kelleher <jk@well.ox.ac.uk>
Date: Mon, 4 Mar 2019 15:29:42 +0000
Subject: [PATCH 08/10] Tidy up low-level C module.

---
 python/_tskitmodule.c | 96 ++++++++++++++++++++-----------------------
 1 file changed, 44 insertions(+), 52 deletions(-)

diff --git a/python/_tskitmodule.c b/python/_tskitmodule.c
index 1ad59902de..d40e98fc6d 100644
--- a/python/_tskitmodule.c
+++ b/python/_tskitmodule.c
@@ -1,21 +1,27 @@
 /*
-** Copyright (C) 2014-2018 University of Oxford
-**
-** This file is part of tskit.
-**
-** tskit is free software: you can redistribute it and/or modify
-** it under the terms of the GNU General Public License as published by
-** the Free Software Foundation, either version 3 of the License, or
-** (at your option) any later version.
-**
-** tskit is distributed in the hope that it will be useful,
-** but WITHOUT ANY WARRANTY; without even the implied warranty of
-** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-** GNU General Public License for more details.
-**
-** You should have received a copy of the GNU General Public License
-** along with tskit.  If not, see <http://www.gnu.org/licenses/>.
-*/
+ * MIT License
+ *
+ * Copyright (c) 2019 Tskit Developers
+ * Copyright (c) 2015-2018 University of Oxford
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
 
 #define PY_SSIZE_T_CLEAN
 #define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION
@@ -28,9 +34,6 @@
 #include "kastore.h"
 #include "tskit.h"
 
-#define MODULE_DOC \
-"Low level interface for tskit"
-
 #define SET_COLS 0
 #define APPEND_COLS 1
 
@@ -8423,8 +8426,6 @@ static PyTypeObject LdCalculatorType = {
 static PyObject *
 tskit_get_kastore_version(PyObject *self)
 {
-    /* TODO if we provide the option of linking against kastore separately, we
-     * should return the link time version using kas_get_version */
     return Py_BuildValue("iii", KAS_VERSION_MAJOR, KAS_VERSION_MINOR, KAS_VERSION_PATCH);
 }
 
@@ -8442,37 +8443,28 @@ static PyMethodDef tskit_methods[] = {
     {NULL}        /* Sentinel */
 };
 
-/* Initialisation code supports Python 2.x and 3.x. The framework uses the
- * recommended structure from http://docs.python.org/howto/cporting.html.
- * I've ignored the point about storing state in globals, as the examples
- * from the Python documentation still use this idiom.
- */
-
 static struct PyModuleDef tskitmodule = {
     PyModuleDef_HEAD_INIT,
-    "_tskit",   /* name of module */
-    MODULE_DOC, /* module documentation, may be NULL */
+    "_tskit",
+    "Low level interface for tskit",
     -1,
     tskit_methods,
     NULL, NULL, NULL, NULL
 };
 
-#define INITERROR return NULL
-
 PyObject *
 PyInit__tskit(void)
-
 {
-PyObject *module = PyModule_Create(&tskitmodule);
+    PyObject *module = PyModule_Create(&tskitmodule);
     if (module == NULL) {
-        INITERROR;
+        return NULL;
     }
     import_array();
 
     /* LightweightTableCollection type */
     LightweightTableCollectionType.tp_new = PyType_GenericNew;
     if (PyType_Ready(&LightweightTableCollectionType) < 0) {
-        INITERROR;
+        return NULL;
     }
     Py_INCREF(&LightweightTableCollectionType);
     PyModule_AddObject(module, "LightweightTableCollection",
@@ -8481,7 +8473,7 @@ PyObject *module = PyModule_Create(&tskitmodule);
     /* IndividualTable type */
     IndividualTableType.tp_new = PyType_GenericNew;
     if (PyType_Ready(&IndividualTableType) < 0) {
-        INITERROR;
+        return NULL;
     }
     Py_INCREF(&IndividualTableType);
     PyModule_AddObject(module, "IndividualTable", (PyObject *) &IndividualTableType);
@@ -8489,7 +8481,7 @@ PyObject *module = PyModule_Create(&tskitmodule);
     /* NodeTable type */
     NodeTableType.tp_new = PyType_GenericNew;
     if (PyType_Ready(&NodeTableType) < 0) {
-        INITERROR;
+        return NULL;
     }
     Py_INCREF(&NodeTableType);
     PyModule_AddObject(module, "NodeTable", (PyObject *) &NodeTableType);
@@ -8497,7 +8489,7 @@ PyObject *module = PyModule_Create(&tskitmodule);
     /* EdgeTable type */
     EdgeTableType.tp_new = PyType_GenericNew;
     if (PyType_Ready(&EdgeTableType) < 0) {
-        INITERROR;
+        return NULL;
     }
     Py_INCREF(&EdgeTableType);
     PyModule_AddObject(module, "EdgeTable", (PyObject *) &EdgeTableType);
@@ -8505,7 +8497,7 @@ PyObject *module = PyModule_Create(&tskitmodule);
     /* MigrationTable type */
     MigrationTableType.tp_new = PyType_GenericNew;
     if (PyType_Ready(&MigrationTableType) < 0) {
-        INITERROR;
+        return NULL;
     }
     Py_INCREF(&MigrationTableType);
     PyModule_AddObject(module, "MigrationTable", (PyObject *) &MigrationTableType);
@@ -8513,7 +8505,7 @@ PyObject *module = PyModule_Create(&tskitmodule);
     /* SiteTable type */
     SiteTableType.tp_new = PyType_GenericNew;
     if (PyType_Ready(&SiteTableType) < 0) {
-        INITERROR;
+        return NULL;
     }
     Py_INCREF(&SiteTableType);
     PyModule_AddObject(module, "SiteTable", (PyObject *) &SiteTableType);
@@ -8521,7 +8513,7 @@ PyObject *module = PyModule_Create(&tskitmodule);
     /* MutationTable type */
     MutationTableType.tp_new = PyType_GenericNew;
     if (PyType_Ready(&MutationTableType) < 0) {
-        INITERROR;
+        return NULL;
     }
     Py_INCREF(&MutationTableType);
     PyModule_AddObject(module, "MutationTable", (PyObject *) &MutationTableType);
@@ -8529,7 +8521,7 @@ PyObject *module = PyModule_Create(&tskitmodule);
     /* PopulationTable type */
     PopulationTableType.tp_new = PyType_GenericNew;
     if (PyType_Ready(&PopulationTableType) < 0) {
-        INITERROR;
+        return NULL;
     }
     Py_INCREF(&PopulationTableType);
     PyModule_AddObject(module, "PopulationTable", (PyObject *) &PopulationTableType);
@@ -8537,7 +8529,7 @@ PyObject *module = PyModule_Create(&tskitmodule);
     /* ProvenanceTable type */
     ProvenanceTableType.tp_new = PyType_GenericNew;
     if (PyType_Ready(&ProvenanceTableType) < 0) {
-        INITERROR;
+        return NULL;
     }
     Py_INCREF(&ProvenanceTableType);
     PyModule_AddObject(module, "ProvenanceTable", (PyObject *) &ProvenanceTableType);
@@ -8545,7 +8537,7 @@ PyObject *module = PyModule_Create(&tskitmodule);
     /* TableCollectionTable type */
     TableCollectionType.tp_new = PyType_GenericNew;
     if (PyType_Ready(&TableCollectionType) < 0) {
-        INITERROR;
+        return NULL;
     }
     Py_INCREF(&TableCollectionType);
     PyModule_AddObject(module, "TableCollection", (PyObject *) &TableCollectionType);
@@ -8553,7 +8545,7 @@ PyObject *module = PyModule_Create(&tskitmodule);
     /* TreeSequence type */
     TreeSequenceType.tp_new = PyType_GenericNew;
     if (PyType_Ready(&TreeSequenceType) < 0) {
-        INITERROR;
+        return NULL;
     }
     Py_INCREF(&TreeSequenceType);
     PyModule_AddObject(module, "TreeSequence", (PyObject *) &TreeSequenceType);
@@ -8561,7 +8553,7 @@ PyObject *module = PyModule_Create(&tskitmodule);
     /* Tree type */
     TreeType.tp_new = PyType_GenericNew;
     if (PyType_Ready(&TreeType) < 0) {
-        INITERROR;
+        return NULL;
     }
     Py_INCREF(&TreeType);
     PyModule_AddObject(module, "Tree", (PyObject *) &TreeType);
@@ -8569,7 +8561,7 @@ PyObject *module = PyModule_Create(&tskitmodule);
     /* TreeDiffIterator type */
     TreeDiffIteratorType.tp_new = PyType_GenericNew;
     if (PyType_Ready(&TreeDiffIteratorType) < 0) {
-        INITERROR;
+        return NULL;
     }
     Py_INCREF(&TreeDiffIteratorType);
     PyModule_AddObject(module, "TreeDiffIterator", (PyObject *) &TreeDiffIteratorType);
@@ -8577,7 +8569,7 @@ PyObject *module = PyModule_Create(&tskitmodule);
     /* VcfConverter type */
     VcfConverterType.tp_new = PyType_GenericNew;
     if (PyType_Ready(&VcfConverterType) < 0) {
-        INITERROR;
+        return NULL;
     }
     Py_INCREF(&VcfConverterType);
     PyModule_AddObject(module, "VcfConverter", (PyObject *) &VcfConverterType);
@@ -8585,7 +8577,7 @@ PyObject *module = PyModule_Create(&tskitmodule);
     /* HaplotypeGenerator type */
     HaplotypeGeneratorType.tp_new = PyType_GenericNew;
     if (PyType_Ready(&HaplotypeGeneratorType) < 0) {
-        INITERROR;
+        return NULL;
     }
     Py_INCREF(&HaplotypeGeneratorType);
     PyModule_AddObject(module, "HaplotypeGenerator",
@@ -8594,7 +8586,7 @@ PyObject *module = PyModule_Create(&tskitmodule);
     /* VariantGenerator type */
     VariantGeneratorType.tp_new = PyType_GenericNew;
     if (PyType_Ready(&VariantGeneratorType) < 0) {
-        INITERROR;
+        return NULL;
     }
     Py_INCREF(&VariantGeneratorType);
     PyModule_AddObject(module, "VariantGenerator", (PyObject *) &VariantGeneratorType);
@@ -8602,7 +8594,7 @@ PyObject *module = PyModule_Create(&tskitmodule);
     /* LdCalculator type */
     LdCalculatorType.tp_new = PyType_GenericNew;
     if (PyType_Ready(&LdCalculatorType) < 0) {
-        INITERROR;
+        return NULL;
     }
     Py_INCREF(&LdCalculatorType);
     PyModule_AddObject(module, "LdCalculator", (PyObject *) &LdCalculatorType);

From cc43325799a99d63ed8480efb00cbc30246c778b Mon Sep 17 00:00:00 2001
From: Jerome Kelleher <jk@well.ox.ac.uk>
Date: Mon, 4 Mar 2019 15:43:40 +0000
Subject: [PATCH 09/10] Remove hacky py2 compatible pickling.

---
 python/tskit/tables.py | 77 +++++-------------------------------------
 1 file changed, 8 insertions(+), 69 deletions(-)

diff --git a/python/tskit/tables.py b/python/tskit/tables.py
index 2e1156e9eb..e95ffaed8a 100644
--- a/python/tskit/tables.py
+++ b/python/tskit/tables.py
@@ -25,7 +25,6 @@
 """
 import base64
 import collections
-import copyreg
 import datetime
 import warnings
 
@@ -146,8 +145,13 @@ def truncate(self, num_rows):
         """
         return self.ll_table.truncate(num_rows)
 
+    # Picle support
+    def __getstate__(self):
+        return self.asdict()
+
     # Unpickle support
     def __setstate__(self, state):
+        self.__init__()
         self.set_columns(**state)
 
     def asdict(self):
@@ -333,11 +337,6 @@ def asdict(self):
         }
 
 
-# Pickle support. See copyreg registration for this function below.
-def _pickle_individual_table(table):
-    return IndividualTable, tuple(), table.asdict()
-
-
 class NodeTable(BaseTable):
     """
     A table defining the nodes in a tree sequence. See the
@@ -529,11 +528,6 @@ def asdict(self):
         }
 
 
-# Pickle support. See copyreg registration for this function below.
-def _pickle_node_table(table):
-    return NodeTable, tuple(), table.asdict()
-
-
 class EdgeTable(BaseTable):
     """
     A table defining the edges in a tree sequence. See the
@@ -663,11 +657,6 @@ def asdict(self):
         }
 
 
-# Pickle support. See copyreg registration for this function below.
-def _edge_table_pickle(table):
-    return EdgeTable, tuple(), table.asdict()
-
-
 class MigrationTable(BaseTable):
     """
     A table defining the migrations in a tree sequence. See the
@@ -827,11 +816,6 @@ def asdict(self):
         }
 
 
-# Pickle support. See copyreg registration for this function below.
-def _migration_table_pickle(table):
-    return MigrationTable, tuple(), table.asdict()
-
-
 class SiteTable(BaseTable):
     """
     A table defining the sites in a tree sequence. See the
@@ -1014,11 +998,6 @@ def asdict(self):
         }
 
 
-# Pickle support. See copyreg registration for this function below.
-def _site_table_pickle(table):
-    return SiteTable, tuple(), table.asdict()
-
-
 class MutationTable(BaseTable):
     """
     A table defining the mutations in a tree sequence. See the
@@ -1229,11 +1208,6 @@ def asdict(self):
         }
 
 
-# Pickle support. See copyreg registration for this function below.
-def _mutation_table_pickle(table):
-    return MutationTable, tuple(), table.asdict()
-
-
 class PopulationTable(BaseTable):
     """
     A table defining the populations referred to in a tree sequence.
@@ -1314,11 +1288,6 @@ def asdict(self):
         }
 
 
-# Pickle support. See copyreg registration for this function below.
-def _population_table_pickle(table):
-    return PopulationTable, tuple(), table.asdict()
-
-
 class ProvenanceTable(BaseTable):
     """
     A table recording the provenance (i.e., history) of this table, so that the
@@ -1405,14 +1374,6 @@ def __str__(self):
             ret += "{}\t{}\t{}\n".format(j, timestamp[j], record[j])
         return ret[:-1]
 
-    # Unpickle support
-    def __setstate__(self, state):
-        self.set_columns(
-            timestamp=state["timestamp"],
-            timestamp_offset=state["timestamp_offset"],
-            record=state["record"],
-            record_offset=state["record_offset"])
-
     def copy(self):
         """
         Returns a deep copy of this table.
@@ -1434,11 +1395,6 @@ def asdict(self):
         }
 
 
-# Pickle support. See copyreg registration for this function below.
-def _provenance_table_pickle(table):
-    return ProvenanceTable, tuple(), table.asdict()
-
-
 class TableCollection(object):
     """
     A collection of mutable tables defining a tree sequence. See the
@@ -1577,6 +1533,9 @@ def __eq__(self, other):
     def __ne__(self, other):
         return not self.__eq__(other)
 
+    def __getstate__(self):
+        return self.asdict()
+
     # Unpickle support
     def __setstate__(self, state):
         self.__init__(state["sequence_length"])
@@ -1773,26 +1732,6 @@ def drop_index(self):
         self.ll_tables.drop_index()
 
 
-# Pickle support. See copyreg registration for this function below.
-def _table_collection_pickle(tables):
-    return TableCollection, tuple(), tables.asdict()
-
-
-# Pickle support for the various tables. We are forced to use copyreg.pickle
-# here to support Python 2. For Python 3, we can just use the __setstate__.
-# It would be cleaner to attach the pickle_*_table functions to the classes
-# themselves, but this causes issues with Mocking on readthedocs. Sigh.
-copyreg.pickle(IndividualTable, _pickle_individual_table)
-copyreg.pickle(NodeTable, _pickle_node_table)
-copyreg.pickle(EdgeTable, _edge_table_pickle)
-copyreg.pickle(MigrationTable, _migration_table_pickle)
-copyreg.pickle(SiteTable, _site_table_pickle)
-copyreg.pickle(MutationTable, _mutation_table_pickle)
-copyreg.pickle(PopulationTable, _population_table_pickle)
-copyreg.pickle(ProvenanceTable, _provenance_table_pickle)
-copyreg.pickle(TableCollection, _table_collection_pickle)
-
-
 #############################################
 # Table functions.
 #############################################

From e70ec66b91e5cd799d8b42b8fce9cc46ad5a65cb Mon Sep 17 00:00:00 2001
From: Jerome Kelleher <jk@well.ox.ac.uk>
Date: Mon, 4 Mar 2019 15:58:05 +0000
Subject: [PATCH 10/10] Update Changelog to thank @hugovk.

Add support for sphinx-issues to do this properly.
---
 docs/Makefile                       |  2 +-
 docs/conf.py                        |  6 +++++-
 docs/rtd_requirements.txt           |  1 +
 python/CHANGELOG.rst                | 12 ++++++++++++
 python/requirements/development.txt |  1 +
 python/tskit/trees.py               |  5 +----
 6 files changed, 21 insertions(+), 6 deletions(-)

diff --git a/docs/Makefile b/docs/Makefile
index a1d061f2df..3043878fc1 100644
--- a/docs/Makefile
+++ b/docs/Makefile
@@ -3,7 +3,7 @@
 
 # You can set these variables from the command line.
 SPHINXOPTS    = -W
-SPHINXBUILD   = sphinx-build
+SPHINXBUILD   = python3 -m sphinx
 SOURCEDIR     = .
 BUILDDIR      = _build
 
diff --git a/docs/conf.py b/docs/conf.py
index 1eb16ef346..ae734a5e41 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -43,7 +43,7 @@ def __getattr__(cls, name):
 # -- Project information -----------------------------------------------------
 
 project = 'tskit'
-copyright = '2018, Tskit developers'
+copyright = '2018-2019, Tskit developers'
 author = 'Tskit developers'
 
 # The short X.Y version
@@ -117,8 +117,12 @@ def handle_item(fieldarg, content):
     'sphinx.ext.intersphinx',
     'sphinx.ext.todo',
     'breathe',
+    'sphinx_issues',
 ]
 
+# Github repo
+issues_github_path = "tskit-dev/tskit"
+
 # Add any paths that contain templates here, relative to this directory.
 templates_path = ['_templates']
 
diff --git a/docs/rtd_requirements.txt b/docs/rtd_requirements.txt
index faa3f1b40b..667bb2d02a 100644
--- a/docs/rtd_requirements.txt
+++ b/docs/rtd_requirements.txt
@@ -3,3 +3,4 @@ svgwrite
 jsonschema
 h5py
 breathe
+sphinx-issues
diff --git a/python/CHANGELOG.rst b/python/CHANGELOG.rst
index e9797a61c0..5fabe3960d 100644
--- a/python/CHANGELOG.rst
+++ b/python/CHANGELOG.rst
@@ -1,3 +1,15 @@
+--------------------
+[0.1.5] - 2019-XX-XX
+--------------------
+
+Under development.
+
+This release removes support for Python 2.
+
+**New features**
+
+- Remove support for Python 2 (:user:`hugovk`). See :issue:`137` and :pr:`140`.
+
 --------------------
 [0.1.4] - 2019-02-01
 --------------------
diff --git a/python/requirements/development.txt b/python/requirements/development.txt
index c70786ea05..2f62b5fd9f 100644
--- a/python/requirements/development.txt
+++ b/python/requirements/development.txt
@@ -11,6 +11,7 @@ jsonschema
 sphinx
 sphinx-argparse
 sphinx_rtd_theme
+sphinx-issues
 breathe
 svgwrite
 pyparsing 
diff --git a/python/tskit/trees.py b/python/tskit/trees.py
index 59b88e9196..43650b24f6 100644
--- a/python/tskit/trees.py
+++ b/python/tskit/trees.py
@@ -2525,11 +2525,8 @@ def haplotypes(self):
             multiletter alleles.
         """
         hapgen = _tskit.HaplotypeGenerator(self._ll_tree_sequence)
-        j = 0
-        # Would use range here except for Python 2.
-        while j < self.num_samples:
+        for j in range(self.num_samples):
             yield hapgen.get_haplotype(j)
-            j += 1
 
     # Samples is experimental for now, so we don't document it.
     def variants(self, as_bytes=False, samples=None):
