diff --git a/.pep8speaks.yml b/.pep8speaks.yml
index 0a0d8c619f..1d8c6f39e9 100644
--- a/.pep8speaks.yml
+++ b/.pep8speaks.yml
@@ -1,12 +1,14 @@
 scanner:
-    diff_only: True  # Only show errors caused by the patch
+    # Only show errors caused by the patch. False means we check everything
+    # according to the config for flake8 in setup.cfg
+    diff_only: False
     linter: flake8
 
-message:  # Customize the comment made by the bot
-    opened:  # Messages when a new PR is submitted
+message: # Customize the comment made by the bot
+    opened: # Messages when a new PR is submitted
         header: "Hello @{name}, thank you for submitting the Pull Request!"
         footer: "To test for issues locally, `pip install flake8` and then run `flake8 nibabel`."
-    updated:  # Messages when new commits are added to the PR
+    updated: # Messages when new commits are added to the PR
         header: "Hello @{name}, Thank you for updating!"
         footer: "To test for issues locally, `pip install flake8` and then run `flake8 nibabel`."
     no_errors: "Cheers! There are no style issues detected in this Pull Request. :beers: "
diff --git a/Makefile b/Makefile
index 2190f815fc..af060fcad1 100644
--- a/Makefile
+++ b/Makefile
@@ -8,7 +8,7 @@ PROJECT=nibabel
 # The Python executable to be used
 #
 PYTHON ?= python
-NOSETESTS = $(PYTHON) $(shell which nosetests)
+TEST_RUNNER ?= pytest
 
 #
 # Determine details on the Python/system
@@ -66,7 +66,6 @@ distclean: clean
 		 -o -iname '#*#' | xargs -L10 rm -f
 	-rm -r dist
 	-rm build-stamp
-	-rm -r .tox
 #	-rm tests/data/*.hdr.* tests/data/*.img.* tests/data/something.nii \
 #		tests/data/noise* tests/data/None.nii
 
@@ -83,22 +82,23 @@ $(WWW_DIR):
 # Tests
 #
 
-test: unittest testmanual
+test: test-style unittest testmanual
 
-
-ut-%: build
-	@PYTHONPATH=.:$(PYTHONPATH) $(NOSETESTS) nibabel/tests/test_$*.py
-
-
-unittest: build
-	@PYTHONPATH=.:$(PYTHONPATH) $(NOSETESTS) nibabel --with-doctest
+unittest: build test-clean
+	export CHECK_TYPE=test; ./tools/ci/check.sh
 
 testmanual: build
-	@cd doc/source && PYTHONPATH=../..:$(PYTHONPATH) $(NOSETESTS) --with-doctest --doctest-extension=.rst . dicom
+	export CHECK_TYPE=doc; ./tools/ci/check.sh
 
+coverage: unittest
 
-coverage: build
-	@PYTHONPATH=.:$(PYTHONPATH) $(NOSETESTS) --with-coverage --cover-package=nibabel
+.PHONY: test-style
+test-style:
+	export CHECK_TYPE=style; ./tools/ci/check.sh
+
+.PHONY: test-clean
+test-clean:
+	rm -rf for_testing
 
 
 #
@@ -252,11 +252,13 @@ sdist-venv: clean
 	rm -rf dist venv
 	unset PYTHONPATH && $(PYTHON) setup.py sdist --formats=zip
 	virtualenv --system-site-packages --python=$(PYTHON) venv
