diff --git a/.flake8 b/.flake8
new file mode 100644
index 0000000000000000000000000000000000000000..2534a45ee555bb5ebefd37210d19399634c7a4ee
--- /dev/null
+++ b/.flake8
@@ -0,0 +1,3 @@
+[flake8]
+max-line-length = 80
+ignore = E501,W503,E302,E402,E203
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..1515754e0bef52e755e793e05c3400ecf532c4c7
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,27 @@
+# See https://pre-commit.com for more information
+# See https://pre-commit.com/hooks.html for more hooks
+repos:
+  - repo: https://github.com/timothycrosley/isort
+    rev: 5.9.3
+    hooks:
+      - id: isort
+        args: [--settings-path, "pyproject.toml"]
+  - repo: https://github.com/psf/black
+    rev: 21.7b0
+    hooks:
+      - id: black
+  - repo: https://gitlab.com/pycqa/flake8
+    rev: 3.9.2
+    hooks:
+      - id: flake8
+  - repo: https://github.com/pre-commit/pre-commit-hooks
+    rev: v4.0.1
+    hooks:
+      - id: check-ast
+      - id: check-case-conflict
+      - id: trailing-whitespace
+      - id: end-of-file-fixer
+      - id: debug-statements
+      - id: check-added-large-files
+      - id: check-yaml
+        exclude: .*/meta.yaml
diff --git a/MANIFEST.in b/MANIFEST.in
index 032a1c60d657027bf19bc2e99186471180495ec4..beaa8a88201e31c8a915a4c835394350c1f2fde9 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -4,4 +4,3 @@ recursive-include bob/pad/face/config/preprocessor/dictionaries *.hdf5
 recursive-include doc *.py *.rst *.ico *.png
 recursive-include bob/pad/face/test/data *.hdf5 *.png
 recursive-include bob/pad/face/config *.xml
-
diff --git a/bob/__init__.py b/bob/__init__.py
index 2ab1e28b150f0549def9963e9e87de3fdd6b2579..edbb4090fca046b19d22d3982711084621bff3be 100644
--- a/bob/__init__.py
+++ b/bob/__init__.py
@@ -1,3 +1,4 @@
 # see https://docs.python.org/3/library/pkgutil.html
 from pkgutil import extend_path
+
 __path__ = extend_path(__path__, __name__)
diff --git a/bob/pad/__init__.py b/bob/pad/__init__.py
index 2ab1e28b150f0549def9963e9e87de3fdd6b2579..edbb4090fca046b19d22d3982711084621bff3be 100644
--- a/bob/pad/__init__.py
+++ b/bob/pad/__init__.py
@@ -1,3 +1,4 @@
 # see https://docs.python.org/3/library/pkgutil.html
 from pkgutil import extend_path
+
 __path__ = extend_path(__path__, __name__)
diff --git a/bob/pad/face/__init__.py b/bob/pad/face/__init__.py
index b8a1d1248f4894fef9d5320ea15c3361765caef6..710ede733751925ecd9639629d72050926834057 100644
--- a/bob/pad/face/__init__.py
+++ b/bob/pad/face/__init__.py
@@ -1,13 +1,13 @@
-from . import extractor, preprocessor, database
+from . import database, extractor, preprocessor  # noqa: F401
 
 
 def get_config():
-    """Returns a string containing the configuration information.
-    """
+    """Returns a string containing the configuration information."""
 
     import bob.extension
+
     return bob.extension.get_config(__name__)
 
 
 # gets sphinx autodoc done right - don't remove it
-__all__ = [_ for _ in dir() if not _.startswith('_')]
+__all__ = [_ for _ in dir() if not _.startswith("_")]
diff --git a/bob/pad/face/config/svm_frames.py b/bob/pad/face/config/svm_frames.py
index 8e17dbf5161d03b648c073aeb604ac5733dd4b9f..2aaa10151390aeba1c72ad3c20e9ff6b2570291e 100644
--- a/bob/pad/face/config/svm_frames.py
+++ b/bob/pad/face/config/svm_frames.py
@@ -1,9 +1,11 @@
-import bob.pipelines as mario
-from bob.pad.face.transformer import VideoToFrames
 from sklearn.model_selection import GridSearchCV
 from sklearn.pipeline import Pipeline
 from sklearn.svm import SVC
 
+import bob.pipelines as mario
+
+from bob.pad.face.transformer import VideoToFrames
+
 preprocessor = globals().get("preprocessor")
 extractor = globals().get("extractor")
 
