Repository: dedupeio/dedupe (language: python)

Issue:
CanopyIndex stop word removal non-determinism
I trained a dedupe model for address matching and ended up seeing different partition() results for the same input across different executions. I traced the non-deterministic behavior to one of my predicates (`TfidfNGramCanopyPredicate: (0.6, address1)` as seen in some log output. More specifically, I would see different sets of stop words being removed across different runs by looking at this log [line](https://github.com/dedupeio/dedupe/blob/main/dedupe/canopy_index.py#L30). 

I put in some debug logs and noticed that the tfidf counts seemed to always be the same but the stop words removed were not always the same which ended up correlating to different end results. I added a log message before this [line](https://github.com/dedupeio/dedupe/blob/main/dedupe/canopy_index.py#L28) to further check out what was happening.

```python
       for wid, docs in self.index._wordinfo.items():
            logger.info(f"  {wid} {self.lexicon._words[wid]} {len(docs)}")   # new log
            if len(docs) > threshold:
                word = self.lexicon._words[wid]
                logger.info("Removing stop word {}".format(word))
                del self.index._wordinfo[wid]
                continue
```

One example set of log output is this:

```
INFO:dedupe.canopy_index:  1  F 277
INFO:dedupe.canopy_index:  2  S 3041
INFO:dedupe.canopy_index:Removing stop word  S
INFO:dedupe.canopy_index:  4 34 252
INFO:dedupe.canopy_index:  5 4  805
```

I noticed the the wid of 3 was missing which tells me it was skipped. I then changed the code to be like this and re-ran:

```python
       for wid, docs in self.index._wordinfo.items():
            logger.info(f"  {wid} {self.lexicon._words[wid]} {len(docs)}")   # new log
            if len(docs) > threshold:
                word = self.lexicon._words[wid]
                logger.info("Removing stop word {}".format(word))
                #del self.index._wordinfo[wid]
                #continue
```

Then I would see output like this (note that it seems like the iteration order is not defined but I think that is expected):

```
INFO:dedupe.canopy_index:  1  D 1064
INFO:dedupe.canopy_index:Removing stop word  D
INFO:dedupe.canopy_index:  2  S 3041
INFO:dedupe.canopy_index:Removing stop word  S
INFO:dedupe.canopy_index:  3 12 561
INFO:dedupe.canopy_index:  4 26 266
INFO:dedupe.canopy_index:  5 6  737
```

In this case, by not calling `del` on the wordinfo entry, it did not skip the next entry after removing a stop word. So it looks like the non-determinism I was seeing in my end partition output is due to different stop words being removed due to this iteration bug across different executions. This situation of different stop words being removed will occur when they happen to be next to each other in wid order. I haven't yet been able to trace through the code to figure out what exactly is going wrong in the `del` operation. Maybe this is incorrect usage or there is some internal bug in the data structure but not sure. 

I am using python 3.8.10 on MacOS with dedupe-2.0.13








Discussion / hints:
Maybe changing the for loop [here](https://github.com/dedupeio/dedupe/blob/main/dedupe/canopy_index.py#L27) to be this instead so the iteration is independent of modifications to the underlying data structure?

```
for wid, docs in list(self.index._wordinfo.items()):
```
fantastic bug report! thank you!

Fix patch:
diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml
index 8a0b765b7..e1ae2dc0e 100644
--- a/.github/workflows/pythonpackage.yml
+++ b/.github/workflows/pythonpackage.yml
@@ -39,6 +39,9 @@ jobs:
     - name: flake8
       run: |
         flake8 dedupe tests benchmarks/benchmarks
+    - name: isort
+      run: |
+        isort --check-only .
     - name: black
       run: |
         black . --check
diff --git a/benchmarks/benchmarks/canonical.py b/benchmarks/benchmarks/canonical.py
index 703418dd9..903a3bde9 100644
--- a/benchmarks/benchmarks/canonical.py
+++ b/benchmarks/benchmarks/canonical.py
@@ -1,9 +1,8 @@
-from itertools import combinations
 import os
 import time
+from itertools import combinations
 
 import dedupe
-
 from benchmarks import common
 
 
diff --git a/benchmarks/benchmarks/canonical_gazetteer.py b/benchmarks/benchmarks/canonical_gazetteer.py
index b1a98c878..0610e1876 100644
--- a/benchmarks/benchmarks/canonical_gazetteer.py
+++ b/benchmarks/benchmarks/canonical_gazetteer.py
@@ -2,9 +2,7 @@
 import time
 
 import dedupe
-
-from benchmarks import canonical_matching
-from benchmarks import common
+from benchmarks import canonical_matching, common
 
 
 def make_report(data, clustering):
diff --git a/benchmarks/benchmarks/canonical_matching.py b/benchmarks/benchmarks/canonical_matching.py
index f8b4de647..f3ba09794 100644
--- a/benchmarks/benchmarks/canonical_matching.py
+++ b/benchmarks/benchmarks/canonical_matching.py
@@ -2,7 +2,6 @@
 import time
 
 import dedupe
-
 from benchmarks import common
 
 
diff --git a/benchmarks/benchmarks/common.py b/benchmarks/benchmarks/common.py
index 9273fb786..0174cd0af 100644
--- a/benchmarks/benchmarks/common.py
+++ b/benchmarks/benchmarks/common.py
@@ -1,11 +1,10 @@
 import csv
-from dataclasses import dataclass
-from itertools import groupby
 import logging
 import optparse
-from pathlib import Path
 import re
-
+from dataclasses import dataclass
+from itertools import groupby
+from pathlib import Path
 
 DATASETS_DIR = Path(__file__).parent / "datasets"
 
diff --git a/benchmarks/setup.py b/benchmarks/setup.py
index 164cf3c6c..73f83b12a 100644
--- a/benchmarks/setup.py
+++ b/benchmarks/setup.py
@@ -1,8 +1,5 @@
 # Dummy file to allow editable installs
-from setuptools import (
-    find_packages,
-    setup,
-)
+from setuptools import find_packages, setup
 
 if __name__ == "__main__":
     setup(
diff --git a/dedupe/_init.py b/dedupe/_init.py
index ccdca580c..726836a72 100644
--- a/dedupe/_init.py
+++ b/dedupe/_init.py
@@ -1,10 +1,15 @@
-from dedupe.api import StaticDedupe, Dedupe  # noqa: F401
-from dedupe.api import StaticRecordLink, RecordLink  # noqa: F401
-from dedupe.api import StaticGazetteer, Gazetteer  # noqa: F401
+from dedupe.api import (  # noqa: F401
+    Dedupe,
+    Gazetteer,
+    RecordLink,
+    StaticDedupe,
+    StaticGazetteer,
+    StaticRecordLink,
+)
 from dedupe.convenience import (  # noqa: F401
+    canonicalize,
     console_label,
     training_data_dedupe,
     training_data_link,
-    canonicalize,
 )
 from dedupe.serializer import read_training, write_training  # noqa: F401
diff --git a/dedupe/_typing.py b/dedupe/_typing.py
index 0744c648c..1fcfb3ad1 100644
--- a/dedupe/_typing.py
+++ b/dedupe/_typing.py
@@ -1,22 +1,22 @@
-import numpy
 import sys
-
 from typing import (
-    Iterator,
-    Tuple,
-    Mapping,
-    Union,
-    Iterable,
-    List,
     Any,
     Callable,
+    Iterable,
+    Iterator,
+    List,
+    Mapping,
     Sequence,
+    Tuple,
+    Union,
 )
 
+import numpy
+
 if sys.version_info >= (3, 8):
-    from typing import TypedDict, Protocol, Literal
+    from typing import Literal, Protocol, TypedDict
 else:
-    from typing_extensions import TypedDict, Protocol, Literal
+    from typing_extensions import Literal, Protocol, TypedDict
 
 
 RecordDict = Mapping[str, Any]
diff --git a/dedupe/api.py b/dedupe/api.py
index 93f5cc554..52625032a 100644
--- a/dedupe/api.py
+++ b/dedupe/api.py
@@ -8,47 +8,39 @@
 
 import itertools
 import logging
-import pickle
 import multiprocessing
-import warnings
 import os
+import pickle
 import sqlite3
 import tempfile
+import warnings
+from typing import BinaryIO, Generator, Iterable, Sequence, TextIO, cast
 
 import numpy
 import sklearn.linear_model
 import sklearn.model_selection
+from typing_extensions import Literal
 
-import dedupe.core as core
-import dedupe.serializer as serializer
 import dedupe.blocking as blocking
 import dedupe.clustering as clustering
+import dedupe.core as core
 import dedupe.datamodel as datamodel
 import dedupe.labeler as labeler
 import dedupe.predicates
-
-from typing import (
-    Generator,
-    Iterable,
-    Sequence,
-    BinaryIO,
-    cast,
-    TextIO,
-)
-from typing_extensions import Literal
+import dedupe.serializer as serializer
 from dedupe._typing import (
-    Data,
-    Clusters,
-    RecordPairs,
-    RecordID,
-    RecordDict,
     Blocks,
-    TrainingExample,
-    LookupResults,
-    Links,
-    TrainingData,
     Classifier,
+    Clusters,
+    Data,
     JoinConstraint,
+    Links,
+    LookupResults,
+    RecordDict,
+    RecordID,
+    RecordPairs,
+    TrainingData,
+    TrainingExample,
     VariableDefinition,
 )
 
diff --git a/dedupe/backport.py b/dedupe/backport.py
index 442111055..9cf28c6f1 100644
--- a/dedupe/backport.py
+++ b/dedupe/backport.py
@@ -11,4 +11,11 @@
     Lock = ctx.Lock
     RLock = ctx.RLock
 else:
-    from multiprocessing import Process, Pool, Queue, SimpleQueue, Lock, RLock  # type: ignore # noqa
+    from multiprocessing import (  # type: ignore # noqa
+        Lock,
+        Pool,
+        Process,
+        Queue,
+        RLock,
+        SimpleQueue,
+    )
diff --git a/dedupe/blocking.py b/dedupe/blocking.py
index c6f9331a1..52c3cb662 100644
--- a/dedupe/blocking.py
+++ b/dedupe/blocking.py
@@ -2,14 +2,13 @@
 # -*- coding: utf-8 -*-
 from __future__ import annotations
 
-from collections import defaultdict
 import logging
 import time
-
+from collections import defaultdict
 from typing import Generator, Iterable, Union
-from dedupe._typing import Record, RecordID, Data
 
 import dedupe.predicates
+from dedupe._typing import Data, Record, RecordID
 
 logger = logging.getLogger(__name__)
 
diff --git a/dedupe/canopy_index.py b/dedupe/canopy_index.py
index 1d48caa08..e9ef09382 100644
--- a/dedupe/canopy_index.py
+++ b/dedupe/canopy_index.py
@@ -1,12 +1,12 @@
-from zope.index.text.lexicon import Lexicon
-from zope.index.text.textindex import TextIndex
-from zope.index.text.cosineindex import CosineIndex
-from zope.index.text.setops import mass_weightedUnion
-
-from BTrees.Length import Length
+import logging
 import math
+
 import numpy
-import logging
+from BTrees.Length import Length
+from zope.index.text.cosineindex import CosineIndex
+from zope.index.text.lexicon import Lexicon
+from zope.index.text.setops import mass_weightedUnion
+from zope.index.text.textindex import TextIndex
 
 logger = logging.getLogger(__name__)
 
diff --git a/dedupe/clustering.py b/dedupe/clustering.py
index 09e3de8fb..32766943e 100644
--- a/dedupe/clustering.py
+++ b/dedupe/clustering.py
@@ -2,17 +2,17 @@
 # -*- coding: utf-8 -*-
 from __future__ import annotations
 
-import itertools
-from collections import defaultdict
 import array
+import itertools
 import logging
 import tempfile
-from typing import Iterable, cast, Generator, Sequence
+from collections import defaultdict
+from typing import Generator, Iterable, Sequence, cast
 
 import numpy
 import scipy.cluster.hierarchy
 
-from dedupe._typing import Clusters, RecordID, Links
+from dedupe._typing import Clusters, Links, RecordID
 
 logger = logging.getLogger(__name__)
 
diff --git a/dedupe/convenience.py b/dedupe/convenience.py
index 246bc778b..c4c513bb9 100644
--- a/dedupe/convenience.py
+++ b/dedupe/convenience.py
@@ -4,24 +4,24 @@
 
 import collections
 import itertools
-import sys
-from typing import Tuple, Iterator
 import random
+import sys
 import warnings
+from typing import Iterator, Tuple
 
 import numpy
 
 import dedupe
-from dedupe.core import unique
-from dedupe.canonical import getCanonicalRep
 from dedupe._typing import (
     Data,
-    TrainingData,
-    RecordDict,
-    TrainingExample,
     Literal,
+    RecordDict,
     RecordID,
+    TrainingData,
+    TrainingExample,
 )
+from dedupe.canonical import getCanonicalRep
+from dedupe.core import unique
 
 IndicesIterator = Iterator[Tuple[int, int]]
 
diff --git a/dedupe/core.py b/dedupe/core.py
index bf4e1ff2e..9f35cc1ce 100644
--- a/dedupe/core.py
+++ b/dedupe/core.py
@@ -2,30 +2,30 @@
 # -*- coding: utf-8 -*-
 from __future__ import annotations
 
-import itertools
-import tempfile
-import os
 import collections
 import functools
+import itertools
 import multiprocessing
 import multiprocessing.dummy
+import os
 import queue
+import tempfile
 from typing import (
+    Any,
+    Generator,
+    Iterable,
     Iterator,
     Mapping,
-    Sequence,
-    Union,
-    Generator,
     Optional,
-    Any,
+    Sequence,
     Type,
-    Iterable,
+    Union,
     overload,
 )
 
 import numpy
 
-from dedupe._typing import RecordPairs, RecordID, Blocks, Data, Literal
+from dedupe._typing import Blocks, Data, Literal, RecordID, RecordPairs
 from dedupe.backport import RLock
 
 
diff --git a/dedupe/datamodel.py b/dedupe/datamodel.py
index 687490691..8b2c4de1a 100644
--- a/dedupe/datamodel.py
+++ b/dedupe/datamodel.py
@@ -1,21 +1,18 @@
 from __future__ import annotations
 
-import pkgutil
-from typing import Iterable, Sequence
 import copyreg
+import pkgutil
 import types
+from typing import Iterable, Sequence
 
 import numpy
 
 import dedupe.variables
-from dedupe.variables.base import (
-    FieldType as FieldVariable,
-    MissingDataType,
-    Variable,
-)
-from dedupe.variables.interaction import InteractionType
+from dedupe._typing import RecordDict, VariableDefinition
 from dedupe.predicates import Predicate
-from dedupe._typing import VariableDefinition, RecordDict
+from dedupe.variables.base import FieldType as FieldVariable
+from dedupe.variables.base import MissingDataType, Variable
+from dedupe.variables.interaction import InteractionType
 
 for _, module, _ in pkgutil.iter_modules(  # type: ignore
     dedupe.variables.__path__, "dedupe.variables."
diff --git a/dedupe/labeler.py b/dedupe/labeler.py
index abeff1b55..db6bf80e6 100644
--- a/dedupe/labeler.py
+++ b/dedupe/labeler.py
@@ -1,18 +1,18 @@
 from __future__ import annotations
 
+import logging
 import random
 from abc import ABC, abstractmethod
-import logging
-from typing import Iterable, Dict
+from typing import Dict, Iterable
 
 import numpy
-from typing_extensions import Protocol
 import sklearn.linear_model
+from typing_extensions import Protocol
 
 import dedupe.core as core
-import dedupe.training as training
 import dedupe.datamodel as datamodel
-from dedupe._typing import TrainingExample, RecordID, RecordDict
+import dedupe.training as training
+from dedupe._typing import RecordDict, RecordID, TrainingExample
 from dedupe.predicates import Predicate
 
 logger = logging.getLogger(__name__)
diff --git a/dedupe/levenshtein.py b/dedupe/levenshtein.py
index 3503e1fd0..d02926d88 100644
--- a/dedupe/levenshtein.py
+++ b/dedupe/levenshtein.py
@@ -1,7 +1,7 @@
 import Levenshtein_search
 
-from .index import Index
 from .core import Enumerator
+from .index import Index
 
 
 class LevenshteinIndex(Index):
diff --git a/dedupe/predicates.py b/dedupe/predicates.py
index 9d7829900..f283c5405 100644
--- a/dedupe/predicates.py
+++ b/dedupe/predicates.py
@@ -2,19 +2,19 @@
 # -*- coding: utf-8 -*-
 from __future__ import annotations
 
-import re
-import math
+import abc
 import itertools
+import math
+import re
 import string
-import abc
-from typing import Sequence, Callable, Any, Iterable
+from typing import Any, Callable, Iterable, Sequence
 
 from doublemetaphone import doublemetaphone
 
-from dedupe.cpredicates import ngrams, initials
-import dedupe.tfidf as tfidf
 import dedupe.levenshtein as levenshtein
+import dedupe.tfidf as tfidf
 from dedupe._typing import RecordDict
+from dedupe.cpredicates import initials, ngrams
 
 words = re.compile(r"[\w']+").findall
 integers = re.compile(r"\d+").findall
diff --git a/dedupe/tfidf.py b/dedupe/tfidf.py
index aa4833b71..10de54428 100644
--- a/dedupe/tfidf.py
+++ b/dedupe/tfidf.py
@@ -3,8 +3,8 @@
 import logging
 
 from dedupe.canopy_index import CanopyIndex
-from dedupe.index import Index
 from dedupe.core import Enumerator
+from dedupe.index import Index
 
 logger = logging.getLogger(__name__)
 
diff --git a/dedupe/training.py b/dedupe/training.py
index 388f01c06..847724742 100644
--- a/dedupe/training.py
+++ b/dedupe/training.py
@@ -1,18 +1,17 @@
 from __future__ import annotations
 
-import itertools
-import logging
 import collections
 import functools
+import itertools
+import logging
+import math
 import random
 from abc import ABC
-import math
-
-from typing import Sequence, Iterable, Dict, FrozenSet, Tuple
+from typing import Dict, FrozenSet, Iterable, Sequence, Tuple
 
 from . import blocking
-from .predicates import Predicate
 from ._typing import Data, RecordID
+from .predicates import Predicate
 
 logger = logging.getLogger(__name__)
 
diff --git a/dedupe/variables/base.py b/dedupe/variables/base.py
index 75493f12b..1989f0d5e 100644
--- a/dedupe/variables/base.py
+++ b/dedupe/variables/base.py
@@ -1,9 +1,9 @@
 from __future__ import annotations
 
-from typing import Callable, ClassVar, Sequence, Type, Any, Iterable
+from typing import Any, Callable, ClassVar, Iterable, Sequence, Type
 
 from dedupe import predicates
-from dedupe._typing import VariableDefinition, Comparator
+from dedupe._typing import Comparator, VariableDefinition
 
 
 class Variable(object):
diff --git a/dedupe/variables/categorical_type.py b/dedupe/variables/categorical_type.py
index 9258f348d..a8cacb4ce 100644
--- a/dedupe/variables/categorical_type.py
+++ b/dedupe/variables/categorical_type.py
@@ -1,9 +1,10 @@
 from __future__ import annotations
 
-from dedupe.variables.base import FieldType, DerivedType
+from categorical import CategoricalComparator
+
 from dedupe import predicates
 from dedupe._typing import VariableDefinition
-from categorical import CategoricalComparator
+from dedupe.variables.base import DerivedType, FieldType
 
 
 class CategoricalType(FieldType):
diff --git a/dedupe/variables/exact.py b/dedupe/variables/exact.py
index 65a44639a..2169cc96a 100644
--- a/dedupe/variables/exact.py
+++ b/dedupe/variables/exact.py
@@ -1,5 +1,5 @@
-from dedupe.variables.base import FieldType
 from dedupe import predicates
+from dedupe.variables.base import FieldType
 
 
 class ExactType(FieldType):
diff --git a/dedupe/variables/exists.py b/dedupe/variables/exists.py
index ceb601870..24257f57f 100644
--- a/dedupe/variables/exists.py
+++ b/dedupe/variables/exists.py
@@ -4,9 +4,9 @@
 
 from categorical import CategoricalComparator
 
+from dedupe._typing import VariableDefinition
 from dedupe.variables.base import DerivedType
 from dedupe.variables.categorical_type import CategoricalType
-from dedupe._typing import VariableDefinition
 
 
 class ExistsType(CategoricalType):
diff --git a/dedupe/variables/interaction.py b/dedupe/variables/interaction.py
index 977808990..1610be775 100644
--- a/dedupe/variables/interaction.py
+++ b/dedupe/variables/interaction.py
@@ -3,8 +3,9 @@
 import itertools
 from typing import Mapping
 
-from dedupe.variables.base import Variable, FieldType as FieldVariable
 from dedupe._typing import VariableDefinition
+from dedupe.variables.base import FieldType as FieldVariable
+from dedupe.variables.base import Variable
 
 
 class InteractionType(Variable):
diff --git a/dedupe/variables/set.py b/dedupe/variables/set.py
index da96d506e..fddfa5c5e 100644
--- a/dedupe/variables/set.py
+++ b/dedupe/variables/set.py
@@ -1,8 +1,8 @@
 from simplecosine.cosine import CosineSetSimilarity
 
 from dedupe import predicates
-from dedupe.variables.base import FieldType
 from dedupe._typing import VariableDefinition
+from dedupe.variables.base import FieldType
 
 
 class SetType(FieldType):
diff --git a/dedupe/variables/string.py b/dedupe/variables/string.py
index f3613c685..21666fa10 100644
--- a/dedupe/variables/string.py
+++ b/dedupe/variables/string.py
@@ -1,12 +1,12 @@
-from typing import Sequence, Callable, Iterable
+from typing import Callable, Iterable, Sequence
 
 from affinegap import normalizedAffineGapDistance as affineGap
 from highered import CRFEditDistance
 from simplecosine.cosine import CosineTextSimilarity
 
-from dedupe.variables.base import FieldType, indexPredicates
 from dedupe import predicates
 from dedupe._typing import VariableDefinition
+from dedupe.variables.base import FieldType, indexPredicates
 
 crfEd = CRFEditDistance()
 
diff --git a/docs/conf.py b/docs/conf.py
index 6e9c54942..046b10d7b 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -12,8 +12,8 @@
 # All configuration values have a default; values that are commented out
 # serve to show the default.
 
-import sys
 import os
+import sys
 
 # If extensions (or modules to document with autodoc) are in another directory,
 # add these directories to sys.path here. If the directory is relative to the
diff --git a/pyproject.toml b/pyproject.toml
index 371f3b083..b0d4bbbe6 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -10,4 +10,8 @@ files = "dedupe"
 [tool.pytest.ini_options]
 minversion = "7.1"
 addopts = "--cov dedupe"
-testpaths = ["tests", "dedupe"]
\ No newline at end of file
+testpaths = ["tests", "dedupe"]
+
+[tool.isort]
+profile = "black"
+src_paths = ["dedupe", "tests", "benchmarks"]
diff --git a/requirements.txt b/requirements.txt
index e78536a73..101fc07c7 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -6,3 +6,4 @@ mypy
 pytest
 pytest-cov
 virtualenv
+isort
diff --git a/setup.py b/setup.py
index f447e3922..35c616d02 100644
--- a/setup.py
+++ b/setup.py
@@ -2,7 +2,7 @@
 # -*- coding: utf-8 -*-
 
 try:
-    from setuptools import setup, Extension
+    from setuptools import Extension, setup
 except ImportError:
     raise ImportError(
         "setuptools module required, please go to https://pypi.python.org/pypi/setuptools and follow the instructions for installing setuptools"


Test patch:
diff --git a/tests/duplicateCluster_memory_case.py b/tests/duplicateCluster_memory_case.py
index 6bbe99d4f..f210498a6 100644
--- a/tests/duplicateCluster_memory_case.py
+++ b/tests/duplicateCluster_memory_case.py
@@ -1,4 +1,5 @@
 import random
+
 import dedupe.core
 import dedupe.dedupe  # noqa: F401
 
diff --git a/tests/test_api.py b/tests/test_api.py
index 52ad62caa..fac41f3de 100644
--- a/tests/test_api.py
+++ b/tests/test_api.py
@@ -1,10 +1,11 @@
-import dedupe
-import dedupe.api
-import unittest
 import itertools
+import unittest
 import warnings
 from collections import OrderedDict
 
+import dedupe
+import dedupe.api
+
 
 def icfi(x):
     return list(itertools.chain.from_iterable(x))
diff --git a/tests/test_blocking.py b/tests/test_blocking.py
index 5de465cf7..35ea426dc 100644
--- a/tests/test_blocking.py
+++ b/tests/test_blocking.py
@@ -1,9 +1,10 @@
-import dedupe
-from collections import defaultdict
 import unittest
+from collections import defaultdict
 
 from future.utils import viewitems, viewvalues
 
+import dedupe
+
 
 class BlockingTest(unittest.TestCase):
     def setUp(self):
diff --git a/tests/test_canonical.py b/tests/test_canonical.py
index 28fbe21e0..8dd28acf9 100644
--- a/tests/test_canonical.py
+++ b/tests/test_canonical.py
@@ -1,4 +1,5 @@
 import unittest
+
 import dedupe.canonical
 
 
diff --git a/tests/test_core.py b/tests/test_core.py
index f8664d017..569123a03 100644
--- a/tests/test_core.py
+++ b/tests/test_core.py
@@ -1,5 +1,5 @@
-import unittest
 import random
+import unittest
 
 import numpy
 import scipy.special
diff --git a/tests/test_dedupe.py b/tests/test_dedupe.py
index 366c69c3d..8331fce69 100644
--- a/tests/test_dedupe.py
+++ b/tests/test_dedupe.py
@@ -1,7 +1,7 @@
 # -*- coding: utf-8 -*-
 
-import unittest
 import itertools
+import unittest
 
 import numpy
 
diff --git a/tests/test_labeler.py b/tests/test_labeler.py
index 48293ab3a..e982d5a0b 100644
--- a/tests/test_labeler.py
+++ b/tests/test_labeler.py
@@ -1,8 +1,10 @@
-import dedupe
-import unittest
 import random
+import unittest
+
 import pytest
 
+import dedupe
+
 SAMPLE = [
     ({"name": "Bob", "age": "50"}, {"name": "Charlie", "age": "75"}),
     ({"name": "Meredith", "age": "40"}, {"name": "Sue", "age": "10"}),
diff --git a/tests/test_predicates.py b/tests/test_predicates.py
index 91e385d56..541339d3f 100644
--- a/tests/test_predicates.py
+++ b/tests/test_predicates.py
@@ -1,7 +1,9 @@
 import unittest
-from dedupe import predicates
+
 from future.builtins import str
 
+from dedupe import predicates
+
 
 class TestPuncStrip(unittest.TestCase):
     def test_sevenchar(self):
diff --git a/tests/test_price.py b/tests/test_price.py
index 9a257bfe6..3fcee0cdd 100644
--- a/tests/test_price.py
+++ b/tests/test_price.py
@@ -1,4 +1,5 @@
 import unittest
+
 from dedupe.variables.price import PriceType
 
 
diff --git a/tests/test_serializer.py b/tests/test_serializer.py
index d422db43b..ab8c0e471 100644
--- a/tests/test_serializer.py
+++ b/tests/test_serializer.py
@@ -1,12 +1,12 @@
 # -*- coding: utf-8 -*-
 from __future__ import print_function
 
-import dedupe
-import unittest
 import codecs
 import json
-
 import sys
+import unittest
+
+import dedupe
 
 
 class SerializerTest(unittest.TestCase):
diff --git a/tests/test_tfidf.py b/tests/test_tfidf.py
index 9d7b7fefa..268766e9b 100644
--- a/tests/test_tfidf.py
+++ b/tests/test_tfidf.py
@@ -1,6 +1,7 @@
-import dedupe
 import unittest
 
+import dedupe
+
 
 class ParsingTest(unittest.TestCase):
     def setUp(self):
diff --git a/tests/test_training.py b/tests/test_training.py
index 24761abf5..1ad9b8919 100644
--- a/tests/test_training.py
+++ b/tests/test_training.py
@@ -1,6 +1,7 @@
+import unittest
+
 import dedupe
 import dedupe.training as training
-import unittest
 
 
 class TrainingTest(unittest.TestCase):