Repository: myokit/myokit (language: python)

Issue:
Drop support for Python 2.7, and <3.7 ?
Testing is being dropped (again): https://github.com/actions/setup-python/issues/672

Upgrading 3 is easy, using virtual environments is getting popular on systems where users don't have full control, so might as well go straight for 3.7+ then?


Discussion / hints:
Stuff that might be nice for Myokit:

- [3.0](https://docs.python.org/3.0/whatsnew/3.0.html)
  - No more `from __future__`
  - New style constructors (`super()`, and no `(object)` after every class)
  - No more `basestring` and needing to think about `bytes` when handling strings (less of it, anyway)
  - keyword-only arguments
  - exception chaining (raise from), and easier access to tracebacks
- [3.4](https://docs.python.org/3.4/whatsnew/3.4.html)
  - enums
  - pathlib
- [3.5](https://docs.python.org/3.5/whatsnew/3.5.html)
  - os.scandir
- [3.6](https://docs.python.org/3.6/whatsnew/3.6.html)
  - f-strings
- [3.7](https://docs.python.org/3.7/whatsnew/3.7.html)
  - Ordinary dict is now ordered (by definition, not as an implementation detail)
But the bigger benefit would just be less clunky code and less testing
At the same time, it's not a lot of work maintaining 2.7 right now. Github might go back on their decision (again), or an easy docker fix or something might show up...

Fix patch:
diff --git a/.coveragerc b/.coveragerc
index dd9957728..45ab56327 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -4,14 +4,12 @@ omit =
     myokit/gui/*
     myokit/tests/*
     myokit/__main__.py
-    myokit/_exec_old.py
     myokit/lib/common.py
 
 [report]
 exclude_lines =
     pragma: no cover
     pragma: no linux cover
-    pragma: no python 3 cover
     def __repr__
     raise AssertionError
     raise NotImplementedError
diff --git a/.flake8 b/.flake8
index e18c20b2e..e1985329f 100644
--- a/.flake8
+++ b/.flake8
@@ -7,7 +7,6 @@ exclude=
     .git,
     .github,
     myokit.egg-info,
-    myokit/_exec_old.py,
     myokit/formats/python/template,
     build,
     venv,
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2480cf459..b528b004e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,7 @@ This page lists the main changes made to Myokit in each release.
 ## Unreleased
 - Added
 - Changed
+  - [#980](https://github.com/myokit/myokit/pull/980) Python 2 is no longer supported. The minimum Python version is 3.6.
 - Deprecated
 - Removed
 - Fixed
diff --git a/myokit/__init__.py b/myokit/__init__.py
index 322f4b7b5..3139c7525 100644
--- a/myokit/__init__.py
+++ b/myokit/__init__.py
@@ -20,16 +20,11 @@
 # Without an explicit __all__, importing * will result in importing all
 # functions and classes described below. No submodules of myokit will be
 # loaded!
-
-
 #
 # GUI and graphical modules should not be auto-included because they define a
 # matplotlib backend to use. If the user requires a different backend, this
 # will generate an error.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 
 #
 # Check python version
@@ -44,27 +39,14 @@
 #
 import sys  # noqa
 if sys.hexversion < 0x03000000:  # pragma: no cover
+    raise Exception('This version of Myokit does not support Python 2.')
+elif sys.hexversion < 0x03070000:  # pragma: no cover
     import logging  # noqa
     log = logging.getLogger(__name__)
     log.warning(
-        'Myokit support for Python 2.7 is nearing the end of its lifetime.'
-        ' Please upgrade as soon as possible! Detected Python version: '
-        + sys.version)
-    del logging, log
-elif sys.hexversion < 0x03060000:  # pragma: no cover
-    import logging  # noqa
-    log = logging.getLogger(__name__)
-    log.warning(
-        'Myokit is not tested on Python 3 versions older than 3.6. Detected'
+        'Myokit is not tested on Python 3 versions older than 3.7. Detected'
         ' Python version: ' + sys.version)
     del logging, log
-
-
-# Exec() that works with Python 2 versions before 2.7.9
-if sys.hexversion < 0x020709F0:     # pragma: no python 3 cover
-    from ._exec_old import _exec    # noqa
-else:
-    from ._exec_new import _exec    # noqa
 del sys
 
 
diff --git a/myokit/__main__.py b/myokit/__main__.py
index b84efe5e6..347231210 100644
--- a/myokit/__main__.py
+++ b/myokit/__main__.py
@@ -4,9 +4,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import warnings
 
 _line_width = 79
@@ -421,10 +418,7 @@ def install():
     if plat == 'Linux':
         yesno = \
             'Install launcher icons and file type associations for Gnome/KDE? '
-        try:
-            yesno = raw_input(yesno)
-        except NameError:   # pragma: no python 2 cover
-            yesno = input(yesno)
+        yesno = input(yesno)
         yesno = (yesno.strip().lower())[:1] == 'y'
 
         if yesno:
@@ -432,10 +426,7 @@ def install():
 
     elif plat == 'Windows':
         yesno = 'Install start menu shortcuts? '
-        try:
-            yesno = raw_input(yesno)
-        except NameError:   # pragma: no python 2 cover
-            yesno = input(yesno)
+        yesno = input(yesno)
         yesno = (yesno.strip().lower())[:1] == 'y'
 
         if yesno:
@@ -867,10 +858,7 @@ def name(x):
     try:
         while True:
             x = 'Select device: '
-            try:
-                x = raw_input(x)
-            except NameError:   # pragma: no python 2 cover
-                x = input(x)    # lgtm [py/use-of-input]
+            x = input(x)
             x = x.strip()
             if x == '':
                 x = None
@@ -932,10 +920,7 @@ def reset(force=False):
         remove = True
     else:
         yesno = 'Remove all Myokit settings files? '
-        try:
-            yesno = raw_input(yesno)
-        except NameError:           # pragma: no python 2 cover
-            yesno = input(yesno)    # lgtm [py/use-of-input]
+        yesno = input(yesno)
         yesno = yesno.strip().lower()
         remove = (yesno[:1] == 'y')
     if remove:
@@ -1329,7 +1314,7 @@ def test_coverage(args):
     try:
         print('Gathering coverage data')
         p = subprocess.Popen([
-            'python3',
+            sys.executable,
             '-m',
             'coverage',
             'run',
@@ -1353,7 +1338,7 @@ def test_coverage(args):
 
         print('Generating coverage report.')
         p = subprocess.Popen([
-            'python3',
+            sys.executable,
             '-m',
             'coverage',
             'report',
@@ -1784,8 +1769,7 @@ def test_examples_single(root, path):
         p = subprocess.Popen(
             cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env
         )
-        stdout, stderr = p.communicate()
-        # TODO: Use p.communicate(timeout=3600) if Python3 only
+        stdout, stderr = p.communicate(timeout=3600)
         if p.returncode != 0:
             # Show failing code, output and errors before returning
             print('ERROR')
diff --git a/myokit/_aux.py b/myokit/_aux.py
index eaa7273c5..18d44090b 100644
--- a/myokit/_aux.py
+++ b/myokit/_aux.py
@@ -6,9 +6,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import myokit
 
 import sys
@@ -29,7 +26,7 @@ def __init__(self):
         warnings.warn(
             'The class `myokit.Benchmarker` is deprecated.'
             ' Please use `myokit.tools.Benchmarker` instead.')
-        super(Benchmarker, self).__init__()
+        super().__init__()
 
 
 def date():
@@ -150,7 +147,7 @@ def format_path(path, root='.'):
     return myokit.tools.format_path(path, root)
 
 
-class ModelComparison(object):
+class ModelComparison:
     """
     Compares two models.
 
@@ -520,9 +517,9 @@ def run(model, protocol, script, stdout=None, stderr=None, progress=None):
     script = '\n' + script
 
     # Class to run scripts
-    class Runner(object):
+    class Runner:
         def __init__(self, model, protocol, script, stdout, stderr, progress):
-            super(Runner, self).__init__()
+            super().__init__()
             self.model = model
             self.protocol = protocol
             self.script = script
@@ -555,7 +552,7 @@ def get_protocol():
                     'get_model': get_model,
                     'get_protocol': get_protocol,
                 }
-                myokit._exec(self.script, environment)
+                exec(self.script, environment)
             finally:
                 if oldstdout is not None:
                     sys.stdout = oldstdout
diff --git a/myokit/_config.py b/myokit/_config.py
index 24ed45992..fd63c449e 100644
--- a/myokit/_config.py
+++ b/myokit/_config.py
@@ -5,27 +5,18 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
-# Load Myokit, at least, the bit that's been setup so far. This just means
-# this method will add a link to the myokit module already being loaded
-# into this method's namespace. This allows us to use the constants defined
-# before this method was called.
-import myokit
-
-# Load standard library modules
+import configparser
 import logging
 import os
 import platform
 import sys
 import warnings
 
-# ConfigParser in Python 2 and 3
-try:
-    from ConfigParser import ConfigParser
-except ImportError:
-    from configparser import RawConfigParser as ConfigParser
+# Load Myokit, at least, the bit that's been setup so far. This just means
+# this method will add a link to the myokit module already being loaded
+# into this method's namespace. This allows us to use the constants defined
+# before this method was called.
+import myokit
 
 
 def _create(path):
@@ -37,7 +28,7 @@ def _create(path):
     system = platform.system()
 
     # Create config parser
-    config = ConfigParser(allow_no_value=True)
+    config = configparser.RawConfigParser(allow_no_value=True)
 
     # Make the parser case sensitive (need for unix paths!)
     config.optionxform = str
@@ -223,7 +214,7 @@ def _load():
         del lines, inline_comment
 
     # Create the config parser (no value allows comments)
-    config = ConfigParser(allow_no_value=True)
+    config = configparser.RawConfigParser(allow_no_value=True)
 
     # Make the parser case sensitive (need for unix paths!)
     config.optionxform = str
diff --git a/myokit/_datablock.py b/myokit/_datablock.py
index 367429cfe..9e2828b38 100644
--- a/myokit/_datablock.py
+++ b/myokit/_datablock.py
@@ -4,15 +4,14 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
+import array
 import os
 import sys
-import array
-import numpy as np
+
 import myokit
 
+import numpy as np
+
 
 # Readme file for DataBlock1d binary files
 README_SAVE_1D = """
@@ -85,7 +84,7 @@
 ENC = 'utf-8'
 
 
-class DataBlock1d(object):
+class DataBlock1d:
     """
     Container for time-series of 1d rectangular data arrays.
 
@@ -476,17 +475,10 @@ def load(filename, progress=None, msg='Loading DataBlock1d'):
                 'This method requires the ``zlib`` module to be installed.')
 
         # Get size of single and double types on this machine
-        try:
-            dsize = {
-                'd': len(array.array('d', [1]).tobytes()),
-                'f': len(array.array('f', [1]).tobytes()),
-            }
-        except (AttributeError, TypeError):  # pragma: no python 3 cover
-            # List dtype as str for Python 2.7.10 (see #225)
-            dsize = {
-                b'd': len(array.array(b'd', [1]).tostring()),
-                b'f': len(array.array(b'f', [1]).tostring()),
-            }
+        dsize = {
+            'd': len(array.array('d', [1]).tobytes()),
+            'f': len(array.array('f', [1]).tobytes()),
+        }
 
         # Read data from file
         try:
@@ -544,8 +536,6 @@ def load(filename, progress=None, msg='Loading DataBlock1d'):
             nt = int(next(head))
             nx = int(next(head))
             dtype = next(head)[1:-1]
-            # Convert dtype to str for Python 2.7.10 (see #225)
-            dtype = str(dtype)
             if dtype not in dsize:
                 raise myokit.DataBlockReadError(
                     'Unable to read DataBlock1d: Unrecognized data type "'
@@ -573,10 +563,7 @@ def load(filename, progress=None, msg='Loading DataBlock1d'):
                     'Unable to read DataBlock1d: Header indicates larger data'
                     ' than found in the body.')
             data = array.array(dtype)
-            try:
-                data.frombytes(body[start:end])
-            except AttributeError:  # pragma: no python 3 cover
-                data.fromstring(body[start:end])
+            data.frombytes(body[start:end])
             if sys.byteorder == 'big':  # pragma: no cover
                 data.byteswap()
             data = np.array(data)
@@ -597,10 +584,7 @@ def load(filename, progress=None, msg='Loading DataBlock1d'):
                         'Unable to read DataBlock1d: Header indicates larger'
                         ' data than found in the body.')
                 data = array.array(dtype)
-                try:
-                    data.frombytes(body[start:end])
-                except AttributeError:  # pragma: no python 3 cover
-                    data.fromstring(body[start:end])
+                data.frombytes(body[start:end])
                 if sys.byteorder == 'big':  # pragma: no cover
                     data.byteswap()
                 data = np.array(data)
@@ -619,10 +603,7 @@ def load(filename, progress=None, msg='Loading DataBlock1d'):
                         'Unable to read DataBlock1d: Header indicates larger'
                         ' data than found in the body.')
                 data = array.array(dtype)
-                try:
-                    data.frombytes(body[start:end])
-                except AttributeError:  # pragma: no python 3 cover
-                    data.fromstring(body[start:end])
+                data.frombytes(body[start:end])
                 if sys.byteorder == 'big':  # pragma: no cover
                     data.byteswap()
                 data = np.array(data).reshape(nt, nx, order='C')
@@ -680,8 +661,7 @@ def save(self, filename):
                 'This method requires the ``zlib`` module to be installed.')
 
         # Data type
-        # Create dtype as str for Python 2.7.10 (see #225)
-        dtype = str('d')     # Only supporting doubles right now
+        dtype = 'd'     # Only supporting doubles right now
 
         # Create header
         head_str = []
@@ -706,10 +686,7 @@ def save(self, filename):
         if sys.byteorder == 'big':  # pragma: no cover
             for ar in body_str:
                 ar.byteswap()
-        try:
-            body_str = b''.join([ar.tobytes() for ar in body_str])
-        except AttributeError:  # pragma: no python 3 cover
-            body_str = b''.join([ar.tostring() for ar in body_str])
+        body_str = b''.join([ar.tobytes() for ar in body_str])
 
         # Write
         head = zipfile.ZipInfo('header_block1d.txt')
@@ -803,7 +780,7 @@ def trace(self, variable, x):
         return self._1d[variable][:, x]
 
 
-class DataBlock2d(object):
+class DataBlock2d:
     """
     Container for time-series of 2d rectangular data arrays.
 
@@ -1274,17 +1251,10 @@ def load(filename, progress=None, msg='Loading DataBlock2d'):
                 'This method requires the ``zlib`` module to be installed.')
 
         # Get size of single and double types on this machine
-        try:
-            dsize = {
-                'd': len(array.array('d', [1]).tobytes()),
-                'f': len(array.array('f', [1]).tobytes()),
-            }
-        except (AttributeError, TypeError):  # pragma: no python 3 cover
-            # List dtype as str for Python 2.7.10 (see #225)
-            dsize = {
-                b'd': len(array.array(b'd', [1]).tostring()),
-                b'f': len(array.array(b'f', [1]).tostring()),
-            }
+        dsize = {
+            'd': len(array.array('d', [1]).tobytes()),
+            'f': len(array.array('f', [1]).tobytes()),
+        }
 
         # Read data from file
         try:
@@ -1353,8 +1323,7 @@ def load(filename, progress=None, msg='Loading DataBlock2d'):
             nx = int(next(head))
 
             # Get dtype
-            # Convert dtype to str for Python 2.7.10 (see #225)
-            dtype = str(next(head))[1:-1]
+            dtype = next(head)[1:-1]
             if dtype not in dsize:
                 raise myokit.DataBlockReadError(
                     'Unable to read DataBlock2d: Unrecognized data type "'
@@ -1384,10 +1353,7 @@ def load(filename, progress=None, msg='Loading DataBlock2d'):
                     ' than found in the body.')
 
             data = array.array(dtype)
-            try:
-                data.frombytes(body[start:end])
-            except AttributeError:  # pragma: no python 3 cover
-                data.fromstring(body[start:end])
+            data.frombytes(body[start:end])
             if sys.byteorder == 'big':  # pragma: no cover
                 data.byteswap()
             data = np.array(data)
@@ -1408,10 +1374,7 @@ def load(filename, progress=None, msg='Loading DataBlock2d'):
                         'Unable to read DataBlock2d: Header indicates larger'
                         ' data than found in the body.')
                 data = array.array(dtype)
-                try:
-                    data.frombytes(body[start:end])
-                except AttributeError:  # pragma: no python 3 cover
-                    data.fromstring(body[start:end])
+                data.frombytes(body[start:end])
                 if sys.byteorder == 'big':  # pragma: no cover
                     data.byteswap()
                 data = np.array(data)
@@ -1430,10 +1393,7 @@ def load(filename, progress=None, msg='Loading DataBlock2d'):
                         'Unable to read DataBlock2d: Header indicates larger'
                         ' data than found in the body.')
                 data = array.array(dtype)
-                try:
-                    data.frombytes(body[start:end])
-                except AttributeError:  # pragma: no python 3 cover
-                    data.fromstring(body[start:end])
+                data.frombytes(body[start:end])
                 if sys.byteorder == 'big':  # pragma: no cover
                     data.byteswap()
                 data = np.array(data).reshape(nt, ny, nx, order='C')
@@ -1496,8 +1456,7 @@ def save(self, filename):
                 'This method requires the ``zlib`` module to be installed.')
 
         # Data type
-        # Create dtype as str for Python 2.7.10 (see #225)
-        dtype = str('d')  # Only supporting doubles right now
+        dtype = 'd'     # Only supporting doubles right now
 
         # Create header
         head_str = []
@@ -1523,10 +1482,7 @@ def save(self, filename):
         if sys.byteorder == 'big':  # pragma: no cover
             for ar in body_str:
                 ar.byteswap()
-        try:
-            body_str = b''.join([ar.tobytes() for ar in body_str])
-        except AttributeError:  # pragma: no python 3 cover
-            body_str = b''.join([ar.tostring() for ar in body_str])
+        body_str = b''.join([ar.tobytes() for ar in body_str])
 
         # Write
         head = zipfile.ZipInfo('header_block2d.txt')
@@ -1671,7 +1627,7 @@ def trace(self, variable, x, y):
         return self._2d[variable][:, y, x]
 
 
-class ColorMap(object):
+class ColorMap:
     """
     *Abstract class*
 
diff --git a/myokit/_datalog.py b/myokit/_datalog.py
index 115702b48..949080721 100644
--- a/myokit/_datalog.py
+++ b/myokit/_datalog.py
@@ -4,22 +4,16 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import os
 import re
 import sys
 import array
+
 import numpy as np
+
 from collections import OrderedDict
-import myokit
 
-# Strings in Python 2 and 3
-try:
-    basestring
-except NameError:   # pragma: no python 2 cover
-    basestring = str
+import myokit
 
 
 # Function to split keys into dimension-key,qname-key pairs
@@ -101,11 +95,11 @@ class DataLog(OrderedDict):
     def __init__(self, other=None, time=None):
         if other is None:
             # Create new
-            super(DataLog, self).__init__()
+            super().__init__()
             self._time = None
         else:
             # Clone
-            super(DataLog, self).__init__(other)
+            super().__init__(other)
             try:
                 self._time = str(other._time)
             except Exception:
@@ -216,10 +210,10 @@ def clone(self, numpy=False):
         return log
 
     def __contains__(self, key):
-        return super(DataLog, self).__contains__(self._parse_key(key))
+        return super().__contains__(self._parse_key(key))
 
     def __delitem__(self, key):
-        return super(DataLog, self).__delitem__(self._parse_key(key))
+        return super().__delitem__(self._parse_key(key))
 
     def extend(self, other):
         """
@@ -328,7 +322,7 @@ def fold(self, period, discard_remainder=True):
         return out
 
     def __getitem__(self, key):
-        return super(DataLog, self).__getitem__(self._parse_key(key))
+        return super().__getitem__(self._parse_key(key))
 
     def has_nan(self):
         """
@@ -517,17 +511,10 @@ def load(filename, progress=None, msg='Loading DataLog'):
                 'This method requires the ``zlib`` module to be installed.')
 
         # Get size of single and double types on this machine
-        try:
-            dsize = {
-                'd': len(array.array('d', [1]).tobytes()),
-                'f': len(array.array('f', [1]).tobytes()),
-            }
-        except (AttributeError, TypeError):  # pragma: no python 3 cover
-            # List dtype as str for Python 2.7.10 (see #225)
-            dsize = {
-                b'd': len(array.array(b'd', [1]).tostring()),
-                b'f': len(array.array(b'f', [1]).tostring()),
-            }
+        dsize = {
+            'd': len(array.array('d', [1]).tobytes()),
+            'f': len(array.array('f', [1]).tobytes()),
+        }
 
         # Read data
         try:
@@ -563,7 +550,7 @@ def load(filename, progress=None, msg='Loading DataLog'):
         head = iter(head.splitlines())
         n = int(next(head))
         data_size = int(next(head))
-        data_type = str(next(head))  # Cast to str for Python 2.7.10 (see #225)
+        data_type = next(head)
         time = next(head)
         if time:
             # Note, this field doesn't have to be present in the log!
@@ -604,10 +591,7 @@ def load(filename, progress=None, msg='Loading DataLog'):
 
                 # Read data
                 ar = array.array(data_type)
-                try:
-                    ar.frombytes(body[start:end])
-                except AttributeError:  # pragma: no python 3 cover
-                    ar.fromstring(body[start:end])
+                ar.frombytes(body[start:end])
                 if sys.byteorder == 'big':  # pragma: no cover
                     ar.byteswap()
                 log[field] = ar
@@ -638,8 +622,7 @@ def load_csv(filename, precision=myokit.DOUBLE_PRECISION):
         filename = os.path.expanduser(filename)
 
         # Typecode dependent on precision
-        # Typecode must be str for Python 2.7.10 (see #225)
-        typecode = str('d' if precision == myokit.DOUBLE_PRECISION else 'f')
+        typecode = 'd' if precision == myokit.DOUBLE_PRECISION else 'f'
 
         # Error raising function
         def e(line, char, msg):
@@ -647,16 +630,9 @@ def e(line, char, msg):
                 'Syntax error on line ' + str(line) + ', character '
                 + str(1 + char) + ': ' + msg)
 
-        def uopen(filename):
-            # Open a filename in 'universal newline' mode, python 2 and 3
-            try:
-                return open(filename, 'r', newline=None)
-            except TypeError:   # pragma: no python 3 cover
-                return open(filename, 'U')
-
         quote = '"'
         delim = ','
-        with uopen(filename) as f:
+        with open(filename, 'r', newline=None) as f:
             # Read header
             keys = []   # The log keys, in order of appearance
 
@@ -936,8 +912,7 @@ def save(self, filename, precision=myokit.DOUBLE_PRECISION):
                 'This method requires the `zlib` module to be installed.')
 
         # Data type
-        # dtype must be str for Python 2.7.10 (see #225)
-        dtype = str('d' if precision == myokit.DOUBLE_PRECISION else 'f')
+        dtype = 'd' if precision == myokit.DOUBLE_PRECISION else 'f'
 
         # Create data strings
         head_str = []
@@ -959,10 +934,7 @@ def save(self, filename, precision=myokit.DOUBLE_PRECISION):
             ar = array.array(dtype, v)
             if sys.byteorder == 'big':  # pragma: no cover
                 ar.byteswap()
-            try:
-                body_str.append(ar.tobytes())
-            except AttributeError:   # pragma: no python 3 cover
-                body_str.append(ar.tostring())
+            body_str.append(ar.tobytes())
         head_str = '\n'.join(head_str)
         body_str = b''.join(body_str)
 
@@ -1006,7 +978,7 @@ def save_csv(
             If a precision argument (for example ``myokit.DOUBLE_PRECISION``)
             is given, the output will be stored in such a way that this amount
             of precision is guaranteed to be present in the string. If the
-            precision argument is set to ``None`` python's default formatting
+            precision argument is set to ``None`` Python's default formatting
             is used, which may lead to smaller files.
         ``order``
             To specify the ordering of the log's arguments, pass in a sequence
@@ -1107,7 +1079,7 @@ def set_time_key(self, key):
         self._time = None if key is None else str(key)
 
     def __setitem__(self, key, value):
-        return super(DataLog, self).__setitem__(
+        return super().__setitem__(
             self._parse_key(key), value)
 
     def split(self, value):
@@ -1422,7 +1394,7 @@ def variable_info(self):
         return infos
 
 
-class LoggedVariableInfo(object):
+class LoggedVariableInfo:
     """
     Contains information about the log entries for each variable. These objects
     should only be created by :meth:`DataLog.variable_info()`.
@@ -1629,9 +1601,7 @@ def prepare_log(
     be specified using the ``precision`` argument.
     """
     # Typecode dependent on precision
-    # Note: Cast to str() here makes it work with older versions of 2.7.x,
-    # where unicode isn't accepted (Python 3 of course doesn't accept bytes)
-    typecode = str('d' if precision == myokit.DOUBLE_PRECISION else 'f')
+    typecode = 'd' if precision == myokit.DOUBLE_PRECISION else 'f'
 
     # Get all options for dimensionality
     if dims is None:
@@ -1874,7 +1844,7 @@ def check_if_allowed_class(var):
             'Argument `log` has unexpected type. Expecting None, integer flag,'
             ' sequence of names, dict or DataLog.')
 
-    if isinstance(log, basestring):
+    if isinstance(log, str):
         raise ValueError(
             'String passed in as `log` argument, should be list'
             ' or other sequence containing strings.')
diff --git a/myokit/_err.py b/myokit/_err.py
index b0b8ba26d..83bce48dd 100644
--- a/myokit/_err.py
+++ b/myokit/_err.py
@@ -4,8 +4,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
 
 
 # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
@@ -24,7 +22,7 @@ class MyokitError(Exception):
     *Extends:* ``Exception``
     """
     def __init__(self, message):
-        super(MyokitError, self).__init__(message)
+        super().__init__(message)
 
 
 class IntegrityError(MyokitError):
@@ -37,7 +35,7 @@ class IntegrityError(MyokitError):
     *Extends:* :class:`myokit.MyokitError`
     """
     def __init__(self, message, token=None):
-        super(IntegrityError, self).__init__(message)
+        super().__init__(message)
         self._token = token
 
     def token(self):
@@ -77,7 +75,7 @@ def __init__(self, cycle):
         # Set token: First item in cycle is model's defining lhs
         tok = cycle[0].var()._token
         # Raise
-        super(CyclicalDependencyError, self).__init__(msg, tok)
+        super().__init__(msg, tok)
 
 
 class DataBlockReadError(MyokitError):
@@ -167,7 +165,7 @@ class IllegalReferenceError(IntegrityError):
     *Extends:* :class:`myokit.IntegrityError`
     """
     def __init__(self, reference, owner):
-        super(IllegalReferenceError, self).__init__(
+        super().__init__(
             'Illegal reference: The referenced variable <' + reference.qname()
             + '> is outside the scope of <' + owner.qname() + '>.')
 
@@ -179,7 +177,7 @@ class IllegalReferenceInInitialValueError(IllegalReferenceError):
     The only way this can occur is if the reference is to a nested variable.
     """
     def __init__(self, reference, owner):
-        super(IllegalReferenceError, self).__init__(
+        super(IntegrityError, self).__init__(
             'Illegal reference made in initial value: The referenced variable'
             ' <' + reference.qname() + '> is nested.')
 
@@ -203,7 +201,7 @@ def __init__(self, model_name, message):
         if model_name:
             msg += ' <' + str(model_name) + '>'
         msg += ': ' + str(message)
-        super(IncompatibleModelError, self).__init__(msg)
+        super().__init__(msg)
 
 
 class IncompatibleUnitError(MyokitError):
@@ -213,7 +211,7 @@ class IncompatibleUnitError(MyokitError):
     *Extends:* :class:`myokit.MyokitError`.
     """
     def __init__(self, message, token=None):
-        super(MyokitError, self).__init__(message)
+        super().__init__(message)
         self._token = token
 
     def token(self):
@@ -288,7 +286,7 @@ class MissingRhsError(IntegrityError):
     def __init__(self, var):
         msg = 'No rhs set for <' + var.qname() + '>.'
         tok = var._token
-        super(MissingRhsError, self).__init__(msg, tok)
+        super().__init__(msg, tok)
 
 
 class MissingTimeVariableError(IntegrityError):
@@ -300,7 +298,7 @@ class MissingTimeVariableError(IntegrityError):
     def __init__(self):
         msg = 'No variable bound to time. At least one of the model\'s' \
               ' variables must be bound to "time".'
-        super(MissingTimeVariableError, self).__init__(msg)
+        super().__init__(msg)
 
 
 class NumericalError(MyokitError):
@@ -339,7 +337,7 @@ def __init__(self, name, line, char, desc, cause=None):
         self.desc = str(desc)
         self.value += ': ' + self.desc
         self.cause = cause
-        super(ParseError, self).__init__(self.value)
+        super().__init__(self.value)
 
 
 class ProtocolEventError(MyokitError):
@@ -383,7 +381,7 @@ class SimulationCancelledError(MyokitError):
     *Extends:* :class:`myokit.MyokitError`
     """
     def __init__(self, message='Operation cancelled by user.'):
-        super(SimulationCancelledError, self).__init__(message)
+        super().__init__(message)
 
 
 class SimultaneousProtocolEventError(MyokitError):
@@ -402,7 +400,7 @@ class UnresolvedReferenceError(IntegrityError):
     *Extends:* :class:`myokit.IntegrityError`
     """
     def __init__(self, reference, extra_message=None):
-        super(UnresolvedReferenceError, self).__init__(
+        super().__init__(
             'Unknown variable: <' + reference + '>.'
             + ((' ' + extra_message) if extra_message else '')
         )
@@ -419,7 +417,7 @@ class UnusedVariableError(IntegrityError):
     def __init__(self, var):
         msg = 'Unused variable: <' + var.qname() + '>.'
         tok = var.lhs()._token
-        super(UnusedVariableError, self).__init__(msg, tok)
+        super().__init__(msg, tok)
 
 
 class VariableMappingError(MyokitError):
diff --git a/myokit/_exec_new.py b/myokit/_exec_new.py
deleted file mode 100644
index e4ea78378..000000000
--- a/myokit/_exec_new.py
+++ /dev/null
@@ -1,15 +0,0 @@
-#
-# Exec function that works with Python versions 2.7.9 and higher
-#
-# This file is part of Myokit.
-# See http://myokit.org for copyright, sharing, and licensing details.
-#
-
-
-def _exec(script, globals=None, locals=None):
-    """
-    Wrapper around the built-in function ``exec`` on Python versions 2.7.9 and
-    higher 2.7.9, or a function calling the ``exec`` statement on earlier
-    versions.
-    """
-    exec(script, globals, locals)
diff --git a/myokit/_exec_old.py b/myokit/_exec_old.py
deleted file mode 100644
index 5efb16560..000000000
--- a/myokit/_exec_old.py
+++ /dev/null
@@ -1,15 +0,0 @@
-#
-# Exec function that works with Python versions before 2.7.9 (0x020709F0)
-#
-# This file is part of Myokit.
-# See http://myokit.org for copyright, sharing, and licensing details.
-#
-
-
-def _exec(script, globals=None, locals=None):
-    """
-    Wrapper around the built-in function ``exec`` on Python versions 2.7.9 and
-    higher 2.7.9, or a function calling the ``exec`` statement on earlier
-    versions.
-    """
-    exec script in globals, locals
diff --git a/myokit/_expressions.py b/myokit/_expressions.py
index 9b520ac0b..edb99fd2f 100644
--- a/myokit/_expressions.py
+++ b/myokit/_expressions.py
@@ -5,26 +5,14 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
+import io
 import math
 import numpy
 
 import myokit
-from myokit import IntegrityError
 
-# StringIO in Python 2 and 3
-try:
-    from cStringIO import StringIO
-except ImportError:
-    from io import StringIO
+from myokit import IntegrityError
 
-# Strings in Python 2 and 3
-try:
-    basestring
-except NameError:   # pragma: no python 2 cover
-    basestring = str
 
 # Expression precedence levels
 FUNCTION_CALL = 70
@@ -37,7 +25,7 @@
 LITERAL = 0
 
 
-class Expression(object):
+class Expression:
     """
     Myokit's most generic interface for expressions. All expressions extend
     this class.
@@ -85,8 +73,8 @@ def __init__(self, operands=None):
         self._cached_unit_tolerant = None
         self._cached_unit_strict = None
 
-    def __bool__(self):     # pragma: no python 2 cover
-        """ Python 3 method to determine the outcome of "if expression". """
+    def __bool__(self):
+        # Determines the outcome of "if expression".
         return True
 
     def bracket(self, op=None):
@@ -142,7 +130,7 @@ def code(self, component=None):
         """
         # Note: Because variable and component names can change, the output of
         # code can not be cached (for non-literal expressions).
-        b = StringIO()
+        b = io.StringIO()
         self._code(b, component)
         return b.getvalue()
 
@@ -540,8 +528,6 @@ def _eval_unit(self, mode):
         raise NotImplementedError
 
     def __float__(self):
-        # Cast to float is required in Python 3: numpy float etc. are ok, but
-        # this is deprecated.
         return float(self.eval())
 
     def __getitem__(self, key):
@@ -551,9 +537,7 @@ def __hash__(self):
         if self._cached_hash is None:
             self._cached_hash = hash(self._polish())
         return self._cached_hash
-        # Note for Python3:
-        #   In Python3, anything that has an __eq__ stops inheriting this hash
-        #   method!
+        # Note: anything that has an __eq__ stops inheriting this hash method!
         # From: https://docs.python.org/3.1/reference/datamodel.html
         # > If a class that overrides __eq__() needs to retain the
         #   implementation of __hash__() from a parent class, the interpreter
@@ -631,10 +615,6 @@ def __len__(self):
     def __ne__(self, other):
         return not self.__eq__(other)
 
-    def __nonzero__(self):  # pragma: no python 3 cover
-        """ Python 2 method to determine the outcome of "if expression". """
-        return True
-
     def operator_rep(self):
         """
         Returns a representation of this expression's type. (For example '+' or
@@ -653,7 +633,7 @@ def _polish(self):
         variable id is immutable in the expression's lifetime.
         """
         if self._cached_polish is None:
-            b = StringIO()
+            b = io.StringIO()
             self._polishb(b)
             self._cached_polish = b.getvalue()
         return self._cached_polish
@@ -670,7 +650,7 @@ def _polishb(self, b):
 
     def pyfunc(self, use_numpy=True):
         """
-        Converts this expression to python and returns the new function's
+        Converts this expression to Python and returns the new function's
         handle.
 
         By default, when converting mathematical functions such as ``log``, the
@@ -691,19 +671,19 @@ def pyfunc(self, use_numpy=True):
         # Create function
         local = {}
         if use_numpy:
-            myokit._exec(c, {'numpy': numpy}, local)
+            exec(c, {'numpy': numpy}, local)
         else:
-            myokit._exec(c, {'math': math}, local)
+            exec(c, {'math': math}, local)
 
         # Return
         return local['ex_pyfunc_generated']
 
     def pystr(self, use_numpy=False):
         """
-        Returns a string representing this expression in python syntax.
+        Returns a string representing this expression in Python syntax.
 
         By default, built-in functions such as 'exp' are converted to the
-        python version 'math.exp'. To use the numpy versions, set
+        Python version 'math.exp'. To use the numpy versions, set
         ``numpy=True``.
         """
         # Get expression writer
@@ -742,7 +722,7 @@ def tree_str(self):
         Returns a string representing the parse tree corresponding to this
         expression.
         """
-        b = StringIO()
+        b = io.StringIO()
         self._tree_str(b, 0)
         return b.getvalue()
 
@@ -855,7 +835,7 @@ class Number(Expression):
     _rbp = LITERAL
 
     def __init__(self, value, unit=None):
-        super(Number, self).__init__()
+        super().__init__()
         if isinstance(value, myokit.Quantity):
             # Conversion from Quantity class
             if unit is not None:
@@ -869,7 +849,7 @@ def __init__(self, value, unit=None):
             self._value = float(value) if value else 0.0
             if unit is None or isinstance(unit, myokit.Unit):
                 self._unit = unit
-            elif isinstance(unit, basestring):
+            elif isinstance(unit, str):
                 self._unit = myokit.parse_unit(unit)
             else:
                 raise ValueError(
@@ -1013,10 +993,10 @@ class Name(LhsExpression):
     *Extends:* :class:`LhsExpression`
     """
     _rbp = LITERAL
-    __hash__ = LhsExpression.__hash__   # For Python3, when __eq__ is present
+    __hash__ = LhsExpression.__hash__
 
     def __init__(self, value):
-        super(Name, self).__init__()
+        super().__init__()
         self._value = value
         self._references = set([self])
         self._proper = isinstance(self._value, myokit.Variable)
@@ -1054,7 +1034,7 @@ def _code(self, b, c):
                     except KeyError:
                         pass
                 b.write(self._value.qname(c))
-        elif isinstance(self._value, basestring):
+        elif isinstance(self._value, str):
             # Allow strings for debugging
             b.write('str:' + str(self._value))
         else:
@@ -1125,7 +1105,7 @@ def is_state_value(self):
         return self._proper and self._value.is_state()
 
     def _polishb(self, b):
-        if isinstance(self._value, basestring):
+        if isinstance(self._value, str):
             # Allow an exception for strings
             b.write('str:')
             b.write(self._value)
@@ -1153,7 +1133,7 @@ def _tree_str(self, b, n):
         b.write(' ' * n + str(self._value) + '\n')
 
     def _validate(self, trail):
-        super(Name, self)._validate(trail)
+        super()._validate(trail)
         # Check value: String is allowed at construction for debugging, but
         # not here!
         if not self._proper:
@@ -1174,10 +1154,10 @@ class Derivative(LhsExpression):
     """
     _rbp = FUNCTION_CALL
     _nargs = [1]    # Allows parsing as a function
-    __hash__ = LhsExpression.__hash__   # For Python3, when __eq__ is present
+    __hash__ = LhsExpression.__hash__
 
     def __init__(self, op):
-        super(Derivative, self).__init__((op,))
+        super().__init__((op,))
         if not isinstance(op, Name):
             raise IntegrityError(
                 'The dot() operator can only be used on variables.',
@@ -1274,7 +1254,7 @@ def var(self):
         return self._op._value
 
     def _validate(self, trail):
-        super(Derivative, self)._validate(trail)
+        super()._validate(trail)
         # Check that value is a variable has already been performed by name
         # Check if value is the name of a state variable
         if not self._op._value.is_state():
@@ -1295,7 +1275,7 @@ class PartialDerivative(LhsExpression):
     """
     _rbp = FUNCTION_CALL
     _nargs = [2]    # Allows parsing as a function
-    __hash__ = LhsExpression.__hash__   # For Python3, when __eq__ is present
+    __hash__ = LhsExpression.__hash__
 
     def __init__(self, var1, var2):
         if not isinstance(var1, (Name, Derivative)):
@@ -1306,7 +1286,7 @@ def __init__(self, var1, var2):
             raise IntegrityError(
                 'The second argument to a partial derivative must be a'
                 ' variable name or an initial value.')
-        super(PartialDerivative, self).__init__((var1, var2))
+        super().__init__((var1, var2))
 
         self._var1 = var1
         self._var2 = var2
@@ -1406,10 +1386,10 @@ class InitialValue(LhsExpression):
     """
     _rbp = FUNCTION_CALL
     _nargs = [1]    # Allows parsing as a function
-    __hash__ = LhsExpression.__hash__   # For Python3, when __eq__ is present
+    __hash__ = LhsExpression.__hash__
 
     def __init__(self, var):
-        super(InitialValue, self).__init__((var, ))
+        super().__init__((var, ))
         if not isinstance(var, Name):
             raise IntegrityError(
                 'The first argument to an initial value must be a variable'
@@ -1468,7 +1448,7 @@ def var(self):
         return self._var._value
 
     def _validate(self, trail):
-        super(InitialValue, self)._validate(trail)
+        super()._validate(trail)
         # Check if value is the name of a state variable
         var = self._var._value
         if not (isinstance(var, myokit.Variable) and var.is_state()):
@@ -1487,7 +1467,7 @@ class PrefixExpression(Expression):
     _rep = None
 
     def __init__(self, op):
-        super(PrefixExpression, self).__init__((op,))
+        super().__init__((op,))
         self._op = op
 
     def bracket(self, op):
@@ -1586,7 +1566,7 @@ class InfixExpression(Expression):
     _spaces_round_operator = True
 
     def __init__(self, left, right):
-        super(InfixExpression, self).__init__((left, right))
+        super().__init__((left, right))
         self._op1 = left
         self._op2 = right
 
@@ -2089,7 +2069,7 @@ class Function(Expression):
     _rbp = FUNCTION_CALL
 
     def __init__(self, *ops):
-        super(Function, self).__init__(ops)
+        super().__init__(ops)
         if self._nargs is not None:
             if not len(ops) in self._nargs:
                 raise IntegrityError(
@@ -2660,7 +2640,7 @@ class If(Function):
     _fname = 'if'
 
     def __init__(self, i, t, e):
-        super(If, self).__init__(i, t, e)
+        super().__init__(i, t, e)
         self._i = i     # if
         self._t = t     # then
         self._e = e     # else
@@ -2766,7 +2746,7 @@ class Piecewise(Function):
     _fname = 'piecewise'
 
     def __init__(self, *ops):
-        super(Piecewise, self).__init__(*ops)
+        super().__init__(*ops)
 
         # Check number of arguments
         n = len(self._operands)
@@ -2854,7 +2834,7 @@ def pieces(self):
         return iter(self._e)
 
 
-class Condition(object):
+class Condition:
     """
     *Abstract class*
 
diff --git a/myokit/_io.py b/myokit/_io.py
index eb5a49bf2..0b13c8107 100644
--- a/myokit/_io.py
+++ b/myokit/_io.py
@@ -6,19 +6,11 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import array
+import io
 import os
 import sys
 
-# StringIO in Python 2 and 3
-try:
-    from cStringIO import StringIO
-except ImportError:  # pragma: no python 2 cover
-    from io import StringIO
-
 import myokit
 
 
@@ -145,8 +137,6 @@ def load_state_bin(filename):
         code = parts[1]
         if code not in ['d', 'f']:  # pragma: no cover
             raise Exception('Invalid state file format [40].')
-        # Convert code to str for Python 2.7.10 (see #225)
-        code = str(code)
 
         size = int(parts[2])
         if size < 0:    # pragma: no cover
@@ -154,10 +144,7 @@ def load_state_bin(filename):
 
         # Create array, read bytes into array
         ar = array.array(code)
-        try:
-            ar.frombytes(f.read(info))
-        except AttributeError:  # pragma: no python 3 cover
-            ar.fromstring(f.read(info))
+        ar.frombytes(f.read(info))
 
         # Always store as little endian
         if sys.byteorder == 'big':  # pragma: no cover
@@ -184,7 +171,7 @@ def save(filename=None, model=None, protocol=None, script=None):
         filename = os.path.expanduser(filename)
         f = open(filename, 'w')
     else:
-        f = StringIO()
+        f = io.StringIO()
     out = None
     try:
         if model is not None:
@@ -297,8 +284,7 @@ def save_state_bin(filename, state, precision=myokit.DOUBLE_PRECISION):
             'This method requires the `zlib` module to be installed.')
 
     # Data type
-    # Convert code to str for Python 2.7.10 (see #225)
-    code = str('d' if precision == myokit.DOUBLE_PRECISION else 'f')
+    code = 'd' if precision == myokit.DOUBLE_PRECISION else 'f'
 
     # Create array, ensure it's little-endian
     ar = array.array(code, state)
@@ -311,10 +297,7 @@ def save_state_bin(filename, state, precision=myokit.DOUBLE_PRECISION):
     info.compress_type = zipfile.ZIP_DEFLATED
 
     # Write to compressed file
-    try:
-        ar = ar.tobytes()
-    except AttributeError:  # pragma: no python 3 cover
-        ar = ar.tostring()
+    ar = ar.tobytes()
     with zipfile.ZipFile(filename, 'w') as f:
         f.writestr(info, ar)
 
diff --git a/myokit/_model_api.py b/myokit/_model_api.py
index c93616d53..1d343a5c6 100644
--- a/myokit/_model_api.py
+++ b/myokit/_model_api.py
@@ -4,25 +4,14 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
-from collections import OrderedDict
+import io
 import math
 import re
-import myokit
 
-# StringIO in Python 2 and 3
-try:
-    from cStringIO import StringIO
-except ImportError:
-    from io import StringIO
+from collections import OrderedDict
+
+import myokit
 
-# Strings in Python 2 and 3
-try:
-    basestring
-except NameError:   # pragma: no cover
-    basestring = str
 
 TAB = ' ' * 4
 NAME = re.compile(r'^[a-zA-Z]\w*$')
@@ -34,8 +23,6 @@ def check_name(name):
     Tests if the given name is a valid myokit name and raises a
     :class:`myokit.InvalidNameError` if it isn't.
     """
-    # Note: Names are stored as str (so unicode in Python3)
-    # But the regex restriction means their format is compatible with ascii.
     # Check str compatibility
     name = str(name)
 
@@ -65,7 +52,7 @@ def __getitem__(self, key):
             raise myokit.InvalidMetaDataNameError(
                 'The key <' + str(key) + '>'
                 ' is not a valid meta-data property identifier.')
-        return super(MetaDataContainer, self).__getitem__(key)
+        return super().__getitem__(key)
 
     def __setitem__(self, key, item):
         # Check item
@@ -75,10 +62,10 @@ def __setitem__(self, key, item):
             raise myokit.InvalidMetaDataNameError(
                 'The key <' + str(key) + '>'
                 ' is not a valid meta-data property identifier.')
-        super(MetaDataContainer, self).__setitem__(key, item)
+        super().__setitem__(key, item)
 
 
-class ObjectWithMeta(object):
+class ObjectWithMeta:
     """
     Base class for objects with meta data.
 
@@ -87,7 +74,7 @@ class ObjectWithMeta(object):
     """
 
     def __init__(self):
-        super(ObjectWithMeta, self).__init__()
+        super().__init__()
         self.meta = MetaDataContainer()
 
     def _clone_metadata(self, clone):
@@ -135,7 +122,7 @@ def __init__(self, parent, name):
         The given parent should be a ModelPart or None. The name should be
         unique within the set of children for the given parent.
         """
-        super(ModelPart, self).__init__()
+        super().__init__()
         self._parent = parent   # This object's parent
         self._model = None      # The model this object belongs to
         self._name = str(name)  # Local name
@@ -153,7 +140,7 @@ def code(self):
         """
         Returns this object in ``mmt`` syntax.
         """
-        b = StringIO()
+        b = io.StringIO()
         self._code(b, 0)
         return b.getvalue()
 
@@ -162,7 +149,7 @@ def _code(self, b, t):
         Internal version of _code(), to be implemented by all subclasses.
 
         The argument ``t`` specifies the number of tabs to indent the code
-        with. The argument ``b`` is a cStringIO buffer.
+        with. The argument ``b`` is a StringIO buffer.
         """
         raise NotImplementedError
 
@@ -249,7 +236,7 @@ def uname(self):
         return self._uname
 
 
-class VarProvider(object):
+class VarProvider:
     """
     *Abstract class*
 
@@ -463,7 +450,7 @@ class VarOwner(ModelPart, VarProvider):
     """
 
     def __init__(self, parent, name):
-        super(VarOwner, self).__init__(parent, name)
+        super().__init__(parent, name)
         self._variables = {}
         # Set component
         self._component = self
@@ -789,7 +776,7 @@ class Model(ObjectWithMeta, VarProvider):
     """
 
     def __init__(self, name=None):
-        super(Model, self).__init__()
+        super().__init__()
 
         # A dictionary of components
         self._components = {}
@@ -1124,7 +1111,7 @@ def code(self, line_numbers=False):
 
         Line numbers can be added by setting ``line_numbers=True``.
         """
-        b = StringIO()
+        b = io.StringIO()
         b.write('[[model]]\n')
         self._code(b, 0)
         if line_numbers:
@@ -1749,7 +1736,7 @@ def import_component(self, external_component, new_name=None, var_map=None,
             'as external_component, or a string if only one '
             'component is provided'
         )
-        if isinstance(new_name, basestring):
+        if isinstance(new_name, str):
             if len(external_component) != 1:
                 raise TypeError(new_name_error_str)
             new_name = [new_name]
@@ -1757,7 +1744,7 @@ def import_component(self, external_component, new_name=None, var_map=None,
             try:
                 ok = (
                     len(new_name) == len(external_component) and
-                    all(isinstance(name, basestring) for name in new_name)
+                    all(isinstance(name, str) for name in new_name)
                 )
                 if not ok:
                     raise TypeError(new_name_error_str)
@@ -1836,7 +1823,7 @@ def import_component(self, external_component, new_name=None, var_map=None,
                             'The variable <' + self_var.qname() + '> in the'
                             ' given var_map\'s values is not part of this'
                             ' model.')
-                elif isinstance(self_var, basestring):
+                elif isinstance(self_var, str):
                     try:
                         self_var = self.var(self_var)
                     except KeyError:
@@ -1858,7 +1845,7 @@ def import_component(self, external_component, new_name=None, var_map=None,
                             'The variable <' + ext_var.qname() + '> in the'
                             ' given var_map\'s keys but is not part of the'
                             ' source model.')
-                elif isinstance(ext_var, basestring):
+                elif isinstance(ext_var, str):
                     try:
                         ext_var = ext_model.var(ext_var)
                     except KeyError:
@@ -2597,7 +2584,7 @@ def map_to_state(self, state):
 
         """
         n = self.count_states()
-        if isinstance(state, basestring):
+        if isinstance(state, str):
             # String given. Parse into name:float map or list
             state = myokit.parse_state(state)
         if isinstance(state, dict):
@@ -2912,7 +2899,7 @@ def set_initial_values(self, values):
         accepted by :meth:`map_to_state`.
         """
         # Use map to state?
-        if isinstance(values, basestring) or isinstance(values, dict):
+        if isinstance(values, str) or isinstance(values, dict):
             self._state_init = [
                 myokit.Number(x) for x in self.map_to_state(values)]
         elif len(values) != len(self._state_vars):
@@ -3655,7 +3642,7 @@ class Component(VarOwner):
     """
 
     def __init__(self, model, name):
-        super(Component, self).__init__(model, name)
+        super().__init__(model, name)
         self._alias_map = {}    # Maps variable names to other variables names
 
     def _clone1(self, model, new_name=None):
@@ -3741,7 +3728,7 @@ def _delete(self):
             var._delete(recursive=True, ignore_siblings=True)
 
         # Delete links to parent
-        super(Component, self)._delete()
+        super()._delete()
 
     def alias(self, name):
         """
@@ -3873,7 +3860,7 @@ class Variable(VarOwner):
     """
 
     def __init__(self, parent, name):
-        super(Variable, self).__init__(parent, name)
+        super().__init__(parent, name)
 
         # Index, only set if this is a state variable
         self._index = None
@@ -4280,7 +4267,7 @@ def _delete(self, recursive=False, ignore_siblings=False):
                 self._remove_variable_internal(kid)
 
         # Remove parent links
-        super(Variable, self)._delete()
+        super()._delete()
 
     def demote(self):
         """
@@ -4498,7 +4485,7 @@ def promote(self, initial_value=0, state_value=None):
         # Handle string and number rhs's
         model = self.model()
         if not isinstance(initial_value, myokit.Expression):
-            if isinstance(initial_value, basestring):
+            if isinstance(initial_value, str):
                 # Expressions are evaluated in model context
                 initial_value = myokit.parse_expression(
                     initial_value, context=model)
@@ -4588,9 +4575,9 @@ def pyfunc(self, use_numpy=True, arguments=False):
         # Create function
         local = {}
         if use_numpy:
-            myokit._exec(func, {'numpy': numpy}, local)
+            exec(func, {'numpy': numpy}, local)
         else:
-            myokit._exec(func, {'math': math}, local)
+            exec(func, {'math': math}, local)
         handle = local['var_pyfunc_generated']
 
         # Return
@@ -4766,7 +4753,7 @@ def _set_initial_value(self, value, make_the_change):
         # Handle strings and floats
         model = self.model()
         if not isinstance(value, myokit.Expression):
-            if isinstance(value, basestring):
+            if isinstance(value, str):
                 value = myokit.parse_expression(value, context=model)
             else:
                 value = myokit.Number(value)
@@ -4834,7 +4821,7 @@ def set_rhs(self, rhs):
         """
         # Handle string and number rhs's
         if not isinstance(rhs, myokit.Expression):
-            if isinstance(rhs, basestring):
+            if isinstance(rhs, str):
                 rhs = myokit.parse_expression(rhs, context=self)
             elif rhs is not None:
                 rhs = myokit.Number(rhs)
@@ -4885,7 +4872,7 @@ def set_unit(self, unit=None):
         """
         if unit is None or isinstance(unit, myokit.Unit):
             self._unit = unit
-        elif isinstance(unit, basestring):
+        elif isinstance(unit, str):
             self._unit = myokit.parse_unit(unit)
         else:
             raise TypeError('Method set_unit() expects a myokit.Unit or None.')
@@ -5068,7 +5055,7 @@ def value(self):
         return self._rhs.eval()
 
 
-class Equation(object):
+class Equation:
     """
     Defines an equation: a statement that a left-hand side (LHS) is equal to a
     right-hand side (RHS) expression.
@@ -5103,7 +5090,7 @@ def clone(self, subst=None, expand=False, retain=None):
 
     def code(self):
         """ Returns an ``.mmt`` representation of this equation. """
-        b = StringIO()
+        b = io.StringIO()
         self._lhs._code(b, None)
         b.write(' = ')
         self._rhs._code(b, None)
@@ -5149,7 +5136,7 @@ def stream(lst):
         return stream(self)
 
 
-class UserFunction(object):
+class UserFunction:
     """
     Represents a user function.
 
diff --git a/myokit/_myokit_version.py b/myokit/_myokit_version.py
index 38ae83222..9e16ba965 100644
--- a/myokit/_myokit_version.py
+++ b/myokit/_myokit_version.py
@@ -4,7 +4,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-import sys
 
 # True if this is a release, False for a development version
 __release__ = False
@@ -15,7 +14,7 @@
 #    incompatibility
 #  - Changes to revision indicate bugfixes, tiny new features
 #  - There is no significance to odd/even numbers
-__version_tuple__ = 1, 34, 1
+__version_tuple__ = 1, 35, 0
 
 # String version of the version number
 __version__ = '.'.join([str(x) for x in __version_tuple__])
@@ -23,6 +22,3 @@
     __version_tuple__ += ('dev', )
     __version__ += '.dev'
 
-# Don't expose x on Python2
-if sys.hexversion < 0x03000000:  # pragma: no python 3 cover
-    del x
diff --git a/myokit/_parsing.py b/myokit/_parsing.py
index c47ea8246..e68d6be0a 100644
--- a/myokit/_parsing.py
+++ b/myokit/_parsing.py
@@ -4,22 +4,14 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import os
 import re
-#import unicodedata
+
 from collections import OrderedDict
 
 import myokit
-from myokit import ParseError, ProtocolParseError
 
-# Strings in Python2 and Python3
-try:
-    basestring
-except NameError:   # pragma: no cover
-    basestring = str
+from myokit import ParseError, ProtocolParseError
 
 
 def parse(source):
@@ -34,7 +26,7 @@ def parse(source):
     """
     # Get raw stream
     raw = source
-    if isinstance(raw, basestring):
+    if isinstance(raw, str):
         raw = raw.splitlines()
     raw = iter(raw)
 
@@ -78,7 +70,7 @@ def parse_model(source):
     """
     # Get raw stream
     raw = source
-    if isinstance(raw, basestring):
+    if isinstance(raw, str):
         raw = raw.splitlines()
     raw = iter(raw)
 
@@ -100,7 +92,7 @@ def parse_protocol(source):
     """
     # Get raw stream
     raw = source
-    if isinstance(raw, basestring):
+    if isinstance(raw, str):
         raw = raw.splitlines()
     raw = iter(raw)
 
@@ -122,7 +114,7 @@ def parse_script(source):
     """
     # Get raw stream
     raw = source
-    if isinstance(raw, basestring):
+    if isinstance(raw, str):
         raw = raw.splitlines()
     raw = iter(raw)
 
@@ -162,7 +154,7 @@ def parse_state(state):
     """
     # Get raw stream
     raw = state
-    if isinstance(raw, basestring):
+    if isinstance(raw, str):
         raw = raw.splitlines()
     raw = iter(raw)
 
@@ -205,7 +197,7 @@ def split(source):
     """
     # Get raw stream
     raw = source
-    if isinstance(raw, basestring):
+    if isinstance(raw, str):
         raw = raw.splitlines()
     raw = iter(raw)
     segments = ['', '', '']
@@ -317,7 +309,7 @@ def unexpected_token(token, expected):
     # Parse expected token(s) or string
     if type(expected) == int:
         expected = [expected]
-    if not isinstance(expected, basestring):
+    if not isinstance(expected, str):
         if len(expected) > 2:
             expected = 'one of [' \
                 + ', '.join([token_str[i] for i in expected]) + ']'
@@ -348,7 +340,7 @@ def reg_token(info, token, obj):
         d[token[3]] = (token, obj)
 
 
-class ParseInfo(object):
+class ParseInfo:
     def __init__(self):
         self.model = None
         self.initial_values = OrderedDict()
@@ -1071,7 +1063,7 @@ def strip_expression_units(model_text, skip_literals=True):
     This method will raise a :class:`myokit.ParseError` if the given code
     cannot be parsed to a valid model.
     """
-    if isinstance(model_text, basestring):
+    if isinstance(model_text, str):
         lines = model_text.splitlines()
     else:
         lines = model_text
@@ -1267,7 +1259,7 @@ def add_token(name, nice):
 _COMPEQ_MAP = [EQEQUAL, NOTEQUAL, MOREEQUAL, LESSEQUAL]
 
 
-class Tokenizer(object):
+class Tokenizer:
     """
     Takes a stream of lines as input and provides a stream interface returning
     tokens.
@@ -1312,7 +1304,7 @@ def __init__(self, stream_of_lines, check_indenting=True):
         self._catcheri = 0
 
         # String given instead of stream of lines? Convert
-        if isinstance(stream_of_lines, basestring):
+        if isinstance(stream_of_lines, str):
             stream_of_lines = iter(stream_of_lines.splitlines())
 
         # Create tokenizer
@@ -1965,7 +1957,7 @@ def format_parse_error(ex, source=None):
     line = None
 
     if ex.line > 0 and source is not None:
-        if isinstance(source, basestring) and os.path.isfile(source):
+        if isinstance(source, str) and os.path.isfile(source):
             # Re-open file, find line
             with open(source, 'r') as f:
                 for i in range(0, ex.line):
@@ -2015,7 +2007,7 @@ def format_parse_error(ex, source=None):
     return '\n'.join(out)
 
 
-class NudParser(object):
+class NudParser:
     """
     Expression parser for nud operators.
 
@@ -2084,7 +2076,7 @@ class FunctionParser(NudParser):
     Parser for function calls.
     """
     def __init__(self):
-        super(FunctionParser, self).__init__()
+        super().__init__()
         self._rbp = myokit.Function._rbp
 
     def parse(self, stream, info):
@@ -2125,7 +2117,7 @@ def parse(self, stream, info):
         return (func, ops, (name,))
 
 
-class LedParser(object):
+class LedParser:
     """
     Expression parser for led operators.
 
diff --git a/myokit/_progress.py b/myokit/_progress.py
index 251241911..b0714a4b2 100644
--- a/myokit/_progress.py
+++ b/myokit/_progress.py
@@ -4,15 +4,12 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import timeit
 
 import myokit
 
 
-class ProgressReporter(object):
+class ProgressReporter:
     """
     Interface for progress updates in Simulations. Also allows some job types
     to be cancelled by the user.
@@ -62,7 +59,7 @@ def update(self, progress):
         """
         pass
 
-    class _Job(object):
+    class _Job:
         def __init__(self, parent, msg):
             self._parent = parent
             self._msg = msg
@@ -97,7 +94,7 @@ class ProgressPrinter(ProgressReporter):
     status every ten percent.
     """
     def __init__(self, digits=1):
-        super(ProgressPrinter, self).__init__()
+        super().__init__()
         self._b = myokit.tools.Benchmarker()
         self._f = None
         self._d = int(digits)
@@ -138,7 +135,7 @@ class Timeout(ProgressReporter):
     Progress reporter that cancels a simulation after ``timeout`` seconds.
     """
     def __init__(self, timeout):
-        super(Timeout, self).__init__()
+        super().__init__()
         self._timeout = float(timeout)
 
     def enter(self, msg=None):
diff --git a/myokit/_protocol.py b/myokit/_protocol.py
index 48eb03036..814fb9413 100644
--- a/myokit/_protocol.py
+++ b/myokit/_protocol.py
@@ -4,9 +4,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import myokit
 
 import numpy as np
@@ -14,7 +11,7 @@
 from bisect import bisect_right
 
 
-class Protocol(object):
+class Protocol:
     """
     Represents a pacing protocol as a sequence of :class:`events
     <ProtocolEvent>`.
@@ -49,7 +46,7 @@ class Protocol(object):
     ``pickle``.
     """
     def __init__(self):
-        super(Protocol, self).__init__()
+        super().__init__()
         self._head = None
 
     def add(self, e):
@@ -536,7 +533,7 @@ def value_at_times(self, times):
         return [p.advance(t) for t in times]
 
 
-class ProtocolEvent(object):
+class ProtocolEvent:
     """
     Describes an event occurring as part of a protocol.
     """
@@ -719,7 +716,7 @@ def __str__(self):
         return self.in_words()
 
 
-class PacingSystem(object):
+class PacingSystem:
     """
     This class uses a :class:`myokit.Protocol` to update the value of a
     pacing variable over time.
@@ -853,7 +850,7 @@ class NotAnUnbrokenSequenceError(myokit.MyokitError):
     pass
 
 
-class TimeSeriesProtocol(object):
+class TimeSeriesProtocol:
     """
     Represents a pacing protocol as a sequence of time value pairs and an
     interpolation method (currently only linear interpolation is supported).
@@ -870,7 +867,7 @@ class TimeSeriesProtocol(object):
     """
 
     def __init__(self, times, values, method=None):
-        super(TimeSeriesProtocol, self).__init__()
+        super().__init__()
 
         if len(times) != len(values):
             raise ValueError('Times and values array must have same size.')
diff --git a/myokit/_sim/__init__.py b/myokit/_sim/__init__.py
index 1d0d1c715..08c0861e7 100644
--- a/myokit/_sim/__init__.py
+++ b/myokit/_sim/__init__.py
@@ -5,10 +5,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
-# Library imports
 import os
 import platform
 import sys
@@ -36,36 +32,23 @@ def _ospop(command, mode='r', bufsize=0):
             ).stdin
     os.popen = _ospop
 
-
-# Setuptools imports
 from setuptools import setup, Extension  # noqa
 
-
-# Myokit imports
 import myokit       # noqa
 import myokit.pype  # noqa
 
 
-# Dynamic module finding and loading in Python 3.5+ and younger
-if sys.hexversion >= 0x03050000:
-    import importlib.machinery
+def load_module(name, path):
+    """ Dynamically find and load a module (Python 3.5+). """
     import importlib
+    import importlib.machinery
 
-    def load_module(name, path):
-        spec = importlib.machinery.PathFinder.find_spec(name, [path])
-        module = importlib.util.module_from_spec(spec)
-        return module
-
-else:  # pragma: no python 3 cover
-    import imp
-
-    def load_module(name, path):
-        (f, pathname, description) = imp.find_module(name, [path])
-        f.close()
-        return imp.load_dynamic(name, pathname)
+    spec = importlib.machinery.PathFinder.find_spec(name, [path])
+    module = importlib.util.module_from_spec(spec)
+    return module
 
 
-class CModule(object):
+class CModule:
     """
     Abstract base class for classes that dynamically create and compile a
     back-end C-module.
diff --git a/myokit/_sim/cable.py b/myokit/_sim/cable.py
index 21f237cdf..a0b315365 100644
--- a/myokit/_sim/cable.py
+++ b/myokit/_sim/cable.py
@@ -4,13 +4,11 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import os
-import myokit
 import platform
 
+import myokit
+
 # Location of source file
 SOURCE_FILE = 'cable.c'
 
@@ -101,7 +99,7 @@ class Simulation1d(myokit.CModule):
     _index = 0      # Unique id for generated module
 
     def __init__(self, model, protocol=None, ncells=50, rl=False):
-        super(Simulation1d, self).__init__()
+        super().__init__()
 
         # Require a valid model
         model.validate()
diff --git a/myokit/_sim/cmodel.h b/myokit/_sim/cmodel.h
index 2ecf824fa..747ae558e 100644
--- a/myokit/_sim/cmodel.h
+++ b/myokit/_sim/cmodel.h
@@ -918,9 +918,7 @@ for eqs in s_output_equations:
  * Private method: Add a variable to the logging lists. Returns 1 if
  * successful.
  *
- * Note: The variable names are all ascii compatible. In Python2, the strings
- * inside log_dict are either unicode or bytes, but they can be matched
- * without conversion.
+ * Note: The variable names are all ascii compatible.
  *
  * Arguments
  *  log_dict : A dictionary mapping variable names to sequences.
diff --git a/myokit/_sim/cmodel.py b/myokit/_sim/cmodel.py
index aeda7cafb..58acf9953 100644
--- a/myokit/_sim/cmodel.py
+++ b/myokit/_sim/cmodel.py
@@ -5,9 +5,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import os
 import traceback
 
@@ -21,7 +18,7 @@
 SOURCE_FILE = 'cmodel.h'
 
 
-class CModel(object):
+class CModel:
     """
     Generates ansi-C code for a model.
 
diff --git a/myokit/_sim/compiler.py b/myokit/_sim/compiler.py
index 31960ce03..eceab322c 100644
--- a/myokit/_sim/compiler.py
+++ b/myokit/_sim/compiler.py
@@ -4,9 +4,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import os
 
 import myokit
@@ -32,7 +29,7 @@ class Compiler(myokit.CModule):
     _compiler = None
 
     def __init__(self):
-        super(Compiler, self).__init__()
+        super().__init__()
         # Create and cache back-end
         Compiler._index += 1
 
diff --git a/myokit/_sim/cvodessim.py b/myokit/_sim/cvodessim.py
index 189494242..f53983f70 100644
--- a/myokit/_sim/cvodessim.py
+++ b/myokit/_sim/cvodessim.py
@@ -5,9 +5,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import os
 import platform
 import tempfile
@@ -152,7 +149,7 @@ class Simulation(myokit.CModule):
     _index = 0  # Simulation id
 
     def __init__(self, model, protocol=None, sensitivities=None, path=None):
-        super(Simulation, self).__init__()
+        super().__init__()
 
         # Require a valid model
         if not model.is_valid():
diff --git a/myokit/_sim/fiber_tissue.py b/myokit/_sim/fiber_tissue.py
index d5ca9433a..25e0a2255 100644
--- a/myokit/_sim/fiber_tissue.py
+++ b/myokit/_sim/fiber_tissue.py
@@ -4,13 +4,11 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import os
-import myokit
 import platform
 
+import myokit
+
 # Location of C and OpenCL sources
 SOURCE_FILE = 'fiber_tissue.c'
 KERNEL_FILE = 'openclsim.cl'
@@ -122,7 +120,7 @@ def __init__(
             ncells_fiber=(128, 2), ncells_tissue=(128, 128), nx_paced=5,
             g_fiber=(9, 6), g_tissue=(9, 6), g_fiber_tissue=9,
             dt=0.005, precision=myokit.SINGLE_PRECISION, native_maths=False):
-        super(FiberTissueSimulation, self).__init__()
+        super().__init__()
 
         # Deprecated since 2021-05-28
         # Un-deprecated on 2022-04-20, following Adam's request
diff --git a/myokit/_sim/jacobian.py b/myokit/_sim/jacobian.py
index 6ed80884a..4aabfea09 100644
--- a/myokit/_sim/jacobian.py
+++ b/myokit/_sim/jacobian.py
@@ -4,14 +4,13 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import os
-import myokit
-import numpy as np
 import platform
 
+import numpy as np
+
+import myokit
+
 # Location of C source file
 SOURCE_FILE = 'jacobian.cpp'
 
@@ -45,7 +44,7 @@ class JacobianTracer(myokit.CppModule):
     _index = 0  # Unique id
 
     def __init__(self, model):
-        super(JacobianTracer, self).__init__()
+        super().__init__()
 
         # Require a valid model
         model.validate()
@@ -216,7 +215,7 @@ class JacobianCalculator(myokit.CppModule):
     _index = 0  # Unique id
 
     def __init__(self, model):
-        super(JacobianCalculator, self).__init__()
+        super().__init__()
         # Require a valid model
         model.validate()
 
diff --git a/myokit/_sim/opencl.py b/myokit/_sim/opencl.py
index 4aa658edb..cf433c718 100644
--- a/myokit/_sim/opencl.py
+++ b/myokit/_sim/opencl.py
@@ -4,24 +4,14 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
+import configparser
 import os
-import myokit
-
-try:
-    # Python2
-    from ConfigParser import ConfigParser
-except ImportError:
-    # Python 3
-    from configparser import RawConfigParser as ConfigParser
 
+import myokit
 
 # Settings file
 SETTINGS_FILE = os.path.join(myokit.DIR_USER, 'preferred-opencl-device.ini')
 
-
 # Location of C source for OpenCL info module
 SOURCE_FILE = 'opencl.c'
 
@@ -41,7 +31,7 @@ class OpenCL(myokit.CModule):
     _message = None
 
     def __init__(self):
-        super(OpenCL, self).__init__()
+        super().__init__()
         # Create back-end and cache it
         OpenCL._index += 1
         mname = 'myokit_opencl_info_' + str(OpenCL._index)
@@ -165,11 +155,8 @@ def load_selection_bytes():
         # Read ini file
         inifile = os.path.expanduser(SETTINGS_FILE)
         if os.path.isfile(inifile):
-            config = ConfigParser()
-            try:
-                config.read(inifile, encoding='ascii')  # Python 3
-            except TypeError:   # pragma: no python 3 cover
-                config.read(inifile)
+            config = configparser.RawConfigParser()
+            config.read(inifile, encoding='ascii')
 
             def get(section, option):
                 x = None
@@ -205,7 +192,7 @@ def save_selection(platform=None, device=None):
             device = device.encode('ascii').decode('ascii')
 
         # Create configuration
-        config = ConfigParser()
+        config = configparser.RawConfigParser()
         config.add_section('selection')
         if platform:
             config.set('selection', 'platform', platform)
@@ -271,7 +258,7 @@ def test_build(code):
     '''
 
 
-class OpenCLInfo(object):
+class OpenCLInfo:
     """
     Represents information about the available OpenCL platforms and devices.
 
@@ -299,7 +286,7 @@ def format(self):
         return '\n'.join(b)
 
 
-class OpenCLPlatformInfo(object):
+class OpenCLPlatformInfo:
     """
     Represents information about an OpenCL platform.
 
@@ -376,7 +363,7 @@ def has_extension(self, extension):
         return extension in self.extensions
 
 
-class OpenCLDeviceInfo(object):
+class OpenCLDeviceInfo:
     """
     Represents information about an OpenCL device.
 
diff --git a/myokit/_sim/openclsim.py b/myokit/_sim/openclsim.py
index 299317179..6964e7fa6 100644
--- a/myokit/_sim/openclsim.py
+++ b/myokit/_sim/openclsim.py
@@ -5,15 +5,15 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import os
-import myokit
-import numpy as np
 import platform
+
 from collections import OrderedDict
 
+import numpy as np
+
+import myokit
+
 
 # Location of C and OpenCL sources
 SOURCE_FILE = 'openclsim.c'
@@ -148,7 +148,7 @@ class SimulationOpenCL(myokit.CModule):
     def __init__(
             self, model, protocol=None, ncells=256, diffusion=True,
             precision=myokit.SINGLE_PRECISION, native_maths=False, rl=False):
-        super(SimulationOpenCL, self).__init__()
+        super().__init__()
 
         # Require a valid model
         if not model.is_valid():
diff --git a/myokit/_sim/pacing.h b/myokit/_sim/pacing.h
index f7552ef30..f14aa04a0 100644
--- a/myokit/_sim/pacing.h
+++ b/myokit/_sim/pacing.h
@@ -353,9 +353,7 @@ ESys_Populate(ESys sys, PyObject* protocol)
     if (protocol != Py_None) {
 
         // Get PyList from protocol (will need to decref!)
-        // Cast to (char*) happens because CallMethod accepts a mutable char*
-        // This should have been const char* and has been fixed in python 3
-        PyObject* list = PyObject_CallMethod(protocol, (char*)"events", NULL); // Returns a new reference
+        PyObject* list = PyObject_CallMethod(protocol, "events", NULL); // Returns a new reference
         if(list == NULL) return ESys_POPULATE_INVALID_PROTOCOL;
         if(!PyList_Check(list)) {
             Py_DECREF(list);
@@ -766,9 +764,7 @@ FSys_Populate(FSys sys, PyObject* protocol)
     if (protocol == Py_None) return FSys_POPULATE_INVALID_PROTOCOL;
 
     // Get PyList from protocol (will need to decref!)
-    // Cast to (char*) happens because CallMethod accepts a mutable char*
-    // This should have been const char* and has been fixed in python 3
-    times_list = PyObject_CallMethod(protocol, (char*)"times", NULL); // Returns a new reference
+    times_list = PyObject_CallMethod(protocol, "times", NULL); // Returns a new reference
     if(times_list == NULL) return FSys_POPULATE_INVALID_PROTOCOL;
     if(!PyList_Check(times_list)) {
         Py_DECREF(times_list);
diff --git a/myokit/_sim/rhs.py b/myokit/_sim/rhs.py
index e38f4b07e..ff011f732 100644
--- a/myokit/_sim/rhs.py
+++ b/myokit/_sim/rhs.py
@@ -4,20 +4,11 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import os
-import timeit
-import myokit
 import platform
+import timeit
 
-# Strings in Python 2 and 3
-try:
-    basestring
-except NameError:   # pragma: no cover
-    basestring = str
-
+import myokit
 
 # Location of C source file
 SOURCE_FILE = 'rhs.c'
@@ -42,7 +33,7 @@ class RhsBenchmarker(myokit.CModule):
     _index = 0  # Unique id for the generated module
 
     def __init__(self, model, variables=None, exclude_selected=False):
-        super(RhsBenchmarker, self).__init__()
+        super().__init__()
 
         # Require a valid model
         model.validate()
@@ -201,7 +192,7 @@ def _check_variables(self, variables):
         else:
             self._variables = set()
             for var in variables:
-                if isinstance(var, basestring):
+                if isinstance(var, str):
                     # String? Then get variable from model
                     var = self._model.get(var)
                 else:
diff --git a/myokit/_sim/sundials.py b/myokit/_sim/sundials.py
index 28c8bf40f..2babbfee8 100644
--- a/myokit/_sim/sundials.py
+++ b/myokit/_sim/sundials.py
@@ -4,9 +4,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import os
 
 import myokit
@@ -32,7 +29,7 @@ class Sundials(myokit.CModule):
     _version = None
 
     def __init__(self):
-        super(Sundials, self).__init__()
+        super().__init__()
         # Create and cache back-end
         Sundials._index += 1
 
diff --git a/myokit/_system.py b/myokit/_system.py
index bc1a3aa29..d524c995d 100644
--- a/myokit/_system.py
+++ b/myokit/_system.py
@@ -4,13 +4,10 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
+import importlib
 import os
-import sys
 import platform
-import importlib
+import sys
 
 import myokit
 
@@ -28,7 +25,7 @@ def system(live_printing=False):
         out = []
     else:
         # Create fake list interface that just prints
-        class Out(object):
+        class Out:
             def append(self, x):
                 print(x)
 
diff --git a/myokit/_unit.py b/myokit/_unit.py
index 91638bcdf..ded9309ef 100644
--- a/myokit/_unit.py
+++ b/myokit/_unit.py
@@ -5,21 +5,12 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import math
 
 import myokit
 
-# Strings in Python 2 and 3
-try:
-    basestring
-except NameError:   # pragma: no python 2 cover
-    basestring = str
-
 
-class Unit(object):
+class Unit:
     """
     Represents a unit.
 
@@ -522,7 +513,7 @@ def register(name, unit, quantifiable=False, output=False):
             preferred representation format.
 
         """
-        if not isinstance(name, basestring):
+        if not isinstance(name, str):
             raise TypeError('Given name must be a string.')
         if not isinstance(unit, Unit):
             raise TypeError('Given unit must be myokit.Unit')
@@ -679,7 +670,7 @@ def __truediv__(self, other):
             self._m - other._m)
 
 
-class Quantity(object):
+class Quantity:
     """
     Represents a quantity with a :class:`unit <myokit.Unit>`. Can be used to
     perform unit-safe arithmetic.
diff --git a/myokit/float.py b/myokit/float.py
index 0b3708427..a5a04a562 100644
--- a/myokit/float.py
+++ b/myokit/float.py
@@ -4,9 +4,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import sys
 
 import myokit
diff --git a/myokit/formats/__init__.py b/myokit/formats/__init__.py
index abe65fefd..f04dabe09 100644
--- a/myokit/formats/__init__.py
+++ b/myokit/formats/__init__.py
@@ -4,15 +4,13 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
-import myokit
 import os
 import sys
 import traceback
 import warnings
 
+import myokit
+
 # Constants
 DIR_FORMATS = os.path.join(myokit.DIR_MYOKIT, 'formats')
 
@@ -23,12 +21,12 @@
 
 
 # Classes & methods
-class Exporter(object):
+class Exporter:
     """
     Abstract base class for exporters.
     """
     def __init__(self):
-        super(Exporter, self).__init__()
+        super().__init__()
 
     def post_export_info(self):
         """
@@ -106,7 +104,7 @@ def exporters():
     return sorted(_EXPORTERS.keys())
 
 
-class ExpressionWriter(object):
+class ExpressionWriter:
     """
     Base class for expression writers, that take myokit expressions as input
     and convert them to text or other formats.
@@ -322,12 +320,12 @@ def ewriters():
     return sorted(_EWRITERS.keys())
 
 
-class Importer(object):
+class Importer:
     """
     Abstract base class for importers.
     """
     def __init__(self):
-        super(Importer, self).__init__()
+        super().__init__()
 
     def component(self, path, model):
         """
@@ -411,7 +409,7 @@ class TemplatedRunnableExporter(Exporter):
     protocol) into a runnable chunk of code.
     """
     def __init__(self):
-        super(TemplatedRunnableExporter, self).__init__()
+        super().__init__()
 
     def runnable(self, path, model, protocol=None, *args):
         """
diff --git a/myokit/formats/ansic/__init__.py b/myokit/formats/ansic/__init__.py
index d21966c72..aac6252eb 100644
--- a/myokit/formats/ansic/__init__.py
+++ b/myokit/formats/ansic/__init__.py
@@ -4,9 +4,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 from ._exporter import AnsiCExporter, AnsiCCableExporter, AnsiCEulerExporter
 from ._ewriter import AnsiCExpressionWriter
 
diff --git a/myokit/formats/ansic/_ewriter.py b/myokit/formats/ansic/_ewriter.py
index 64fee58e8..8e6669edc 100644
--- a/myokit/formats/ansic/_ewriter.py
+++ b/myokit/formats/ansic/_ewriter.py
@@ -4,10 +4,8 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import myokit
+
 from myokit.formats.python import PythonExpressionWriter
 
 
@@ -17,7 +15,7 @@ class AnsiCExpressionWriter(PythonExpressionWriter):
     equations for variables in a C-style syntax.
     """
     def __init__(self):
-        super(AnsiCExpressionWriter, self).__init__()
+        super().__init__()
         self._function_prefix = ''
         self._fcond = None
 
diff --git a/myokit/formats/ansic/_exporter.py b/myokit/formats/ansic/_exporter.py
index 99d51e304..9acc1d8c8 100644
--- a/myokit/formats/ansic/_exporter.py
+++ b/myokit/formats/ansic/_exporter.py
@@ -4,9 +4,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import os
 
 import myokit.formats
diff --git a/myokit/formats/axon/__init__.py b/myokit/formats/axon/__init__.py
index 9c1a98add..7a67b93bc 100644
--- a/myokit/formats/axon/__init__.py
+++ b/myokit/formats/axon/__init__.py
@@ -4,13 +4,11 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 from ._abf import AbfFile, Sweep, Channel  # noqa
 from ._atf import AtfFile, load_atf, save_atf  # noqa
 from ._importer import AbfImporter
 
+
 # Importers
 _importers = {
     'abf': AbfImporter,
diff --git a/myokit/formats/axon/_abf.py b/myokit/formats/axon/_abf.py
index 3148b9e4f..683a07eb5 100755
--- a/myokit/formats/axon/_abf.py
+++ b/myokit/formats/axon/_abf.py
@@ -139,23 +139,22 @@
 # Information - but no direct code - from the matlab script get_abf_header.m
 # was also used: http://neurodata.hg.sourceforge.net/hgweb/neurodata/neurodata/
 #------------------------------------------------------------------------------
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
-from collections import OrderedDict
-import numpy as np
-import traceback
 import datetime
 import logging
-import struct
 import os
+import struct
+import traceback
+
+import numpy as np
+
+from collections import OrderedDict
 
 
 # Encoding for text parts of files
 _ENC = 'latin-1'
 
 
-class AbfFile(object):
+class AbfFile:
     """
     Represents a read-only Axon Binary Format file (``.abf``), stored at the
     location pointed to by ``filepath``.
@@ -199,7 +198,6 @@ class AbfFile(object):
     """
     def __init__(self, filepath, is_protocol_file=None):
         # The path to the file and its basename
-        # Ensure it's a `str` for Python2/3 compatibility
         filepath = str(filepath)
         self._filepath = os.path.abspath(filepath)
         self._filename = os.path.basename(filepath)
@@ -702,7 +700,6 @@ def read_f(f, form, offset=None):
             """
             Read and unpack a file section using the given format ``form``.
             """
-            # Form must be a str (so bytes on Python 2)
             form = str(form)
             if offset is not None:
                 f.seek(offset)
@@ -1306,14 +1303,14 @@ def _set_conversion_factors(self):
                 self._adc_offsets.append(s)
 
 
-class Sweep(object):
+class Sweep:
     """
     Represents a single sweep (also called an 'episode')
 
     A sweep is represented as a fixed-size list of channels.
     """
     def __init__(self, n):
-        super(Sweep, self).__init__()
+        super().__init__()
         n = int(n)
         if n < 0:   # pragma: no cover
             raise ValueError('Number channels cannot be negative.')
@@ -1336,14 +1333,14 @@ def __setitem__(self, key, value):
         self._channels[key] = value
 
 
-class Channel(object):
+class Channel:
     """
     Represents an analog signal for a single channel.
 
     To obtain this channel's formatted data, use times() and trace()
     """
     def __init__(self, parent_file):
-        super(Channel, self).__init__()
+        super().__init__()
         self._parent_file = parent_file  # The abf file this channel is from
         self._type = TYPE_UNKNOWN   # Type of recording
 
@@ -1386,7 +1383,7 @@ def name(self):
     def number(self):
         """
         Returns the channel index used by pClamp. Note that this does not
-        necessarily equal its index in the python sweep data!
+        necessarily equal its index in the Python sweep data!
         """
         return self._numb
 
diff --git a/myokit/formats/axon/_atf.py b/myokit/formats/axon/_atf.py
index d786afb39..e5fdc96f0 100644
--- a/myokit/formats/axon/_atf.py
+++ b/myokit/formats/axon/_atf.py
@@ -4,20 +4,19 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
-from collections import OrderedDict
-import numpy as np
 import os
 import re
 
+import numpy as np
+
+from collections import OrderedDict
+
 
 # Format used for any text
 _ENC = 'ascii'
 
 
-class AtfFile(object):
+class AtfFile:
     """
     Represents an Axon Text File (ATF) stored on disk.
 
diff --git a/myokit/formats/axon/_importer.py b/myokit/formats/axon/_importer.py
index d6a32016f..af74fb206 100644
--- a/myokit/formats/axon/_importer.py
+++ b/myokit/formats/axon/_importer.py
@@ -4,9 +4,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import myokit.formats
 
 
diff --git a/myokit/formats/cellml/__init__.py b/myokit/formats/cellml/__init__.py
index c549e0c07..9bf9a71b7 100644
--- a/myokit/formats/cellml/__init__.py
+++ b/myokit/formats/cellml/__init__.py
@@ -4,9 +4,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 from ._exporter import CellMLExporter, CellML1Exporter, CellML2Exporter
 from ._ewriter import CellMLExpressionWriter
 from ._importer import CellMLImporter, CellMLImporterError  # noqa
diff --git a/myokit/formats/cellml/_ewriter.py b/myokit/formats/cellml/_ewriter.py
index 92affff11..4ef950f23 100644
--- a/myokit/formats/cellml/_ewriter.py
+++ b/myokit/formats/cellml/_ewriter.py
@@ -4,9 +4,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 from lxml import etree
 
 import myokit
@@ -24,8 +21,8 @@ class CellMLExpressionWriter(MathMLExpressionWriter):
     unit names with :meth:`set_unit_function()`.
     """
     def __init__(self, version='1.0'):
-        super(CellMLExpressionWriter, self).__init__()
-        super(CellMLExpressionWriter, self).set_mode(presentation=False)
+        super().__init__()
+        super().set_mode(presentation=False)
 
         # Units lookup method
         self._funits = None
@@ -54,7 +51,7 @@ def set_unit_function(self, f):
         self._funits = f
 
     def _ex_number(self, e, t):
-        x = super(CellMLExpressionWriter, self)._ex_number(e, t)
+        x = super()._ex_number(e, t)
         unit = e.unit()
         if self._funits is not None and unit is not None:
             units = self._funits(e.unit())
diff --git a/myokit/formats/cellml/_exporter.py b/myokit/formats/cellml/_exporter.py
index 185d56eea..1af073536 100644
--- a/myokit/formats/cellml/_exporter.py
+++ b/myokit/formats/cellml/_exporter.py
@@ -4,9 +4,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import myokit
 import myokit.lib.guess
 
@@ -18,7 +15,7 @@ class CellMLExporter(myokit.formats.Exporter):
     This:class:`Exporter <myokit.formats.Exporter>` creates a CellML model.
     """
     def __init__(self):
-        super(CellMLExporter, self).__init__()
+        super().__init__()
 
     def model(self, path, model, protocol=None, version='1.0'):
         """
@@ -69,7 +66,7 @@ class CellML1Exporter(CellMLExporter):
     This:class:`Exporter <myokit.formats.Exporter>` creates a CellML 1.0 model.
     """
     def model(self, path, model, protocol=None):
-        super(CellML1Exporter, self).model(path, model, protocol, '1.0')
+        super().model(path, model, protocol, '1.0')
 
 
 class CellML2Exporter(CellMLExporter):
@@ -77,5 +74,5 @@ class CellML2Exporter(CellMLExporter):
     This:class:`Exporter <myokit.formats.Exporter>` creates a CellML 2.0 model.
     """
     def model(self, path, model, protocol=None):
-        super(CellML2Exporter, self).model(path, model, protocol, '2.0')
+        super().model(path, model, protocol, '2.0')
 
diff --git a/myokit/formats/cellml/_importer.py b/myokit/formats/cellml/_importer.py
index ad9807a2d..d52f34ed3 100644
--- a/myokit/formats/cellml/_importer.py
+++ b/myokit/formats/cellml/_importer.py
@@ -5,9 +5,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 from lxml import etree
 
 import myokit
@@ -28,7 +25,7 @@ class CellMLImporter(myokit.formats.Importer):
     from CellML.
     """
     def __init__(self, verbose=False):
-        super(CellMLImporter, self).__init__()
+        super().__init__()
 
     def model(self, path):
         """
diff --git a/myokit/formats/cellml/v1/__init__.py b/myokit/formats/cellml/v1/__init__.py
index 2521cacd7..b24c1f11c 100644
--- a/myokit/formats/cellml/v1/__init__.py
+++ b/myokit/formats/cellml/v1/__init__.py
@@ -4,9 +4,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 from ._api import (     # noqa
     AnnotatableElement,
     CellMLError,
@@ -22,7 +19,6 @@
     is_valid_identifier,
 )
 
-
 from ._parser import (  # noqa
     parse_file,
     parse_string,
diff --git a/myokit/formats/cellml/v1/_api.py b/myokit/formats/cellml/v1/_api.py
index 939b3a61a..2adc096f6 100644
--- a/myokit/formats/cellml/v1/_api.py
+++ b/myokit/formats/cellml/v1/_api.py
@@ -4,9 +4,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import collections
 import re
 import warnings
@@ -116,7 +113,7 @@ def create_unit_name(unit):
     return name
 
 
-class AnnotatableElement(object):
+class AnnotatableElement:
     """
     Represents a CellML 1.0 or 1.1 element that can have a cmeta:id.
 
@@ -187,7 +184,7 @@ class UnsupportedBaseUnitsError(UnitsError):
     """
     def __init__(self, units):
         self.units = units
-        super(UnsupportedBaseUnitsError, self).__init__(
+        super().__init__(
             'Unsupported base units "' + units + '".')
 
 
@@ -196,7 +193,7 @@ class UnsupportedUnitOffsetError(UnitsError):
     Raised when units with non-zero offsets are used.
     """
     def __init__(self):
-        super(UnsupportedUnitOffsetError, self).__init__(
+        super().__init__(
             'Units with non-zero offsets are not supported.')
 
 
@@ -206,7 +203,7 @@ class Component(AnnotatableElement):
     :meth:`Model.add_component()`.
     """
     def __init__(self, model, name):
-        super(Component, self).__init__(model)
+        super().__init__(model)
 
         # Store model
         self._model = model
@@ -474,7 +471,7 @@ class Model(AnnotatableElement):
 
     """
     def __init__(self, name, version='1.0'):
-        super(Model, self).__init__(self)
+        super().__init__(self)
 
         # Check and store name
         if not is_valid_identifier(name):
@@ -1165,7 +1162,7 @@ def version(self):
         return self._version
 
 
-class Units(object):
+class Units:
     """
     Represents a CellML units definition, should not be created directly but
     only via :meth:`Model.add_units()` or :meth:`Component.add_units()`.
@@ -1470,7 +1467,7 @@ class Variable(AnnotatableElement):
     """
     def __init__(self, component, name, units, public_interface='none',
                  private_interface='none'):
-        super(Variable, self).__init__(component.model())
+        super().__init__(component.model())
 
         # Store component
         self._component = component
diff --git a/myokit/formats/cellml/v1/_parser.py b/myokit/formats/cellml/v1/_parser.py
index a52471862..53a52edb8 100644
--- a/myokit/formats/cellml/v1/_parser.py
+++ b/myokit/formats/cellml/v1/_parser.py
@@ -4,9 +4,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import collections
 import warnings
 
@@ -60,10 +57,10 @@ def __init__(self, message, element=None):
                 message = 'Error on line ' + line + '. ' + message
             except AttributeError:
                 pass
-        super(CellMLParsingError, self).__init__(message)
+        super().__init__(message)
 
 
-class CellMLParser(object):
+class CellMLParser:
     """
     Parses CellML 1.0 and 1.1 documents, and performs (partial) validation of a
     CellML document.
diff --git a/myokit/formats/cellml/v1/_writer.py b/myokit/formats/cellml/v1/_writer.py
index d1717cde2..f41a79e8d 100644
--- a/myokit/formats/cellml/v1/_writer.py
+++ b/myokit/formats/cellml/v1/_writer.py
@@ -4,21 +4,12 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 from lxml import etree
+from urllib.parse import quote
 
 import myokit
 import myokit.formats.cellml as cellml
 
-# Quoting URI strings in Python2 and Python3
-try:
-    from urllib.parse import quote
-except ImportError:     # pragma: no python 3 cover
-    # Python 2
-    from urllib import quote
-
 
 # Left-over from old exporter. Not sure if this had any function.
 # Add name in 'tmp-documentation' format
@@ -45,7 +36,7 @@ def write_string(model):
     return CellMLWriter().write_string(model)
 
 
-class CellMLWriter(object):
+class CellMLWriter:
     """
     Writes CellML 1.0 or 1.1 documents.
     """
diff --git a/myokit/formats/cellml/v2/__init__.py b/myokit/formats/cellml/v2/__init__.py
index ece9250ae..f6e3d109c 100644
--- a/myokit/formats/cellml/v2/__init__.py
+++ b/myokit/formats/cellml/v2/__init__.py
@@ -4,9 +4,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 from ._api import (     # noqa
     AnnotatableElement,
     CellMLError,
diff --git a/myokit/formats/cellml/v2/_api.py b/myokit/formats/cellml/v2/_api.py
index e6943b355..44ac88cb0 100644
--- a/myokit/formats/cellml/v2/_api.py
+++ b/myokit/formats/cellml/v2/_api.py
@@ -4,21 +4,12 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import collections
 import re
 import warnings
 
 import myokit
 
-# Strings in Python2 and Python3
-try:
-    basestring
-except NameError:   # pragma: no cover
-    basestring = str
-
 
 # Data types
 _cellml_identifier = re.compile(r'^[a-zA-Z][a-zA-Z0-9_]*$')
@@ -142,7 +133,7 @@ def create_unit_name(unit):
     return name
 
 
-class AnnotatableElement(object):
+class AnnotatableElement:
     """
     Represents a CellML 2.0 element that can be annotated (using a public dict
     ``meta`` that stores key:value pairs).
@@ -167,7 +158,7 @@ class Component(AnnotatableElement):
     :meth:`Model.add_component()`.
     """
     def __init__(self, model, name):
-        super(Component, self).__init__()
+        super().__init__()
 
         # Store model
         self._model = model
@@ -323,7 +314,7 @@ class Model(AnnotatableElement):
 
     """
     def __init__(self, name, version='2.0'):
-        super(Model, self).__init__()
+        super().__init__()
 
         # Check and store name
         if not is_identifier(name):
@@ -1068,7 +1059,7 @@ def version(self):
         return self._version
 
 
-class Units(object):
+class Units:
     """
     Represents a CellML units definition; should not be created directly but
     only via :meth:`Model.add_units()` or :meth:`Component.add_units()`.
@@ -1359,7 +1350,7 @@ class Variable(AnnotatableElement):
 
     """
     def __init__(self, component, name, units, interface='none'):
-        super(Variable, self).__init__()
+        super().__init__()
 
         # Store model and component
         self._model = component.model()
@@ -1615,7 +1606,7 @@ def set_initial_value(self, value):
         if value is not None:
 
             # Check value is a real number string
-            if isinstance(value, basestring):
+            if isinstance(value, str):
                 if is_real_number_string(value):
                     value = float(value)
                 else:
@@ -1642,7 +1633,7 @@ def units(self):
         return self._units
 
 
-class ConnectedVariableSet(object):
+class ConnectedVariableSet:
     """
     Represents a set of connected variables.
 
diff --git a/myokit/formats/cellml/v2/_parser.py b/myokit/formats/cellml/v2/_parser.py
index 813a76604..491525214 100644
--- a/myokit/formats/cellml/v2/_parser.py
+++ b/myokit/formats/cellml/v2/_parser.py
@@ -4,9 +4,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import warnings
 
 from lxml import etree
@@ -59,10 +56,10 @@ def __init__(self, message, element=None):
                 message = 'Error on line ' + line + '. ' + message
             except AttributeError:
                 pass
-        super(CellMLParsingError, self).__init__(message)
+        super().__init__(message)
 
 
-class CellMLParser(object):
+class CellMLParser:
     """
     Parses CellML 2.0 documents, and performs (partial) validation.
 
diff --git a/myokit/formats/cellml/v2/_writer.py b/myokit/formats/cellml/v2/_writer.py
index f95702197..5b111471e 100644
--- a/myokit/formats/cellml/v2/_writer.py
+++ b/myokit/formats/cellml/v2/_writer.py
@@ -4,9 +4,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 from lxml import etree
 
 import myokit
@@ -27,7 +24,7 @@ def write_string(model):
     return CellMLWriter().write_string(model)
 
 
-class CellMLWriter(object):
+class CellMLWriter:
     """
     Writes CellML 2.0 documents.
     """
diff --git a/myokit/formats/channelml/__init__.py b/myokit/formats/channelml/__init__.py
index a960f8846..19d105e57 100644
--- a/myokit/formats/channelml/__init__.py
+++ b/myokit/formats/channelml/__init__.py
@@ -4,9 +4,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 from ._importer import ChannelMLImporter, ChannelMLError  # noqa
 
 
diff --git a/myokit/formats/channelml/_importer.py b/myokit/formats/channelml/_importer.py
index a2b21b831..b2eb4fecd 100644
--- a/myokit/formats/channelml/_importer.py
+++ b/myokit/formats/channelml/_importer.py
@@ -4,23 +4,13 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
+import io
 import os
 import textwrap
+import warnings
 import xml.dom
 import xml.dom.minidom
 
-import warnings
-
-try:
-    # Python 2
-    from cStringIO import StringIO
-except ImportError:
-    # Python3
-    from io import StringIO
-
 import myokit
 from myokit import formats
 from myokit import Name, Number, Minus, Multiply, Divide, Power
@@ -71,7 +61,7 @@ class ChannelMLImporter(formats.Importer):
     from the ChannelML format.
     """
     def __init__(self, verbose=False):
-        super(ChannelMLImporter, self).__init__()
+        super().__init__()
         self.generated_name_index = 0
 
     def component(self, path, model):
@@ -373,7 +363,7 @@ def _rip_meta(self, root):
         if not root.hasAttribute('xmlns:meta'):
             return None
         ns = root.getAttribute('xmlns:meta')
-        b = StringIO()
+        b = io.StringIO()
 
         def scan(parent):
             kid = dom_child(parent)
diff --git a/myokit/formats/cpp/__init__.py b/myokit/formats/cpp/__init__.py
index 07295c085..56fe0d2a7 100644
--- a/myokit/formats/cpp/__init__.py
+++ b/myokit/formats/cpp/__init__.py
@@ -4,10 +4,8 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import myokit.formats.ansic
+
 from ._ewriter import CppExpressionWriter
 
 
diff --git a/myokit/formats/cpp/_ewriter.py b/myokit/formats/cpp/_ewriter.py
index f344d557d..c6a8d100b 100644
--- a/myokit/formats/cpp/_ewriter.py
+++ b/myokit/formats/cpp/_ewriter.py
@@ -4,9 +4,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 from myokit.formats.ansic import AnsiCExpressionWriter
 
 
diff --git a/myokit/formats/cuda/__init__.py b/myokit/formats/cuda/__init__.py
index 71a8facd7..9ed969718 100644
--- a/myokit/formats/cuda/__init__.py
+++ b/myokit/formats/cuda/__init__.py
@@ -4,9 +4,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 from ._exporter import CudaKernelExporter, CudaKernelRLExporter
 from ._ewriter import CudaExpressionWriter
 from myokit.formats import ansic
diff --git a/myokit/formats/cuda/_ewriter.py b/myokit/formats/cuda/_ewriter.py
index 943c1dce6..b20c9eacb 100644
--- a/myokit/formats/cuda/_ewriter.py
+++ b/myokit/formats/cuda/_ewriter.py
@@ -4,10 +4,8 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import myokit
+
 from myokit.formats.python import PythonExpressionWriter
 
 
@@ -17,7 +15,7 @@ class CudaExpressionWriter(PythonExpressionWriter):
     Myokit :class:`expressions <myokit.Expression>` to their CUDA equivalents.
     """
     def __init__(self, precision=myokit.SINGLE_PRECISION):
-        super(CudaExpressionWriter, self).__init__()
+        super().__init__()
         self._function_prefix = ''
         self._sp = (precision == myokit.SINGLE_PRECISION)
 
diff --git a/myokit/formats/cuda/_exporter.py b/myokit/formats/cuda/_exporter.py
index 2b354cebd..c9941e5b7 100644
--- a/myokit/formats/cuda/_exporter.py
+++ b/myokit/formats/cuda/_exporter.py
@@ -4,9 +4,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import os
 
 import myokit.formats
diff --git a/myokit/formats/easyml/__init__.py b/myokit/formats/easyml/__init__.py
index 3d994ea0b..186c974da 100644
--- a/myokit/formats/easyml/__init__.py
+++ b/myokit/formats/easyml/__init__.py
@@ -4,9 +4,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 from ._exporter import EasyMLExporter
 from ._ewriter import EasyMLExpressionWriter
 
diff --git a/myokit/formats/easyml/_ewriter.py b/myokit/formats/easyml/_ewriter.py
index 9689936d3..dbb3616f6 100644
--- a/myokit/formats/easyml/_ewriter.py
+++ b/myokit/formats/easyml/_ewriter.py
@@ -4,12 +4,10 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import warnings
 
 import myokit
+
 from myokit.formats.python import PythonExpressionWriter
 
 
@@ -19,7 +17,7 @@ class EasyMLExpressionWriter(PythonExpressionWriter):
     equations for variables in EasyML syntax.
     """
     def __init__(self):
-        super(EasyMLExpressionWriter, self).__init__()
+        super().__init__()
         self._function_prefix = ''
 
     #def _ex_name(self, e):
@@ -39,7 +37,7 @@ def _ex_minus(self, e):
         if isinstance(e[1], myokit.Exp) and isinstance(e[0], myokit.Number):
             if e[0].eval() == 1:
                 return '-expm1(' + self.ex(e[1][0]) + ')'
-        return super(EasyMLExpressionWriter, self)._ex_minus(e)
+        return super()._ex_minus(e)
 
     #def _ex_multiply(self, e):
     #def _ex_divide(self, e):
@@ -58,23 +56,23 @@ def _ex_power(self, e):
 
     def _ex_sin(self, e):
         warnings.warn('Potentially unsupported function: sin()')
-        return super(EasyMLExpressionWriter, self)._ex_sin(e)
+        return super()._ex_sin(e)
 
     #def _ex_cos(self, e):
 
     def _ex_tan(self, e):
         warnings.warn('Potentially unsupported function: tan()')
-        return super(EasyMLExpressionWriter, self)._ex_tan(e)
+        return super()._ex_tan(e)
 
     def _ex_asin(self, e):
         warnings.warn('Potentially unsupported function: asin()')
-        return super(EasyMLExpressionWriter, self)._ex_asin(e)
+        return super()._ex_asin(e)
 
     #def _ex_acos(self, e):
 
     def _ex_atan(self, e):
         warnings.warn('Potentially unsupported function: atan()')
-        return super(EasyMLExpressionWriter, self)._ex_atan(e)
+        return super()._ex_atan(e)
 
     #def _ex_exp(self, e):
 
@@ -87,11 +85,11 @@ def _ex_log(self, e):
 
     def _ex_floor(self, e):
         warnings.warn('Potentially unsupported function: floor()')
-        return super(EasyMLExpressionWriter, self)._ex_floor(e)
+        return super()._ex_floor(e)
 
     def _ex_ceil(self, e):
         warnings.warn('Potentially unsupported function: ceil()')
-        return super(EasyMLExpressionWriter, self)._ex_ceil(e)
+        return super()._ex_ceil(e)
 
     def _ex_abs(self, e):
         return self._ex_function(e, 'fabs')
diff --git a/myokit/formats/easyml/_exporter.py b/myokit/formats/easyml/_exporter.py
index cb9b8f1d0..04398dfa5 100644
--- a/myokit/formats/easyml/_exporter.py
+++ b/myokit/formats/easyml/_exporter.py
@@ -4,9 +4,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import collections
 import os
 import warnings
diff --git a/myokit/formats/html/__init__.py b/myokit/formats/html/__init__.py
index 35cb318db..e2a5314a7 100644
--- a/myokit/formats/html/__init__.py
+++ b/myokit/formats/html/__init__.py
@@ -4,9 +4,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 from ._exporter import HTMLExporter
 from ._flatten import html2ascii    # noqa
 
diff --git a/myokit/formats/html/_exporter.py b/myokit/formats/html/_exporter.py
index d70461ffb..125ca5d23 100644
--- a/myokit/formats/html/_exporter.py
+++ b/myokit/formats/html/_exporter.py
@@ -4,9 +4,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 from lxml import etree
 
 import myokit
diff --git a/myokit/formats/html/_flatten.py b/myokit/formats/html/_flatten.py
index ca1a7da7b..4180890ac 100644
--- a/myokit/formats/html/_flatten.py
+++ b/myokit/formats/html/_flatten.py
@@ -4,19 +4,11 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
+import html
+import html.parser
 import re
-import sys
 import textwrap
 
-# HTML Parser in Python 2 and 3
-try:
-    from HTMLParser import HTMLParser
-except ImportError:
-    from html.parser import HTMLParser
-
 
 def html2ascii(html, width=79, indent='  '):
     """
@@ -32,21 +24,13 @@ def html2ascii(html, width=79, indent='  '):
     return f.get_text()
 
 
-class Asciifier(HTMLParser):
+class Asciifier(html.parser.HTMLParser):
     INDENT = 1
     DEDENT = -1
     WHITE = [' ', '\t', '\f', '\r', '\n']
 
     def __init__(self, line_width=79, indent='  '):
-        if sys.hexversion < 0x03000000:     # pragma: no python 3 cover
-            # HTMLParser requires old-school constructor
-            HTMLParser.__init__(self)
-        else:                               # pragma: no python 2 cover
-            super(Asciifier, self).__init__(convert_charrefs=False)
-
-            # Unescape method is deprecated
-            import html
-            self.unescape = html.unescape
+        super().__init__(convert_charrefs=False)
 
         # In <head> yes/no
         self.inhead = False
@@ -226,7 +210,7 @@ def handle_entityref(self, name):
             return
 
         # Convert html characters
-        data = self.unescape('&' + name + ';')
+        data = html.unescape('&' + name + ';')
 
         # Convert to ascii and back to strip out non-ascii content
         data = data.encode('ascii', 'ignore').decode('ascii')
diff --git a/myokit/formats/latex/__init__.py b/myokit/formats/latex/__init__.py
index 914f43575..74907b384 100644
--- a/myokit/formats/latex/__init__.py
+++ b/myokit/formats/latex/__init__.py
@@ -4,9 +4,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 from ._exporter import PdfExporter, PosterExporter
 from ._ewriter import LatexExpressionWriter
 
diff --git a/myokit/formats/latex/_ewriter.py b/myokit/formats/latex/_ewriter.py
index 489c92377..cab534ac7 100644
--- a/myokit/formats/latex/_ewriter.py
+++ b/myokit/formats/latex/_ewriter.py
@@ -4,9 +4,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import myokit.formats
 
 
@@ -16,7 +13,7 @@ class LatexExpressionWriter(myokit.formats.ExpressionWriter):
     Myokit :class:`expressions <myokit.Expression>` to their Tex equivalent.
     """
     def __init__(self):
-        super(LatexExpressionWriter, self).__init__()
+        super().__init__()
 
         # Default time variable
         self._time = None
diff --git a/myokit/formats/latex/_exporter.py b/myokit/formats/latex/_exporter.py
index 5d493f009..7ea79530d 100644
--- a/myokit/formats/latex/_exporter.py
+++ b/myokit/formats/latex/_exporter.py
@@ -4,12 +4,10 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import os
 
 import myokit
+
 from ._ewriter import LatexExpressionWriter
 
 
@@ -19,7 +17,7 @@ class PdfExporter(myokit.formats.Exporter):
     a simple latex document.
     """
     def __init__(self):
-        super(PdfExporter, self).__init__()
+        super().__init__()
 
     def _clean(self, text):
         """
@@ -111,7 +109,7 @@ class PosterExporter(myokit.formats.Exporter):
     beholders.
     """
     def __init__(self):
-        super(PosterExporter, self).__init__()
+        super().__init__()
 
     def post_export_info(self):
         return '\n'.join((
diff --git a/myokit/formats/mathml/__init__.py b/myokit/formats/mathml/__init__.py
index 34f188845..4a8db8c44 100644
--- a/myokit/formats/mathml/__init__.py
+++ b/myokit/formats/mathml/__init__.py
@@ -4,9 +4,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 from ._ewriter import MathMLExpressionWriter
 from ._parser import (   # noqa
     MathMLError,
diff --git a/myokit/formats/mathml/_ewriter.py b/myokit/formats/mathml/_ewriter.py
index b7fb2bfea..4a6fad78c 100644
--- a/myokit/formats/mathml/_ewriter.py
+++ b/myokit/formats/mathml/_ewriter.py
@@ -4,19 +4,10 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 from lxml import etree
 
 import myokit.formats
 
-# Strings in Python 2 and 3
-try:
-    basestring
-except NameError:   # pragma: no cover
-    basestring = str
-
 
 class MathMLExpressionWriter(myokit.formats.ExpressionWriter):
     """
@@ -25,7 +16,7 @@ class MathMLExpressionWriter(myokit.formats.ExpressionWriter):
     Presentation MathML.
     """
     def __init__(self):
-        super(MathMLExpressionWriter, self).__init__()
+        super().__init__()
 
         # Default mode
         self._pres = False
@@ -33,7 +24,7 @@ def __init__(self):
         # Default lhs conversion function
         def flhs(lhs):
             var = lhs.var()
-            if isinstance(var, basestring):
+            if isinstance(var, str):
                 # This can happen with time variable of derivative if the
                 # proper variable isn't set!
                 return var
diff --git a/myokit/formats/mathml/_parser.py b/myokit/formats/mathml/_parser.py
index 9d2728aae..69a6388c2 100644
--- a/myokit/formats/mathml/_parser.py
+++ b/myokit/formats/mathml/_parser.py
@@ -4,10 +4,8 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import myokit
+
 from myokit.formats.xml import split
 
 
@@ -63,10 +61,10 @@ def __init__(self, message, element=None):
                 message = 'Error on line ' + line + '. ' + message
             except AttributeError:
                 pass
-        super(MathMLError, self).__init__(message)
+        super().__init__(message)
 
 
-class MathMLParser(object):
+class MathMLParser:
     """
     Parses MathML expressions into :class:`myokit.Expression` objects.
 
diff --git a/myokit/formats/matlab/__init__.py b/myokit/formats/matlab/__init__.py
index 379ee5a29..142f70cae 100644
--- a/myokit/formats/matlab/__init__.py
+++ b/myokit/formats/matlab/__init__.py
@@ -4,9 +4,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 from ._exporter import MatlabExporter
 from ._ewriter import MatlabExpressionWriter
 
diff --git a/myokit/formats/matlab/_ewriter.py b/myokit/formats/matlab/_ewriter.py
index 1acc67989..8214252d0 100644
--- a/myokit/formats/matlab/_ewriter.py
+++ b/myokit/formats/matlab/_ewriter.py
@@ -4,9 +4,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 from myokit.formats.python import PythonExpressionWriter
 
 
@@ -16,7 +13,7 @@ class MatlabExpressionWriter(PythonExpressionWriter):
     Myokit :class:`expressions <myokit.Expression>` to a Matlab syntax.
     """
     def __init__(self):
-        super(MatlabExpressionWriter, self).__init__()
+        super().__init__()
         self._function_prefix = ''
         self._fcond = 'ifthenelse'
 
diff --git a/myokit/formats/matlab/_exporter.py b/myokit/formats/matlab/_exporter.py
index 214da5158..e0c9390b7 100644
--- a/myokit/formats/matlab/_exporter.py
+++ b/myokit/formats/matlab/_exporter.py
@@ -4,9 +4,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import os
 
 import myokit.formats
diff --git a/myokit/formats/opencl/__init__.py b/myokit/formats/opencl/__init__.py
index b41c750cd..226505bb4 100644
--- a/myokit/formats/opencl/__init__.py
+++ b/myokit/formats/opencl/__init__.py
@@ -4,9 +4,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 from ._exporter import OpenCLExporter, OpenCLRLExporter
 from ._ewriter import OpenCLExpressionWriter
 from myokit.formats import ansic
diff --git a/myokit/formats/opencl/_ewriter.py b/myokit/formats/opencl/_ewriter.py
index 23766179a..2f176551f 100644
--- a/myokit/formats/opencl/_ewriter.py
+++ b/myokit/formats/opencl/_ewriter.py
@@ -4,10 +4,8 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import myokit
+
 from myokit.formats.python import PythonExpressionWriter
 
 
@@ -17,7 +15,7 @@ class OpenCLExpressionWriter(PythonExpressionWriter):
     Myokit :class:`expressions <myokit.Expression>` to OpenCL syntax.
     """
     def __init__(self, precision=myokit.SINGLE_PRECISION, native_math=True):
-        super(OpenCLExpressionWriter, self).__init__()
+        super().__init__()
         self._function_prefix = ''
         self._sp = (precision == myokit.SINGLE_PRECISION)
         self._nm = bool(native_math)
diff --git a/myokit/formats/opencl/_exporter.py b/myokit/formats/opencl/_exporter.py
index a5608e5a8..4b67c23ac 100644
--- a/myokit/formats/opencl/_exporter.py
+++ b/myokit/formats/opencl/_exporter.py
@@ -4,9 +4,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import os
 
 import myokit.formats
diff --git a/myokit/formats/python/__init__.py b/myokit/formats/python/__init__.py
index 6addec248..6619c9cb5 100644
--- a/myokit/formats/python/__init__.py
+++ b/myokit/formats/python/__init__.py
@@ -4,9 +4,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 from ._exporter import PythonExporter
 from ._ewriter import PythonExpressionWriter, NumPyExpressionWriter
 
diff --git a/myokit/formats/python/_ewriter.py b/myokit/formats/python/_ewriter.py
index 42c936e7c..28b42fcbb 100644
--- a/myokit/formats/python/_ewriter.py
+++ b/myokit/formats/python/_ewriter.py
@@ -4,9 +4,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import myokit.formats
 
 
@@ -17,7 +14,7 @@ class PythonExpressionWriter(myokit.formats.ExpressionWriter):
     equivalent.
     """
     def __init__(self):
-        super(PythonExpressionWriter, self).__init__()
+        super().__init__()
 
         self._flhs = None
         self.set_lhs_function(lambda v: str(v))
@@ -192,7 +189,7 @@ class NumPyExpressionWriter(PythonExpressionWriter):
     intended for use in NumPy arrays.
     """
     def __init__(self):
-        super(NumPyExpressionWriter, self).__init__()
+        super().__init__()
         self._function_prefix = 'numpy.'
     #def _ex_name(self, e):
     #def _ex_derivative(self, e):
diff --git a/myokit/formats/python/_exporter.py b/myokit/formats/python/_exporter.py
index e646c6fec..9e33ddbab 100644
--- a/myokit/formats/python/_exporter.py
+++ b/myokit/formats/python/_exporter.py
@@ -4,9 +4,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import os
 
 import myokit.formats
diff --git a/myokit/formats/python/template/sim.py b/myokit/formats/python/template/sim.py
index d8c2bc714..8bba28a00 100644
--- a/myokit/formats/python/template/sim.py
+++ b/myokit/formats/python/template/sim.py
@@ -73,7 +73,7 @@ def c(comp):
     w.set_lhs_function(lambda x: v(x, comp))
 
     ?>
-class <?= names[1] ?>(object):
+class <?= names[1] ?>:
     def __init__(self):
 <?
     for var in comp.variables(deep=True):
@@ -118,7 +118,7 @@ def _constants(self):
 #
 # Engine component
 #
-class Engine(object):
+class Engine:
     """
     Calculates the derivatives in the current state
     """
@@ -213,10 +213,10 @@ def test_step():
 #
 # Pacing
 #
-class Protocol(object):
+class Protocol:
     """ Holds an ordered set of ProtocolEvent objects """
     def __init__(self):
-        super(Protocol, self).__init__()
+        super().__init__()
         self.head = None
     def add(self, e):
         """ Schedules an event """
@@ -238,9 +238,11 @@ def pop(self):
         if self.head is not None:
             self.head = self.head.next
         return e
-class ProtocolEvent(object):
+
+
+class ProtocolEvent:
     def __init__(self, level, start, duration, period=0, multiplier=0):
-        super(ProtocolEvent, self).__init__()
+        super().__init__()
         self.level = float(level)
         self.start = float(start)
         self.duration = float(duration)
@@ -256,6 +258,7 @@ def __init__(self, level, start, duration, period=0, multiplier=0):
             raise Exception('Non-periodic event cannot occur more than once')
         self.next = None
 
+
 def pacing_protocol():
     pacing = Protocol()
 <?
@@ -377,10 +380,7 @@ def beat(stepSmall = 0.005, stepLarge = 0.01):
         except ArithmeticError as e:
             print('Arithmetic error occurred')
             y = 'Continue with smaller stepsize? (y/n): '
-            try:
-                y = raw_input(y)    # Python 2
-            except NameError:
-                y = input(y)        # Python 3
+            y = input(y)
             if y.lower()[0:1] == 'y':
                 small /= 2
                 large /= 2
diff --git a/myokit/formats/sbml/__init__.py b/myokit/formats/sbml/__init__.py
index 1507fb65a..478528bb6 100644
--- a/myokit/formats/sbml/__init__.py
+++ b/myokit/formats/sbml/__init__.py
@@ -4,9 +4,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 from ._api import (  # noqa
     Compartment,
     CSymbolVariable,
diff --git a/myokit/formats/sbml/_api.py b/myokit/formats/sbml/_api.py
index 93b028ed1..9602f0714 100644
--- a/myokit/formats/sbml/_api.py
+++ b/myokit/formats/sbml/_api.py
@@ -1,3 +1,9 @@
+#
+# SBML API
+#
+# This file is part of Myokit.
+# See http://myokit.org for copyright, sharing, and licensing details.
+#
 import collections
 import warnings
 import re
@@ -16,7 +22,7 @@ class SBMLError(Exception):
     """Raised if something goes wrong when working with an SBML model."""
 
 
-class Quantity(object):
+class Quantity:
     """
     Base class for anything that has a numerical value in an SBML model, and
     can be set by rules, reactions, or initial assignments.
@@ -89,7 +95,7 @@ def value(self):
         return self._value
 
 
-class CSymbolVariable(object):
+class CSymbolVariable:
     """
     Represents a CSymbol that can appear in SBML expressions, but which has a
     predetermind value and/or meaning, e.g. "time".
@@ -122,7 +128,7 @@ class Compartment(Quantity):
 
     """
     def __init__(self, model, sid):
-        super(Compartment, self).__init__()
+        super().__init__()
 
         if not isinstance(model, Model):
             raise SBMLError(
@@ -185,7 +191,7 @@ def __str__(self):
         return '<Compartment ' + self._sid + '>'
 
 
-class Model(object):
+class Model:
     """
     Represents a model in SBML.
 
@@ -581,7 +587,7 @@ class Parameter(Quantity):
 
     """
     def __init__(self, model, sid):
-        super(Parameter, self).__init__()
+        super().__init__()
 
         if not isinstance(model, Model):
             raise SBMLError(
@@ -612,7 +618,7 @@ def units(self):
         return self._units
 
 
-class Reaction(object):
+class Reaction:
     """
     Represents an SBML reaction; to create a reaction use
     :meth:`Model.add_reaction()`.
@@ -772,7 +778,7 @@ class Species(Quantity):
 
     """
     def __init__(self, compartment, sid, is_amount, is_constant, is_boundary):
-        super(Species, self).__init__()
+        super().__init__()
 
         if not isinstance(compartment, Compartment):
             raise SBMLError(
@@ -914,7 +920,7 @@ class SpeciesReference(Quantity):
     MODIFIER = 2
 
     def __init__(self, species, sid=None):
-        super(SpeciesReference, self).__init__()
+        super().__init__()
 
         if not isinstance(species, Species):
             raise SBMLError(
@@ -936,11 +942,11 @@ def __str__(self):
         return '<SpeciesReference ' + self._sid + '>'
 
 
-class ModifierSpeciesReference(object):
+class ModifierSpeciesReference:
     """Represents a reference to a modifier species in an SBML reaction."""
 
     def __init__(self, species, sid=None):
-        super(ModifierSpeciesReference, self).__init__()
+        super().__init__()
 
         if not isinstance(species, Species):
             raise SBMLError(
@@ -971,7 +977,7 @@ def convert_name(name):
     return name
 
 
-class _MyokitConverter(object):
+class _MyokitConverter:
     """
     Converts SBML Models to Myokit models.
     """
diff --git a/myokit/formats/sbml/_importer.py b/myokit/formats/sbml/_importer.py
index 0df2a0422..63dd603bf 100644
--- a/myokit/formats/sbml/_importer.py
+++ b/myokit/formats/sbml/_importer.py
@@ -7,9 +7,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import myokit.formats
 
 
@@ -22,7 +19,7 @@ class SBMLImporter(myokit.formats.Importer):
     :class:`myokit.formats.sbml.SBMLParser`.
     """
     def __init__(self):
-        super(SBMLImporter, self).__init__()
+        super().__init__()
 
     def supports_model(self):
         """Returns ``True``."""
diff --git a/myokit/formats/sbml/_parser.py b/myokit/formats/sbml/_parser.py
index 42b776bcd..f2d5b7ff1 100644
--- a/myokit/formats/sbml/_parser.py
+++ b/myokit/formats/sbml/_parser.py
@@ -8,9 +8,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import re
 import warnings
 
@@ -76,10 +73,10 @@ def __init__(self, message, element=None):
                 message = 'Error on line ' + line + '. ' + message
             except AttributeError:  # pragma: no cover
                 pass
-        super(SBMLParsingError, self).__init__(message)
+        super().__init__(message)
 
 
-class SBMLParser(object):
+class SBMLParser:
     """
     Parses SBML models, creating an SBML Model structure that can be converted
     to a :class:`myokit.Model` object.
diff --git a/myokit/formats/stan/__init__.py b/myokit/formats/stan/__init__.py
index 00d32b887..20506c93b 100644
--- a/myokit/formats/stan/__init__.py
+++ b/myokit/formats/stan/__init__.py
@@ -4,9 +4,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 from ._exporter import StanExporter
 from ._ewriter import StanExpressionWriter
 
diff --git a/myokit/formats/stan/_ewriter.py b/myokit/formats/stan/_ewriter.py
index 2da8a34d9..95917bd8b 100644
--- a/myokit/formats/stan/_ewriter.py
+++ b/myokit/formats/stan/_ewriter.py
@@ -6,10 +6,8 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import myokit
+
 from myokit.formats.python import PythonExpressionWriter
 
 
@@ -19,7 +17,7 @@ class StanExpressionWriter(PythonExpressionWriter):
     Myokit:class:`expressions <myokit.Expression>` to a Stan syntax.
     """
     def __init__(self):
-        super(StanExpressionWriter, self).__init__()
+        super().__init__()
         self._function_prefix = ''
 
         self._fcond = None
diff --git a/myokit/formats/stan/_exporter.py b/myokit/formats/stan/_exporter.py
index 966160f53..b62c7d828 100644
--- a/myokit/formats/stan/_exporter.py
+++ b/myokit/formats/stan/_exporter.py
@@ -4,9 +4,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import os
 
 import myokit.formats
@@ -61,7 +58,7 @@ def runnable(
             ion current).
 
         """
-        super(StanExporter, self).runnable(
+        super().runnable(
             path, model, protocol, parameters, output)
 
     def _vars(self, model, protocol, parameters, output):
diff --git a/myokit/formats/sympy/__init__.py b/myokit/formats/sympy/__init__.py
index 7591b6482..ee95a517d 100644
--- a/myokit/formats/sympy/__init__.py
+++ b/myokit/formats/sympy/__init__.py
@@ -4,9 +4,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 from ._ereader import SymPyExpressionReader
 from ._ewriter import SymPyExpressionWriter
 
diff --git a/myokit/formats/sympy/_ereader.py b/myokit/formats/sympy/_ereader.py
index af2a58c83..352b6f2f3 100644
--- a/myokit/formats/sympy/_ereader.py
+++ b/myokit/formats/sympy/_ereader.py
@@ -4,14 +4,11 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import myokit
 import myokit.formats
 
 
-class SymPyExpressionReader(object):
+class SymPyExpressionReader:
     """
     Converts Sympy expressions to Myokit expressions.
 
diff --git a/myokit/formats/sympy/_ewriter.py b/myokit/formats/sympy/_ewriter.py
index 66882ec74..9d2d8ce75 100644
--- a/myokit/formats/sympy/_ewriter.py
+++ b/myokit/formats/sympy/_ewriter.py
@@ -4,16 +4,13 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
+import myokit.formats
 
 try:
     import sympy as sp
 except ImportError:
     pass
 
-import myokit.formats
-
 
 class SymPyExpressionWriter(myokit.formats.ExpressionWriter):
     """
@@ -23,7 +20,7 @@ class SymPyExpressionWriter(myokit.formats.ExpressionWriter):
     The returned type is a SymPy object, not a string!
     """
     def __init__(self):
-        super(SymPyExpressionWriter, self).__init__()
+        super().__init__()
 
         self._flhs = None
         self.set_lhs_function(lambda lhs: str(lhs))
diff --git a/myokit/formats/wcp/__init__.py b/myokit/formats/wcp/__init__.py
index 0696e93fa..bddf1fec3 100644
--- a/myokit/formats/wcp/__init__.py
+++ b/myokit/formats/wcp/__init__.py
@@ -4,9 +4,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 from ._wcp import WcpFile  # noqa
 # Importers
 # Exporters
diff --git a/myokit/formats/wcp/_wcp.py b/myokit/formats/wcp/_wcp.py
index f482c0a61..278f1c5fb 100755
--- a/myokit/formats/wcp/_wcp.py
+++ b/myokit/formats/wcp/_wcp.py
@@ -4,9 +4,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import os
 import struct
 import numpy as np
@@ -17,7 +14,7 @@
 _ENC = 'ascii'
 
 
-class WcpFile(object):
+class WcpFile:
     """
     Represents a read-only WinWCP file (``.wcp``), stored at the location
     pointed to by ``filepath``.
@@ -35,7 +32,6 @@ class WcpFile(object):
     """
     def __init__(self, filepath):
         # The path to the file and its basename
-        # Ensure it's a `str` for Python2/3 compatibility
         filepath = str(filepath)
         self._filepath = os.path.abspath(filepath)
         self._filename = os.path.basename(filepath)
@@ -160,8 +156,6 @@ def _read(self, f):
             rtype = f.read(4)
 
             # Group number (float set by the user)
-            # Note: First argument to struct.unpack must be a str (so bytes on
-            # Python 2).
             group_number = struct.unpack(str('<f'), f.read(4))[0]
 
             # Time of recording, as float, not sure how to interpret
diff --git a/myokit/formats/xml/__init__.py b/myokit/formats/xml/__init__.py
index 2b7c64655..b5c4cff46 100644
--- a/myokit/formats/xml/__init__.py
+++ b/myokit/formats/xml/__init__.py
@@ -4,9 +4,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 from ._exporter import XMLExporter
 from ._split import split   # noqa
 
diff --git a/myokit/formats/xml/_exporter.py b/myokit/formats/xml/_exporter.py
index d70c3da1a..76399715b 100644
--- a/myokit/formats/xml/_exporter.py
+++ b/myokit/formats/xml/_exporter.py
@@ -4,9 +4,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import os
 
 from lxml import etree
diff --git a/myokit/formats/xml/_split.py b/myokit/formats/xml/_split.py
index fa0aa575a..8436cd85e 100644
--- a/myokit/formats/xml/_split.py
+++ b/myokit/formats/xml/_split.py
@@ -4,9 +4,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 
 def split(tag):
     """
diff --git a/myokit/gui/__init__.py b/myokit/gui/__init__.py
index 05fe2408f..328386dea 100644
--- a/myokit/gui/__init__.py
+++ b/myokit/gui/__init__.py
@@ -4,17 +4,12 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
-# Library imports
 import logging
 import os
 import platform
 import signal
 import sys
 
-# Myokit imports
 import myokit
 
 # Detect platform
diff --git a/myokit/gui/datablock_viewer.py b/myokit/gui/datablock_viewer.py
index d814967c9..4e705a0a7 100644
--- a/myokit/gui/datablock_viewer.py
+++ b/myokit/gui/datablock_viewer.py
@@ -4,24 +4,18 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
+import collections
+import configparser
 import os
 import sys
-import numpy as np
 import traceback
-import collections
+
+import numpy as np
 
 import myokit
 import myokit.gui
-from myokit.gui import QtWidgets, QtGui, QtCore, Qt
 
-# ConfigParser in Python 2 and 3
-try:
-    import ConfigParser as configparser
-except ImportError:
-    import configparser
+from myokit.gui import QtWidgets, QtGui, QtCore, Qt
 
 
 # Application title
@@ -92,7 +86,7 @@ class DataBlockViewer(myokit.gui.MyokitApplication):
     """
 
     def __init__(self, filename=None):
-        super(DataBlockViewer, self).__init__()
+        super().__init__()
         # Set application icon
         self.setWindowIcon(icon())
         # Set size, center
@@ -951,7 +945,7 @@ class VideoScene(QtWidgets.QGraphicsScene):
     double_click = QtCore.Signal(float, float)
 
     def __init__(self, *args):
-        super(VideoScene, self).__init__(*args)
+        super().__init__(*args)
         self.setBackgroundBrush(QtGui.QColor(192, 192, 192))
         self._w = None
         self._h = None
@@ -1008,7 +1002,7 @@ class VideoView(QtWidgets.QGraphicsView):
     resize_event = QtCore.Signal()
 
     def __init__(self, scene):
-        super(VideoView, self).__init__(scene)
+        super().__init__(scene)
         # Disable scrollbars
         self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
         self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
@@ -1091,7 +1085,7 @@ class GraphArea(QtWidgets.QWidget):
     mouse_moved = QtCore.Signal(float, float)
 
     def __init__(self):
-        super(GraphArea, self).__init__()
+        super().__init__()
         # DataBlock 2d, and its time vector
         self._data = None
         self._time = None
diff --git a/myokit/gui/datalog_viewer.py b/myokit/gui/datalog_viewer.py
index 78ffa27b3..9658767c1 100755
--- a/myokit/gui/datalog_viewer.py
+++ b/myokit/gui/datalog_viewer.py
@@ -4,23 +4,16 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
-# Standard library imports
+import configparser
 import gc
 import os
 import sys
 import traceback
 
-# Qt imports
 from myokit.gui import QtWidgets, QtGui, QtCore, Qt
 
-# Myokit
 import myokit
 import myokit.gui
-
-# Myokit components
 import myokit.formats.axon
 import myokit.formats.wcp
 
@@ -39,12 +32,6 @@
 except ImportError:
     has_scipy = False
 
-# ConfigParser in Python 2 and 3
-try:
-    import ConfigParser as configparser
-except ImportError:
-    import configparser
-
 
 # Application title
 TITLE = 'Myokit DataLog Viewer (PROTOTYPE)'
@@ -115,7 +102,7 @@ class DataLogViewer(myokit.gui.MyokitApplication):
     Graphical interface for viewing DataLog data.
     """
     def __init__(self, *filenames):
-        super(DataLogViewer, self).__init__()
+        super().__init__()
 
         # Set Title, icon
         self.setWindowTitle(TITLE + ' ' + myokit.__version__)
@@ -508,7 +495,7 @@ def show(self):
         """
         Shows this viewer.
         """
-        super(DataLogViewer, self).show()
+        super().show()
         QtWidgets.QApplication.processEvents()
 
     def fileTabChangeEvent(self, index):
@@ -555,7 +542,7 @@ class AbfTab(TabWidget):
     A widget displaying an ABF file.
     """
     def __init__(self, parent, abf):
-        super(AbfTab, self).__init__(parent)
+        super().__init__(parent)
         self.setTabsClosable(False)
         self.setTabPosition(self.East)
         self._abf = abf
@@ -649,7 +636,7 @@ def deleteLater(self):
             axes.cla()
         del self._figures, self._axes
         gc.collect()
-        super(AbfTab, self).deleteLater()
+        super().deleteLater()
 
 
 class AtfTab(TabWidget):
@@ -657,7 +644,7 @@ class AtfTab(TabWidget):
     A widget displaying an AGF file.
     """
     def __init__(self, parent, atf):
-        super(AtfTab, self).__init__(parent)
+        super().__init__(parent)
         self._atf = atf
 
         self.setTabsClosable(False)
@@ -720,7 +707,7 @@ def deleteLater(self):
             axes.cla()
         del self._figures, self._axes
         gc.collect()
-        super(AtfTab, self).deleteLater()
+        super().deleteLater()
 
 
 class CsvTab(TabWidget):
@@ -730,7 +717,7 @@ class CsvTab(TabWidget):
     The given log must have a time variable set.
     """
     def __init__(self, parent, log, filename):
-        super(CsvTab, self).__init__(parent)
+        super().__init__(parent)
         self.setTabsClosable(False)
         self.setTabPosition(self.East)
         self._log = log.npview()
@@ -819,7 +806,7 @@ def deleteLater(self):
             axes.cla()
         del self._figures, self._axes
         gc.collect()
-        super(CsvTab, self).deleteLater()
+        super().deleteLater()
 
 
 class MatTab(TabWidget):
@@ -827,7 +814,7 @@ class MatTab(TabWidget):
     A widget displaying a MAT file.
     """
     def __init__(self, parent, mat, filename):
-        super(MatTab, self).__init__(parent)
+        super().__init__(parent)
         self.setTabsClosable(False)
         self.setTabPosition(self.East)
         self._figures = []
@@ -907,7 +894,7 @@ def deleteLater(self):
             axes.cla()
         del self._figures, self._axes
         gc.collect()
-        super(MatTab, self).deleteLater()
+        super().deleteLater()
 
 
 class TxtTab(TabWidget):
@@ -915,7 +902,7 @@ class TxtTab(TabWidget):
     A widget displaying a TXT file (with lots of heuristics!).
     """
     def __init__(self, parent, data, filename):
-        super(TxtTab, self).__init__(parent)
+        super().__init__(parent)
         self.setTabsClosable(False)
         self.setTabPosition(self.East)
         self._figures = []
@@ -989,7 +976,7 @@ def deleteLater(self):
             axes.cla()
         del self._figures, self._axes
         gc.collect()
-        super(TxtTab, self).deleteLater()
+        super().deleteLater()
 
 
 class WcpTab(TabWidget):
@@ -997,7 +984,7 @@ class WcpTab(TabWidget):
     A widget displaying a WCP file.
     """
     def __init__(self, parent, wcp):
-        super(WcpTab, self).__init__(parent)
+        super().__init__(parent)
         self.setTabsClosable(False)
         self.setTabPosition(self.East)
         self._wcp = wcp
@@ -1044,5 +1031,5 @@ def deleteLater(self):
             axes.cla()
         del self._figures, self._axes
         gc.collect()
-        super(WcpTab, self).deleteLater()
+        super().deleteLater()
 
diff --git a/myokit/gui/explorer.py b/myokit/gui/explorer.py
index 57be09079..2c7a05b72 100644
--- a/myokit/gui/explorer.py
+++ b/myokit/gui/explorer.py
@@ -4,25 +4,16 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
-# Standard library imports
-
-# Myokit
 import myokit
+import myokit.gui
 
-# Qt imports
 from myokit.gui import QtCore, QtGui, QtWidgets
-
-# GUI components
-import myokit.gui
+from myokit.gui import matplotlib_backend as backend
 from . import progress
 
 # Matplotlib (must be imported _after_ gui has had chance to set backend)
 import matplotlib
 import matplotlib.figure
-from myokit.gui import matplotlib_backend as backend
 
 
 # Constants
@@ -44,7 +35,7 @@ class Explorer(QtWidgets.QDialog):
     *Extends:* ``QtWidgets.QDialog``
     """
     def __init__(self, parent, sim_method, output_stream, duration=1000):
-        super(Explorer, self).__init__(parent)
+        super().__init__(parent)
         self.setWindowTitle('Myokit Explorer')
         self._sim_method = sim_method
         self._stream = output_stream
diff --git a/myokit/gui/ide.py b/myokit/gui/ide.py
index 6a04325f4..bb4e89d84 100755
--- a/myokit/gui/ide.py
+++ b/myokit/gui/ide.py
@@ -5,10 +5,7 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
-# Standard library imports
+import configparser
 import gc
 import os
 import sys
@@ -16,17 +13,14 @@
 import traceback
 import warnings
 
-# Myokit
 import myokit
 import myokit.formats
+import myokit.gui
 import myokit.lib.deps
 import myokit.lib.guess
 
-# Qt imports
 from myokit.gui import QtWidgets, QtGui, QtCore, Qt
 
-# GUI components
-import myokit.gui
 from . import source
 from . import explorer
 from . import progress
@@ -37,12 +31,6 @@
 import matplotlib
 matplotlib.interactive(True)        # Allows plt.show()
 
-# ConfigParser in Python 2 and 3
-try:
-    import ConfigParser as configparser
-except ImportError:
-    import configparser
-
 
 # Application title
 TITLE = 'Myokit IDE'
@@ -110,7 +98,7 @@ class MyokitIDE(myokit.gui.MyokitApplication):
     New GUI for editing ``.mmt`` files.
     """
     def __init__(self, filename=None):
-        super(MyokitIDE, self).__init__()
+        super().__init__()
 
         # Set application icon
         self.setWindowIcon(icon())
@@ -2637,7 +2625,7 @@ class TabbedToolBar(QtWidgets.QTabWidget):
     tab_toggled = QtCore.Signal(QtWidgets.QWidget, bool)
 
     def __init__(self, parent=None):
-        super(TabbedToolBar, self).__init__(parent)
+        super().__init__(parent)
 
         self.setTabsClosable(True)
         self.tabCloseRequested.connect(self._close_button_hit)
@@ -2666,7 +2654,7 @@ def keyPressEvent(self, event):
             self.setFocus()
             self.focusPreviousChild()
         else:
-            super(TabbedToolBar, self).keyPressEvent(event)
+            super().keyPressEvent(event)
 
     def toggle(self, widget, new_status=None):
         """
@@ -2720,7 +2708,7 @@ class ModelNavigator(QtWidgets.QWidget):
     item_changed = QtCore.Signal(int)
 
     def __init__(self, parent=None):
-        super(ModelNavigator, self).__init__(parent)
+        super().__init__(parent)
 
         # List widget
         self._list_widget = QtWidgets.QListWidget()
@@ -2764,7 +2752,7 @@ class Console(QtWidgets.QPlainTextEdit):
     *Extends*: ``QtWidgets.QPlainTextEdit``
     """
     def __init__(self, parent=None):
-        super(Console, self).__init__(parent)
+        super().__init__(parent)
         self.setReadOnly(True)
         font = myokit.gui.qtMonospaceFont()
         font.setPointSize(10)
diff --git a/myokit/gui/progress.py b/myokit/gui/progress.py
index 617d418cc..a58aaef56 100644
--- a/myokit/gui/progress.py
+++ b/myokit/gui/progress.py
@@ -4,9 +4,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import myokit
 from myokit.gui import Qt, QtCore, QtWidgets
 
@@ -22,7 +19,7 @@ class ProgressBar(QtWidgets.QProgressDialog):
     and other tasks implementing the ``ProgressReporter`` interface.
     """
     def __init__(self, parent, message):
-        super(ProgressBar, self).__init__(
+        super().__init__(
             message, 'Cancel', 0, N, parent=parent)
         self.setWindowModality(Qt.WindowModal)
         self.setAutoClose(False)
@@ -51,7 +48,7 @@ class ProgressBarReporter(myokit.ProgressReporter):
     """
 
     def __init__(self, pd):
-        super(ProgressBarReporter, self).__init__()
+        super().__init__()
         self._pd = pd
 
     def enter(self, msg=None):
diff --git a/myokit/gui/source.py b/myokit/gui/source.py
index 16771cf52..3a98e699f 100644
--- a/myokit/gui/source.py
+++ b/myokit/gui/source.py
@@ -6,12 +6,11 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import myokit
+
 from myokit.gui import Qt, QtCore, QtGui, QtWidgets
 
+
 # GUI components
 # Constants
 SPACE = ' '
@@ -97,7 +96,7 @@ class Editor(QtWidgets.QPlainTextEdit):
     status bar.
     """
     def __init__(self, parent=None):
-        super(Editor, self).__init__(parent)
+        super().__init__(parent)
 
         # Current style
         self._palette = QtGui.QGuiApplication.palette()
@@ -491,7 +490,7 @@ def keyPressEvent(self, event):
             # Reset smart up/down behavior
             self._last_column = None
             # Pass to parent class
-            super(Editor, self).keyPressEvent(event)
+            super().keyPressEvent(event)
 
         elif key == Qt.Key_Insert and mod == Qt.NoModifier:
             # Insert/replace
@@ -539,7 +538,7 @@ def keyPressEvent(self, event):
                 pass
             else:
                 # Let parent class handle it
-                super(Editor, self).keyPressEvent(event)
+                super().keyPressEvent(event)
 
     def _line_number_area_width(self):
         """ Returns the required width for the number area. """
@@ -583,7 +582,7 @@ def paintEvent(self, e):
         """ Paints this editor. """
 
         # Paint the editor
-        super(Editor, self).paintEvent(e)
+        super().paintEvent(e)
 
         # Paint a line between the editor and the line number area
         x = int(
@@ -610,7 +609,7 @@ def replace(self, text):
 
     def resizeEvent(self, event):
         """ Qt event: Editor is resized. """
-        super(Editor, self).resizeEvent(event)
+        super().resizeEvent(event)
         # Update line number area
         rect = self.contentsRect()
         self._line_number_area.setGeometry(
@@ -722,7 +721,7 @@ class LineNumberArea(QtWidgets.QWidget):
     """
 
     def __init__(self, editor):
-        super(LineNumberArea, self).__init__(editor)
+        super().__init__(editor)
         self._editor = editor
         self._editor.blockCountChanged.connect(self.update_width)
         self._editor.updateRequest.connect(self.update_contents)
@@ -766,7 +765,7 @@ class FindReplaceWidget(QtWidgets.QWidget):
     find_action = QtCore.Signal(str)
 
     def __init__(self, parent, editor):
-        super(FindReplaceWidget, self).__init__(parent)
+        super().__init__(parent)
         self._editor = editor
 
         # Create widgets
@@ -920,7 +919,7 @@ def keyPressEvent(self, event):
         if key == Qt.Key_Enter or key == Qt.Key_Return:
             self.action_find()
         else:
-            super(FindReplaceWidget, self).keyPressEvent(event)
+            super().keyPressEvent(event)
 
     def load_config(self, config, section):
         """
@@ -956,7 +955,7 @@ class ModelHighlighter(QtGui.QSyntaxHighlighter):
     ANNOT_KEYS = ['in', 'bind', 'label']
 
     def __init__(self, document):
-        super(ModelHighlighter, self).__init__(document)
+        super().__init__(document)
 
         # Expressions used to find strings & comments
         self._string_start = QtCore.QRegExp(r'"""')
@@ -1101,7 +1100,7 @@ class ProtocolHighlighter(QtGui.QSyntaxHighlighter):
     Syntax highlighter for ``mmt`` protocol definitions.
     """
     def __init__(self, document):
-        super(ProtocolHighlighter, self).__init__(document)
+        super().__init__(document)
 
         # Headers and units
         self._rule_head = QtCore.QRegExp(r'^(\s*)\[\[[a-zA-Z0-9_]+\]\]')
@@ -1145,7 +1144,7 @@ class ScriptHighlighter(QtGui.QSyntaxHighlighter):
     Syntax highlighter for ``mmt`` script files.
     """
     def __init__(self, document):
-        super(ScriptHighlighter, self).__init__(document)
+        super().__init__(document)
 
         # Headers and units
         self._rule_head = QtCore.QRegExp(r'^(\s*)\[\[[a-zA-Z0-9_]+\]\]')
@@ -1274,79 +1273,74 @@ def find_start(text, next):
 # List of essential built-in python functions
 _PYFUNC = [
     'abs()',
-    'divmod()',
-    'input()',
-    'open()',
-    'staticmethod()',
+    'aiter()',
     'all()',
-    'enumerate()',
-    'int()',
-    'ord()',
-    'str()',
     'any()',
-    'eval()',
-    'isinstance()',
-    'pow()',
-    'sum()',
-    'basestring()',
-    'execfile()',
-    'issubclass()',
-    'print()',
-    'super()',
+    'anext()',
+    'ascii()',
     'bin()',
-    'file()',
-    'iter()',
-    'property()',
-    'tuple()',
     'bool()',
-    'filter()',
-    'len()',
-    'range()',
-    'type()',
+    'breakpoint()',
     'bytearray()',
-    'float()',
-    'list()',
-    'raw_input()',
-    'unichr()',
+    'bytes()',
     'callable()',
-    'format()',
-    'locals()',
-    'reduce()',
-    'unicode()',
     'chr()',
-    'frozenset()',
-    'long()',
-    'reload()',
-    'vars()',
     'classmethod()',
+    'compile()',
+    'complex()',
+    'delattr()',
+    'dict()',
+    'dir()',
+    'divmod()',
+    'enumerate()',
+    'eval()',
+    'exec()',
+    'filter()',
+    'float()',
+    'format()',
+    'frozenset()',
     'getattr()',
-    'map()',
-    'repr()',
-    'range()',
-    'cmp()',
     'globals()',
-    'max()',
-    'reversed()',
-    'zip()',
-    'compile()',
     'hasattr()',
-    'memoryview()',
-    'round()',
-    '__import__()',
-    'complex()',
     'hash()',
-    'min()',
-    'set()',
-    'delattr()',
     'help()',
-    'next()',
-    'setattr()',
-    'dict()',
     'hex()',
-    'object()',
-    'slice()',
-    'dir()',
     'id()',
+    'input()',
+    'int()',
+    'isinstance()',
+    'issubclass()',
+    'iter()',
+    'len()',
+    'list()',
+    'locals()',
+    'map()',
+    'max()',
+    'memoryview()',
+    'min()',
+    'next()',
+    'object()',
     'oct()',
+    'open()',
+    'ord()',
+    'pow()',
+    'print()',
+    'property()',
+    'range()',
+    'repr()',
+    'reversed()',
+    'round()',
+    'set()',
+    'setattr()',
+    'slice()',
     'sorted()',
+    'staticmethod()',
+    'str()',
+    'sum()',
+    'super()',
+    'tuple()',
+    'type()',
+    'vars()',
+    'zip()',
+    '__import__()',
 ]
diff --git a/myokit/gui/vargrapher.py b/myokit/gui/vargrapher.py
index 27fd48bc8..eba925095 100644
--- a/myokit/gui/vargrapher.py
+++ b/myokit/gui/vargrapher.py
@@ -4,23 +4,13 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
-# Standard library imports
-
-# Myokit
 import myokit
+import myokit.gui
 
-# Qt imports
 from myokit.gui import QtGui, QtWidgets
 
-# NumPy
 import numpy as np
 
-# GUI components
-import myokit.gui
-
 # Matplotlib (must be imported _after_ gui has had chance to set backend)
 import matplotlib
 import matplotlib.figure
@@ -50,7 +40,7 @@ class VarGrapher(QtWidgets.QDialog):
     *Extends:* ``QtWidgets.QDialog``
     """
     def __init__(self, parent, title, var, func, args):
-        super(VarGrapher, self).__init__(parent)
+        super().__init__(parent)
         self.setFixedSize(700, 600)
         self.setWindowTitle(title)
 
diff --git a/myokit/lib/deps.py b/myokit/lib/deps.py
index 3464f8645..51a544571 100644
--- a/myokit/lib/deps.py
+++ b/myokit/lib/deps.py
@@ -5,12 +5,11 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
-import random
 import collections
+import random
+
 import myokit
+
 # Don't import pyplot yet, this will crash if no window environment is loaded
 
 
@@ -174,7 +173,7 @@ def c(v):
     return a
 
 
-class DiGraph(object):
+class DiGraph:
     """
     A simple directed graph implementation.
 
@@ -182,7 +181,7 @@ class DiGraph(object):
     for example ``matrix=[[0, 1, 1], [0, 1, 0], [0, 0, 0]]``
     """
     def __init__(self, matrix=None):
-        super(DiGraph, self).__init__()
+        super().__init__()
         if isinstance(matrix, DiGraph):
             # Clone
             self.nodes = collections.OrderedDict()
@@ -526,7 +525,7 @@ def uid_or_node(self, test):
         return test
 
 
-class Node(object):
+class Node:
     """
     Defines a node in a graph
     """
@@ -534,7 +533,7 @@ def __init__(self, uid):
         """
         Creates a new, graphless node with the given identifier
         """
-        super(Node, self).__init__()
+        super().__init__()
         self.graph = None
         self.uid = uid
         self.edgo = set()
diff --git a/myokit/lib/guess.py b/myokit/lib/guess.py
index 5feef6b55..0a7600f21 100644
--- a/myokit/lib/guess.py
+++ b/myokit/lib/guess.py
@@ -5,9 +5,8 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
 import collections
+
 import myokit
 
 
diff --git a/myokit/lib/hh.py b/myokit/lib/hh.py
index 7f5fc76f4..26961ddab 100644
--- a/myokit/lib/hh.py
+++ b/myokit/lib/hh.py
@@ -4,14 +4,12 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import numpy as np
+
 import myokit
 
 
-class HHModel(object):
+class HHModel:
     """
     Represents a Hodgkin-Huxley (HH)-style model of an ion channel, extracted
     from a :class:`myokit.Model`.
@@ -94,7 +92,7 @@ class HHModel(object):
     # A HHModel can return a function to calculate state values, but it never
     # updates its internal state in any way.
     def __init__(self, model, states, parameters=None, current=None, vm=None):
-        super(HHModel, self).__init__()
+        super().__init__()
 
         #
         # Check input
@@ -533,7 +531,7 @@ def steady_state(self, membrane_potential=None, parameters=None):
         return self._steady_state_function(*inputs)
 
 
-class AnalyticalSimulation(object):
+class AnalyticalSimulation:
     """
     Analytically evaluates a :class:`HHModel`'s state for a given set of points
     in time.
@@ -583,7 +581,7 @@ class AnalyticalSimulation(object):
 
     """
     def __init__(self, model, protocol=None):
-        super(AnalyticalSimulation, self).__init__()
+        super().__init__()
 
         # Check model
         if not isinstance(model, HHModel):
diff --git a/myokit/lib/markov.py b/myokit/lib/markov.py
index ccc9b648b..a954b95b9 100644
--- a/myokit/lib/markov.py
+++ b/myokit/lib/markov.py
@@ -4,16 +4,14 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import collections
+
 import numpy as np
 
 import myokit
 
 
-class LinearModel(object):
+class LinearModel:
     """
     Represents a linear Markov model of an ion channel extracted from a
     :class:`myokit.Model`.
@@ -120,7 +118,7 @@ class LinearModel(object):
     # for its default values or newly passed in values, but it never updates
     # its internal state in any way!
     def __init__(self, model, states, parameters=None, current=None, vm=None):
-        super(LinearModel, self).__init__()
+        super().__init__()
 
         # Get a clone of the model, with all markov models written in full ODE
         # form.
@@ -389,7 +387,7 @@ def _generate_functions(self):
         globl = {'numpy': np, 'n': n}
         local = {}
 
-        myokit._exec(code, globl, local)
+        exec(code, globl, local)
         self._matrix_function = local['matrix_function']
 
         #
@@ -408,7 +406,7 @@ def _generate_functions(self):
         code = head + '\n' + '\n'.join(['    ' + line for line in body])
         globl = {'numpy': np}
         local = {}
-        myokit._exec(code, globl, local)
+        exec(code, globl, local)
         self._rate_list_function = local['rate_list_function']
 
     def current(self):
@@ -635,7 +633,7 @@ def steady_state(self, membrane_potential=None, parameters=None):
         return x
 
 
-class AnalyticalSimulation(object):
+class AnalyticalSimulation:
     """
     Analytically evaluates a :class:`LinearModel`'s state over a given set of
     points in time.
@@ -689,7 +687,7 @@ class AnalyticalSimulation(object):
 
     """
     def __init__(self, model, protocol=None):
-        super(AnalyticalSimulation, self).__init__()
+        super().__init__()
         # Check model
         if not isinstance(model, LinearModel):
             raise ValueError('First parameter must be a `LinearModel`.')
@@ -1093,7 +1091,7 @@ def state(self):
         return list(self._state)
 
 
-class DiscreteSimulation(object):
+class DiscreteSimulation:
     """
     Performs stochastic simulations of a :class:`LinearModel`'s behavior for a
     finite number of channels.
@@ -1563,7 +1561,7 @@ def state(self):
         return list(self._state)
 
 
-class MarkovModel(object):
+class MarkovModel:
     """
     **Deprecated**: This class has been replaced by the classes
     :class:`LinearModel` and :class:`AnalyticalSimulation`. Please update your
diff --git a/myokit/lib/multi.py b/myokit/lib/multi.py
index 0727541d4..0c33ac5e8 100644
--- a/myokit/lib/multi.py
+++ b/myokit/lib/multi.py
@@ -4,11 +4,9 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
+import os
 
 import myokit
-import os
 
 
 #
diff --git a/myokit/lib/plots.py b/myokit/lib/plots.py
index f724ba54f..84dc28017 100644
--- a/myokit/lib/plots.py
+++ b/myokit/lib/plots.py
@@ -5,10 +5,8 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import numpy as np
+
 # Don't import pyplot yet, this will cause a crash if no window environment
 # is loaded.
 
diff --git a/myokit/pacing.py b/myokit/pacing.py
index 25fac11d9..739788821 100644
--- a/myokit/pacing.py
+++ b/myokit/pacing.py
@@ -4,9 +4,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import myokit
 """
 This module contains factory methods to create Protocol objects
diff --git a/myokit/pype.py b/myokit/pype.py
index c3f0e2ce1..e7c920cae 100755
--- a/myokit/pype.py
+++ b/myokit/pype.py
@@ -4,25 +4,14 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import ast
+import io
 import re
 import sys
 import traceback
 
-import myokit
-
-try:
-    # Python 2
-    from cStringIO import StringIO
-except ImportError:
-    # Python3
-    from io import StringIO
-
 
-class TemplateEngine(object):
+class TemplateEngine:
     """
     A tiny templating engine using a php style syntax.
 
@@ -40,7 +29,7 @@ class TemplateEngine(object):
     stream specified by the user.
     """
     def __init__(self):
-        super(TemplateEngine, self).__init__()
+        super().__init__()
         self.stream = None
         self.error = None
 
@@ -71,8 +60,8 @@ def process(self, filename, variables={}):
         script = self._convert(filename)
 
         # Get or create output stream
-        stdout = self.stream if self.stream else StringIO()
-        stderr = StringIO()
+        stdout = self.stream if self.stream else io.StringIO()
+        stderr = io.StringIO()
 
         # Run and handle errors
         error = None
@@ -84,7 +73,7 @@ def process(self, filename, variables={}):
                 syserr = sys.stderr
                 sys.stdout = stdout
                 sys.stderr = stderr
-                myokit._exec(script, variables)
+                exec(script, variables)
             except Exception:
                 error = sys.exc_info()
             finally:
@@ -245,5 +234,5 @@ class PypeError(Exception):
     *Extends:* Exception
     """
     def __init__(self, message):
-        super(PypeError, self).__init__(message)
+        super().__init__(message)
 
diff --git a/myokit/tools.py b/myokit/tools.py
index 7e0da38ea..3f56360af 100644
--- a/myokit/tools.py
+++ b/myokit/tools.py
@@ -6,9 +6,7 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
+import io
 import os
 import re
 import shutil
@@ -18,18 +16,12 @@
 import threading
 import timeit
 
-# StringIO in Python 2 and 3
-try:
-    from cStringIO import StringIO
-except ImportError:  # pragma: no python 2 cover
-    from io import StringIO
-
 
 # Natural sort regex
 _natural_sort_regex = re.compile('([0-9]+)')
 
 
-class Benchmarker(object):
+class Benchmarker:
     """
     Allows benchmarking using the with statement.
 
@@ -97,7 +89,7 @@ def time(self):
         return timeit.default_timer() - self._start
 
 
-class capture(object):
+class capture:
     """
     Context manager that temporarily redirects the current standard output and
     error streams, and captures anything that's written to them.
@@ -224,8 +216,8 @@ def _start(self):
         # Redirect
         if not self._fd:
             # Create temporary output and error streams
-            self._tmp_out = StringIO()
-            self._tmp_err = StringIO()
+            self._tmp_out = io.StringIO()
+            self._tmp_err = io.StringIO()
 
             # Redirect, attempting to flush first
             try:
@@ -391,10 +383,6 @@ def err(self):
             return ''
         text = self._txt_err
 
-        # In Python 2, the text needs to be decoded from ascii
-        if sys.hexversion < 0x03000000:  # pragma: no python 3 cover
-            text = text.decode('ascii', 'ignore')
-
         return text
 
     def out(self):
@@ -406,10 +394,6 @@ def out(self):
             return ''
         text = self._txt_out
 
-        # In Python 2, the text needs to be decoded from ascii
-        if sys.hexversion < 0x03000000:  # pragma: no python 3 cover
-            text = text.decode('ascii', 'ignore')
-
         return text
 
     def text(self):
diff --git a/myokit/units.py b/myokit/units.py
index 07b4b3e84..77fcb3b28 100644
--- a/myokit/units.py
+++ b/myokit/units.py
@@ -4,9 +4,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import myokit
 
 
diff --git a/setup.py b/setup.py
index ae913f27f..3b750f687 100644
--- a/setup.py
+++ b/setup.py
@@ -4,8 +4,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
 from setuptools import setup, find_packages
 
 
@@ -58,7 +56,6 @@
         'Intended Audience :: Science/Research',
         'License :: OSI Approved :: BSD License',
         'Operating System :: OS Independent',
-        'Programming Language :: Python :: 2.7',
         'Programming Language :: Python :: 3',
         'Topic :: Scientific/Engineering',
         'Topic :: Scientific/Engineering :: Bio-Informatics',
@@ -79,11 +76,14 @@
         'console_scripts': ['myokit = myokit.__main__:main']
     },
 
+    # Python version
+    python_requires='>=3.6',
+
     # List of dependencies
     install_requires=[
         'configparser',
         'lxml',
-        'matplotlib>=1.5',
+        'matplotlib>=2.2',
         'numpy',
         'setuptools',
         # PyQT or PySide?


Test patch:
diff --git a/.github/workflows/unit-tests-ubuntu.yml b/.github/workflows/unit-tests-ubuntu.yml
index aa418e3bf..d94af2f8f 100644
--- a/.github/workflows/unit-tests-ubuntu.yml
+++ b/.github/workflows/unit-tests-ubuntu.yml
@@ -17,7 +17,7 @@ jobs:
     strategy:
       matrix:
         # All Python versions except latest, which is tested by coverage.
-        python-version: ['2.7', '3.7', '3.8', '3.9', '3.10']
+        python-version: ['3.7', '3.8', '3.9', '3.10']
 
     steps:
       - uses: actions/checkout@v3
diff --git a/myokit/tests/__init__.py b/myokit/tests/__init__.py
index e41090dc3..b32bf88d5 100644
--- a/myokit/tests/__init__.py
+++ b/myokit/tests/__init__.py
@@ -8,9 +8,6 @@
 # The TemporaryDirectory class was adapted from Pints
 # See: https://github.com/pints-team/pints
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import os
 import tempfile
 import warnings
@@ -43,14 +40,14 @@
     del info
 
 
-class TemporaryDirectory(object):
+class TemporaryDirectory:
     """
     ContextManager that provides a temporary directory to create temporary
     files in. Deletes the directory and its contents when the context is
     exited.
     """
     def __init__(self):
-        super(TemporaryDirectory, self).__init__()
+        super().__init__()
         self._dir = None
 
     def __enter__(self):
@@ -138,7 +135,7 @@ def update(self, f):
         return self.okays >= 0
 
 
-class WarningCollector(object):
+class WarningCollector:
     """
     Wrapper around warnings.catch_warnings() that gathers all messages into a
     single string.
diff --git a/myokit/tests/ansic_event_based_pacing.py b/myokit/tests/ansic_event_based_pacing.py
index af7470a7b..f94b1d75b 100644
--- a/myokit/tests/ansic_event_based_pacing.py
+++ b/myokit/tests/ansic_event_based_pacing.py
@@ -5,9 +5,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import os
 
 import myokit
@@ -26,7 +23,7 @@ class AnsicEventBasedPacing(myokit.CModule):
     _index = 0
 
     def __init__(self, protocol):
-        super(AnsicEventBasedPacing, self).__init__()
+        super().__init__()
 
         # Unique id
         AnsicEventBasedPacing._index += 1
diff --git a/myokit/tests/ansic_fixed_form_pacing.py b/myokit/tests/ansic_fixed_form_pacing.py
index cfd6a69ad..3e561babc 100644
--- a/myokit/tests/ansic_fixed_form_pacing.py
+++ b/myokit/tests/ansic_fixed_form_pacing.py
@@ -5,9 +5,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import os
 
 import myokit
@@ -25,7 +22,7 @@ class AnsicFixedFormPacing(myokit.CModule):
     _index = 0
 
     def __init__(self, protocol):
-        super(AnsicFixedFormPacing, self).__init__()
+        super().__init__()
 
         # Unique id
         AnsicFixedFormPacing._index += 1
diff --git a/myokit/tests/test_aux.py b/myokit/tests/test_aux.py
index c6079a7fc..f06982309 100755
--- a/myokit/tests/test_aux.py
+++ b/myokit/tests/test_aux.py
@@ -5,25 +5,11 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
+import io
 import os
 import re
 import unittest
 
-# StringIO in Python 2 and 3
-try:
-    from cStringIO import StringIO
-except ImportError:  # pragma: no python 2 cover
-    from io import StringIO
-
-# Strings in Python2 and Python3
-try:
-    basestring
-except NameError:   # pragma: no cover
-    basestring = str
-
 import myokit
 
 from myokit.tests import DIR_DATA, WarningCollector
@@ -87,7 +73,7 @@ def test_default_script(self):
 
         # Test without a model
         script = myokit.default_script()
-        self.assertTrue(isinstance(script, basestring))
+        self.assertTrue(isinstance(script, str))
         self.assertIn('run(1000)', script)
 
         # Test adapting the time unit
@@ -95,29 +81,29 @@ def test_default_script(self):
         t = model.add_component('c').add_variable('t')
         t.set_rhs(0)
         script = myokit.default_script(model)
-        self.assertTrue(isinstance(script, basestring))
+        self.assertTrue(isinstance(script, str))
         self.assertIn('run(1000)', script)
 
         t.set_binding('time')
         script = myokit.default_script(model)
-        self.assertTrue(isinstance(script, basestring))
+        self.assertTrue(isinstance(script, str))
         self.assertIn('run(1000)', script)
 
         t.set_unit('s')
         script = myokit.default_script(model)
-        self.assertTrue(isinstance(script, basestring))
+        self.assertTrue(isinstance(script, str))
         self.assertIn('run(1.0)', script)
 
         t.set_unit('ms')
         script = myokit.default_script(model)
-        self.assertTrue(isinstance(script, basestring))
+        self.assertTrue(isinstance(script, str))
         self.assertIn('run(1000)', script)
 
         # Test plotting membrane potential
         v = model.get('c').add_variable('v')
         v.set_label('membrane_potential')
         script = myokit.default_script(model)
-        self.assertTrue(isinstance(script, basestring))
+        self.assertTrue(isinstance(script, str))
         self.assertIn("var = 'c.v'", script)
 
         # TODO: Run with tiny model?
@@ -283,7 +269,7 @@ def test_run(self):
 
         # Test with stringio
         x = "print('Hi there')"
-        s = StringIO()
+        s = io.StringIO()
         with myokit.tools.capture() as c:
             myokit.run(m, p, x, stderr=s, stdout=s)
         self.assertEqual(c.text(), '')
@@ -563,7 +549,7 @@ def test_version(self):
 
         # Raw version
         raw = myokit.version(raw=True)
-        self.assertIsInstance(raw, basestring)
+        self.assertIsInstance(raw, str)
         parts = raw.split('.')
         self.assertTrue(len(parts) in [3, 4])
 
diff --git a/myokit/tests/test_cellml_v1_api.py b/myokit/tests/test_cellml_v1_api.py
index 7cc0621ff..b83b77684 100755
--- a/myokit/tests/test_cellml_v1_api.py
+++ b/myokit/tests/test_cellml_v1_api.py
@@ -5,9 +5,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import unittest
 
 import myokit
@@ -15,18 +12,6 @@
 
 from myokit.tests import WarningCollector
 
-# Unit testing in Python 2 and 3
-try:
-    unittest.TestCase.assertRaisesRegex
-except AttributeError:
-    unittest.TestCase.assertRaisesRegex = unittest.TestCase.assertRaisesRegexp
-
-# Strings in Python2 and Python3
-try:
-    basestring
-except NameError:   # pragma: no cover
-    basestring = str
-
 
 class TestCellML1AnnotatedElement(unittest.TestCase):
     """ Tests for cellml.AnnotatableElement. """
@@ -1503,7 +1488,7 @@ def test_si_unit_names(self):
         names = [x for x in cellml.Units.si_unit_names()]
         self.assertTrue(len(names) > 10)
         for name in names:
-            self.assertTrue(isinstance(name, basestring))
+            self.assertTrue(isinstance(name, str))
             if name == 'celsius':
                 continue
             self.assertTrue(
diff --git a/myokit/tests/test_cellml_v1_parser.py b/myokit/tests/test_cellml_v1_parser.py
index 7f6478a8e..0633551a5 100755
--- a/myokit/tests/test_cellml_v1_parser.py
+++ b/myokit/tests/test_cellml_v1_parser.py
@@ -5,9 +5,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import os
 import unittest
 
@@ -20,18 +17,6 @@
 # CellML directory
 DIR = os.path.join(DIR_FORMATS, 'cellml')
 
-# Unit testing in Python 2 and 3
-try:
-    unittest.TestCase.assertRaisesRegex
-except AttributeError:
-    unittest.TestCase.assertRaisesRegex = unittest.TestCase.assertRaisesRegexp
-
-# Strings in Python2 and Python3
-try:
-    basestring
-except NameError:   # pragma: no cover
-    basestring = str
-
 
 class TestCellMLParser(unittest.TestCase):
     """ Tests the CellML 1.0/1.1 parser (mostly for errors). """
diff --git a/myokit/tests/test_cellml_v1_writer.py b/myokit/tests/test_cellml_v1_writer.py
index 10e550556..528aeb904 100755
--- a/myokit/tests/test_cellml_v1_writer.py
+++ b/myokit/tests/test_cellml_v1_writer.py
@@ -5,9 +5,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import re
 import unittest
 
@@ -16,12 +13,6 @@
 
 from myokit.tests import TemporaryDirectory, WarningCollector
 
-# Unit testing in Python 2 and 3
-try:
-    unittest.TestCase.assertRaisesRegex
-except AttributeError:
-    unittest.TestCase.assertRaisesRegex = unittest.TestCase.assertRaisesRegexp
-
 
 class TestCellMLWriter(unittest.TestCase):
     """ Tests for cellml.Writer. """
diff --git a/myokit/tests/test_cellml_v2_api.py b/myokit/tests/test_cellml_v2_api.py
index c992196f1..b762983e6 100755
--- a/myokit/tests/test_cellml_v2_api.py
+++ b/myokit/tests/test_cellml_v2_api.py
@@ -5,9 +5,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import unittest
 
 import myokit
@@ -15,18 +12,6 @@
 
 from myokit.tests import WarningCollector
 
-# Unit testing in Python 2 and 3
-try:
-    unittest.TestCase.assertRaisesRegex
-except AttributeError:
-    unittest.TestCase.assertRaisesRegex = unittest.TestCase.assertRaisesRegexp
-
-# Strings in Python2 and Python3
-try:
-    basestring
-except NameError:   # pragma: no cover
-    basestring = str
-
 
 class TestCellML2AnnotatableElement(unittest.TestCase):
     """ Tests for ``cellml.v2.AnnotatableElement``. """
@@ -1810,7 +1795,7 @@ def test_si_unit_names(self):
         names = [x for x in cellml.Units.si_unit_names()]
         self.assertTrue(len(names) > 10)
         for name in names:
-            self.assertTrue(isinstance(name, basestring))
+            self.assertTrue(isinstance(name, str))
             self.assertTrue(
                 isinstance(cellml.Units.find_units(name), cellml.Units))
 
diff --git a/myokit/tests/test_cellml_v2_parser.py b/myokit/tests/test_cellml_v2_parser.py
index b48dabe90..b28070c85 100755
--- a/myokit/tests/test_cellml_v2_parser.py
+++ b/myokit/tests/test_cellml_v2_parser.py
@@ -5,9 +5,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import os
 import unittest
 
@@ -19,18 +16,6 @@
 # CellML directory
 DIR = os.path.join(DIR_FORMATS, 'cellml')
 
-# Unit testing in Python 2 and 3
-try:
-    unittest.TestCase.assertRaisesRegex
-except AttributeError:
-    unittest.TestCase.assertRaisesRegex = unittest.TestCase.assertRaisesRegexp
-
-# Strings in Python2 and Python3
-try:
-    basestring
-except NameError:   # pragma: no cover
-    basestring = str
-
 
 class TestCellMLParser(unittest.TestCase):
     """ Tests the CellML 2.0 parser (mostly for errors / model validation). """
diff --git a/myokit/tests/test_cellml_v2_writer.py b/myokit/tests/test_cellml_v2_writer.py
index a75d43848..4bd0fd416 100755
--- a/myokit/tests/test_cellml_v2_writer.py
+++ b/myokit/tests/test_cellml_v2_writer.py
@@ -5,9 +5,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import re
 import unittest
 
@@ -16,12 +13,6 @@
 
 from myokit.tests import TemporaryDirectory, WarningCollector
 
-# Unit testing in Python 2 and 3
-try:
-    unittest.TestCase.assertRaisesRegex
-except AttributeError:
-    unittest.TestCase.assertRaisesRegex = unittest.TestCase.assertRaisesRegexp
-
 
 class TestCellMLWriter(unittest.TestCase):
     """ Tests for cellml.Writer. """
diff --git a/myokit/tests/test_cmodel.py b/myokit/tests/test_cmodel.py
index df55f27bb..9058f7744 100755
--- a/myokit/tests/test_cmodel.py
+++ b/myokit/tests/test_cmodel.py
@@ -5,9 +5,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import os
 import unittest
 
@@ -15,12 +12,6 @@
 
 from myokit.tests import DIR_DATA
 
-# Unit testing in Python 2 and 3
-try:
-    unittest.TestCase.assertRaisesRegex
-except AttributeError:
-    unittest.TestCase.assertRaisesRegex = unittest.TestCase.assertRaisesRegexp
-
 
 class CModelTest(unittest.TestCase):
     """
diff --git a/myokit/tests/test_compiler_detection.py b/myokit/tests/test_compiler_detection.py
index d20eb562f..d75bd2d4b 100755
--- a/myokit/tests/test_compiler_detection.py
+++ b/myokit/tests/test_compiler_detection.py
@@ -5,28 +5,18 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import unittest
 
 import myokit
 
 
-# Strings in Python2 and Python3
-try:
-    basestring
-except NameError:   # pragma: no cover
-    basestring = str
-
-
 class CompilerDetectionTest(unittest.TestCase):
     """
     Tests the compiler detection.
     """
     def test_compiler(self):
         # Test the compiler detection.
-        self.assertIsInstance(myokit.Compiler.info(), basestring)
+        self.assertIsInstance(myokit.Compiler.info(), str)
 
 
 if __name__ == '__main__':
diff --git a/myokit/tests/test_component.py b/myokit/tests/test_component.py
index cffc40fdc..2ef84a459 100755
--- a/myokit/tests/test_component.py
+++ b/myokit/tests/test_component.py
@@ -5,20 +5,10 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import myokit
 import unittest
 
 
-# Unit testing in Python 2 and 3
-try:
-    unittest.TestCase.assertRaisesRegex
-except AttributeError:
-    unittest.TestCase.assertRaisesRegex = unittest.TestCase.assertRaisesRegexp
-
-
 class VarOwnerTest(unittest.TestCase):
     """
     Tests parts of :class:`myokit.VarOwner`.
diff --git a/myokit/tests/test_config.py b/myokit/tests/test_config.py
index 892bde666..201ab9693 100755
--- a/myokit/tests/test_config.py
+++ b/myokit/tests/test_config.py
@@ -5,9 +5,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import os
 import sys
 import unittest
@@ -16,7 +13,6 @@
 
 from myokit.tests import TemporaryDirectory, WarningCollector
 
-
 debug = False
 
 
@@ -44,7 +40,7 @@
 """
 
 # Config with empty paths and spaces
-config_paths_1 = """
+config_paths = """
 [sundials]
 lib = five
 inc = three;four;
@@ -53,16 +49,6 @@
 inc =
 """
 
-# Config with empty paths and " ;", which in Python 2 is ignored
-config_paths_2 = """
-[sundials]
-lib = five ; six
-inc = three;four;
-[opencl]
-lib = one ;;   two point five ;three;
-inc =
-"""
-
 
 class TestConfig(unittest.TestCase):
 
@@ -205,7 +191,7 @@ def test_load_paths(self):
         myokit.OPENCL_LIB = []
         myokit.OPENCL_INC = []
         with open(self._temp_dir.path('myokit.ini'), 'w') as f:
-            f.write(config_paths_1)
+            f.write(config_paths)
         self._config_module._load()
         self.assertEqual(myokit.SUNDIALS_LIB, ['five'])
         self.assertEqual(myokit.SUNDIALS_INC, ['three', 'four'])
@@ -214,25 +200,6 @@ def test_load_paths(self):
             ['one', 'two point five', 'three'])
         self.assertEqual(myokit.OPENCL_INC, [])
 
-        # Even if the list contains " ;", which Python 2's config parser treats
-        # as a comment
-        myokit.SUNDIALS_LIB = []
-        myokit.SUNDIALS_INC = []
-        myokit.OPENCL_LIB = []
-        myokit.OPENCL_INC = []
-        with open(self._temp_dir.path('myokit.ini'), 'w') as f:
-            f.write(config_paths_2)
-        if sys.hexversion < 0x03020000:
-            self.assertRaises(ImportError, self._config_module._load)
-        else:
-            self._config_module._load()
-            self.assertEqual(myokit.SUNDIALS_LIB, ['five', 'six'])
-            self.assertEqual(myokit.SUNDIALS_INC, ['three', 'four'])
-            self.assertEqual(
-                myokit.OPENCL_LIB,
-                ['one', 'two point five', 'three'])
-            self.assertEqual(myokit.OPENCL_INC, [])
-
     def test_load_gui_backend(self):
         # Tests setting the GUI back end
 
diff --git a/myokit/tests/test_datablock.py b/myokit/tests/test_datablock.py
index 8bd927dab..0440735d2 100755
--- a/myokit/tests/test_datablock.py
+++ b/myokit/tests/test_datablock.py
@@ -5,11 +5,9 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import os
 import unittest
+
 import numpy as np
 
 import myokit
@@ -23,12 +21,6 @@
     WarningCollector,
 )
 
-# Unit testing in Python 2 and 3
-try:
-    unittest.TestCase.assertRaisesRegex
-except AttributeError:
-    unittest.TestCase.assertRaisesRegex = unittest.TestCase.assertRaisesRegexp
-
 
 class DataBlock1dTest(unittest.TestCase):
     """
diff --git a/myokit/tests/test_datalog.py b/myokit/tests/test_datalog.py
index 6f9cd1451..1c2edf979 100755
--- a/myokit/tests/test_datalog.py
+++ b/myokit/tests/test_datalog.py
@@ -5,11 +5,9 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import os
 import unittest
+
 import numpy as np
 
 import myokit
@@ -23,21 +21,6 @@
     WarningCollector,
 )
 
-# Unit testing in Python 2 and 3
-try:
-    unittest.TestCase.assertRaisesRegex
-except AttributeError:
-    unittest.TestCase.assertRaisesRegex = unittest.TestCase.assertRaisesRegexp
-
-
-# Universal newline mode in Python 2 and 3
-def uopen(filename):
-    try:
-        return open(filename, 'r', newline=None)
-    except TypeError:
-        return open(filename, 'U')
-
-
 # Extra output
 debug = False
 
@@ -1395,12 +1378,12 @@ def test_save_csv(self):
         with TemporaryDirectory() as td:
             fname = td.path('test.csv')
             d.save_csv(fname, order=['a.b', 'c.d', 'e.f'])
-            with uopen(fname) as f:
+            with open(fname, 'r', newline=None) as f:
                 header = f.readline()
             self.assertEqual(header, '"a.b","c.d","e.f"\n')
 
             d.save_csv(fname, order=['e.f', 'a.b', 'c.d'])
-            with uopen(fname) as f:
+            with open(fname, 'r', newline=None) as f:
                 header = f.readline()
             self.assertEqual(header, '"e.f","a.b","c.d"\n')
 
@@ -1422,7 +1405,7 @@ def test_save_csv(self):
         with TemporaryDirectory() as td:
             fname = td.path('test.csv')
             d.save_csv(fname)
-            with uopen(fname) as f:
+            with open(fname, 'r', newline=None) as f:
                 order = [x[1:-1] for x in f.readline().strip().split(',')]
             self.assertEqual(len(order), 9)
             self.assertEqual(order[0], 'e.t')
diff --git a/myokit/tests/test_dependency_checking.py b/myokit/tests/test_dependency_checking.py
index 7b9ff4e76..e99d8affa 100755
--- a/myokit/tests/test_dependency_checking.py
+++ b/myokit/tests/test_dependency_checking.py
@@ -6,9 +6,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import os
 import unittest
 
@@ -16,18 +13,6 @@
 
 from myokit.tests import DIR_DATA
 
-# Unit testing in Python 2 and 3
-try:
-    unittest.TestCase.assertRaisesRegex
-except AttributeError:
-    unittest.TestCase.assertRaisesRegex = unittest.TestCase.assertRaisesRegexp
-
-# Strings in Python 2 and 3
-try:
-    basestring
-except NameError:   # pragma: no cover
-    basestring = str
-
 
 # Extra output
 debug = False
@@ -55,7 +40,7 @@ def has_lhs(self, depmap, key, *deps):
         """
         # If a string lhs is given, interpret it as a variable name and get
         # the lhs of its defining equation
-        if isinstance(key, basestring):
+        if isinstance(key, str):
             key = self.m.get(key)
             if isinstance(key, myokit.Variable):
                 key = key.lhs()
@@ -64,7 +49,7 @@ def has_lhs(self, depmap, key, *deps):
         # convert it to a Name. Ensure no duplicates are given.
         deps = list(set(deps))
         for k, dep in enumerate(deps):
-            if isinstance(dep, basestring):
+            if isinstance(dep, str):
                 deps[k] = self.n(dep)
 
         # Get dep map
@@ -449,7 +434,7 @@ def has(lhs, *deps):
             return self.has_lhs(depmap, lhs, *deps)
 
         def nhas(lhs):
-            if isinstance(lhs, basestring):
+            if isinstance(lhs, str):
                 lhs = self.m.get(lhs).lhs()
             self.assertNotIn(lhs, depmap)
 
@@ -594,7 +579,7 @@ def has(lhs, *deps):
             return self.has_lhs(depmap, lhs, *deps)
 
         def nhas(lhs):
-            if isinstance(lhs, basestring):
+            if isinstance(lhs, str):
                 lhs = self.m.get(lhs).lhs()
             self.assertNotIn(lhs, depmap)
 
@@ -1360,10 +1345,10 @@ def eq(lhs):
 
         def before(lhs1, *lhs2s):
             """ Asserts lhs1 comes before lhs2 in the current component """
-            if isinstance(lhs1, basestring):
+            if isinstance(lhs1, str):
                 lhs1 = self.m.get(self.ccomp + '.' + lhs1).lhs()
             for lhs2 in lhs2s:
-                if isinstance(lhs2, basestring):
+                if isinstance(lhs2, str):
                     lhs2 = self.m.get(self.ccomp + '.' + lhs2).lhs()
                 i1 = i2 = None
                 for i, eq in enumerate(self.order[self.ccomp]):
@@ -1508,14 +1493,14 @@ def test_expressions_for(self):
 
         def before(lhs1, *lhs2s):
             """ Asserts lhs2 comes before lhs1 """
-            if isinstance(lhs1, basestring):
+            if isinstance(lhs1, str):
                 if lhs1.startswith('dot('):
                     lhs1 = myokit.Derivative(myokit.Name(
                         self.m.get(lhs1[4:-1])))
                 else:
                     lhs1 = myokit.Name(self.m.get(lhs1))
             for lhs2 in lhs2s:
-                if isinstance(lhs2, basestring):
+                if isinstance(lhs2, str):
                     if lhs2.startswith('dot('):
                         lhs2 = myokit.Derivative(myokit.Name(
                             self.m.get(lhs2[4:-1])))
diff --git a/myokit/tests/test_expressions.py b/myokit/tests/test_expressions.py
index 29f13be9d..c3a04f813 100755
--- a/myokit/tests/test_expressions.py
+++ b/myokit/tests/test_expressions.py
@@ -5,9 +5,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import pickle
 import unittest
 
@@ -15,12 +12,6 @@
 
 import myokit
 
-# Unit testing in Python 2 and 3
-try:
-    unittest.TestCase.assertRaisesRegex
-except AttributeError:
-    unittest.TestCase.assertRaisesRegex = unittest.TestCase.assertRaisesRegexp
-
 
 # Unit --> See test_units.py
 # Quantity --> See test_units.py
diff --git a/myokit/tests/test_float.py b/myokit/tests/test_float.py
index a21dbb9bd..e1eefd37a 100755
--- a/myokit/tests/test_float.py
+++ b/myokit/tests/test_float.py
@@ -5,9 +5,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import sys
 import unittest
 
@@ -91,8 +88,7 @@ def test_str(self):
         # Test float to string conversion.
 
         # String should be passed through
-        # Note: convert to str() to test in python 2 and 3.
-        self.assertEqual(myokit.float.str(str('123')), '123')
+        self.assertEqual(myokit.float.str('123'), '123')
 
         # Simple numbers
         self.assertEqual(myokit.float.str(0), '0')
diff --git a/myokit/tests/test_formats.py b/myokit/tests/test_formats.py
index a4c5b16f4..670a9c3fa 100755
--- a/myokit/tests/test_formats.py
+++ b/myokit/tests/test_formats.py
@@ -5,9 +5,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import unittest
 
 import myokit
@@ -15,12 +12,6 @@
 
 from myokit.tests import TemporaryDirectory
 
-# Unit testing in Python 2 and 3
-try:
-    unittest.TestCase.assertRaisesRegex
-except AttributeError:
-    unittest.TestCase.assertRaisesRegex = unittest.TestCase.assertRaisesRegexp
-
 
 class FormatsTest(unittest.TestCase):
     """
diff --git a/myokit/tests/test_formats_axon.py b/myokit/tests/test_formats_axon.py
index babb3bc21..9fcc37b9c 100755
--- a/myokit/tests/test_formats_axon.py
+++ b/myokit/tests/test_formats_axon.py
@@ -5,11 +5,9 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import os
 import unittest
+
 import numpy as np
 
 import myokit
@@ -17,12 +15,6 @@
 
 from myokit.tests import TemporaryDirectory, DIR_FORMATS
 
-# Unit testing in Python 2 and 3
-try:
-    unittest.TestCase.assertRaisesRegex
-except AttributeError:
-    unittest.TestCase.assertRaisesRegex = unittest.TestCase.assertRaisesRegexp
-
 
 class AbfTest(unittest.TestCase):
     """
diff --git a/myokit/tests/test_formats_cellml.py b/myokit/tests/test_formats_cellml.py
index 809bdb99e..2e04bff77 100755
--- a/myokit/tests/test_formats_cellml.py
+++ b/myokit/tests/test_formats_cellml.py
@@ -5,9 +5,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import os
 import re
 import unittest
@@ -23,18 +20,6 @@
 # CellML dir
 DIR = os.path.join(DIR_FORMATS, 'cellml')
 
-# Unit testing in Python 2 and 3
-try:
-    unittest.TestCase.assertRaisesRegex
-except AttributeError:
-    unittest.TestCase.assertRaisesRegex = unittest.TestCase.assertRaisesRegexp
-
-# Strings in Python 2 and 3
-try:
-    basestring
-except NameError:   # pragma: no python 2 cover
-    basestring = str
-
 
 class CellMLExporterTest(unittest.TestCase):
     """
diff --git a/myokit/tests/test_formats_channelml.py b/myokit/tests/test_formats_channelml.py
index e1466f315..87b4f6dc8 100755
--- a/myokit/tests/test_formats_channelml.py
+++ b/myokit/tests/test_formats_channelml.py
@@ -5,9 +5,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import os
 import unittest
 
@@ -20,18 +17,6 @@
 # ChannelML dir
 DIR = os.path.join(DIR_FORMATS, 'channelml')
 
-# Unit testing in Python 2 and 3
-try:
-    unittest.TestCase.assertRaisesRegex
-except AttributeError:  # pragma: no python 3 cover
-    unittest.TestCase.assertRaisesRegex = unittest.TestCase.assertRaisesRegexp
-
-# Strings in Python 2 and 3
-try:
-    basestring
-except NameError:   # pragma: no python 2 cover
-    basestring = str
-
 
 class ChannelMLTest(unittest.TestCase):
     """ Test ChannelML importing. """
diff --git a/myokit/tests/test_formats_easyml.py b/myokit/tests/test_formats_easyml.py
index edb7866f2..8994e0901 100755
--- a/myokit/tests/test_formats_easyml.py
+++ b/myokit/tests/test_formats_easyml.py
@@ -5,9 +5,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import os
 import unittest
 
@@ -17,17 +14,6 @@
 
 from myokit.tests import TemporaryDirectory, WarningCollector, DIR_DATA
 
-# Unit testing in Python 2 and 3
-try:
-    unittest.TestCase.assertRaisesRegex
-except AttributeError:  # pragma: no python 3 cover
-    unittest.TestCase.assertRaisesRegex = unittest.TestCase.assertRaisesRegexp
-
-# Strings in Python 2 and 3
-try:
-    basestring
-except NameError:   # pragma: no python 2 cover
-    basestring = str
 
 # Model that requires unit conversion
 units_model = """
diff --git a/myokit/tests/test_formats_exporters.py b/myokit/tests/test_formats_exporters.py
index 209b934e2..ade94d839 100755
--- a/myokit/tests/test_formats_exporters.py
+++ b/myokit/tests/test_formats_exporters.py
@@ -5,9 +5,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import os
 import unittest
 
@@ -16,18 +13,6 @@
 
 from myokit.tests import TemporaryDirectory
 
-# Unit testing in Python 2 and 3
-try:
-    unittest.TestCase.assertRaisesRegex
-except AttributeError:  # pragma: no python 3 cover
-    unittest.TestCase.assertRaisesRegex = unittest.TestCase.assertRaisesRegexp
-
-# Strings in Python 2 and 3
-try:
-    basestring
-except NameError:   # pragma: no python 2 cover
-    basestring = str
-
 
 class ExportTest(unittest.TestCase):
     """
@@ -38,7 +23,7 @@ def _test(self, e, model=None, protocol=None):
         """ Test a given exporter `e`. """
 
         # Test info method.
-        self.assertIsInstance(e.post_export_info(), basestring)
+        self.assertIsInstance(e.post_export_info(), str)
 
         # Load model, protocol
         m, p = model, protocol
diff --git a/myokit/tests/test_formats_expression_writers.py b/myokit/tests/test_formats_expression_writers.py
index d4b0f0139..fca45f9d7 100755
--- a/myokit/tests/test_formats_expression_writers.py
+++ b/myokit/tests/test_formats_expression_writers.py
@@ -5,9 +5,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import unittest
 
 import myokit
@@ -20,19 +17,6 @@
 import myokit.formats.stan
 
 
-# Unit testing in Python 2 and 3
-try:
-    unittest.TestCase.assertRaisesRegex
-except AttributeError:  # pragma: no python 3 cover
-    unittest.TestCase.assertRaisesRegex = unittest.TestCase.assertRaisesRegexp
-
-# Strings in Python 2 and 3
-try:
-    basestring
-except NameError:   # pragma: no python 2 cover
-    basestring = str
-
-
 class ExpressionWriterTest(unittest.TestCase):
     """ Test shared expression writer functionality. """
 
@@ -45,7 +29,7 @@ def test_ewriter_interface(self):
 
         # Create one of each
         for e in es:
-            self.assertIsInstance(e, basestring)
+            self.assertIsInstance(e, str)
             e = myokit.formats.ewriter(e)
             self.assertTrue(isinstance(e, myokit.formats.ExpressionWriter))
 
diff --git a/myokit/tests/test_formats_html.py b/myokit/tests/test_formats_html.py
index 6c412182f..08d0f38c7 100755
--- a/myokit/tests/test_formats_html.py
+++ b/myokit/tests/test_formats_html.py
@@ -5,9 +5,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import unittest
 
 import myokit.formats.html
diff --git a/myokit/tests/test_formats_importers.py b/myokit/tests/test_formats_importers.py
index e07fd2c9c..6e0d84603 100755
--- a/myokit/tests/test_formats_importers.py
+++ b/myokit/tests/test_formats_importers.py
@@ -5,9 +5,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import os
 import unittest
 
@@ -16,18 +13,6 @@
 
 from myokit.tests import DIR_FORMATS
 
-# Unit testing in Python 2 and 3
-try:
-    unittest.TestCase.assertRaisesRegex
-except AttributeError:  # pragma: no python 3 cover
-    unittest.TestCase.assertRaisesRegex = unittest.TestCase.assertRaisesRegexp
-
-# Strings in Python 2 and 3
-try:
-    basestring
-except NameError:   # pragma: no python 2 cover
-    basestring = str
-
 
 class ImporterTest(unittest.TestCase):
     """ Test shared importer functionality. """
@@ -37,7 +22,7 @@ def test_importer_interface(self):
         ims = myokit.formats.importers()
         self.assertTrue(len(ims) > 0)
         for i in ims:
-            self.assertIsInstance(i, basestring)
+            self.assertIsInstance(i, str)
             i = myokit.formats.importer(i)
             self.assertTrue(isinstance(i, myokit.formats.Importer))
 
diff --git a/myokit/tests/test_formats_mathml_content.py b/myokit/tests/test_formats_mathml_content.py
index c103eecf2..cf42c831e 100755
--- a/myokit/tests/test_formats_mathml_content.py
+++ b/myokit/tests/test_formats_mathml_content.py
@@ -5,9 +5,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import re
 import unittest
 
@@ -16,12 +13,6 @@
 import myokit
 import myokit.formats.mathml as mathml
 
-# Unit testing in Python 2 and 3
-try:
-    unittest.TestCase.assertRaisesRegex
-except AttributeError:
-    unittest.TestCase.assertRaisesRegex = unittest.TestCase.assertRaisesRegexp
-
 
 class ContentMathMLParserTest(unittest.TestCase):
     """ Tests content MathML parsing. """
diff --git a/myokit/tests/test_formats_mathml_presentation.py b/myokit/tests/test_formats_mathml_presentation.py
index ad7980b82..206d9efbc 100755
--- a/myokit/tests/test_formats_mathml_presentation.py
+++ b/myokit/tests/test_formats_mathml_presentation.py
@@ -5,21 +5,12 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import re
 import unittest
 
 import myokit
 import myokit.formats.mathml as mathml
 
-# Unit testing in Python 2 and 3
-try:
-    unittest.TestCase.assertRaisesRegex
-except AttributeError:
-    unittest.TestCase.assertRaisesRegex = unittest.TestCase.assertRaisesRegexp
-
 
 class PresentationMathMLTest(unittest.TestCase):
     """
diff --git a/myokit/tests/test_formats_opencl.py b/myokit/tests/test_formats_opencl.py
index 71dad9db5..88c185e9e 100755
--- a/myokit/tests/test_formats_opencl.py
+++ b/myokit/tests/test_formats_opencl.py
@@ -5,9 +5,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import unittest
 
 import myokit
@@ -15,13 +12,6 @@
 import myokit.formats.opencl
 
 
-# Unit testing in Python 2 and 3
-try:
-    unittest.TestCase.assertRaisesRegex
-except AttributeError:  # pragma: no python 3 cover
-    unittest.TestCase.assertRaisesRegex = unittest.TestCase.assertRaisesRegexp
-
-
 class OpenCLExpressionWriterTest(unittest.TestCase):
     """ Test the OpenCL ewriter class. """
 
diff --git a/myokit/tests/test_formats_sbml.py b/myokit/tests/test_formats_sbml.py
index 0c63c81d4..6edc1c362 100755
--- a/myokit/tests/test_formats_sbml.py
+++ b/myokit/tests/test_formats_sbml.py
@@ -5,9 +5,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import os
 import unittest
 
@@ -20,18 +17,6 @@
 # from shared import DIR_FORMATS, WarningCollector
 from myokit.tests import DIR_FORMATS, WarningCollector
 
-# Unit testing in Python 2 and 3
-try:
-    unittest.TestCase.assertRaisesRegex
-except AttributeError:  # pragma: no python 3 cover
-    unittest.TestCase.assertRaisesRegex = unittest.TestCase.assertRaisesRegexp
-
-# Strings in Python 2 and 3
-try:
-    basestring
-except NameError:   # pragma: no python 2 cover
-    basestring = str
-
 
 class SBMLImporterTest(unittest.TestCase):
     """
diff --git a/myokit/tests/test_formats_sympy.py b/myokit/tests/test_formats_sympy.py
index abd16e353..900c6af3b 100755
--- a/myokit/tests/test_formats_sympy.py
+++ b/myokit/tests/test_formats_sympy.py
@@ -5,20 +5,11 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import unittest
 
 import myokit
 import myokit.formats.sympy as mypy
 
-# Unit testing in Python 2 and 3
-try:
-    unittest.TestCase.assertRaisesRegex
-except AttributeError:
-    unittest.TestCase.assertRaisesRegex = unittest.TestCase.assertRaisesRegexp
-
 
 class SymPyReadWriteTest(unittest.TestCase):
     """
diff --git a/myokit/tests/test_formats_wcp.py b/myokit/tests/test_formats_wcp.py
index e974e3010..3d7998d4e 100755
--- a/myokit/tests/test_formats_wcp.py
+++ b/myokit/tests/test_formats_wcp.py
@@ -5,11 +5,9 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import os
 import unittest
+
 import numpy as np
 
 import myokit.formats.wcp as wcp
diff --git a/myokit/tests/test_io.py b/myokit/tests/test_io.py
index 435863892..2f8bf6886 100755
--- a/myokit/tests/test_io.py
+++ b/myokit/tests/test_io.py
@@ -6,23 +6,15 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import os
 import unittest
+
 import numpy as np
 
 import myokit
 
 from myokit.tests import DIR_DATA, TemporaryDirectory
 
-# Strings in Python 2 and 3
-try:
-    basestring
-except NameError:
-    basestring = str
-
 
 class LoadSaveMmtTest(unittest.TestCase):
     """Tests load/save functions for mmt files."""
@@ -105,7 +97,7 @@ def test_load_partial(self):
         m, p, x = myokit.load(spath)
         self.assertIsNone(m)
         self.assertIsNone(p)
-        self.assertTrue(isinstance(x, basestring))
+        self.assertTrue(isinstance(x, str))
 
         self.assertRaises(
             myokit.SectionNotFoundError, myokit.load_model, ppath)
@@ -128,7 +120,7 @@ def test_save(self):
         m, p, x = myokit.load('example')
         self.assertIsInstance(m, myokit.Model)
         self.assertIsInstance(p, myokit.Protocol)
-        self.assertTrue(isinstance(x, basestring))
+        self.assertTrue(isinstance(x, str))
 
         # Save all three and reload
         with TemporaryDirectory() as d:
@@ -318,10 +310,10 @@ def test_save_script(self):
         ipath = os.path.join(DIR_DATA, 'lr-1991.mmt')
         # Test example loading
         x = myokit.load_script('example')
-        self.assertTrue(isinstance(x, basestring))
+        self.assertTrue(isinstance(x, str))
         # Test file loading
         x = myokit.load_script(ipath)
-        self.assertTrue(isinstance(x, basestring))
+        self.assertTrue(isinstance(x, str))
         with TemporaryDirectory() as d:
             opath = d.path('test.mmt')
             myokit.save_script(opath, x)
@@ -333,7 +325,7 @@ def test_save_script(self):
             self.assertTrue('[[script]]' in text)
             # Test reloading
             xx = myokit.load_script(opath)
-            self.assertTrue(isinstance(xx, basestring))
+            self.assertTrue(isinstance(xx, str))
             self.assertEqual(x, xx)
 
 
diff --git a/myokit/tests/test_jacobian_calculator.py b/myokit/tests/test_jacobian_calculator.py
index 571d64194..3acc631af 100755
--- a/myokit/tests/test_jacobian_calculator.py
+++ b/myokit/tests/test_jacobian_calculator.py
@@ -5,23 +5,15 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import os
 import unittest
+
 import numpy as np
 
 import myokit
 
 from myokit.tests import DIR_DATA
 
-# Unit testing in Python 2 and 3
-try:
-    unittest.TestCase.assertRaisesRegex
-except AttributeError:
-    unittest.TestCase.assertRaisesRegex = unittest.TestCase.assertRaisesRegexp
-
 
 class JacobianCalculatorTest(unittest.TestCase):
     """
diff --git a/myokit/tests/test_jacobian_tracer.py b/myokit/tests/test_jacobian_tracer.py
index 62d784eeb..c663be721 100755
--- a/myokit/tests/test_jacobian_tracer.py
+++ b/myokit/tests/test_jacobian_tracer.py
@@ -5,9 +5,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import os
 import unittest
 
@@ -15,12 +12,6 @@
 
 from myokit.tests import DIR_DATA
 
-# Unit testing in Python 2 and 3
-try:
-    unittest.TestCase.assertRaisesRegex
-except AttributeError:
-    unittest.TestCase.assertRaisesRegex = unittest.TestCase.assertRaisesRegexp
-
 
 class JacobianTracerTest(unittest.TestCase):
     """
diff --git a/myokit/tests/test_lib_deps.py b/myokit/tests/test_lib_deps.py
index d1707473f..970af910a 100755
--- a/myokit/tests/test_lib_deps.py
+++ b/myokit/tests/test_lib_deps.py
@@ -5,21 +5,12 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import random
 import unittest
 
 import myokit
 import myokit.lib.deps as deps
 
-# Unit testing in Python 2 and 3
-try:
-    unittest.TestCase.assertRaisesRegex
-except AttributeError:
-    unittest.TestCase.assertRaisesRegex = unittest.TestCase.assertRaisesRegexp
-
 
 class LibDepsTest(unittest.TestCase):
 
diff --git a/myokit/tests/test_lib_guess.py b/myokit/tests/test_lib_guess.py
index 701f2bd4f..761c4a379 100755
--- a/myokit/tests/test_lib_guess.py
+++ b/myokit/tests/test_lib_guess.py
@@ -5,20 +5,11 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import unittest
 
 import myokit
 import myokit.lib.guess as guess
 
-# Unit testing in Python 2 and 3
-try:
-    unittest.TestCase.assertRaisesRegex
-except AttributeError:
-    unittest.TestCase.assertRaisesRegex = unittest.TestCase.assertRaisesRegexp
-
 
 class LibGuessTest(unittest.TestCase):
     """
diff --git a/myokit/tests/test_lib_hh.py b/myokit/tests/test_lib_hh.py
index 7f8ed25ea..f19c14b4e 100755
--- a/myokit/tests/test_lib_hh.py
+++ b/myokit/tests/test_lib_hh.py
@@ -5,11 +5,9 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import os
 import unittest
+
 import numpy as np
 
 import myokit
@@ -17,12 +15,6 @@
 
 from myokit.tests import DIR_DATA
 
-# Unit testing in Python 2 and 3
-try:
-    unittest.TestCase.assertRaisesRegex
-except AttributeError:
-    unittest.TestCase.assertRaisesRegex = unittest.TestCase.assertRaisesRegexp
-
 
 MODEL = """
 [[model]]
diff --git a/myokit/tests/test_lib_markov.py b/myokit/tests/test_lib_markov.py
index 362268286..eaf0b0f95 100755
--- a/myokit/tests/test_lib_markov.py
+++ b/myokit/tests/test_lib_markov.py
@@ -5,11 +5,9 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import os
 import unittest
+
 import numpy as np
 
 import myokit
@@ -17,12 +15,6 @@
 
 from myokit.tests import DIR_DATA, WarningCollector
 
-# Unit testing in Python 2 and 3
-try:
-    unittest.TestCase.assertRaisesRegex
-except AttributeError:
-    unittest.TestCase.assertRaisesRegex = unittest.TestCase.assertRaisesRegexp
-
 
 class LinearModelTest(unittest.TestCase):
     """
diff --git a/myokit/tests/test_lib_multi.py b/myokit/tests/test_lib_multi.py
index c439c95e3..df542e09d 100755
--- a/myokit/tests/test_lib_multi.py
+++ b/myokit/tests/test_lib_multi.py
@@ -5,9 +5,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import os
 import unittest
 
@@ -15,12 +12,6 @@
 
 from myokit.tests import DIR_DATA, WarningCollector
 
-# Unit testing in Python 2 and 3
-try:
-    unittest.TestCase.assertRaisesRegex
-except AttributeError:
-    unittest.TestCase.assertRaisesRegex = unittest.TestCase.assertRaisesRegexp
-
 # Path to multi-model testing files
 DIR_MULTI = os.path.join(DIR_DATA, 'multi')
 
diff --git a/myokit/tests/test_lib_plots.py b/myokit/tests/test_lib_plots.py
index 468a670e5..b46a52a09 100755
--- a/myokit/tests/test_lib_plots.py
+++ b/myokit/tests/test_lib_plots.py
@@ -6,9 +6,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import os
 import unittest
 
diff --git a/myokit/tests/test_meta.py b/myokit/tests/test_meta.py
index 7249be695..9936355eb 100755
--- a/myokit/tests/test_meta.py
+++ b/myokit/tests/test_meta.py
@@ -5,9 +5,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import unittest
 
 import myokit
diff --git a/myokit/tests/test_model.py b/myokit/tests/test_model.py
index ca8a20ba3..03fb46c07 100755
--- a/myokit/tests/test_model.py
+++ b/myokit/tests/test_model.py
@@ -8,9 +8,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import pickle
 import re
 import unittest
@@ -20,13 +17,6 @@
 from myokit.tests import TemporaryDirectory, WarningCollector
 
 
-# Unit testing in Python 2 and 3
-try:
-    unittest.TestCase.assertRaisesRegex
-except AttributeError:
-    unittest.TestCase.assertRaisesRegex = unittest.TestCase.assertRaisesRegexp
-
-
 class ModelTest(unittest.TestCase):
     """
     Tests parts of :class:`myokit.Model`.
diff --git a/myokit/tests/test_model_building.py b/myokit/tests/test_model_building.py
index b164434cb..c2872c85f 100755
--- a/myokit/tests/test_model_building.py
+++ b/myokit/tests/test_model_building.py
@@ -8,24 +8,9 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import myokit
 import unittest
 
-# Strings in Python 2 and 3
-try:
-    basestring
-except NameError:
-    basestring = str
-
-# Unit testing in Python 2 and 3
-try:
-    unittest.TestCase.assertRaisesRegex
-except AttributeError:
-    unittest.TestCase.assertRaisesRegex = unittest.TestCase.assertRaisesRegexp
-
 # Further model API tests are found in:
 #  - test_dependency_checking.py
 
@@ -300,10 +285,10 @@ def has(*v):
 
         # Test dependency mapping
         def has(var, *dps):
-            lst = vrs[m.get(var).lhs() if isinstance(var, basestring) else var]
+            lst = vrs[m.get(var).lhs() if isinstance(var, str) else var]
             self.assertEqual(len(lst), len(dps))
             for d in dps:
-                d = m.get(d).lhs() if isinstance(d, basestring) else d
+                d = m.get(d).lhs() if isinstance(d, str) else d
                 self.assertIn(d, lst)
 
         vrs = m.map_shallow_dependencies(omit_states=False)
diff --git a/myokit/tests/test_opencl_info.py b/myokit/tests/test_opencl_info.py
index f2459966e..888b5d925 100755
--- a/myokit/tests/test_opencl_info.py
+++ b/myokit/tests/test_opencl_info.py
@@ -5,21 +5,12 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import unittest
 
 import myokit
 
 from myokit.tests import TemporaryDirectory, OpenCL_FOUND
 
-# Strings in Python2 and Python3
-try:
-    basestring
-except NameError:
-    basestring = str
-
 
 @unittest.skipIf(not OpenCL_FOUND, 'OpenCL not found on this system.')
 class OpenCLTest(unittest.TestCase):
@@ -35,12 +26,12 @@ def test_current_info(self):
         # Tests the method to query the current device
         self.assertIsInstance(
             myokit.OpenCL.current_info(), myokit.OpenCLPlatformInfo)
-        self.assertIsInstance(myokit.OpenCL.current_info(True), basestring)
+        self.assertIsInstance(myokit.OpenCL.current_info(True), str)
 
     def test_info(self):
         # Tests the method to query the current device
         self.assertIsInstance(myokit.OpenCL.info(), myokit.OpenCLInfo)
-        self.assertIsInstance(myokit.OpenCL.info(True), basestring)
+        self.assertIsInstance(myokit.OpenCL.info(True), str)
 
     def test_load_save_selection(self):
         # Tests the load_selection method
@@ -83,9 +74,9 @@ def test_selection_info(self):
         item = info[0]
         self.assertEqual(len(item), 3)
         platform, device, specs = item
-        self.assertIsInstance(platform, basestring)
-        self.assertIsInstance(device, basestring)
-        self.assertIsInstance(specs, basestring)
+        self.assertIsInstance(platform, str)
+        self.assertIsInstance(device, str)
+        self.assertIsInstance(specs, str)
 
     def test_supported(self):
         # Tested as condition to get in, so must be true
diff --git a/myokit/tests/test_pacing_factory.py b/myokit/tests/test_pacing_factory.py
index 5417102f5..958b643b3 100755
--- a/myokit/tests/test_pacing_factory.py
+++ b/myokit/tests/test_pacing_factory.py
@@ -5,9 +5,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import unittest
 
 import myokit
diff --git a/myokit/tests/test_pacing_system_c.py b/myokit/tests/test_pacing_system_c.py
index ac90423a9..6f7a283db 100755
--- a/myokit/tests/test_pacing_system_c.py
+++ b/myokit/tests/test_pacing_system_c.py
@@ -5,9 +5,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import unittest
 import numpy as np
 
@@ -16,12 +13,6 @@
 from myokit.tests.ansic_event_based_pacing import AnsicEventBasedPacing
 from myokit.tests.ansic_fixed_form_pacing import AnsicFixedFormPacing
 
-# Unit testing in Python 2 and 3
-try:
-    unittest.TestCase.assertRaisesRegex
-except AttributeError:
-    unittest.TestCase.assertRaisesRegex = unittest.TestCase.assertRaisesRegexp
-
 
 class EventBasedPacingAnsicTest(unittest.TestCase):
     """
diff --git a/myokit/tests/test_pacing_system_py.py b/myokit/tests/test_pacing_system_py.py
index d4d90f971..8a3c524e5 100644
--- a/myokit/tests/test_pacing_system_py.py
+++ b/myokit/tests/test_pacing_system_py.py
@@ -5,19 +5,10 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import unittest
 
 import myokit
 
-# Unit testing in Python 2 and 3
-try:
-    unittest.TestCase.assertRaisesRegex
-except AttributeError:
-    unittest.TestCase.assertRaisesRegex = unittest.TestCase.assertRaisesRegexp
-
 
 class EventBasedPacingPythonTest(unittest.TestCase):
     """
diff --git a/myokit/tests/test_parsing.py b/myokit/tests/test_parsing.py
index a647f3df5..dcea7cfa3 100755
--- a/myokit/tests/test_parsing.py
+++ b/myokit/tests/test_parsing.py
@@ -6,9 +6,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import os
 import unittest
 
@@ -17,18 +14,6 @@
 
 from myokit.tests import DIR_DATA, TemporaryDirectory
 
-# Unit testing in Python 2 and 3
-try:
-    unittest.TestCase.assertRaisesRegex
-except AttributeError:
-    unittest.TestCase.assertRaisesRegex = unittest.TestCase.assertRaisesRegexp
-
-# Strings in Python 2 and 3
-try:
-    basestring
-except NameError:   # pragma: no python 2 cover
-    basestring = str
-
 
 class TokenizerTest(unittest.TestCase):
     """
@@ -908,29 +893,29 @@ def test_parse_script(self):
             '[[script]]',
         )
         script = parse_script(code)
-        self.assertIsInstance(script, basestring)
+        self.assertIsInstance(script, str)
         script = parse_script(''.join(code))
-        self.assertIsInstance(script, basestring)
+        self.assertIsInstance(script, str)
 
         code = (
             '[[script]]\n',
         )
         script = parse_script(code)
-        self.assertIsInstance(script, basestring)
+        self.assertIsInstance(script, str)
 
         code = (
             '[[script]]\n',
             'print("hi")',
         )
         script = parse_script(code)
-        self.assertIsInstance(script, basestring)
+        self.assertIsInstance(script, str)
 
         code = (
             '[[script]]\n',
             'print("hi")\n',
         )
         script = parse_script(code)
-        self.assertIsInstance(script, basestring)
+        self.assertIsInstance(script, str)
 
         # Not a script
         code = (
diff --git a/myokit/tests/test_progress_reporters.py b/myokit/tests/test_progress_reporters.py
index 55098ee66..86eaca43f 100755
--- a/myokit/tests/test_progress_reporters.py
+++ b/myokit/tests/test_progress_reporters.py
@@ -5,9 +5,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import unittest
 import re
 import time
diff --git a/myokit/tests/test_protocol.py b/myokit/tests/test_protocol.py
index 2c3ec0dc4..e60a400a6 100755
--- a/myokit/tests/test_protocol.py
+++ b/myokit/tests/test_protocol.py
@@ -5,9 +5,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import pickle
 import unittest
 
@@ -15,12 +12,6 @@
 
 from myokit.tests import WarningCollector
 
-# Unit testing in Python 2 and 3
-try:
-    unittest.TestCase.assertRaisesRegex
-except AttributeError:
-    unittest.TestCase.assertRaisesRegex = unittest.TestCase.assertRaisesRegexp
-
 
 class ProtocolTest(unittest.TestCase):
     """
diff --git a/myokit/tests/test_protocol_floating_point.py b/myokit/tests/test_protocol_floating_point.py
index 9e79fdde7..4be149e7f 100755
--- a/myokit/tests/test_protocol_floating_point.py
+++ b/myokit/tests/test_protocol_floating_point.py
@@ -26,21 +26,12 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import unittest
 
 import myokit
 
 from myokit.tests.ansic_event_based_pacing import AnsicEventBasedPacing
 
-# Unit testing in Python 2 and 3
-try:
-    unittest.TestCase.assertRaisesRegex
-except AttributeError:
-    unittest.TestCase.assertRaisesRegex = unittest.TestCase.assertRaisesRegexp
-
 
 class ProtocolFloatingPointTest(unittest.TestCase):
 
diff --git a/myokit/tests/test_protocol_time_series.py b/myokit/tests/test_protocol_time_series.py
index 9a63e6cdf..e320af10d 100755
--- a/myokit/tests/test_protocol_time_series.py
+++ b/myokit/tests/test_protocol_time_series.py
@@ -5,22 +5,12 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import unittest
 import pickle
 
 import myokit
 
 
-# Unit testing in Python 2 and 3
-try:
-    unittest.TestCase.assertRaisesRegex
-except AttributeError:
-    unittest.TestCase.assertRaisesRegex = unittest.TestCase.assertRaisesRegexp
-
-
 class TimeSeriesProtocolTest(unittest.TestCase):
     """
     Tests the TimeSeriesProtocol class.
diff --git a/myokit/tests/test_pype.py b/myokit/tests/test_pype.py
index 57b68b4ec..46b2e6046 100755
--- a/myokit/tests/test_pype.py
+++ b/myokit/tests/test_pype.py
@@ -5,9 +5,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import unittest
 
 import myokit
@@ -15,12 +12,6 @@
 
 from myokit.tests import TemporaryDirectory
 
-# Unit testing in Python 2 and 3
-try:
-    unittest.TestCase.assertRaisesRegex
-except AttributeError:
-    unittest.TestCase.assertRaisesRegex = unittest.TestCase.assertRaisesRegexp
-
 
 class PypeTest(unittest.TestCase):
 
diff --git a/myokit/tests/test_quantity.py b/myokit/tests/test_quantity.py
index ce666ebf5..0c14c7a05 100755
--- a/myokit/tests/test_quantity.py
+++ b/myokit/tests/test_quantity.py
@@ -5,19 +5,10 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import unittest
 
 import myokit
 
-# Unit testing in Python 2 and 3
-try:
-    unittest.TestCase.assertRaisesRegex
-except AttributeError:
-    unittest.TestCase.assertRaisesRegex = unittest.TestCase.assertRaisesRegexp
-
 
 class QuantityTest(unittest.TestCase):
     """
diff --git a/myokit/tests/test_rhs_benchmarker.py b/myokit/tests/test_rhs_benchmarker.py
index 479faaf9a..a987af775 100755
--- a/myokit/tests/test_rhs_benchmarker.py
+++ b/myokit/tests/test_rhs_benchmarker.py
@@ -5,20 +5,12 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import unittest
+
 import numpy as np
 
 import myokit
 
-# Unit testing in Python 2 and 3
-try:
-    unittest.TestCase.assertRaisesRegex
-except AttributeError:
-    unittest.TestCase.assertRaisesRegex = unittest.TestCase.assertRaisesRegexp
-
 
 class RhsBenchmarkerTest(unittest.TestCase):
     """
diff --git a/myokit/tests/test_sbml_api.py b/myokit/tests/test_sbml_api.py
index b8d92cd7f..a16f42578 100755
--- a/myokit/tests/test_sbml_api.py
+++ b/myokit/tests/test_sbml_api.py
@@ -5,9 +5,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import unittest
 
 import myokit
@@ -18,18 +15,6 @@
 
 from myokit.tests import WarningCollector
 
-# Unit testing in Python 2 and 3
-try:
-    unittest.TestCase.assertRaisesRegex
-except AttributeError:  # pragma: no python 3 cover
-    unittest.TestCase.assertRaisesRegex = unittest.TestCase.assertRaisesRegexp
-
-# Strings in Python 2 and 3
-try:
-    basestring
-except NameError:   # pragma: no python 2 cover
-    basestring = str
-
 
 class TestCompartment(unittest.TestCase):
     """
diff --git a/myokit/tests/test_sbml_parser.py b/myokit/tests/test_sbml_parser.py
index 6de6b3d3f..2da6776a7 100755
--- a/myokit/tests/test_sbml_parser.py
+++ b/myokit/tests/test_sbml_parser.py
@@ -5,9 +5,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import unittest
 
 import myokit
@@ -17,18 +14,6 @@
 
 from myokit.tests import WarningCollector
 
-# Unit testing in Python 2 and 3
-try:
-    unittest.TestCase.assertRaisesRegex
-except AttributeError:  # pragma: no python 3 cover
-    unittest.TestCase.assertRaisesRegex = unittest.TestCase.assertRaisesRegexp
-
-# Strings in Python 2 and 3
-try:
-    basestring
-except NameError:   # pragma: no python 2 cover
-    basestring = str
-
 
 class TestSBMLParser(unittest.TestCase):
     """
diff --git a/myokit/tests/test_simulation_1d.py b/myokit/tests/test_simulation_1d.py
index 50978451e..29893625c 100755
--- a/myokit/tests/test_simulation_1d.py
+++ b/myokit/tests/test_simulation_1d.py
@@ -5,24 +5,15 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import os
 import unittest
+
 import numpy as np
 
 import myokit
 
 from myokit.tests import DIR_DATA, CancellingReporter
 
-# Unit testing in Python 2 and 3
-try:
-    unittest.TestCase.assertRaisesRegex
-except AttributeError:
-    unittest.TestCase.assertRaisesRegex = unittest.TestCase.assertRaisesRegexp
-
-
 # Show simulation output
 debug = False
 
diff --git a/myokit/tests/test_simulation_cvodes.py b/myokit/tests/test_simulation_cvodes.py
index 5b5c72423..23637f45e 100755
--- a/myokit/tests/test_simulation_cvodes.py
+++ b/myokit/tests/test_simulation_cvodes.py
@@ -5,9 +5,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import os
 import pickle
 import platform
@@ -26,12 +23,6 @@
     WarningCollector,
 )
 
-# Unit testing in Python 2 and 3
-try:
-    unittest.TestCase.assertRaisesRegex
-except AttributeError:
-    unittest.TestCase.assertRaisesRegex = unittest.TestCase.assertRaisesRegexp
-
 
 class SimulationTest(unittest.TestCase):
     """
diff --git a/myokit/tests/test_simulation_cvodes_from_disk.py b/myokit/tests/test_simulation_cvodes_from_disk.py
index fa6fbe10b..c968686d7 100755
--- a/myokit/tests/test_simulation_cvodes_from_disk.py
+++ b/myokit/tests/test_simulation_cvodes_from_disk.py
@@ -5,9 +5,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import os
 import unittest
 
diff --git a/myokit/tests/test_simulation_fiber_tissue.py b/myokit/tests/test_simulation_fiber_tissue.py
index 6a453e718..a6a99b1be 100755
--- a/myokit/tests/test_simulation_fiber_tissue.py
+++ b/myokit/tests/test_simulation_fiber_tissue.py
@@ -5,11 +5,9 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import os
 import unittest
+
 import numpy as np
 
 import myokit
@@ -21,13 +19,6 @@
     WarningCollector,
 )
 
-# Unit testing in Python 2 and 3
-try:
-    unittest.TestCase.assertRaisesRegex
-except AttributeError:
-    unittest.TestCase.assertRaisesRegex = unittest.TestCase.assertRaisesRegexp
-
-
 # Show simulation output
 debug = False
 
diff --git a/myokit/tests/test_simulation_log_interval.py b/myokit/tests/test_simulation_log_interval.py
index dc5673a92..82a720ab2 100755
--- a/myokit/tests/test_simulation_log_interval.py
+++ b/myokit/tests/test_simulation_log_interval.py
@@ -5,9 +5,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import os
 import unittest
 
@@ -17,12 +14,6 @@
 
 from myokit.tests import DIR_DATA
 
-# Unit testing in Python 2 and 3
-try:
-    unittest.TestCase.assertRaisesRegex
-except AttributeError:
-    unittest.TestCase.assertRaisesRegex = unittest.TestCase.assertRaisesRegexp
-
 # Extra output
 debug = False
 
diff --git a/myokit/tests/test_simulation_opencl.py b/myokit/tests/test_simulation_opencl.py
index a4cdc34fe..43d7ddfe6 100755
--- a/myokit/tests/test_simulation_opencl.py
+++ b/myokit/tests/test_simulation_opencl.py
@@ -8,11 +8,9 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import os
 import unittest
+
 import numpy as np
 
 import myokit
@@ -25,13 +23,6 @@
     WarningCollector,
 )
 
-# Unit testing in Python 2 and 3
-try:
-    unittest.TestCase.assertRaisesRegex
-except AttributeError:
-    unittest.TestCase.assertRaisesRegex = unittest.TestCase.assertRaisesRegexp
-
-
 # Show simulation output
 debug = False
 
diff --git a/myokit/tests/test_simulation_opencl_log_interval.py b/myokit/tests/test_simulation_opencl_log_interval.py
index 3dedbdec0..66cf657bc 100755
--- a/myokit/tests/test_simulation_opencl_log_interval.py
+++ b/myokit/tests/test_simulation_opencl_log_interval.py
@@ -5,11 +5,9 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import os
 import unittest
+
 import numpy as np
 
 import myokit
diff --git a/myokit/tests/test_simulation_opencl_vs_cvode.py b/myokit/tests/test_simulation_opencl_vs_cvode.py
index fa5dd77b4..ac7e707c8 100755
--- a/myokit/tests/test_simulation_opencl_vs_cvode.py
+++ b/myokit/tests/test_simulation_opencl_vs_cvode.py
@@ -5,24 +5,15 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import os
 import unittest
+
 import numpy as np
 
 import myokit
 
 from myokit.tests import OpenCL_DOUBLE_PRECISION, DIR_DATA
 
-# Unit testing in Python 2 and 3
-try:
-    unittest.TestCase.assertRaisesRegex
-except AttributeError:
-    unittest.TestCase.assertRaisesRegex = unittest.TestCase.assertRaisesRegexp
-
-
 # Show simulation output
 debug = False
 
diff --git a/myokit/tests/test_simulation_opencl_vs_sim1d.py b/myokit/tests/test_simulation_opencl_vs_sim1d.py
index 141721cb2..2793da644 100755
--- a/myokit/tests/test_simulation_opencl_vs_sim1d.py
+++ b/myokit/tests/test_simulation_opencl_vs_sim1d.py
@@ -5,24 +5,15 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import os
 import unittest
+
 import numpy as np
 
 import myokit
 
 from myokit.tests import OpenCL_DOUBLE_PRECISION, DIR_DATA
 
-# Unit testing in Python 2 and 3
-try:
-    unittest.TestCase.assertRaisesRegex
-except AttributeError:
-    unittest.TestCase.assertRaisesRegex = unittest.TestCase.assertRaisesRegexp
-
-
 # Show simulation output
 debug = False
 
diff --git a/myokit/tests/test_system_info.py b/myokit/tests/test_system_info.py
index cdd1250da..6628c8080 100755
--- a/myokit/tests/test_system_info.py
+++ b/myokit/tests/test_system_info.py
@@ -5,21 +5,11 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import unittest
 
 import myokit
 
 
-# Strings in Python2 and Python3
-try:
-    basestring
-except NameError:   # pragma: no cover
-    basestring = str
-
-
 class SystemInfoTest(unittest.TestCase):
     """
     Tests the system info method.
@@ -29,7 +19,7 @@ def test_system_info(self):
         import matplotlib
         matplotlib.use('template')
 
-        self.assertIsInstance(myokit.system(), basestring)
+        self.assertIsInstance(myokit.system(), str)
         with myokit.tools.capture():
             myokit.system(live_printing=True)
 
diff --git a/myokit/tests/test_tools.py b/myokit/tests/test_tools.py
index bbf4c39a1..0c9c05a67 100755
--- a/myokit/tests/test_tools.py
+++ b/myokit/tests/test_tools.py
@@ -5,9 +5,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import os
 import re
 import sys
@@ -15,12 +12,6 @@
 import time
 import unittest
 
-# Strings in Python2 and Python3
-try:
-    basestring
-except NameError:   # pragma: no cover
-    basestring = str
-
 import myokit
 
 from myokit.tests import TemporaryDirectory
diff --git a/myokit/tests/test_unit.py b/myokit/tests/test_unit.py
index 4af40e96f..2f547558e 100755
--- a/myokit/tests/test_unit.py
+++ b/myokit/tests/test_unit.py
@@ -5,19 +5,10 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import unittest
 
 import myokit
 
-# Unit testing in Python 2 and 3
-try:
-    unittest.TestCase.assertRaisesRegex
-except AttributeError:
-    unittest.TestCase.assertRaisesRegex = unittest.TestCase.assertRaisesRegexp
-
 
 class MyokitUnitTest(unittest.TestCase):
 
diff --git a/myokit/tests/test_user_functions.py b/myokit/tests/test_user_functions.py
index 21d5beda2..5e50c09e5 100644
--- a/myokit/tests/test_user_functions.py
+++ b/myokit/tests/test_user_functions.py
@@ -5,19 +5,9 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import unittest
 import myokit
 
-# Unit testing in Python 2 and 3
-try:
-    unittest.TestCase.assertRaisesRegex
-except AttributeError:
-    unittest.TestCase.assertRaisesRegex = unittest.TestCase.assertRaisesRegexp
-
-
 # Further testing in test_model.py
 
 
diff --git a/myokit/tests/test_variable.py b/myokit/tests/test_variable.py
index 3bbaaaad4..4adf02bdf 100755
--- a/myokit/tests/test_variable.py
+++ b/myokit/tests/test_variable.py
@@ -5,9 +5,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-from __future__ import absolute_import, division
-from __future__ import print_function, unicode_literals
-
 import unittest
 
 import numpy as np
@@ -17,13 +14,6 @@
 from myokit.tests import WarningCollector
 
 
-# Unit testing in Python 2 and 3
-try:
-    unittest.TestCase.assertRaisesRegex
-except AttributeError:
-    unittest.TestCase.assertRaisesRegex = unittest.TestCase.assertRaisesRegexp
-
-
 class VariableTest(unittest.TestCase):
     """
     Tests parts of :class:`myokit.Variable`.