@@ -21,7 +23,9 @@ param_grid = [
 
 classifier = GridSearchCV(SVC(), param_grid=param_grid, cv=3)
 classifier = mario.wrap(
-    ["sample"], classifier, fit_extra_arguments=[("y", "is_bonafide")],
+    ["sample"],
+    classifier,
+    fit_extra_arguments=[("y", "is_bonafide")],
 )
 
 
diff --git a/bob/pad/face/database/__init__.py b/bob/pad/face/database/__init__.py
index 3a831f7fbd1335f23dceca7be79e501d41be12a4..e30b9a7f52a4bbccb24f306dfb02e4b6864a658d 100644
--- a/bob/pad/face/database/__init__.py
+++ b/bob/pad/face/database/__init__.py
@@ -1,24 +1,24 @@
-from .database import VideoPadSample  # noqa: F401
 from .casiafasd import CasiaFasdPadDatabase
 from .casiasurf import CasiaSurfPadDatabase
+from .database import VideoPadSample  # noqa: F401
 from .maskattack import MaskAttackPadDatabase
+from .oulunpu import OulunpuPadDatabase
 from .replay_attack import ReplayAttackPadDatabase
 from .replay_mobile import ReplayMobilePadDatabase
 from .swan import SwanPadDatabase
-from .oulunpu import OulunpuPadDatabase
 
 
 # gets sphinx autodoc done right - don't remove it
 def __appropriate__(*args):
     """Says object was actually declared here, and not in the import module.
-  Fixing sphinx warnings of not being able to find classes, when path is
-  shortened. Parameters:
+    Fixing sphinx warnings of not being able to find classes, when path is
+    shortened. Parameters:
 
-    *args: An iterable of objects to modify
+      *args: An iterable of objects to modify
 
-  Resolves `Sphinx referencing issues
-  <https://github.com/sphinx-doc/sphinx/issues/3048>`
-  """
+    Resolves `Sphinx referencing issues
+    <https://github.com/sphinx-doc/sphinx/issues/3048>`
+    """
 
     for obj in args:
         obj.__module__ = __name__
@@ -34,4 +34,4 @@ __appropriate__(
     OulunpuPadDatabase,
 )
 
-__all__ = [_ for _ in dir() if not _.startswith('_')]
+__all__ = [_ for _ in dir() if not _.startswith("_")]
diff --git a/bob/pad/face/database/casiasurf.py b/bob/pad/face/database/casiasurf.py
index 68f34f86a86fa554409418b412328895451a6dae..652dbd00965f58a9c75b685340c31f77e2d873c1 100644
--- a/bob/pad/face/database/casiasurf.py
+++ b/bob/pad/face/database/casiasurf.py
@@ -1,14 +1,17 @@
 import logging
 import os
+
 from functools import partial
 
+from sklearn.preprocessing import FunctionTransformer
+
 import bob.io.base
+
 from bob.bio.video import VideoLikeContainer
 from bob.extension import rc
 from bob.extension.download import get_file
 from bob.pad.base.database import FileListPadDatabase
 from bob.pipelines import DelayedSample
-from sklearn.preprocessing import FunctionTransformer
 
 logger = logging.getLogger(__name__)
 
@@ -45,7 +48,9 @@ def casia_surf_multistream_load(samples, original_directory, stream_type):
         paths = []
         for mod in mods:
             paths.append(
-                os.path.join(original_directory or "", getattr(sample, mod_to_attr[mod]))
+                os.path.join(
+                    original_directory or "", getattr(sample, mod_to_attr[mod])
+                )
             )
         data = partial(load_multi_stream, mods, paths)
         return DelayedSample(data, parent=sample, annotations=None)
@@ -56,7 +61,9 @@ def casia_surf_multistream_load(samples, original_directory, stream_type):
 def CasiaSurfMultiStreamSample(original_directory, stream_type):
     return FunctionTransformer(
         casia_surf_multistream_load,
-        kw_args=dict(original_directory=original_directory, stream_type=stream_type),
+        kw_args=dict(
+            original_directory=original_directory, stream_type=stream_type
+        ),
     )
 
 
diff --git a/bob/pad/face/database/database.py b/bob/pad/face/database/database.py
index b32aa67b1d78af4c97edb60eace8ce1060ebb50c..32d04a3488e3e17c1736111d7ae0073c60e07a6b 100644
--- a/bob/pad/face/database/database.py
+++ b/bob/pad/face/database/database.py
@@ -1,16 +1,18 @@
-from functools import partial
 import os
-import bob.bio.video
-from bob.bio.base.utils.annotations import read_annotation_file
+
+from functools import partial
+
 from sklearn.preprocessing import FunctionTransformer
+
+from bob.bio.base.utils.annotations import read_annotation_file
 from bob.bio.video import VideoAsArray
 from bob.pipelines import DelayedSample
 
-from ..utils import frames, number_of_frames
 
 def get_no_transform(x):
     return None
 
+
 def delayed_video_load(
     samples,
     original_directory,
diff --git a/bob/pad/face/database/replay_mobile.py b/bob/pad/face/database/replay_mobile.py
index 08f6f7fd7e2faf7ecb5b66e5f02ad71f9d8e269a..3ac0550cef4f28a4310a5cd58d28ab92b7ab77e4 100644
--- a/bob/pad/face/database/replay_mobile.py
+++ b/bob/pad/face/database/replay_mobile.py
@@ -1,13 +1,12 @@
 import logging
 
-import numpy as np
+from sklearn.pipeline import make_pipeline
+
 from bob.extension import rc
 from bob.extension.download import get_file
 from bob.pad.base.database import FileListPadDatabase
 from bob.pad.face.database import VideoPadSample
-from bob.pipelines.transformers import Str_To_Types
-from bob.pipelines.transformers import str_to_bool
-from sklearn.pipeline import make_pipeline
+from bob.pipelines.transformers import Str_To_Types, str_to_bool
 
 logger = logging.getLogger(__name__)
 
diff --git a/bob/pad/face/extractor/__init__.py b/bob/pad/face/extractor/__init__.py
index 967814c8227ed65640d25919c445673eefb890b6..7688ad81bdc108f93b0c92ec1a209e194473ceaa 100644
--- a/bob/pad/face/extractor/__init__.py
+++ b/bob/pad/face/extractor/__init__.py
@@ -1,5 +1,3 @@
-
-
 def __appropriate__(*args):
     """Says object was actually declared here, and not in the import module.
     Fixing sphinx warnings of not being able to find classes, when path is
@@ -19,4 +17,4 @@ def __appropriate__(*args):
 
 
 __appropriate__()
-__all__ = [_ for _ in dir() if not _.startswith('_')]
+__all__ = [_ for _ in dir() if not _.startswith("_")]
diff --git a/bob/pad/face/preprocessor/Patch.py b/bob/pad/face/preprocessor/Patch.py
index 58503190310334d1394cdedb95d2aa709612bc0c..48d57c7c1bd1c5998c336c197d0205d697a93b06 100644
--- a/bob/pad/face/preprocessor/Patch.py
+++ b/bob/pad/face/preprocessor/Patch.py
@@ -1,8 +1,11 @@
-from bob.bio.base.annotator.FailSafe import translate_kwargs
+from collections import OrderedDict
+
 from sklearn.base import BaseEstimator, TransformerMixin
-from ..utils import extract_patches
+
+from bob.bio.base.annotator.FailSafe import translate_kwargs
 from bob.bio.video import VideoLikeContainer
-from collections import OrderedDict
+
+from ..utils import extract_patches
 
 
 class ImagePatches(TransformerMixin, BaseEstimator):
@@ -55,7 +58,10 @@ class VideoPatches(TransformerMixin, BaseEstimator):
 
     def transform(self, videos, annotations=None):
         kwargs = translate_kwargs(dict(annotations=annotations), len(videos))
-        return [self.transform_one_video(vid, **kw) for vid, kw in zip(videos, kwargs)]
+        return [
+            self.transform_one_video(vid, **kw)
+            for vid, kw in zip(videos, kwargs)
+        ]
 
     def transform_one_video(self, frames, annotations=None):
         annotations = annotations or {}
@@ -76,7 +82,10 @@ class VideoPatches(TransformerMixin, BaseEstimator):
 
             # extract patches
             patches = extract_patches(
-                preprocessed, self.block_size, self.block_overlap, self.n_random_patches
+                preprocessed,
+                self.block_size,
+                self.block_overlap,
+                self.n_random_patches,
             )
             all_patches.extend(patches)
 
diff --git a/bob/pad/face/preprocessor/__init__.py b/bob/pad/face/preprocessor/__init__.py
index f1248dbf31454d131fe26356cb3404a6e472a6e2..c83f67c25c73722dc0b6e8f87a83d99e2d9e5d02 100644
--- a/bob/pad/face/preprocessor/__init__.py
+++ b/bob/pad/face/preprocessor/__init__.py
@@ -23,4 +23,4 @@ __appropriate__(
     ImagePatches,
     VideoPatches,
 )
-__all__ = [_ for _ in dir() if not _.startswith('_')]
+__all__ = [_ for _ in dir() if not _.startswith("_")]
diff --git a/bob/pad/face/script/statistics.py b/bob/pad/face/script/statistics.py
index 0550d4ddc80bdbb6dae6d31d2c1a2a136c2c3950..8aa9a1bbb04386fd1f1984dacfeb4e83879379de 100644
--- a/bob/pad/face/script/statistics.py
+++ b/bob/pad/face/script/statistics.py
@@ -1,18 +1,21 @@
 """Gets statistics on the average face size in a video database.
 """
 import logging
+
+from os.path import expanduser
+
 import click
 import numpy as np
-from os.path import expanduser
-from bob.extension.scripts.click_helper import (
-    verbosity_option,
-    ConfigCommand,
-    ResourceOption,
-)
+
 from bob.bio.face.annotator import (
+    BoundingBox,
     bounding_box_from_annotation,
     expected_eye_positions,
-    BoundingBox,
+)
+from bob.extension.scripts.click_helper import (
+    ConfigCommand,
+    ResourceOption,
+    verbosity_option,
 )
 
 logger = logging.getLogger(__name__)
@@ -108,14 +111,20 @@ def statistics(database, output, database_directories_file, **kwargs):
         ):
             click.echo(
                 "min: {}, mean: {}, max: {}, std: {:.1f} for {}".format(
-                    array.min(), int(array.mean()), array.max(), array.std(), name
+                    array.min(),
+                    int(array.mean()),
+                    array.max(),
+                    array.std(),
+                    name,
                 )
             )
         # print the average eye distance assuming bounding boxes are from
         # bob.ip.facedetect or the annotations had eye locations in them
         bbx = BoundingBox((0, 0), face_sizes.mean(axis=0))
         annot = expected_eye_positions(bbx)