-	. venv/bin/activate && pip install --ignore-installed nose
+	. venv/bin/activate && pip install -r dev-requirements.txt
 	mkdir venv/tmp
 	cd venv/tmp && unzip ../../dist/*.zip
 	. venv/bin/activate && cd venv/tmp/nibabel* && python setup.py install
-	unset PYTHONPATH && . venv/bin/activate && cd venv && nosetests --with-doctest nibabel nisext
+	unset PYTHONPATH && . \
+		venv/bin/activate && \
+		cd venv && $(TEST_RUNNER) --doctest-modules --doctest-plus nibabel nisext
 
 source-release: distclean
 	$(PYTHON) -m compileall .
@@ -269,17 +271,7 @@ venv-tests:
 	make distclean
 	- rm -rf $(VIRTUAL_ENV)/lib/python$(PYVER)/site-packages/nibabel
 	$(PYTHON) setup.py install
-	cd .. && nosetests $(VIRTUAL_ENV)/lib/python$(PYVER)/site-packages/nibabel
-
-tox-fresh:
-	# tox tests with fresh-installed virtualenvs.  Needs network.  And
-	# pytox, obviously.
-	tox -c tox.ini
-
-tox-stale:
-	# tox tests with MB's already-installed virtualenvs (numpy and nose
-	# installed)
-	tox -e python25,python26,python27,python32,np-1.2.1
+	cd .. && $(TEST_RUNNER) $(VIRTUAL_ENV)/lib/python$(PYVER)/site-packages/nibabel
 
 refresh-readme:
 	$(PYTHON) tools/refresh_readme.py
diff --git a/dev-requirements.txt b/dev-requirements.txt
index 69302061bc..a6485b1706 100644
--- a/dev-requirements.txt
+++ b/dev-requirements.txt
@@ -1,3 +1,5 @@
 # Requirements for running tests
 -r requirements.txt
 pytest
+pytest-doctestplus
+flake8
diff --git a/doc-requirements.txt b/doc-requirements.txt
index c934d76e6b..517bde85e9 100644
--- a/doc-requirements.txt
+++ b/doc-requirements.txt
@@ -1,6 +1,8 @@
 # Requirements for building docs
 -r requirements.txt
 sphinx<3
-numpydoc
+jinja2<3
+numpydoc<1.3
+MarkupSafe<2.1
 texext
 matplotlib >=1.3.1
diff --git a/doc/source/conf.py b/doc/source/conf.py
index 6155d0bc3b..c0ef7ac8a2 100644
--- a/doc/source/conf.py
+++ b/doc/source/conf.py
@@ -19,18 +19,17 @@
 # All configuration values have a default; values that are commented out
 # serve to show the default.
 
-import sys
 import os
 from runpy import run_path
 from configparser import ConfigParser
 
 # Check for external Sphinx extensions we depend on
 try:
-    import numpydoc
+    import numpydoc  # noqa: F401
 except ImportError:
     raise RuntimeError('Need to install "numpydoc" package for doc build')
 try:
-    import texext
+    import texext  # noqa: F401
 except ImportError:
     raise RuntimeError('Need to install "texext" package for doc build')
 
@@ -64,7 +63,6 @@
 # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
 extensions = ['sphinx.ext.autodoc',
               'sphinx.ext.doctest',
-              #'sphinx.ext.intersphinx',
               'sphinx.ext.todo',
               'sphinx.ext.mathjax',
               'sphinx.ext.inheritance_diagram',
@@ -76,7 +74,7 @@
 
 # the following doesn't work with sphinx < 1.0, but will make a separate
 # sphinx-autogen run obsolete in the future
-#autosummary_generate = True
+# autosummary_generate = True
 
 # Add any paths that contain templates here, relative to this directory.
 templates_path = ['_templates']
@@ -85,7 +83,7 @@
 source_suffix = '.rst'
 
 # The encoding of source files.
-#source_encoding = 'utf-8'
+# source_encoding = 'utf-8'
 
 # The master toctree document.
 master_doc = 'index'
@@ -105,11 +103,11 @@
 
 # The language for content autogenerated by Sphinx. Refer to documentation
 # for a list of supported languages.
-#language = None
+# language = None
 
 # There are two options for replacing |today|: either, you set today to some
 # non-false value, then it is used:
-#today = ''
+# today = ''
 # Else, today_fmt is used as the format for a strftime call.
 today_fmt = '%B %d, %Y, %H:%M PDT'
 
@@ -124,24 +122,24 @@
 exclude_trees = ['_build']
 
 # The reST default role (used for this markup: `text`) to use for all documents
-#default_role = None
+# default_role = None
 
 # If true, '()' will be appended to :func: etc. cross-reference text.
-#add_function_parentheses = True
+# add_function_parentheses = True
 
 # If true, the current module name will be prepended to all description
 # unit titles (such as .. function::).
-#add_module_names = True
+# add_module_names = True
 
 # If true, sectionauthor and moduleauthor directives will be shown in the
 # output. They are ignored by default.
-#show_authors = False
+# show_authors = False
 
 # The name of the Pygments (syntax highlighting) style to use.
 pygments_style = 'sphinx'
 
 # A list of ignored prefixes for module index sorting.
-#modindex_common_prefix = []
+# modindex_common_prefix = []
 
 # -- Sphinxext configuration --------------------------------------------------
 
@@ -168,26 +166,26 @@
 # Theme options are theme-specific and customize the look and feel of a theme
 # further.  For a list of options available for each theme, see the
 # documentation.
-#html_theme_options = {}
+# html_theme_options = {}
 
 # Add any paths that contain custom themes here, relative to this directory.
-#html_theme_path = []
+# html_theme_path = []
 
 # The name for this set of Sphinx documents.  If None, it defaults to
 # "<project> v<release> documentation".
-#html_title = ''
+# html_title = ''
 
 # A shorter title for the navigation bar.  Default is the same as html_title.
-#html_short_title = None
+# html_short_title = None
 
 # The name of an image file (relative to this directory) to place at the top
 # of the sidebar.
-#html_logo = None
+# html_logo = None
 
 # The name of an image file (within the static path) to use as favicon of the
 # docs.  This file should be a Windows icon file (.ico) being 16x16 or 32x32
 # pixels large.
-#html_favicon = None
+# html_favicon = None
 
 # Add any paths that contain custom static files (such as style sheets) here,
 # relative to this directory. They are copied after the builtin static files,
@@ -196,14 +194,14 @@
 
 # If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
 # using the given strftime format.
-#html_last_updated_fmt = '%b %d, %Y'
+# html_last_updated_fmt = '%b %d, %Y'
 
 # Content template for the index page.
 html_index = 'index.html'
 
 # If true, SmartyPants will be used to convert quotes and dashes to
 # typographically correct entities.
-#html_use_smartypants = True
+# html_use_smartypants = True
 
 # Custom sidebar templates, maps document names to template names.
 html_sidebars = {'index': ['localtoc.html', 'relations.html', 'sourcelink.html',
@@ -211,16 +209,16 @@
 
 # Additional templates that should be rendered to pages, maps page names to
 # template names.
-#html_additional_pages = {'index': 'index.html'}
+# html_additional_pages = {'index': 'index.html'}
 
 # If false, no module index is generated.
-#html_use_modindex = True
+# html_use_modindex = True
 
 # If false, no index is generated.
-#html_use_index = True
+# html_use_index = True
 
 # If true, the index is split into individual pages for each letter.
-#html_split_index = False
+# html_split_index = False
 
 # If true, links to the reST sources are added to the pages.
 html_show_sourcelink = True
@@ -228,10 +226,10 @@
 # If true, an OpenSearch description file will be output, and all pages will
 # contain a <link> tag referring to it.  The value of this option must be the
 # base URL from which the finished HTML is served.
-#html_use_opensearch = ''
+# html_use_opensearch = ''
 
 # If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml").
-#html_file_suffix = ''
+# html_file_suffix = ''
 
 # Output file base name for HTML help builder.
 htmlhelp_basename = 'nibabeldoc'
@@ -241,34 +239,34 @@
 # -- Options for LaTeX output -------------------------------------------------
 
 # The paper size ('letter' or 'a4').
-#latex_paper_size = 'letter'
+# latex_paper_size = 'letter'
 
 # The font size ('10pt', '11pt' or '12pt').
-#latex_font_size = '10pt'
+# latex_font_size = '10pt'
 
 # Grouping the document tree into LaTeX files. List of tuples
 # (source start file, target name, title, author,
-#  documentclass [howto/manual]).
+# documentclass [howto/manual]).
 latex_documents = [
     ('index', 'nibabel.tex', u'NiBabel Documentation', u'NiBabel Authors',
      'manual')]
 
 # The name of an image file (relative to this directory) to place at the top of
 # the title page.
-#latex_logo = None
+# latex_logo = None
 
 # For "manual" documents, if this is true, then toplevel headings are parts,
 # not chapters.
-#latex_use_parts = False
+# latex_use_parts = False
 
 # Additional stuff for the LaTeX preamble.
-#latex_preamble = ''
+# latex_preamble = ''
 
 # Documents to append as an appendix to all manuals.
-#latex_appendices = []
+# latex_appendices = []
 
 # If false, no module index is generated.
-#latex_use_modindex = True
+# latex_use_modindex = True
 
 
 # Example configuration for intersphinx: refer to the Python standard library.
diff --git a/doc/source/dicom/derivations/spm_dicom_orient.py b/doc/source/dicom/derivations/spm_dicom_orient.py
index 936e807ce1..033abc486e 100644
--- a/doc/source/dicom/derivations/spm_dicom_orient.py
+++ b/doc/source/dicom/derivations/spm_dicom_orient.py
@@ -46,7 +46,7 @@ def numbered_vector(nrows, symbol_prefix):
 R = zeros(4, 2)
 R[:3, :] = R3
 
-# The following is specific to the SPM algorithm. 
+# The following is specific to the SPM algorithm.
 x1 = ones(4, 1)
 y1 = ones(4, 1)
 y1[:3, :] = pos_pat_0
diff --git a/doc/source/scripts/make_coord_examples.py b/doc/source/scripts/make_coord_examples.py
index f763b28c28..d54650eb6f 100644
--- a/doc/source/scripts/make_coord_examples.py
+++ b/doc/source/scripts/make_coord_examples.py
@@ -68,6 +68,7 @@
 x, y = 0, 1
 # Make a rectangular box with these sides
 
+
 def make_ortho_box(bl, x_len, y_len):
     """ Make a box with sides parallel to the axes
     """
@@ -76,6 +77,7 @@ def make_ortho_box(bl, x_len, y_len):
                      [bl[x], bl[y] + y_len],
                      [bl[x] + x_len, bl[y] + y_len]))
 
+
 orth_epi_box = make_ortho_box(epi_bl, epi_x_len, epi_y_len)
 
 # Structural bounding box
@@ -126,6 +128,7 @@ def plot_localizer():
 def save_plot():
     # Plot using global variables
     plot_localizer()
+
     def vx2mm(pts):
         return pts - iso_center
     plot_box(vx2mm(rot_box), label='EPI bounding box')
diff --git a/doc/tools/apigen.py b/doc/tools/apigen.py
index 52966300e2..db1a15f9aa 100644
--- a/doc/tools/apigen.py
+++ b/doc/tools/apigen.py
@@ -407,7 +407,6 @@ def discover_modules(self):
 
     def write_modules_api(self, modules, outdir):
         # upper-level modules
-        main_module = modules[0].split('.')[0]
         ulms = ['.'.join(m.split('.')[:2]) if m.count('.') >= 1
                 else m.split('.')[0] for m in modules]
 
diff --git a/doc/tools/build_modref_templates.py b/doc/tools/build_modref_templates.py
index 6ec6848579..d19ea12081 100755
--- a/doc/tools/build_modref_templates.py
+++ b/doc/tools/build_modref_templates.py
@@ -37,7 +37,7 @@ def abort(error):
 
     try:
         __import__(package)
-    except ImportError as e:
+    except ImportError:
         abort("Can not import " + package)
 
     module = sys.modules[package]
@@ -71,7 +71,11 @@ def abort(error):
     print('***', source_version)
 
     if source_version != installed_version:
-        abort("Installed version does not match source version")
+        abort(
+            "Installed version does not match source version. "
+            "source_version=" + str(source_version) +
+            "installed_version=" + str(installed_version)
+        )
 
     docwriter = ApiDocWriter(package, rst_extension='.rst',
                              other_defines=other_defines)
diff --git a/nisext/py3builder.py b/nisext/py3builder.py
index 7bcaf2348c..abf5523cf7 100644
--- a/nisext/py3builder.py
+++ b/nisext/py3builder.py
@@ -6,20 +6,22 @@
 except ImportError:
     # 2.x - no parsing of code
     from distutils.command.build_py import build_py
-else: # Python 3
+else:  # Python 3
     # Command to also apply 2to3 to doctests
     from distutils import log
+
     class build_py(build_py_2to3):
         def run_2to3(self, files):
             # Add doctest parsing; this stuff copied from distutils.utils in
             # python 3.2 source
             if not files:
                 return
-            fixer_names, options, explicit = (self.fixer_names,
-                                              self.options,
-                                              self.explicit)
+            fixer_names, options, _ = (self.fixer_names,
+                                       self.options,
+                                       self.explicit)
             # Make this class local, to delay import of 2to3
             from lib2to3.refactor import RefactoringTool, get_fixers_from_package
+
             class DistutilsRefactoringTool(RefactoringTool):
                 def log_error(self, msg, *args, **kw):
                     log.error(msg, *args)
diff --git a/nisext/sexts.py b/nisext/sexts.py
index 37a8adcc7c..885ac0a055 100644
--- a/nisext/sexts.py
+++ b/nisext/sexts.py
@@ -131,7 +131,7 @@ def package_check(pkg_name, version=None,
        dependencies.  If dict fill key values ``install_requires`` and
        ``extras_require`` for non-optional and optional dependencies.
     """
-    setuptools_mode = not setuptools_args is None
+    setuptools_mode = setuptools_args is not None
     optional_tf = bool(optional)
     if version_getter is None:
         def version_getter(pkg_name):
@@ -169,7 +169,7 @@ def version_getter(pkg_name):
         log.warn(msgs['version too old'] % (have_version,
                                             pkg_name,
                                             version)
-                    + msgs['opt suffix'])
+                 + msgs['opt suffix'])
         return
     # setuptools mode
     if optional_tf and not isinstance(optional, str):
@@ -178,7 +178,7 @@ def version_getter(pkg_name):
     if version:
         dependency += '>=' + version
     if optional_tf:
-        if not 'extras_require' in setuptools_args:
+        if 'extras_require' not in setuptools_args:
             setuptools_args['extras_require'] = {}
         _add_append_key(setuptools_args['extras_require'],
                         optional,
@@ -205,7 +205,7 @@ def _package_status(pkg_name, version, version_getter, checker):
 
 
 BAT_TEMPLATE = \
-r"""@echo off
+r"""@echo off # noqa: E122
 REM wrapper to use shebang first line of {FNAME}
 set mypath=%~dp0
 set pyscript="%mypath%{FNAME}"
@@ -218,6 +218,7 @@ def _package_status(pkg_name, version, version_getter, checker):
 call "%py_exe%" %pyscript% %*
 """
 
+
 class install_scripts_bat(install_scripts):
     """ Make scripts executable on Windows
 
@@ -247,7 +248,7 @@ def run(self):
             if not (first_line.startswith('#!') and
                     'python' in first_line.lower()):
                 log.info("No #!python executable found, skipping .bat "
-                            "wrapper")
+                         "wrapper")
                 continue
             pth, fname = psplit(filepath)
             froot, ext = splitext(fname)
diff --git a/setup.cfg b/setup.cfg
index e81b1db10b..cef6b2fd6b 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -96,6 +96,8 @@ nibabel =
 max-line-length = 100
 ignore = D100,D101,D102,D103,D104,D105,D200,D201,D202,D204,D205,D208,D209,D210,D300,D301,D400,D401,D403,E24,E121,E123,E126,E226,E266,E402,E704,E731,F821,I100,I101,I201,N802,N803,N804,N806,W503,W504,W605
 exclude =
+    venv
+    build
     *test*
     *sphinx*
     nibabel/externals/*
diff --git a/tools/bisect_nose.py b/tools/bisect_nose.py
deleted file mode 100755
index 3f9092564b..0000000000
--- a/tools/bisect_nose.py
+++ /dev/null
@@ -1,105 +0,0 @@
-#!/usr/bin/env python
-""" Utility for git-bisecting nose failures
-"""
-DESCRIP = 'Check nose output for given text, set sys exit for git bisect'
-EPILOG = \
-"""
-Imagine you've just detected a nose test failure.  The failure is in a
-particular test or test module - here 'test_analyze.py'.  The failure *is* in
-git branch ``main-master`` but it *is not* in tag ``v1.6.1``. Then you can
-bisect with something like::
-
-    git co main-master
-    git bisect start HEAD v1.6.1 --
-    git bisect run /path/to/bisect_nose.py nibabel/tests/test_analyze.py:TestAnalyzeImage.test_str
-
-You might well want to test that::
-
-    nosetests nibabel/tests/test_analyze.py:TestAnalyzeImage.test_str
-
-works as you expect first.
-
-Let's say instead that you prefer to recognize the failure with an output
-string.  Maybe this is because there are lots of errors but you are only
-interested in one of them, or because you are looking for a Segmentation fault
-instead of a test failure. Then::
-
-    git co main-master
-    git bisect start HEAD v1.6.1 --
-    git bisect run /path/to/bisect_nose.py --error-txt='HeaderDataError: data dtype "int64" not recognized'  nibabel/tests/test_analyze.py
-
-where ``error-txt`` is in fact a regular expression.
-
-You will need 'argparse' installed somewhere. This is in the system libraries
-for python 2.7 and python 3.2 onwards.
-
-We run the tests in a temporary directory, so the code you are testing must be
-on the python path.
-"""
-import os
-import sys
-import shutil
-import tempfile
-import re
-from functools import partial
-from subprocess import check_call, Popen, PIPE, CalledProcessError
-
-from argparse import ArgumentParser, RawDescriptionHelpFormatter
-
-caller = partial(check_call, shell=True)
-popener = partial(Popen, stdout=PIPE, stderr=PIPE, shell=True)
-
-# git bisect exit codes
-UNTESTABLE = 125
-GOOD = 0
-BAD = 1
-
-
-def call_or_untestable(cmd):
-    try:
-        caller(cmd)
-    except CalledProcessError:
-        sys.exit(UNTESTABLE)
-
-
-def main():
-    parser = ArgumentParser(description=DESCRIP,
-                            epilog=EPILOG,
-                            formatter_class=RawDescriptionHelpFormatter)
-    parser.add_argument('test_path', type=str,
-                        help='Path to test')
-    parser.add_argument('--error-txt', type=str,
-                        help='regular expression for error of interest')
-    parser.add_argument('--clean', action='store_true',
-                        help='Clean git tree before running tests')
-    parser.add_argument('--build', action='store_true',
-                        help='Build git tree before running tests')
-    # parse the command line
-    args = parser.parse_args()
-    path = os.path.abspath(args.test_path)
-    if args.clean:
-        print("Cleaning")
-        call_or_untestable('git clean -fxd')
-    if args.build:
-        print("Building")
-        call_or_untestable('python setup.py build_ext -i')
-    cwd = os.getcwd()
-    tmpdir = tempfile.mkdtemp()
-    try:
-        os.chdir(tmpdir)
-        print("Testing")
-        proc = popener('nosetests ' + path)
-        stdout, stderr = proc.communicate()
-    finally:
-        os.chdir(cwd)
-        shutil.rmtree(tmpdir)
-    if args.error_txt:
-        regex = re.compile(args.error_txt)
-        if regex.search(stderr):
-            sys.exit(BAD)
-        sys.exit(GOOD)
-    sys.exit(proc.returncode)
-
-
-if __name__ == '__main__':
-    main()
diff --git a/tools/ci/activate.sh b/tools/ci/activate.sh
index ebef3b650b..6dbde8294e 100644
--- a/tools/ci/activate.sh
+++ b/tools/ci/activate.sh
@@ -1,10 +1,10 @@
-if [ -e virtenv/bin/activate ]; then
-    source virtenv/bin/activate
-elif [ -e virtenv/Scripts/activate ]; then
-    source virtenv/Scripts/activate
+if [ -e venv/bin/activate ]; then
+    source venv/bin/activate
+elif [ -e venv/Scripts/activate ]; then
+    source venv/Scripts/activate
 else
     echo Cannot activate virtual environment
-    ls -R virtenv
+    ls -R venv
     false
 fi
 
diff --git a/tools/ci/check.sh b/tools/ci/check.sh
index e497833521..f40052bc0c 100755
--- a/tools/ci/check.sh
+++ b/tools/ci/check.sh
@@ -14,8 +14,7 @@ set -x
 export NIBABEL_DATA_DIR="$PWD/nibabel-data"
 
 if [ "${CHECK_TYPE}" == "style" ]; then
-    # Run styles only on core nibabel code.
-    flake8 nibabel
+    flake8
 elif [ "${CHECK_TYPE}" == "doc" ]; then
     cd doc
     make html && make doctest
diff --git a/tools/ci/create_venv.sh b/tools/ci/create_venv.sh
index 7a28767396..b3441af010 100755
--- a/tools/ci/create_venv.sh
+++ b/tools/ci/create_venv.sh
@@ -12,7 +12,7 @@ echo SETUP_REQUIRES = $SETUP_REQUIRES
 set -x
 
 python -m pip install --upgrade pip virtualenv
-virtualenv --python=python virtenv
+virtualenv --python=python venv
 source tools/ci/activate.sh
 python --version
 python -m pip install -U $SETUP_REQUIRES
diff --git a/tools/gitwash_dumper.py b/tools/gitwash_dumper.py
index 156976daf5..43223462d7 100755
--- a/tools/gitwash_dumper.py
+++ b/tools/gitwash_dumper.py
@@ -24,7 +24,7 @@ def clone_repo(url, branch):
         os.chdir(tmpdir)
         cmd = f'git checkout {branch}'
         call(cmd, shell=True)
-    except:
+    except Exception:
         shutil.rmtree(tmpdir)
         raise
     finally:
@@ -72,7 +72,7 @@ def copy_replace(replace_pairs,
                  out_path,
                  cp_globs=('*',),
                  rep_globs=('*',),
-                 renames = ()):
+                 renames=()):
     out_fnames = cp_files(repo_path, cp_globs, out_path)
     renames = [(re.compile(in_exp), out_exp) for in_exp, out_exp in renames]
     fnames = []
@@ -115,8 +115,8 @@ def make_link_targets(proj_name,
     .. _`proj_name` mailing list: url
     """
     link_contents = open(known_link_fname, 'rt').readlines()
-    have_url = not url is None
-    have_ml_url = not ml_url is None
+    have_url = url is not None
+    have_ml_url = ml_url is not None
     have_gh_url = None
     for line in link_contents:
         if not have_url:
@@ -135,12 +135,12 @@ def make_link_targets(proj_name,
         raise RuntimeError('Need command line or known project '
                            'and / or mailing list URLs')
     lines = []
-    if not url is None:
+    if url is not None:
         lines.append(f'.. _{proj_name}: {url}\n')
     if not have_gh_url:
         gh_url = f'https://github.com/{user_name}/{repo_name}\n'
         lines.append(f'.. _`{proj_name} github`: {gh_url}\n')
-    if not ml_url is None:
+    if ml_url is not None:
         lines.append(f'.. _`{proj_name} mailing list`: {ml_url}\n')
     if len(lines) == 0:
         # Nothing to do
@@ -175,7 +175,7 @@ def main():
                       help="github username for main repo - e.g fperez",
                       metavar="MAIN_GH_USER")
     parser.add_option("--gitwash-url", dest="gitwash_url",
-                      help=f"URL to gitwash repository - default {GITWASH_CENTRAL}", 
+                      help=f"URL to gitwash repository - default {GITWASH_CENTRAL}",
                       default=GITWASH_CENTRAL,
                       metavar="GITWASH_URL")
     parser.add_option("--gitwash-branch", dest="gitwash_branch",
diff --git a/tools/make_tarball.py b/tools/make_tarball.py
deleted file mode 100755
index afbde3d48d..0000000000
--- a/tools/make_tarball.py
+++ /dev/null
@@ -1,22 +0,0 @@
-#!/usr/bin/env python
-"""Simple script to create a tarball with proper git info.
-"""
-
-import commands
-import os
-
-from  toollib import *
-
-tag = commands.getoutput('git describe')
-base_name = f'nibabel-{tag}'
-tar_name = f'{base_name}.tgz'
-
-# git archive is weird:  Even if I give it a specific path, it still won't
-# archive the whole tree.  It seems the only way to get the whole tree is to cd
-# to the top of the tree.  There are long threads (since 2007) on the git list
-# about this and it still doesn't work in a sensible way...
-
-start_dir = os.getcwd()
-cd('..')
-c(f'git archive --format=tar --prefix={base_name}/ HEAD | gzip > {tar_name}')
-c(f'mv {tar_name} tools/')
diff --git a/tools/mpkg_wrapper.py b/tools/mpkg_wrapper.py
index d79f84caad..0a96156e4d 100644
--- a/tools/mpkg_wrapper.py
+++ b/tools/mpkg_wrapper.py
@@ -17,6 +17,7 @@
 
 import sys
 
+
 def main():
     del sys.argv[0]
     sys.argv.insert(1, 'bdist_mpkg')
@@ -25,5 +26,6 @@ def main():
     g['__name__'] = '__main__'
     execfile(sys.argv[0], g, g)
 
+
 if __name__ == '__main__':
     main()
diff --git a/tox.ini b/tox.ini
deleted file mode 100644
index a0002e12b6..0000000000
--- a/tox.ini
+++ /dev/null
@@ -1,20 +0,0 @@
-[tox]
-# From-scratch tox-default-name virtualenvs
-envlist = py25,py26,py27,py32
-[testenv]
-deps =
-    nose
-    numpy
-commands=nosetests --with-doctest
-# MBs virtualenvs; numpy, nose already installed.  Run these with:
-# tox -e python25,python26,python27,python32,np-1.2.1
-[testenv:python25]
-deps =
-[testenv:python26]
-deps =
-[testenv:python27]
-deps =
-[testenv:python32]
-deps =
-[testenv:np-1.2.1]
-deps =