-        eye_distance = np.linalg.norm(np.array(annot["reye"]) - np.array(annot["leye"]))
+        eye_distance = np.linalg.norm(
+            np.array(annot["reye"]) - np.array(annot["leye"])
+        )
         click.echo("Average eye locations: {}".format(annot))
         click.echo("Average eye distance: {}".format(int(eye_distance)))
 
@@ -141,7 +150,11 @@ def statistics(database, output, database_directories_file, **kwargs):
         # ax.plot_surface(X, Y, Z, rstride=1, cstride=1, cmap=cm.viridis)
 
         plt.hist(
-            face_sizes[:, 1], density=True, bins="auto", label=attack_type, alpha=0.5
+            face_sizes[:, 1],
+            density=True,
+            bins="auto",
+            label=attack_type,
+            alpha=0.5,
         )
     if output:
         plt.xlabel("Width of faces")
diff --git a/bob/pad/face/test/dummy/database.py b/bob/pad/face/test/dummy/database.py
index 3928d5e797a8f1f36555794396ef79264248aaac..00cbeec41a817f115dea5f07dc00c7fc0f1c03c4 100644
--- a/bob/pad/face/test/dummy/database.py
+++ b/bob/pad/face/test/dummy/database.py
@@ -1,14 +1,24 @@
-import bob.io.base
 import os
-from bob.pipelines import DelayedSample
-from bob.pad.base.pipelines.vanilla_pad.abstract_classes import Database
-from bob.bio.base.database.legacy import check_parameters_for_validity, convert_names_to_lowlevel
-from bob.bio.video import VideoLikeContainer
+
+import bob.io.base
+
 from bob.bio.base.database import AtntBioDatabase
+from bob.bio.base.database.legacy import (
+    check_parameters_for_validity,
+    convert_names_to_lowlevel,
+)
+from bob.bio.video import VideoLikeContainer
+from bob.pad.base.pipelines.vanilla_pad.abstract_classes import Database
+from bob.pipelines import DelayedSample
 
 
 def DummyPadSample(
-    path, original_directory, client_id, key, attack_type, none_annotations=False
+    path,
+    original_directory,
+    client_id,
+    key,
+    attack_type,
+    none_annotations=False,
 ):
     def load():
         file_name = os.path.join(original_directory, path + ".pgm")
diff --git a/bob/pad/face/test/test_block.py b/bob/pad/face/test/test_block.py
index 6c23374e5e532230e497e3b25a1040a643229ac3..594eec85a71d11c168d544c6e624bd8339177824 100644
--- a/bob/pad/face/test/test_block.py
+++ b/bob/pad/face/test/test_block.py
@@ -2,11 +2,19 @@ import numpy
 
 A_org = numpy.array(range(1, 17), "float64").reshape((4, 4))
 A_ans_0_3D = numpy.array(
-    [[[1, 2], [5, 6]], [[3, 4], [7, 8]], [[9, 10], [13, 14]], [[11, 12], [15, 16]]],
+    [
+        [[1, 2], [5, 6]],
+        [[3, 4], [7, 8]],
+        [[9, 10], [13, 14]],
+        [[11, 12], [15, 16]],
+    ],
     "float64",
 )
 A_ans_0_4D = numpy.array(
-    [[[[1, 2], [5, 6]], [[3, 4], [7, 8]]], [[[9, 10], [13, 14]], [[11, 12], [15, 16]]]],
+    [
+        [[[1, 2], [5, 6]], [[3, 4], [7, 8]]],
+        [[[9, 10], [13, 14]], [[11, 12], [15, 16]]],
+    ],
     "float64",
 )
 
diff --git a/bob/pad/face/test/test_databases.py b/bob/pad/face/test/test_databases.py
index 0683529a1034e167b5c0013c23671cfbb9b9d106..511a8c4823aaf0ddc501daaa8a8075f13a95f716 100644
--- a/bob/pad/face/test/test_databases.py
+++ b/bob/pad/face/test/test_databases.py
@@ -2,10 +2,11 @@
 # vim: set fileencoding=utf-8 :
 # Thu May 24 10:41:42 CEST 2012
 
+import numpy as np
+
 from nose.plugins.skip import SkipTest
 
 import bob.bio.base
-import numpy as np
 
 
 def test_replayattack():
@@ -31,10 +32,13 @@ def test_replayattack():
     assert len(database.samples(groups=["train", "dev"])) == 720
     assert len(database.samples(groups=["train"])) == 360
     assert (
-        len(database.samples(groups=["train", "dev", "eval"], purposes="real")) == 200
+        len(database.samples(groups=["train", "dev", "eval"], purposes="real"))
+        == 200
     )
     assert (
-        len(database.samples(groups=["train", "dev", "eval"], purposes="attack"))
+        len(
+            database.samples(groups=["train", "dev", "eval"], purposes="attack")
+        )
         == 1000
     )
 
@@ -71,10 +75,14 @@ def test_replaymobile():
     assert len(database.samples(groups=["train", "dev"])) == 728
     assert len(database.samples(groups=["train"])) == 312
     assert (
-        len(database.samples(groups=["train", "dev", "eval"], purposes="real")) == 390
+        len(database.samples(groups=["train", "dev", "eval"], purposes="real"))
+        == 390
     )
     assert (
-        len(database.samples(groups=["train", "dev", "eval"], purposes="attack")) == 640
+        len(
+            database.samples(groups=["train", "dev", "eval"], purposes="attack")
+        )
+        == 640
     )
 
     all_samples = database.sort(database.samples())
@@ -133,11 +141,18 @@ def test_maskattack():
     )
     # all real sequences: 2 sessions, 5 recordings for 17 individuals
     assert (
-        len(maskattack.samples(groups=["train", "dev", "eval"], purposes="real")) == 170
+        len(
+            maskattack.samples(groups=["train", "dev", "eval"], purposes="real")
+        )
+        == 170
     )
     # all attacks: 1 session, 5 recordings for 17 individuals
     assert (
-        len(maskattack.samples(groups=["train", "dev", "eval"], purposes="attack"))
+        len(
+            maskattack.samples(
+                groups=["train", "dev", "eval"], purposes="attack"
+            )
+        )
         == 85
     )
 
@@ -189,11 +204,17 @@ def test_casiasurf_color_protocol():
     assert len(casiasurf.samples(groups=["train"], purposes="attack")) == 20324
     assert len(casiasurf.samples(groups=("dev",), purposes=("real",))) == 2994
     assert len(casiasurf.samples(groups=("dev",), purposes=("attack",))) == 6614
-    assert len(casiasurf.samples(groups=("dev",), purposes=("real", "attack"))) == 9608
+    assert (
+        len(casiasurf.samples(groups=("dev",), purposes=("real", "attack")))
+        == 9608
+    )
     assert len(casiasurf.samples(groups=("eval",), purposes=("real",))) == 17458
-    assert len(casiasurf.samples(groups=("eval",), purposes=("attack",))) == 40252
     assert (
-        len(casiasurf.samples(groups=("eval",), purposes=("real", "attack"))) == 57710
+        len(casiasurf.samples(groups=("eval",), purposes=("attack",))) == 40252
+    )
+    assert (
+        len(casiasurf.samples(groups=("eval",), purposes=("real", "attack")))
+        == 57710
     )
 
 
@@ -234,10 +255,13 @@ def test_swan():
     assert len(database.samples(groups=["train", "dev"])) == 2803
     assert len(database.samples(groups=["train"])) == 2001
     assert (
-        len(database.samples(groups=["train", "dev", "eval"], purposes="real")) == 3300
+        len(database.samples(groups=["train", "dev", "eval"], purposes="real"))
+        == 3300
     )
     assert (
-        len(database.samples(groups=["train", "dev", "eval"], purposes="attack"))
+        len(
+            database.samples(groups=["train", "dev", "eval"], purposes="attack")
+        )
         == 2502
     )
 
@@ -287,7 +311,10 @@ def test_oulunpu():
         "Protocol_4_6",
     ]
     assert database.groups() == ["dev", "eval", "train"]
-    assert len(database.samples(groups=["train", "dev", "eval"])) == 1200 + 900 + 600
+    assert (
+        len(database.samples(groups=["train", "dev", "eval"]))
+        == 1200 + 900 + 600
+    )
     assert len(database.samples(groups=["train", "dev"])) == 1200 + 900
     assert len(database.samples(groups=["train"])) == 1200
     assert (
@@ -295,7 +322,9 @@ def test_oulunpu():
         == 240 + 180 + 120
     )
     assert (
-        len(database.samples(groups=["train", "dev", "eval"], purposes="attack"))
+        len(
+            database.samples(groups=["train", "dev", "eval"], purposes="attack")
+        )
         == 960 + 720 + 480
     )
 
diff --git a/bob/pad/face/test/test_transformers.py b/bob/pad/face/test/test_transformers.py
index fb6989bc09cabc1093b59ba307e60bfd1c5851f9..90d3209cf79f205a334f19b6bb21f93b4988492c 100644
--- a/bob/pad/face/test/test_transformers.py
+++ b/bob/pad/face/test/test_transformers.py
@@ -1,4 +1,5 @@
 import bob.pipelines as mario
+
 from bob.bio.video import VideoLikeContainer
 from bob.pad.face.transformer import VideoToFrames
 
diff --git a/bob/pad/face/test/test_utils.py b/bob/pad/face/test/test_utils.py
index 609b109b374fa9a566c05528bc21499be79f1195..1bdc971d3cb452185234e639fa815a2ab8a33b96 100644
--- a/bob/pad/face/test/test_utils.py
+++ b/bob/pad/face/test/test_utils.py
@@ -1,12 +1,22 @@
-from bob.pad.face.test.dummy.database import DummyDatabase as Database
-from bob.pad.face.utils import yield_faces, scale_face, blocks, frames, number_of_frames
-from nose.tools import raises
-import numpy
 import imageio
+import numpy
+
+from nose.tools import raises
+
+from bob.pad.face.test.dummy.database import DummyDatabase as Database
+from bob.pad.face.utils import (
+    blocks,
+    frames,
+    number_of_frames,
+    scale_face,
+    yield_faces,
+)
 
 
 def get_pad_sample(none_annotations=False):
-    sample = Database(none_annotations=none_annotations).samples(("train", "dev"))[0]
+    sample = Database(none_annotations=none_annotations).samples(
+        ("train", "dev")
+    )[0]
     return sample
 
 
@@ -15,7 +25,9 @@ image = get_pad_sample().data[0]
 
 def test_video_frames():
     # get the path to cockatoo.mp4 from imageio-ffmpeg
-    path = imageio.core.Request("imageio:cockatoo.mp4", "r").get_local_filename()
+    path = imageio.core.Request(
+        "imageio:cockatoo.mp4", "r"
+    ).get_local_filename()
     # read 2 frames
     for i, frame in enumerate(frames(path)):
         assert frame.shape == (3, 720, 1280), frame.shape
@@ -32,7 +44,6 @@ def test_video_frames():
     assert n_frames == 280, n_frames
 
 
-
 def dummy_cropper(frame, annotations=None):
     return frame
 
diff --git a/bob/pad/face/transformer/VideoToFrames.py b/bob/pad/face/transformer/VideoToFrames.py
index 45351b77c98983ec6d4116e6e9c29560d318026a..f88bb7b96007b56e3ca45ec145ce26bc4895806c 100644
--- a/bob/pad/face/transformer/VideoToFrames.py
+++ b/bob/pad/face/transformer/VideoToFrames.py
@@ -1,14 +1,16 @@
-from sklearn.base import TransformerMixin, BaseEstimator
+import logging
+
+from sklearn.base import BaseEstimator, TransformerMixin
+
 import bob.pipelines as mario
+
 from bob.pipelines.wrappers import _frmt
-import logging
 
 logger = logging.getLogger(__name__)
 
 
 class VideoToFrames(TransformerMixin, BaseEstimator):
-    """Expands video samples to frame-based samples only when transform is called.
-    """
+    """Expands video samples to frame-based samples only when transform is called."""
 
     def transform(self, video_samples):
         logger.debug(f"{_frmt(self)}.transform")
diff --git a/bob/pad/face/transformer/__init__.py b/bob/pad/face/transformer/__init__.py
index 1307b1b59638c03a4b3056ca8289ac934682aee8..f5f5aaa6bf3bcd0e05162a2efbea65af42054692 100644
--- a/bob/pad/face/transformer/__init__.py
+++ b/bob/pad/face/transformer/__init__.py
@@ -22,4 +22,4 @@ def __appropriate__(*args):
 __appropriate__(
     VideoToFrames,
 )
-__all__ = [_ for _ in dir() if not _.startswith('_')]
+__all__ = [_ for _ in dir() if not _.startswith("_")]
diff --git a/bob/pad/face/utils/__init__.py b/bob/pad/face/utils/__init__.py
index 2a16d37923d62d018f86441141f69fdbb60f7600..dffa28ce76788db55380bf6820103bea33c022d4 100644
--- a/bob/pad/face/utils/__init__.py
+++ b/bob/pad/face/utils/__init__.py
@@ -1,8 +1,18 @@
-from .load_utils import (
-    frames, number_of_frames, yield_faces, scale_face, blocks, bbx_cropper,
-    min_face_size_normalizer, color_augmentation, blocks_generator,
-    the_giant_video_loader, random_sample, random_patches, extract_patches
+from .load_utils import (  # noqa: F401
+    bbx_cropper,
+    blocks,
+    blocks_generator,
+    color_augmentation,
+    extract_patches,
+    frames,
+    min_face_size_normalizer,
+    number_of_frames,
+    random_patches,
+    random_sample,
+    scale_face,
+    the_giant_video_loader,
+    yield_faces,
 )
 
 # gets sphinx autodoc done right - don't remove it
-__all__ = [_ for _ in dir() if not _.startswith('_')]
+__all__ = [_ for _ in dir() if not _.startswith("_")]
diff --git a/bob/pad/face/utils/load_utils.py b/bob/pad/face/utils/load_utils.py
index 04f82a8b76f320efcc905faab5e284da49743bfd..7fc47a6a913a0d4091aef941a3e793d2247077dd 100644
--- a/bob/pad/face/utils/load_utils.py
+++ b/bob/pad/face/utils/load_utils.py
@@ -1,15 +1,22 @@
 import random
+
 from collections import OrderedDict
 from functools import partial
 
-import bob.io.image
 import numpy
-from bob.bio.face.annotator import bounding_box_from_annotation, min_face_size_validator
-from bob.bio.video.annotator import normalize_annotations
-from bob.bio.face.color import rgb_to_hsv, rgb_to_yuv
+
 from imageio import get_reader
 from PIL import Image
 
+import bob.io.image
+
+from bob.bio.face.annotator import (
+    bounding_box_from_annotation,
+    min_face_size_validator,
+)
+from bob.bio.face.color import rgb_to_hsv, rgb_to_yuv
+from bob.bio.video.annotator import normalize_annotations
+
 
 def block(X, block_size, block_overlap, flat=False):
     """
@@ -46,7 +53,9 @@ def block(X, block_size, block_overlap, flat=False):
             ]
 
     if flat:
-        return blocks.reshape(n_blocks_h * n_blocks_w, blocks.shape[2], blocks.shape[3])
+        return blocks.reshape(
+            n_blocks_h * n_blocks_w, blocks.shape[2], blocks.shape[3]
+        )
 
     return blocks
 
@@ -69,7 +78,9 @@ def scale(image, scaling_factor):
     """
 
     if isinstance(scaling_factor, float):
-        new_size = tuple((numpy.array(image.shape) * scaling_factor).astype(numpy.int))
+        new_size = tuple(
+            (numpy.array(image.shape) * scaling_factor).astype(numpy.int)
+        )
         return bob.io.image.to_bob(
             numpy.array(
                 Image.fromarray(bob.io.image.to_matplotlib(image)).resize(
@@ -264,6 +275,61 @@ def blocks(data, block_size, block_overlap=(0, 0)):
     return output
 
 
+def block_generator(input, block_size, block_overlap=(0, 0)):
+    """Performs a block decomposition of a 2D or 3D array/image
+
+    It works exactly as :any:`bob.ip.base.block` except that it yields the blocks
+    one by one instead of concatenating them. It also works with color images.
+
+    Parameters
+    ----------
+    input : :any:`numpy.ndarray`
+        A 2D array (Height, Width) or a color image (Bob format: Channels,
+        Height, Width).
+    block_size : (:obj:`int`, :obj:`int`)
+        The size of the blocks in which the image is decomposed.
+    block_overlap : (:obj:`int`, :obj:`int`), optional
+        The overlap of the blocks.
+
+    Yields
+    ------
+    array_like
+        A block view of the image. Modifying the blocks will change the original
+        image as well. This is different from :any:`bob.ip.base.block`.
+
+    Raises
+    ------
+    ValueError
+        If the block_overlap is not smaller than block_size.
+        If the block_size is bigger than the image size.
+    """
+    block_h, block_w = block_size
+    overlap_h, overlap_w = block_overlap
+    img_h, img_w = input.shape[-2:]
+
+    if overlap_h >= block_h or overlap_w >= block_w:
+        raise ValueError(
+            "block_overlap: {} must be smaller than block_size: {}.".format(
+                block_overlap, block_size
+            )
+        )
+    if img_h < block_h or img_w < block_w:
+        raise ValueError(
+            "block_size: {} must be smaller than the image size: {}.".format(
+                block_size, input.shape[-2:]
+            )
+        )
+
+    # Determine the number of block per row and column
+    size_ov_h = block_h - overlap_h
+    size_ov_w = block_w - overlap_w
+
+    # Perform the block decomposition
+    for h in range(0, img_h - block_h + 1, size_ov_h):
+        for w in range(0, img_w - block_w + 1, size_ov_w):
+            yield input[..., h : h + block_h, w : w + block_w]
+
+
 def blocks_generator(data, block_size, block_overlap=(0, 0)):
     """Yields patches of an image
 
@@ -347,12 +413,16 @@ def random_patches(image, block_size, n_random_patches=1):
         yield image[..., ch : ch + bh, cw : cw + bw]
 
 
-def extract_patches(image, block_size, block_overlap=(0, 0), n_random_patches=None):
+def extract_patches(
+    image, block_size, block_overlap=(0, 0), n_random_patches=None
+):
     """Yields either all patches from an image or N random patches."""
     if n_random_patches is None:
         return blocks_generator(image, block_size, block_overlap)
     else:
-        return random_patches(image, block_size, n_random_patches=n_random_patches)
+        return random_patches(
+            image, block_size, n_random_patches=n_random_patches
+        )
 
 
 def the_giant_video_loader(
@@ -416,7 +486,9 @@ def the_giant_video_loader(
     if region == "whole":
         generator = iter(pad_sample.data)
     elif region == "crop":
-        generator = yield_faces(pad_sample, cropper=cropper, normalizer=normalizer)
+        generator = yield_faces(
+            pad_sample, cropper=cropper, normalizer=normalizer
+        )
     else:
         raise ValueError("Invalid region value: `{}'".format(region))
 
@@ -436,7 +508,8 @@ def the_giant_video_loader(
                 patch
                 for frame in generator
                 for patch in random_sample(
-                    blocks(frame, block_size, block_overlap), random_patches_per_frame
+                    blocks(frame, block_size, block_overlap),
+                    random_patches_per_frame,
                 )
             )
 
@@ -444,9 +517,13 @@ def the_giant_video_loader(
         generator = (augment(frame) for frame in generator)
 
     if keep_pa_samples is not None and not pad_sample.is_bonafide:
-        generator = (frame for frame in generator if random.random() < keep_pa_samples)
+        generator = (
+            frame for frame in generator if random.random() < keep_pa_samples
+        )
 
     if keep_bf_samples is not None and pad_sample.is_bonafide:
-        generator = (frame for frame in generator if random.random() < keep_bf_samples)
+        generator = (
+            frame for frame in generator if random.random() < keep_bf_samples
+        )
 
     return generator
diff --git a/buildout.cfg b/buildout.cfg
index abdf98d139d98e6c4c2f4fd8ff9c9a1b49efdd18..5596f8465ceb0ef01fd4651023f9561d9e1a45d5 100644
--- a/buildout.cfg
+++ b/buildout.cfg
@@ -11,4 +11,4 @@ verbose = true
 
 [scripts]
 recipe = bob.buildout:scripts
-dependent-scripts = true
\ No newline at end of file
+dependent-scripts = true
diff --git a/doc/conf.py b/doc/conf.py
index 0f7ce4f359b3dacaf5b84eee4fb32ea65215b66d..b241c7e6bb4572841e2a2789aa7a5904a98b88d0 100644
--- a/doc/conf.py
+++ b/doc/conf.py
@@ -2,30 +2,28 @@
 # vim: set fileencoding=utf-8 :
 
 import os
-import sys
-import glob
+
 import pkg_resources
 
 # -- General configuration -----------------------------------------------------
 
 # If your documentation needs a minimal Sphinx version, state it here.
-needs_sphinx = '1.3'
+needs_sphinx = "1.3"
 
 # Add any Sphinx extension module names here, as strings. They can be extensions
 # coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
 extensions = [
-    'sphinx.ext.todo',
-    'sphinx.ext.coverage',
-    'sphinx.ext.ifconfig',
-    'sphinx.ext.autodoc',
-    'sphinx.ext.autosummary',
-    'sphinx.ext.doctest',
-    'sphinx.ext.graphviz',
-    'sphinx.ext.intersphinx',
-    'sphinx.ext.napoleon',
-    'sphinx.ext.viewcode',
-    'sphinx.ext.mathjax',
-    #'matplotlib.sphinxext.plot_directive'
+    "sphinx.ext.todo",
+    "sphinx.ext.coverage",
+    "sphinx.ext.ifconfig",
+    "sphinx.ext.autodoc",
+    "sphinx.ext.autosummary",
+    "sphinx.ext.doctest",
+    "sphinx.ext.graphviz",
+    "sphinx.ext.intersphinx",
+    "sphinx.ext.napoleon",
+    "sphinx.ext.viewcode",
+    "sphinx.ext.mathjax",
 ]
 
 # Be picky about warnings
@@ -35,8 +33,8 @@ nitpicky = False
 nitpick_ignore = []
 
 # Allows the user to override warnings from a separate file
-if os.path.exists('nitpick-exceptions.txt'):
-    for line in open('nitpick-exceptions.txt'):
+if os.path.exists("nitpick-exceptions.txt"):
+    for line in open("nitpick-exceptions.txt"):
         if line.strip() == "" or line.startswith("#"):
             continue
         dtype, target = line.split(None, 1)
@@ -57,25 +55,27 @@ autosummary_generate = True
 numfig = True
 
 # If we are on OSX, the 'dvipng' path maybe different
-dvipng_osx = '/opt/local/libexec/texlive/binaries/dvipng'
-if os.path.exists(dvipng_osx): pngmath_dvipng = dvipng_osx
+dvipng_osx = "/opt/local/libexec/texlive/binaries/dvipng"
+if os.path.exists(dvipng_osx):
+    pngmath_dvipng = dvipng_osx
 
 # Add any paths that contain templates here, relative to this directory.
-templates_path = ['_templates']
+templates_path = ["_templates"]
 
 # The suffix of source filenames.
-source_suffix = '.rst'
+source_suffix = ".rst"
 
 # The encoding of source files.
-#source_encoding = 'utf-8-sig'
+# source_encoding = 'utf-8-sig'
 
 # The master toctree document.
-master_doc = 'index'
+master_doc = "index"
 
 # General information about the project.
-project = u'bob.pad.face'
+project = u"bob.pad.face"
 import time
-copyright = u'%s, Idiap Research Institute' % time.strftime('%Y')
+
+copyright = u"%s, Idiap Research Institute" % time.strftime("%Y")
 
 # Grab the setup entry
 distribution = pkg_resources.require(project)[0]
@@ -91,122 +91,123 @@ release = distribution.version
 
 # 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'
+# today_fmt = '%B %d, %Y'
 
 # List of patterns, relative to source directory, that match files and
 # directories to ignore when looking for source files.
-exclude_patterns = ['links.rst']
+exclude_patterns = ["links.rst"]
 
 # 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'
+pygments_style = "sphinx"
 
 # A list of ignored prefixes for module index sorting.
-#modindex_common_prefix = []
+# modindex_common_prefix = []
 
 # Some variables which are useful for generated material
-project_variable = project.replace('.', '_')
-short_description = u'Presentation Attack Detection in Face Biometrics'
-owner = [u'Idiap Research Institute']
+project_variable = project.replace(".", "_")
+short_description = u"Presentation Attack Detection in Face Biometrics"
+owner = [u"Idiap Research Institute"]
 
 # -- Options for HTML output ---------------------------------------------------
 
 # The theme to use for HTML and HTML Help pages.  See the documentation for
 # a list of builtin themes.
 import sphinx_rtd_theme
-html_theme = 'sphinx_rtd_theme'
+
+html_theme = "sphinx_rtd_theme"
 
 # 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 = [sphinx_rtd_theme.get_html_theme_path()]
 
 # The name for this set of Sphinx documents.  If None, it defaults to
 # "<project> v<release> documentation".
-#html_title = None
+# html_title = None
 
 # A shorter title for the navigation bar.  Default is the same as html_title.
-#html_short_title = project_variable
+# html_short_title = project_variable
 
 # The name of an image file (relative to this directory) to place at the top
 # of the sidebar.
-html_logo = 'img/logo.png'
+html_logo = "img/logo.png"
 
 # 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 = 'img/favicon.ico'
+html_favicon = "img/favicon.ico"
 
 # 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,
 # so a file named "default.css" will overwrite the builtin "default.css".
-#html_static_path = ['_static']
+# html_static_path = ['_static']
 
 # 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'
 
 # 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 = {}
+# html_sidebars = {}
 
 # Additional templates that should be rendered to pages, maps page names to
 # template names.
-#html_additional_pages = {}
+# html_additional_pages = {}
 
 # If false, no module index is generated.
-#html_domain_indices = True
+# html_domain_indices = 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
+# html_show_sourcelink = True
 
 # If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
-#html_show_sphinx = True
+# html_show_sphinx = True
 
 # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
-#html_show_copyright = True
+# html_show_copyright = True
 
 # 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 = ''
 
 # This is the file name suffix for HTML files (e.g. ".xhtml").
-#html_file_suffix = None
+# html_file_suffix = None
 
 # Output file base name for HTML help builder.
-htmlhelp_basename = project_variable + u'_doc'
+htmlhelp_basename = project_variable + u"_doc"
 
 # -- Post configuration --------------------------------------------------------
 
@@ -215,25 +216,27 @@ rst_epilog = """
 .. |project| replace:: Bob
 .. |version| replace:: %s
 .. |current-year| date:: %%Y
-""" % (version, )
+""" % (
+    version,
+)
 
 # Default processing flags for sphinx
-autoclass_content = 'class'
-autodoc_member_order = 'bysource'
+autoclass_content = "class"
+autodoc_member_order = "bysource"
 autodoc_default_options = {
-  "members": True,
-  "undoc-members": True,
-  "show-inheritance": True,
+    "members": True,
+    "undoc-members": True,
+    "show-inheritance": True,
 }
 
 # For inter-documentation mapping:
 from bob.extension.utils import link_documentation, load_requirements
+
 sphinx_requirements = "extra-intersphinx.txt"
 if os.path.exists(sphinx_requirements):
     intersphinx_mapping = link_documentation(
-        additional_packages=['python','numpy'] + \
-            load_requirements(sphinx_requirements)
-            )
+        additional_packages=["python", "numpy"]
+        + load_requirements(sphinx_requirements)
+    )
 else:
     intersphinx_mapping = link_documentation()
-
diff --git a/doc/index.rst b/doc/index.rst
index ba3762b8a171e013731da184bd44a6b84701d709..27fc0ff46935ee8c2e3da4246b9e8ec76f5e14ec 100644
--- a/doc/index.rst
+++ b/doc/index.rst
@@ -29,5 +29,3 @@ Users Guide
 .. todolist::
 
 .. include:: links.rst
-
-
diff --git a/doc/other_pad_algorithms.rst b/doc/other_pad_algorithms.rst
index 4aa4348203e6b865bb98794e4ab6353b9ee7d6a8..6e433971df6f9d4573901b33257ff2f2fd774229 100644
--- a/doc/other_pad_algorithms.rst
+++ b/doc/other_pad_algorithms.rst
@@ -40,7 +40,7 @@ Usually, it is a good idea to have at least verbose level 2 (i.e., calling
    To run the experiments in parallel, you can use an existing or (define a new)
    SGE grid or local host multiprocessing configuration. To run the experiment
    in the Idiap SGE grid, you can simply add the ``--dask-client sge`` command
-   line option. To run experiments in parallel on the local machine, add the 
+   line option. To run experiments in parallel on the local machine, add the
    ``--dask-client local-parallel`` option.
 
    See :any:`this <pipeline_simple_features>` for more
@@ -54,5 +54,3 @@ is available on the section :ref:`bob.pad.face.resources`.
 
 
 .. include:: links.rst
-
-
diff --git a/doc/resources.rst b/doc/resources.rst
index ed25618dcd5e5db2c0f4576e30ab50bae4e7eeae..b988ac996550c8171dfff131b10eb0024b76eb43 100644
--- a/doc/resources.rst
+++ b/doc/resources.rst
@@ -57,4 +57,3 @@ The configuration files contain at least the following arguments of the
 ``bob pad vanilla-pad`` command:
 
     * ``pipeline`` containing zero, one, or more Transformers and one Classifier
-
diff --git a/pyproject.toml b/pyproject.toml
index bb5e83cb40b5871b321beb790ae14606b010e4ca..b738dc847ff9705c5769673db7415f2eb9a75f4d 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,3 +1,12 @@
 [build-system]
-requires = ["setuptools", "wheel", "bob.extension"]
-build-backend = "setuptools.build_meta"
+    requires = ["setuptools", "wheel", "bob.extension"]
+    build-backend = "setuptools.build_meta"
+
+[tool.isort]
+    profile = "black"
+    line_length = 80
+    order_by_type = true
+    lines_between_types = 1
+
+[tool.black]
+    line-length = 80
diff --git a/setup.py b/setup.py
index c970d6d5eb5376f6ed49600f98c62fa899012e4f..a9296ae6020248aa2f31556173defa9716fce51f 100644
--- a/setup.py
+++ b/setup.py
@@ -1,12 +1,12 @@
 #!/usr/bin/env python
 # vim: set fileencoding=utf-8 :
 
-from setuptools import setup, dist
+from setuptools import dist, setup
 
 dist.Distribution(dict(setup_requires=["bob.extension"]))
 
 # load the requirements.txt for additional requirements
-from bob.extension.utils import load_requirements, find_packages
+from bob.extension.utils import find_packages, load_requirements
 
 install_requires = load_requirements()
 
@@ -52,8 +52,7 @@ setup(
     # the version of bob.
     entry_points={
         # scripts should be declared using this entry:
-        "console_scripts": [
-        ],
+        "console_scripts": [],
         # registered databases:
         "bob.pad.database": [
             "replay-attack = bob.pad.face.config.replay_attack:database",