Repository: datajoint/datajoint-python (language: python)

Issue:
Multi processing bug fix
This PR proposes a fix to the bug described in #1013. In brief, when a user tries to populate a table which has already been populated while also setting processes to be greater than 1, the population should be skipped, just like it is when processes is equal to 1. Instead, a value error is thrown. 

This PR also alters the return value to also return the number of keys processed. This can be useful in automating workflows.

Closes #1013 
Implement multiprocessing in `populate`
* Fix issue #695
* Merges branch `mp` (created by #704) into `master`


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/development.yaml b/.github/workflows/development.yaml
index 39308c026..166ef8a01 100644
--- a/.github/workflows/development.yaml
+++ b/.github/workflows/development.yaml
@@ -9,6 +9,23 @@ on:
       - '**' # every branch
       - '!stage*' # exclude branches beginning with stage
 jobs:
+  build-docs:
+    runs-on: ubuntu-latest
+    env:
+      DOCKER_CLIENT_TIMEOUT: "120"
+      COMPOSE_HTTP_TIMEOUT: "120"
+    steps:
+      - uses: actions/checkout@v2
+      - name: Compile docs static artifacts
+        run: |
+          export HOST_UID=$(id -u)
+          docker-compose -f ./docs-api/docker-compose.yaml up --exit-code-from docs-builder --build 
+      - name: Add docs static artifacts
+        uses: actions/upload-artifact@v2
+        with:
+          name: docs-api-static
+          path: docs-api/build/html
+          retention-days: 1
   test:
     if: github.event_name == 'push' || github.event_name == 'pull_request'
     runs-on: ubuntu-latest
@@ -30,7 +47,7 @@ jobs:
       - name: Install dependencies
         run: |
           python -m pip install --upgrade pip
-          pip install flake8
+          pip install flake8 black
       - name: Run syntax tests
         run: flake8 datajoint --count --select=E9,F63,F7,F82 --show-source --statistics
       - name: Run primary tests
@@ -47,5 +64,46 @@ jobs:
         run: docker-compose -f LNX-docker-compose.yml up --build --exit-code-from app
       - name: Run style tests
         run: |
-          flake8 --ignore=E121,E123,E126,E226,E24,E704,W503,W504,E722,F401,W605 datajoint \
+          flake8 --ignore=E203,E722,F401,W503 datajoint \
                  --count --max-complexity=62 --max-line-length=127 --statistics
+          black datajoint --check -v
+          black tests --check -v
+  publish-docs:
+    if: |
+      github.event_name == 'push' &&
+      (
+        github.repository_owner == 'datajoint' ||
+        github.repository_owner == 'datajoint-company' ||
+        github.repository_owner == 'dj-sciops'
+      )
+    needs: build-docs
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v2
+      - name: Fetch docs static artifacts
+        uses: actions/download-artifact@v2
+        with:
+          name: docs-api-static
+          path: docs-api/build/html
+      - name: Commit documentation changes
+        run: |
+          git clone https://github.com/${GITHUB_REPOSITORY}.git \
+              --branch gh-pages --single-branch gh-pages
+          rm -R gh-pages/*
+          cp -r docs-api/build/html/* gh-pages/
+          cp .gitignore gh-pages/
+          touch gh-pages/.nojekyll
+          echo "docs-api.datajoint.org" > gh-pages/CNAME
+          cd gh-pages
+          git config --local user.email "action@github.com"
+          git config --local user.name "GitHub Action"
+          git add . --all
+          git commit -m "Update documentation" -a || true
+          # The above command will fail if no changes were present, so we ignore
+          # the return code.
+      - name: Push changes
+        uses: ad-m/github-push-action@master
+        with:
+          branch: gh-pages
+          directory: gh-pages
+          github_token: ${{secrets.GITHUB_TOKEN}}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f8ce167f0..82a714d3d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,16 @@
 ## Release notes
 
+### 0.13.5 -- May 19, 2022
+* Update - Import ABC from collections.abc for Python 3.10 compatibility
+* Bugfix - Fix multiprocessing value error (#1013) PR #1026
+
+### 0.13.4 -- March, 28 2022
+* Add - Allow reading blobs produced by legacy 32-bit compiled mYm library for matlab. PR #995
+* Bugfix - Add missing `jobs` argument for multiprocessing PR #997
+* Add - Test for multiprocessing PR #1008
+* Bugfix - Fix external store key name doesn't allow '-' (#1005) PR #1006
+* Add - Adopted black formatting into code base PR #998
+
 ### 0.13.3 -- Feb 9, 2022
 * Bugfix - Fix error in listing ancestors, descendants with part tables.
 * Bugfix - Fix Python 3.10 compatibility (#983) PR #972
diff --git a/LNX-docker-compose.yml b/LNX-docker-compose.yml
index c6367c3c1..fedefeee1 100644
--- a/LNX-docker-compose.yml
+++ b/LNX-docker-compose.yml
@@ -32,7 +32,7 @@ services:
       interval: 1s
   fakeservices.datajoint.io:
     <<: *net
-    image: datajoint/nginx:v0.0.19
+    image: datajoint/nginx:v0.1.1
     environment:
     - ADD_db_TYPE=DATABASE
     - ADD_db_ENDPOINT=db:3306
diff --git a/README.md b/README.md
index c08f59599..8d25fac4d 100644
--- a/README.md
+++ b/README.md
@@ -8,10 +8,8 @@
 # Welcome to DataJoint for Python!
 DataJoint for Python is a framework for scientific workflow management based on relational principles. DataJoint is built on the foundation of the relational data model and prescribes a consistent method for organizing, populating, computing, and querying data.
 
-DataJoint was initially developed in 2009 by Dimitri Yatsenko in Andreas Tolias' Lab for the distributed processing and management of large volumes of data streaming from regular experiments. Starting in 2011, DataJoint has been available as an open-source project adopted by other labs and improved through contributions from several developers.
-
-Vathes LLC supports DataJoint for Python as an open-source project and everyone is welcome to contribute.
-Its DataJoint Neuro (https://djneuro.io) business provides support to neuroscience labs for developing and executing custom data pipelines.
+DataJoint was initially developed in 2009 by Dimitri Yatsenko in Andreas Tolias' Lab at Baylor College of Medicine for the distributed processing and management of large volumes of data streaming from regular experiments. Starting in 2011, DataJoint has been available as an open-source project adopted by other labs and improved through contributions from several developers.
+Presently, the primary developer of DataJoint open-source software is the company DataJoint (https://datajoint.com). Related resources are listed at https://datajoint.org.
 
 ## Installation
 ```
@@ -22,7 +20,25 @@ If you already have an older version of DataJoint installed using `pip`, upgrade
 ```bash
 pip3 install --upgrade datajoint
 ```
+
+## Documentation and Tutorials
+
+* https://datajoint.org  -- start page
+* https://docs.datajoint.org -- up-to-date documentation
+* https://tutorials.datajoint.io -- step-by-step tutorials
+* https://elements.datajoint.org -- catalog of example pipelines
+* https://codebook.datajoint.io -- interactive online tutorials
+
+## Citation
++ If your work uses DataJoint for Python, please cite the following Research Resource Identifier (RRID) and manuscript.
+
++ DataJoint ([RRID:SCR_014543](https://scicrunch.org/resolver/SCR_014543)) - DataJoint for Python (version `<Enter version number>`)
+
++ Yatsenko D, Reimer J, Ecker AS, Walker EY, Sinz F, Berens P, Hoenselaar A, Cotton RJ, Siapas AS, Tolias AS. DataJoint: managing big scientific data using MATLAB or Python. bioRxiv. 2015 Jan 1:031658. doi: https://doi.org/10.1101/031658
+
 ## Python Native Blobs
+<details>
+<summary>Click to expand details</summary>
 
 DataJoint 0.12 adds full support for all native python data types in blobs: tuples, lists, sets, dicts, strings, bytes, `None`, and all their recursive combinations.
 The new blobs are a superset of the old functionality and are fully backward compatible.
@@ -92,16 +108,23 @@ the situation, but generally the following strategies may apply:
 As always, be sure that your data is safely backed up before modifying any
 important DataJoint schema or records.
 
-## Documentation and Tutorials
-A number of labs are currently adopting DataJoint and we are quickly getting the documentation in shape in February 2017.
+</details>
 
-* https://datajoint.io  -- start page
-* https://docs.datajoint.io -- up-to-date documentation
-* https://tutorials.datajoint.io -- step-by-step tutorials
-* https://catalog.datajoint.io -- catalog of example pipelines
+### API docs
 
-## Running Tests Locally
+The API documentation can be built using sphinx by running
 
+``` bash
+pip install sphinx sphinx_rtd_theme 
+(cd docs-api/sphinx && make html)
+```
+
+Generated docs are written to `docs-api/docs/html/index.html`.
+More details in [docs-api/README.md](docs-api/README.md).
+
+## Running Tests Locally
+<details>
+<summary>Click to expand details</summary>
 
 * Create an `.env` with desired development environment values e.g.
 ``` sh
@@ -136,9 +159,9 @@ GID=1000
 * Add entry in `/etc/hosts` for `127.0.0.1 fakeservices.datajoint.io`
 
 
-
-
 ### Launch Jupyter Notebook for Interactive Use
 * Navigate to `localhost:8888`
 * Input Jupyter password
 * Launch a notebook i.e. `New > Python 3`
+
+</details>
\ No newline at end of file
diff --git a/datajoint/__init__.py b/datajoint/__init__.py
index d0303d2dd..8cbe5be8a 100644
--- a/datajoint/__init__.py
+++ b/datajoint/__init__.py
@@ -10,21 +10,47 @@
 that any use of DataJoint leading to a publication be acknowledged in the publication.
 
 Please cite:
-    http://biorxiv.org/content/early/2015/11/14/031658
-    http://dx.doi.org/10.1101/031658
+
+  - http://biorxiv.org/content/early/2015/11/14/031658
+  - http://dx.doi.org/10.1101/031658
 """
 
 __author__ = "DataJoint Contributors"
 __date__ = "November 7, 2020"
-__all__ = ['__author__', '__version__',
-           'config', 'conn', 'Connection',
-           'Schema', 'schema', 'VirtualModule', 'create_virtual_module',
-           'list_schemas', 'Table', 'FreeTable',
-           'Manual', 'Lookup', 'Imported', 'Computed', 'Part',
-           'Not', 'AndList', 'U', 'Diagram', 'Di', 'ERD',
-           'set_password', 'kill',
-           'MatCell', 'MatStruct', 'AttributeAdapter',
-           'errors', 'DataJointError', 'key', 'key_hash']
+__all__ = [
+    "__author__",
+    "__version__",
+    "config",
+    "conn",
+    "Connection",
+    "Schema",
+    "schema",
+    "VirtualModule",
+    "create_virtual_module",
+    "list_schemas",
+    "Table",
+    "FreeTable",
+    "Manual",
+    "Lookup",
+    "Imported",
+    "Computed",
+    "Part",
+    "Not",
+    "AndList",
+    "U",
+    "Diagram",
+    "Di",
+    "ERD",
+    "set_password",
+    "kill",
+    "MatCell",
+    "MatStruct",
+    "AttributeAdapter",
+    "errors",
+    "DataJointError",
+    "key",
+    "key_hash",
+]
 
 from .version import __version__
 from .settings import config
@@ -44,6 +70,6 @@
 from .errors import DataJointError
 from .migrate import migrate_dj011_external_blob_storage_to_dj012
 
-ERD = Di = Diagram                      # Aliases for Diagram
-schema = Schema                         # Aliases for Schema
-create_virtual_module = VirtualModule   # Aliases for VirtualModule
+ERD = Di = Diagram  # Aliases for Diagram
+schema = Schema  # Aliases for Schema
+create_virtual_module = VirtualModule  # Aliases for VirtualModule
diff --git a/datajoint/admin.py b/datajoint/admin.py
index db2f61ccc..667789ef3 100644
--- a/datajoint/admin.py
+++ b/datajoint/admin.py
@@ -5,25 +5,30 @@
 from .utils import user_choice
 
 
-def set_password(new_password=None, connection=None, update_config=None):   # pragma: no cover
+def set_password(
+    new_password=None, connection=None, update_config=None
+):  # pragma: no cover
     connection = conn() if connection is None else connection
     if new_password is None:
-        new_password = getpass('New password: ')
-        confirm_password = getpass('Confirm password: ')
+        new_password = getpass("New password: ")
+        confirm_password = getpass("Confirm password: ")
         if new_password != confirm_password:
-            print('Failed to confirm the password! Aborting password change.')
+            print("Failed to confirm the password! Aborting password change.")
             return
     connection.query("SET PASSWORD = PASSWORD('%s')" % new_password)
-    print('Password updated.')
+    print("Password updated.")
 
-    if update_config or (update_config is None and user_choice('Update local setting?') == 'yes'):
-        config['database.password'] = new_password
+    if update_config or (
+        update_config is None and user_choice("Update local setting?") == "yes"
+    ):
+        config["database.password"] = new_password
         config.save_local(verbose=True)
 
 
 def kill(restriction=None, connection=None, order_by=None):  # pragma: no cover
     """
     view and kill database connections.
+
     :param restriction: restriction to be applied to processlist
     :param connection: a datajoint.Connection object. Default calls datajoint.conn()
     :param order_by: order by a single attribute or the list of attributes. defaults to 'id'.
@@ -40,24 +45,32 @@ def kill(restriction=None, connection=None, order_by=None):  # pragma: no cover
         connection = conn()
 
     if order_by is not None and not isinstance(order_by, str):
-        order_by = ','.join(order_by)
+        order_by = ",".join(order_by)
 
-    query = 'SELECT * FROM information_schema.processlist WHERE id <> CONNECTION_ID()' + (
-        "" if restriction is None else ' AND (%s)' % restriction) + (
-            ' ORDER BY %s' % (order_by or 'id'))
+    query = (
+        "SELECT * FROM information_schema.processlist WHERE id <> CONNECTION_ID()"
+        + ("" if restriction is None else " AND (%s)" % restriction)
+        + (" ORDER BY %s" % (order_by or "id"))
+    )
 
     while True:
-        print('  ID USER         HOST          STATE         TIME    INFO')
-        print('+--+ +----------+ +-----------+ +-----------+ +-----+')
-        cur = ({k.lower(): v for k, v in elem.items()}
-               for elem in connection.query(query, as_dict=True))
+        print("  ID USER         HOST          STATE         TIME    INFO")
+        print("+--+ +----------+ +-----------+ +-----------+ +-----+")
+        cur = (
+            {k.lower(): v for k, v in elem.items()}
+            for elem in connection.query(query, as_dict=True)
+        )
         for process in cur:
             try:
-                print('{id:>4d} {user:<12s} {host:<12s} {state:<12s} {time:>7d}  {info}'.format(**process))
+                print(
+                    "{id:>4d} {user:<12s} {host:<12s} {state:<12s} {time:>7d}  {info}".format(
+                        **process
+                    )
+                )
             except TypeError:
                 print(process)
         response = input('process to kill or "q" to quit > ')
-        if response == 'q':
+        if response == "q":
             break
         if response:
             try:
@@ -66,14 +79,15 @@ def kill(restriction=None, connection=None, order_by=None):  # pragma: no cover
                 pass  # ignore non-numeric input
             else:
                 try:
-                    connection.query('kill %d' % pid)
+                    connection.query("kill %d" % pid)
                 except pymysql.err.InternalError:
-                    print('Process not found')
+                    print("Process not found")
 
 
 def kill_quick(restriction=None, connection=None):
     """
     Kill database connections without prompting. Returns number of terminated connections.
+
     :param restriction: restriction to be applied to processlist
     :param connection: a datajoint.Connection object. Default calls datajoint.conn()
 
@@ -86,13 +100,17 @@ def kill_quick(restriction=None, connection=None):
     if connection is None:
         connection = conn()
 
-    query = 'SELECT * FROM information_schema.processlist WHERE id <> CONNECTION_ID()' + (
-        "" if restriction is None else ' AND (%s)' % restriction)
+    query = (
+        "SELECT * FROM information_schema.processlist WHERE id <> CONNECTION_ID()"
+        + ("" if restriction is None else " AND (%s)" % restriction)
+    )
 
-    cur = ({k.lower(): v for k, v in elem.items()}
-           for elem in connection.query(query, as_dict=True))
+    cur = (
+        {k.lower(): v for k, v in elem.items()}
+        for elem in connection.query(query, as_dict=True)
+    )
     nkill = 0
     for process in cur:
-        connection.query('kill %d' % process['id'])
+        connection.query("kill %d" % process["id"])
         nkill += 1
     return nkill
diff --git a/datajoint/attribute_adapter.py b/datajoint/attribute_adapter.py
index dc1c45706..2814064b3 100644
--- a/datajoint/attribute_adapter.py
+++ b/datajoint/attribute_adapter.py
@@ -7,28 +7,32 @@ class AttributeAdapter:
     """
     Base class for adapter objects for user-defined attribute types.
     """
+
     @property
     def attribute_type(self):
         """
         :return: a supported DataJoint attribute type to use; e.g. "longblob", "blob@store"
         """
-        raise NotImplementedError('Undefined attribute adapter')
+        raise NotImplementedError("Undefined attribute adapter")
 
     def get(self, value):
         """
         convert value retrieved from the the attribute in a table into the adapted type
+
         :param value: value from the database
+
         :return: object of the adapted type
         """
-        raise NotImplementedError('Undefined attribute adapter')
+        raise NotImplementedError("Undefined attribute adapter")
 
     def put(self, obj):
         """
         convert an object of the adapted type into a value that DataJoint can store in a table attribute
+
         :param obj: an object of the adapted type
         :return: value to store in the database
         """
-        raise NotImplementedError('Undefined attribute adapter')
+        raise NotImplementedError("Undefined attribute adapter")
 
 
 def get_adapter(context, adapter_name):
@@ -36,19 +40,32 @@ def get_adapter(context, adapter_name):
     Extract the AttributeAdapter object by its name from the context and validate.
     """
     if not _support_adapted_types():
-        raise DataJointError('Support for Adapted Attribute types is disabled.')
-    adapter_name = adapter_name.lstrip('<').rstrip('>')
+        raise DataJointError("Support for Adapted Attribute types is disabled.")
+    adapter_name = adapter_name.lstrip("<").rstrip(">")
     try:
-        adapter = (context[adapter_name] if adapter_name in context
-                   else type_plugins[adapter_name]['object'].load())
+        adapter = (
+            context[adapter_name]
+            if adapter_name in context
+            else type_plugins[adapter_name]["object"].load()
+        )
     except KeyError:
         raise DataJointError(
-            "Attribute adapter '{adapter_name}' is not defined.".format(adapter_name=adapter_name))
+            "Attribute adapter '{adapter_name}' is not defined.".format(
+                adapter_name=adapter_name
+            )
+        )
     if not isinstance(adapter, AttributeAdapter):
         raise DataJointError(
             "Attribute adapter '{adapter_name}' must be an instance of datajoint.AttributeAdapter".format(
-                adapter_name=adapter_name))
-    if not isinstance(adapter.attribute_type, str) or not re.match(r'^\w', adapter.attribute_type):
-        raise DataJointError("Invalid attribute type {type} in attribute adapter '{adapter_name}'".format(
-            type=adapter.attribute_type, adapter_name=adapter_name))
+                adapter_name=adapter_name
+            )
+        )
+    if not isinstance(adapter.attribute_type, str) or not re.match(
+        r"^\w", adapter.attribute_type
+    ):
+        raise DataJointError(
+            "Invalid attribute type {type} in attribute adapter '{adapter_name}'".format(
+                type=adapter.attribute_type, adapter_name=adapter_name
+            )
+        )
     return adapter
diff --git a/datajoint/autopopulate.py b/datajoint/autopopulate.py
index b60441599..a5b04939c 100644
--- a/datajoint/autopopulate.py
+++ b/datajoint/autopopulate.py
@@ -17,6 +17,7 @@
 
 # --- helper functions for multiprocessing --
 
+
 def _initialize_populate(table, jobs, populate_kwargs):
     """
     Initialize the process for mulitprocessing.
@@ -45,6 +46,7 @@ class AutoPopulate:
     Auto-populated relations must inherit from both Relation and AutoPopulate,
     must define the property `key_source`, and must define the callback method `make`.
     """
+
     _key_source = None
     _allow_insert = False
 
@@ -57,17 +59,29 @@ def key_source(self):
         Subclasses may override they key_source to change the scope or the granularity
         of the make calls.
         """
+
         def _rename_attributes(table, props):
-            return (table.proj(
-                **{attr: ref for attr, ref in props['attr_map'].items() if attr != ref})
-                if props['aliased'] else table.proj())
+            return (
+                table.proj(
+                    **{
+                        attr: ref
+                        for attr, ref in props["attr_map"].items()
+                        if attr != ref
+                    }
+                )
+                if props["aliased"]
+                else table.proj()
+            )
 
         if self._key_source is None:
             parents = self.target.parents(
-                primary=True, as_objects=True, foreign_key_info=True)
+                primary=True, as_objects=True, foreign_key_info=True
+            )
             if not parents:
-                raise DataJointError('A table must have dependencies '
-                                     'from its primary key for auto-populate to work')
+                raise DataJointError(
+                    "A table must have dependencies "
+                    "from its primary key for auto-populate to work"
+                )
             self._key_source = _rename_attributes(*parents[0])
             for q in parents[1:]:
                 self._key_source *= _rename_attributes(*q)
@@ -80,7 +94,8 @@ def make(self, key):
         computes secondary attributes, and inserts the new tuples into self.
         """
         raise NotImplementedError(
-            'Subclasses of AutoPopulate must implement the method `make`')
+            "Subclasses of AutoPopulate must implement the method `make`"
+        )
 
     @property
     def target(self):
@@ -104,8 +119,10 @@ def _jobs_to_do(self, restrictions):
         :return: the relation containing the keys to be computed (derived from self.key_source)
         """
         if self.restriction:
-            raise DataJointError('Cannot call populate on a restricted table. '
-                                 'Instead, pass conditions to populate() as arguments.')
+            raise DataJointError(
+                "Cannot call populate on a restricted table. "
+                "Instead, pass conditions to populate() as arguments."
+            )
         todo = self.key_source
 
         # key_source is a QueryExpression subclass -- trigger instantiation
@@ -113,22 +130,36 @@ def _jobs_to_do(self, restrictions):
             todo = todo()
 
         if not isinstance(todo, QueryExpression):
-            raise DataJointError('Invalid key_source value')
+            raise DataJointError("Invalid key_source value")
 
         try:
             # check if target lacks any attributes from the primary key of key_source
             raise DataJointError(
-                'The populate target lacks attribute %s '
-                'from the primary key of key_source' % next(
-                    name for name in todo.heading.primary_key
-                    if name not in self.target.heading))
+                "The populate target lacks attribute %s "
+                "from the primary key of key_source"
+                % next(
+                    name
+                    for name in todo.heading.primary_key
+                    if name not in self.target.heading
+                )
+            )
         except StopIteration:
             pass
         return (todo & AndList(restrictions)).proj()
 
-    def populate(self, *restrictions, suppress_errors=False, return_exception_objects=False,
-                 reserve_jobs=False, order="original", limit=None, max_calls=None,
-                 display_progress=False, processes=1, make_kwargs=None):
+    def populate(
+        self,
+        *restrictions,
+        suppress_errors=False,
+        return_exception_objects=False,
+        reserve_jobs=False,
+        order="original",
+        limit=None,
+        max_calls=None,
+        display_progress=False,
+        processes=1,
+        make_kwargs=None
+    ):
         """
         ``table.populate()`` calls ``table.make(key)`` for every primary key in
         ``self.key_source`` for which there is not already a tuple in table.
@@ -142,26 +173,31 @@ def populate(self, *restrictions, suppress_errors=False, return_exception_object
         :param limit: if not None, check at most this many keys
         :param max_calls: if not None, populate at most this many keys
         :param display_progress: if True, report progress_bar
-        :param processes: number of processes to use. When set to a large number, then
-            uses as many as CPU cores
+        :param processes: number of processes to use. Set to None to use all cores
         :param make_kwargs: Keyword arguments which do not affect the result of computation
             to be passed down to each ``make()`` call. Computation arguments should be
             specified within the pipeline e.g. using a `dj.Lookup` table.
         :type make_kwargs: dict, optional
         """
         if self.connection.in_transaction:
-            raise DataJointError('Populate cannot be called during a transaction.')
+            raise DataJointError("Populate cannot be called during a transaction.")
 
-        valid_order = ['original', 'reverse', 'random']
+        valid_order = ["original", "reverse", "random"]
         if order not in valid_order:
-            raise DataJointError('The order argument must be one of %s' % str(valid_order))
-        jobs = self.connection.schemas[self.target.database].jobs if reserve_jobs else None
+            raise DataJointError(
+                "The order argument must be one of %s" % str(valid_order)
+            )
+        jobs = (
+            self.connection.schemas[self.target.database].jobs if reserve_jobs else None
+        )
 
         # define and set up signal handler for SIGTERM:
         if reserve_jobs:
+
             def handler(signum, frame):
-                logger.info('Populate terminated by SIGTERM')
-                raise SystemExit('SIGTERM received')
+                logger.info("Populate terminated by SIGTERM")
+                raise SystemExit("SIGTERM received")
+
             old_handler = signal.signal(signal.SIGTERM, handler)
 
         keys = (self._jobs_to_do(restrictions) - self.target).fetch("KEY", limit=limit)
@@ -170,22 +206,26 @@ def handler(signum, frame):
         elif order == "random":
             random.shuffle(keys)
 
-        logger.info('Found %d keys to populate' % len(keys))
+        logger.info("Found %d keys to populate" % len(keys))
 
         keys = keys[:max_calls]
         nkeys = len(keys)
+        if not nkeys:
+            return
 
-        if processes > 1:
-            processes = min(processes, nkeys, mp.cpu_count())
+        processes = min(*(_ for _ in (processes, nkeys, mp.cpu_count()) if _))
 
         error_list = []
         populate_kwargs = dict(
             suppress_errors=suppress_errors,
             return_exception_objects=return_exception_objects,
-            make_kwargs=make_kwargs)
+            make_kwargs=make_kwargs,
+        )
 
         if processes == 1:
-            for key in tqdm(keys, desc=self.__class__.__name__) if display_progress else keys:
+            for key in (
+                tqdm(keys, desc=self.__class__.__name__) if display_progress else keys
+            ):
                 error = self._populate1(key, jobs, **populate_kwargs)
                 if error is not None:
                     error_list.append(error)
@@ -193,7 +233,9 @@ def handler(signum, frame):
             # spawn multiple processes
             self.connection.close()  # disconnect parent process from MySQL server
             del self.connection._conn.ctx  # SSLContext is not pickleable
-            with mp.Pool(processes, _initialize_populate, (self, populate_kwargs)) as pool:
+            with mp.Pool(
+                processes, _initialize_populate, (self, jobs, populate_kwargs)
+            ) as pool:
                 if display_progress:
                     with tqdm(desc="Processes: ", total=nkeys) as pbar:
                         for error in pool.imap(_call_populate1, keys, chunksize=1):
@@ -213,7 +255,9 @@ def handler(signum, frame):
         if suppress_errors:
             return error_list
 
-    def _populate1(self, key, jobs, suppress_errors, return_exception_objects, make_kwargs=None):
+    def _populate1(
+        self, key, jobs, suppress_errors, return_exception_objects, make_kwargs=None
+    ):
         """
         populates table for one source key, calling self.make inside a transaction.
         :param jobs: the jobs table or None if not reserve_jobs
@@ -222,7 +266,7 @@ def _populate1(self, key, jobs, suppress_errors, return_exception_objects, make_
         :param return_exception_objects: if True, errors must be returned as objects
         :return: (key, error) when suppress_errors=True, otherwise None
         """
-        make = self._make_tuples if hasattr(self, '_make_tuples') else self.make
+        make = self._make_tuples if hasattr(self, "_make_tuples") else self.make
 
         if jobs is None or jobs.reserve(self.target.table_name, self._job_key(key)):
             self.connection.start_transaction()
@@ -231,7 +275,7 @@ def _populate1(self, key, jobs, suppress_errors, return_exception_objects, make_
                 if jobs is not None:
                     jobs.complete(self.target.table_name, self._job_key(key))
             else:
-                logger.info('Populating: ' + str(key))
+                logger.info("Populating: " + str(key))
                 self.__class__._allow_insert = True
                 try:
                     make(dict(key), **(make_kwargs or {}))
@@ -240,14 +284,18 @@ def _populate1(self, key, jobs, suppress_errors, return_exception_objects, make_
                         self.connection.cancel_transaction()
                     except LostConnectionError:
                         pass
-                    error_message = '{exception}{msg}'.format(
+                    error_message = "{exception}{msg}".format(
                         exception=error.__class__.__name__,
-                        msg=': ' + str(error) if str(error) else '')
+                        msg=": " + str(error) if str(error) else "",
+                    )
                     if jobs is not None:
                         # show error name and error message (if any)
                         jobs.error(
-                            self.target.table_name, self._job_key(key),
-                            error_message=error_message, error_stack=traceback.format_exc())
+                            self.target.table_name,
+                            self._job_key(key),
+                            error_message=error_message,
+                            error_stack=traceback.format_exc(),
+                        )
                     if not suppress_errors or isinstance(error, SystemExit):
                         raise
                     else:
@@ -269,9 +317,17 @@ def progress(self, *restrictions, display=True):
         total = len(todo)
         remaining = len(todo - self.target)
         if display:
-            print('%-20s' % self.__class__.__name__,
-                  'Completed %d of %d (%2.1f%%)   %s' % (
-                      total - remaining, total, 100 - 100 * remaining / (total+1e-12),
-                      datetime.datetime.strftime(datetime.datetime.now(),
-                                                 '%Y-%m-%d %H:%M:%S')), flush=True)
+            print(
+                "%-20s" % self.__class__.__name__,
+                "Completed %d of %d (%2.1f%%)   %s"
+                % (
+                    total - remaining,
+                    total,
+                    100 - 100 * remaining / (total + 1e-12),
+                    datetime.datetime.strftime(
+                        datetime.datetime.now(), "%Y-%m-%d %H:%M:%S"
+                    ),
+                ),
+                flush=True,
+            )
         return remaining, total
diff --git a/datajoint/blob.py b/datajoint/blob.py
index d3837cb6a..e6ff2a581 100644
--- a/datajoint/blob.py
+++ b/datajoint/blob.py
@@ -14,36 +14,41 @@
 from .settings import config
 
 
-mxClassID = dict((
-    # see http://www.mathworks.com/help/techdoc/apiref/mxclassid.html
-    ('mxUNKNOWN_CLASS', None),
-    ('mxCELL_CLASS', None),
-    ('mxSTRUCT_CLASS', None),
-    ('mxLOGICAL_CLASS', np.dtype('bool')),
-    ('mxCHAR_CLASS', np.dtype('c')),
-    ('mxVOID_CLASS', np.dtype('O')),
-    ('mxDOUBLE_CLASS', np.dtype('float64')),
-    ('mxSINGLE_CLASS', np.dtype('float32')),
-    ('mxINT8_CLASS', np.dtype('int8')),
-    ('mxUINT8_CLASS', np.dtype('uint8')),
-    ('mxINT16_CLASS', np.dtype('int16')),
-    ('mxUINT16_CLASS', np.dtype('uint16')),
-    ('mxINT32_CLASS', np.dtype('int32')),
-    ('mxUINT32_CLASS', np.dtype('uint32')),
-    ('mxINT64_CLASS', np.dtype('int64')),
-    ('mxUINT64_CLASS', np.dtype('uint64')),
-    ('mxFUNCTION_CLASS', None)))
+mxClassID = dict(
+    (
+        # see http://www.mathworks.com/help/techdoc/apiref/mxclassid.html
+        ("mxUNKNOWN_CLASS", None),
+        ("mxCELL_CLASS", None),
+        ("mxSTRUCT_CLASS", None),
+        ("mxLOGICAL_CLASS", np.dtype("bool")),
+        ("mxCHAR_CLASS", np.dtype("c")),
+        ("mxVOID_CLASS", np.dtype("O")),
+        ("mxDOUBLE_CLASS", np.dtype("float64")),
+        ("mxSINGLE_CLASS", np.dtype("float32")),
+        ("mxINT8_CLASS", np.dtype("int8")),
+        ("mxUINT8_CLASS", np.dtype("uint8")),
+        ("mxINT16_CLASS", np.dtype("int16")),
+        ("mxUINT16_CLASS", np.dtype("uint16")),
+        ("mxINT32_CLASS", np.dtype("int32")),
+        ("mxUINT32_CLASS", np.dtype("uint32")),
+        ("mxINT64_CLASS", np.dtype("int64")),
+        ("mxUINT64_CLASS", np.dtype("uint64")),
+        ("mxFUNCTION_CLASS", None),
+    )
+)
 
 rev_class_id = {dtype: i for i, dtype in enumerate(mxClassID.values())}
 dtype_list = list(mxClassID.values())
 type_names = list(mxClassID)
 
-compression = {
-    b'ZL123\0': zlib.decompress
-}
+compression = {b"ZL123\0": zlib.decompress}
 
 bypass_serialization = False  # runtime setting to bypass blob (en|de)code
 
+# runtime setting to read integers as 32-bit to read blobs created by the 32-bit
+# version of the mYm library for MATLAB
+use_32bit_dims = False
+
 
 def len_u64(obj):
     return np.uint64(len(obj)).tobytes()
@@ -54,12 +59,14 @@ def len_u32(obj):
 
 
 class MatCell(np.ndarray):
-    """ a numpy ndarray representing a Matlab cell array """
+    """a numpy ndarray representing a Matlab cell array"""
+
     pass
 
 
 class MatStruct(np.recarray):
-    """ numpy.recarray representing a Matlab struct array """
+    """numpy.recarray representing a Matlab struct array"""
+
     pass
 
 
@@ -71,9 +78,11 @@ def __init__(self, squeeze=False):
         self.protocol = None
 
     def set_dj0(self):
-        if not config.get('enable_python_native_blobs'):
-            raise DataJointError("""v0.12+ python native blobs disabled.
-                See also: https://github.com/datajoint/datajoint-python#python-native-blobs""")
+        if not config.get("enable_python_native_blobs"):
+            raise DataJointError(
+                """v0.12+ python native blobs disabled.
+                See also: https://github.com/datajoint/datajoint-python#python-native-blobs"""
+            )
 
         self.protocol = b"dj0\0"  # when using new blob features
 
@@ -91,52 +100,57 @@ def unpack(self, blob):
         self._blob = blob
         try:
             # decompress
-            prefix = next(p for p in compression if self._blob[self._pos:].startswith(p))
+            prefix = next(
+                p for p in compression if self._blob[self._pos :].startswith(p)
+            )
         except StopIteration:
             pass  # assume uncompressed but could be unrecognized compression
         else:
             self._pos += len(prefix)
-            blob_size = self.read_value('uint64')
-            blob = compression[prefix](self._blob[self._pos:])
+            blob_size = self.read_value()
+            blob = compression[prefix](self._blob[self._pos :])
             assert len(blob) == blob_size
             self._blob = blob
             self._pos = 0
         blob_format = self.read_zero_terminated_string()
-        if blob_format in ('mYm', 'dj0'):
+        if blob_format in ("mYm", "dj0"):
             return self.read_blob(n_bytes=len(self._blob) - self._pos)
 
     def read_blob(self, n_bytes=None):
         start = self._pos
-        data_structure_code = chr(self.read_value('uint8'))
+        data_structure_code = chr(self.read_value("uint8"))
         try:
             call = {
                 # MATLAB-compatible, inherited from original mYm
-                "A": self.read_array,         # matlab-compatible numeric arrays and scalars with ndim==0
+                "A": self.read_array,  # matlab-compatible numeric arrays and scalars with ndim==0
                 "P": self.read_sparse_array,  # matlab sparse array -- not supported yet
-                "S": self.read_struct,        # matlab struct array
-                "C": self.read_cell_array,    # matlab cell array
+                "S": self.read_struct,  # matlab struct array
+                "C": self.read_cell_array,  # matlab cell array
                 # basic data types
-                "\xFF": self.read_none,    # None
-                "\x01": self.read_tuple,     # a Sequence (e.g. tuple)
-                "\x02": self.read_list,      # a MutableSequence (e.g. list)
-                "\x03": self.read_set,       # a Set
-                "\x04": self.read_dict,      # a Mapping (e.g. dict)
-                "\x05": self.read_string,    # a UTF8-encoded string
-                "\x06": self.read_bytes,     # a ByteString
-                "\x0a": self.read_int,       # unbounded scalar int
-                "\x0b": self.read_bool,      # scalar boolean
-                "\x0c": self.read_complex,   # scalar 128-bit complex number
-                "\x0d": self.read_float,     # scalar 64-bit float
-                "F": self.read_recarray,     # numpy array with fields, including recarrays
-                "d": self.read_decimal,      # a decimal
-                "t": self.read_datetime,     # date, time, or datetime
-                "u": self.read_uuid,         # UUID
+                "\xFF": self.read_none,  # None
+                "\x01": self.read_tuple,  # a Sequence (e.g. tuple)
+                "\x02": self.read_list,  # a MutableSequence (e.g. list)
+                "\x03": self.read_set,  # a Set
+                "\x04": self.read_dict,  # a Mapping (e.g. dict)
+                "\x05": self.read_string,  # a UTF8-encoded string
+                "\x06": self.read_bytes,  # a ByteString
+                "\x0a": self.read_int,  # unbounded scalar int
+                "\x0b": self.read_bool,  # scalar boolean
+                "\x0c": self.read_complex,  # scalar 128-bit complex number
+                "\x0d": self.read_float,  # scalar 64-bit float
+                "F": self.read_recarray,  # numpy array with fields, including recarrays
+                "d": self.read_decimal,  # a decimal
+                "t": self.read_datetime,  # date, time, or datetime
+                "u": self.read_uuid,  # UUID
             }[data_structure_code]
         except KeyError:
-            raise DataJointError('Unknown data structure code "%s". Upgrade datajoint.' % data_structure_code)
+            raise DataJointError(
+                'Unknown data structure code "%s". Upgrade datajoint.'
+                % data_structure_code
+            )
         v = call()
         if n_bytes is not None and self._pos - start != n_bytes:
-            raise DataJointError('Blob length check failed! Invalid blob')
+            raise DataJointError("Blob length check failed! Invalid blob")
         return v
 
     def pack_blob(self, obj):
@@ -174,39 +188,47 @@ def pack_blob(self, obj):
             return self.pack_decimal(obj)
         if isinstance(obj, uuid.UUID):
             return self.pack_uuid(obj)
-        if isinstance(obj, collections.Mapping):
+        if isinstance(obj, collections.abc.Mapping):
             return self.pack_dict(obj)
         if isinstance(obj, str):
             return self.pack_string(obj)
-        if isinstance(obj, collections.ByteString):
+        if isinstance(obj, collections.abc.ByteString):
             return self.pack_bytes(obj)
-        if isinstance(obj, collections.MutableSequence):
+        if isinstance(obj, collections.abc.MutableSequence):
             return self.pack_list(obj)
-        if isinstance(obj, collections.Sequence):
+        if isinstance(obj, collections.abc.Sequence):
             return self.pack_tuple(obj)
-        if isinstance(obj, collections.Set):
+        if isinstance(obj, collections.abc.Set):
             return self.pack_set(obj)
         if obj is None:
             return self.pack_none()
-        raise DataJointError("Packing object of type %s currently not supported!" % type(obj))
+        raise DataJointError(
+            "Packing object of type %s currently not supported!" % type(obj)
+        )
 
     def read_array(self):
-        n_dims = int(self.read_value('uint64'))
-        shape = self.read_value('uint64', count=n_dims)
+        n_dims = int(self.read_value())
+        shape = self.read_value(count=n_dims)
         n_elem = np.prod(shape, dtype=int)
-        dtype_id, is_complex = self.read_value('uint32', 2)
+        dtype_id, is_complex = self.read_value("uint32", 2)
         dtype = dtype_list[dtype_id]
 
-        if type_names[dtype_id] == 'mxVOID_CLASS':
+        if type_names[dtype_id] == "mxVOID_CLASS":
             data = np.array(
-                list(self.read_blob(self.read_value()) for _ in range(n_elem)), dtype=np.dtype('O'))
-        elif type_names[dtype_id] == 'mxCHAR_CLASS':
+                list(self.read_blob(self.read_value()) for _ in range(n_elem)),
+                dtype=np.dtype("O"),
+            )
+        elif type_names[dtype_id] == "mxCHAR_CLASS":
             # compensate for MATLAB packing of char arrays
             data = self.read_value(dtype, count=2 * n_elem)
-            data = data[::2].astype('U1')
+            data = data[::2].astype("U1")
             if n_dims == 2 and shape[0] == 1 or n_dims == 1:
                 compact = data.squeeze()
-                data = compact if compact.shape == () else np.array(''.join(data.squeeze()))
+                data = (
+                    compact
+                    if compact.shape == ()
+                    else np.array("".join(data.squeeze()))
+                )
                 shape = (1,)
         else:
             data = self.read_value(dtype, count=n_elem)
@@ -218,21 +240,33 @@ def pack_array(self, array):
         """
         Serialize an np.ndarray into bytes.  Scalars are encoded with ndim=0.
         """
-        blob = b"A" + np.uint64(array.ndim).tobytes() + np.array(array.shape, dtype=np.uint64).tobytes()
+        blob = (
+            b"A"
+            + np.uint64(array.ndim).tobytes()
+            + np.array(array.shape, dtype=np.uint64).tobytes()
+        )
         is_complex = np.iscomplexobj(array)
         if is_complex:
             array, imaginary = np.real(array), np.imag(array)
-        type_id = (rev_class_id[array.dtype] if array.dtype.char != 'U'
-                   else rev_class_id[np.dtype('O')])
+        type_id = (
+            rev_class_id[array.dtype]
+            if array.dtype.char != "U"
+            else rev_class_id[np.dtype("O")]
+        )
         if dtype_list[type_id] is None:
             raise DataJointError("Type %s is ambiguous or unknown" % array.dtype)
 
         blob += np.array([type_id, is_complex], dtype=np.uint32).tobytes()
-        if type_names[type_id] == 'mxVOID_CLASS':  # array of dtype('O')
-            blob += b"".join(len_u64(it) + it for it in (self.pack_blob(e) for e in array.flatten(order="F")))
+        if type_names[type_id] == "mxVOID_CLASS":  # array of dtype('O')
+            blob += b"".join(
+                len_u64(it) + it
+                for it in (self.pack_blob(e) for e in array.flatten(order="F"))
+            )
             self.set_dj0()  # not supported by original mym
-        elif type_names[type_id] == 'mxCHAR_CLASS':  # array of dtype('c')
-            blob += array.view(np.uint8).astype(np.uint16).tobytes()  # convert to 16-bit chars for MATLAB
+        elif type_names[type_id] == "mxCHAR_CLASS":  # array of dtype('c')
+            blob += (
+                array.view(np.uint8).astype(np.uint16).tobytes()
+            )  # convert to 16-bit chars for MATLAB
         else:  # numeric arrays
             if array.ndim == 0:  # not supported by original mym
                 self.set_dj0()
@@ -245,55 +279,74 @@ def read_recarray(self):
         """
         Serialize an np.ndarray with fields, including recarrays
         """
-        n_fields = self.read_value('uint32')
+        n_fields = self.read_value("uint32")
         if not n_fields:
             return np.array(None)  # empty array
         field_names = [self.read_zero_terminated_string() for _ in range(n_fields)]
         arrays = [self.read_blob() for _ in range(n_fields)]
-        rec = np.empty(arrays[0].shape, np.dtype([(f, t.dtype) for f, t in zip(field_names, arrays)]))
+        rec = np.empty(
+            arrays[0].shape,
+            np.dtype([(f, t.dtype) for f, t in zip(field_names, arrays)]),
+        )
         for f, t in zip(field_names, arrays):
             rec[f] = t
         return rec.view(np.recarray)
 
     def pack_recarray(self, array):
-        """ Serialize a Matlab struct array """
-        return (b"F" + len_u32(array.dtype) +  # number of fields
-                '\0'.join(array.dtype.names).encode() + b"\0" +  # field names
-                b"".join(self.pack_recarray(array[f]) if array[f].dtype.fields else self.pack_array(array[f])
-                         for f in array.dtype.names))
+        """Serialize a Matlab struct array"""
+        return (
+            b"F"
+            + len_u32(array.dtype)
+            + "\0".join(array.dtype.names).encode()  # number of fields
+            + b"\0"
+            + b"".join(  # field names
+                self.pack_recarray(array[f])
+                if array[f].dtype.fields
+                else self.pack_array(array[f])
+                for f in array.dtype.names
+            )
+        )
 
     def read_sparse_array(self):
-        raise DataJointError('datajoint-python does not yet support sparse arrays. Issue (#590)')
+        raise DataJointError(
+            "datajoint-python does not yet support sparse arrays. Issue (#590)"
+        )
 
     def read_int(self):
-        return int.from_bytes(self.read_binary(self.read_value('uint16')), byteorder='little', signed=True)
+        return int.from_bytes(
+            self.read_binary(self.read_value("uint16")), byteorder="little", signed=True
+        )
 
     @staticmethod
     def pack_int(v):
         n_bytes = v.bit_length() // 8 + 1
-        assert 0 < n_bytes <= 0xFFFF, 'Integers are limited to 65535 bytes'
-        return b"\x0a" + np.uint16(n_bytes).tobytes() + v.to_bytes(n_bytes, byteorder='little', signed=True)
+        assert 0 < n_bytes <= 0xFFFF, "Integers are limited to 65535 bytes"
+        return (
+            b"\x0a"
+            + np.uint16(n_bytes).tobytes()
+            + v.to_bytes(n_bytes, byteorder="little", signed=True)
+        )
 
     def read_bool(self):
-        return bool(self.read_value('bool'))
+        return bool(self.read_value("bool"))
 
     @staticmethod
     def pack_bool(v):
-        return b"\x0b" + np.array(v, dtype='bool').tobytes()
+        return b"\x0b" + np.array(v, dtype="bool").tobytes()
 
     def read_complex(self):
-        return complex(self.read_value('complex128'))
+        return complex(self.read_value("complex128"))
 
     @staticmethod
     def pack_complex(v):
-        return b"\x0c" + np.array(v, dtype='complex128').tobytes()
+        return b"\x0c" + np.array(v, dtype="complex128").tobytes()
 
     def read_float(self):
-        return float(self.read_value('float64'))
+        return float(self.read_value("float64"))
 
     @staticmethod
     def pack_float(v):
-        return b"\x0d" + np.array(v, dtype='float64').tobytes()
+        return b"\x0d" + np.array(v, dtype="float64").tobytes()
 
     def read_decimal(self):
         return Decimal(self.read_string())
@@ -326,82 +379,129 @@ def pack_none():
         return b"\xFF"
 
     def read_tuple(self):
-        return tuple(self.read_blob(self.read_value()) for _ in range(self.read_value()))
+        return tuple(
+            self.read_blob(self.read_value()) for _ in range(self.read_value())
+        )
 
     def pack_tuple(self, t):
-        return b"\1" + len_u64(t) + b"".join(
-            len_u64(it) + it for it in (self.pack_blob(i) for i in t))
+        return (
+            b"\1"
+            + len_u64(t)
+            + b"".join(len_u64(it) + it for it in (self.pack_blob(i) for i in t))
+        )
 
     def read_list(self):
         return list(self.read_blob(self.read_value()) for _ in range(self.read_value()))
 
     def pack_list(self, t):
-        return b"\2" + len_u64(t) + b"".join(
-            len_u64(it) + it for it in (self.pack_blob(i) for i in t))
+        return (
+            b"\2"
+            + len_u64(t)
+            + b"".join(len_u64(it) + it for it in (self.pack_blob(i) for i in t))
+        )
 
     def read_set(self):
         return set(self.read_blob(self.read_value()) for _ in range(self.read_value()))
 
     def pack_set(self, t):
-        return b"\3" + len_u64(t) + b"".join(
-            len_u64(it) + it for it in (self.pack_blob(i) for i in t))
+        return (
+            b"\3"
+            + len_u64(t)
+            + b"".join(len_u64(it) + it for it in (self.pack_blob(i) for i in t))
+        )
 
     def read_dict(self):
-        return dict((self.read_blob(self.read_value()), self.read_blob(self.read_value()))
-                    for _ in range(self.read_value()))
+        return dict(
+            (self.read_blob(self.read_value()), self.read_blob(self.read_value()))
+            for _ in range(self.read_value())
+        )
 
     def pack_dict(self, d):
-        return b"\4" + len_u64(d) + b"".join(
-            b"".join((len_u64(it) + it) for it in packed)
-            for packed in (map(self.pack_blob, pair) for pair in d.items()))
+        return (
+            b"\4"
+            + len_u64(d)
+            + b"".join(
+                b"".join((len_u64(it) + it) for it in packed)
+                for packed in (map(self.pack_blob, pair) for pair in d.items())
+            )
+        )
 
     def read_struct(self):
         """deserialize matlab stuct"""
         n_dims = self.read_value()
         shape = self.read_value(count=n_dims)
         n_elem = np.prod(shape, dtype=int)
-        n_fields = self.read_value('uint32')
+        n_fields = self.read_value("uint32")
         if not n_fields:
             return np.array(None)  # empty array
         field_names = [self.read_zero_terminated_string() for _ in range(n_fields)]
         raw_data = [
-            tuple(self.read_blob(n_bytes=int(self.read_value('uint64'))) for _ in range(n_fields))
-            for __ in range(n_elem)]
+            tuple(
+                self.read_blob(n_bytes=int(self.read_value())) for _ in range(n_fields)
+            )
+            for __ in range(n_elem)
+        ]
         data = np.array(raw_data, dtype=list(zip(field_names, repeat(object))))
-        return self.squeeze(data.reshape(shape, order="F"), convert_to_scalar=False).view(MatStruct)
+        return self.squeeze(
+            data.reshape(shape, order="F"), convert_to_scalar=False
+        ).view(MatStruct)
 
     def pack_struct(self, array):
-        """ Serialize a Matlab struct array """
-        return (b"S" + np.array((array.ndim,) + array.shape, dtype=np.uint64).tobytes() +  # dimensionality
-                len_u32(array.dtype.names) +  # number of fields
-                "\0".join(array.dtype.names).encode() + b"\0" +  # field names
-                b"".join(len_u64(it) + it for it in (
-                    self.pack_blob(e) for rec in array.flatten(order="F") for e in rec)))  # values
+        """Serialize a Matlab struct array"""
+        return (
+            b"S"
+            + np.array((array.ndim,) + array.shape, dtype=np.uint64).tobytes()
+            + len_u32(array.dtype.names)  # dimensionality
+            + "\0".join(array.dtype.names).encode()  # number of fields
+            + b"\0"
+            + b"".join(  # field names
+                len_u64(it) + it
+                for it in (
+                    self.pack_blob(e) for rec in array.flatten(order="F") for e in rec
+                )
+            )
+        )  # values
 
     def read_cell_array(self):
-        """ deserialize MATLAB cell array """
+        """deserialize MATLAB cell array"""
         n_dims = self.read_value()
         shape = self.read_value(count=n_dims)
         n_elem = int(np.prod(shape))
         result = [self.read_blob(n_bytes=self.read_value()) for _ in range(n_elem)]
-        return (self.squeeze(np.array(result).reshape(shape, order="F"), convert_to_scalar=False)).view(MatCell)
+        return (
+            self.squeeze(
+                np.array(result).reshape(shape, order="F"), convert_to_scalar=False
+            )
+        ).view(MatCell)
 
     def pack_cell_array(self, array):
-        return (b"C" + np.array((array.ndim,) + array.shape, dtype=np.uint64).tobytes() +
-                b"".join(len_u64(it) + it for it in (self.pack_blob(e) for e in array.flatten(order="F"))))
+        return (
+            b"C"
+            + np.array((array.ndim,) + array.shape, dtype=np.uint64).tobytes()
+            + b"".join(
+                len_u64(it) + it
+                for it in (self.pack_blob(e) for e in array.flatten(order="F"))
+            )
+        )
 
     def read_datetime(self):
-        """ deserialize datetime.date, .time, or .datetime """
-        date, time = self.read_value('int32'), self.read_value('int64')
-        date = datetime.date(
-            year=date // 10000,
-            month=(date // 100) % 100,
-            day=date % 100) if date >= 0 else None
-        time = datetime.time(
-            hour=(time // 10000000000) % 100,
-            minute=(time // 100000000) % 100,
-            second=(time // 1000000) % 100,
-            microsecond=time % 1000000) if time >= 0 else None
+        """deserialize datetime.date, .time, or .datetime"""
+        date, time = self.read_value("int32"), self.read_value("int64")
+        date = (
+            datetime.date(year=date // 10000, month=(date // 100) % 100, day=date % 100)
+            if date >= 0
+            else None
+        )
+        time = (
+            datetime.time(
+                hour=(time // 10000000000) % 100,
+                minute=(time // 100000000) % 100,
+                second=(time // 1000000) % 100,
+                microsecond=time % 1000000,
+            )
+            if time >= 0
+            else None
+        )
         return time and date and datetime.datetime.combine(date, time) or time or date
 
     @staticmethod
@@ -413,9 +513,16 @@ def pack_datetime(d):
         else:
             date, time = None, d
         return b"t" + (
-            np.int32(-1 if date is None else (date.year*100 + date.month)*100 + date.day).tobytes() +
-            np.int64(-1 if time is None else
-                     ((time.hour*100 + time.minute)*100 + time.second)*1000000 + time.microsecond).tobytes())
+            np.int32(
+                -1 if date is None else (date.year * 100 + date.month) * 100 + date.day
+            ).tobytes()
+            + np.int64(
+                -1
+                if time is None
+                else ((time.hour * 100 + time.minute) * 100 + time.second) * 1000000
+                + time.microsecond
+            ).tobytes()
+        )
 
     def read_uuid(self):
         q = self.read_binary(16)
@@ -426,26 +533,30 @@ def pack_uuid(obj):
         return b"u" + obj.bytes
 
     def read_zero_terminated_string(self):
-        target = self._blob.find(b'\0', self._pos)
-        data = self._blob[self._pos:target].decode()
+        target = self._blob.find(b"\0", self._pos)
+        data = self._blob[self._pos : target].decode()
         self._pos = target + 1
         return data
 
-    def read_value(self, dtype='uint64', count=1):
+    def read_value(self, dtype=None, count=1):
+        if dtype is None:
+            dtype = "uint32" if use_32bit_dims else "uint64"
         data = np.frombuffer(self._blob, dtype=dtype, count=count, offset=self._pos)
         self._pos += data.dtype.itemsize * data.size
         return data[0] if count == 1 else data
 
     def read_binary(self, size):
         self._pos += int(size)
-        return self._blob[self._pos-int(size):self._pos]
+        return self._blob[self._pos - int(size) : self._pos]
 
     def pack(self, obj, compress):
         self.protocol = b"mYm\0"  # will be replaced with dj0 if new features are used
-        blob = self.pack_blob(obj)  # this may reset the protocol and must precede protocol evaluation
+        blob = self.pack_blob(
+            obj
+        )  # this may reset the protocol and must precede protocol evaluation
         blob = self.protocol + blob
         if compress and len(blob) > 1000:
-            compressed = b'ZL123\0' + len_u64(blob) + zlib.compress(blob)
+            compressed = b"ZL123\0" + len_u64(blob) + zlib.compress(blob)
             if len(compressed) < len(blob):
                 blob = compressed
         return blob
@@ -454,7 +565,9 @@ def pack(self, obj, compress):
 def pack(obj, compress=True):
     if bypass_serialization:
         # provide a way to move blobs quickly without de/serialization
-        assert isinstance(obj, bytes) and obj.startswith((b'ZL123\0', b'mYm\0', b'dj0\0'))
+        assert isinstance(obj, bytes) and obj.startswith(
+            (b"ZL123\0", b"mYm\0", b"dj0\0")
+        )
         return obj
     return Blob().pack(obj, compress=compress)
 
@@ -462,7 +575,9 @@ def pack(obj, compress=True):
 def unpack(blob, squeeze=False):
     if bypass_serialization:
         # provide a way to move blobs quickly without de/serialization
-        assert isinstance(blob, bytes) and blob.startswith((b'ZL123\0', b'mYm\0', b'dj0\0'))
+        assert isinstance(blob, bytes) and blob.startswith(
+            (b"ZL123\0", b"mYm\0", b"dj0\0")
+        )
         return blob
     if blob is not None:
         return Blob(squeeze=squeeze).unpack(blob)
diff --git a/datajoint/condition.py b/datajoint/condition.py
index fed138cf1..397f68b53 100644
--- a/datajoint/condition.py
+++ b/datajoint/condition.py
@@ -15,6 +15,7 @@ class PromiscuousOperand:
     """
     A container for an operand to ignore join compatibility
     """
+
     def __init__(self, operand):
         self.operand = operand
 
@@ -30,6 +31,7 @@ class AndList(list):
     is equivalent to
     expr2 = expr & cond1 & cond2 & cond3
     """
+
     def append(self, restriction):
         if isinstance(restriction, AndList):
             # extend to reduce nesting
@@ -39,7 +41,8 @@ def append(self, restriction):
 
 
 class Not:
-    """ invert restriction """
+    """invert restriction"""
+
     def __init__(self, restriction):
         self.restriction = restriction
 
@@ -59,13 +62,21 @@ def assert_join_compatibility(expr1, expr2):
     for rel in (expr1, expr2):
         if not isinstance(rel, (U, QueryExpression)):
             raise DataJointError(
-                'Object %r is not a QueryExpression and cannot be joined.' % rel)
-    if not isinstance(expr1, U) and not isinstance(expr2, U):  # dj.U is always compatible
+                "Object %r is not a QueryExpression and cannot be joined." % rel
+            )
+    if not isinstance(expr1, U) and not isinstance(
+        expr2, U
+    ):  # dj.U is always compatible
         try:
             raise DataJointError(
-                "Cannot join query expressions on dependent attribute `%s`" % next(
-                    r for r in set(expr1.heading.secondary_attributes).intersection(
-                        expr2.heading.secondary_attributes)))
+                "Cannot join query expressions on dependent attribute `%s`"
+                % next(
+                    r
+                    for r in set(expr1.heading.secondary_attributes).intersection(
+                        expr2.heading.secondary_attributes
+                    )
+                )
+            )
         except StopIteration:
             pass  # all ok
 
@@ -90,13 +101,16 @@ def prep_value(k, v):
                     v = uuid.UUID(v)
                 except (AttributeError, ValueError):
                     raise DataJointError(
-                        'Badly formed UUID {v} in restriction by `{k}`'.format(k=k, v=v))
+                        "Badly formed UUID {v} in restriction by `{k}`".format(k=k, v=v)
+                    )
             return "X'%s'" % v.bytes.hex()
-        if isinstance(v, (datetime.date, datetime.datetime, datetime.time, decimal.Decimal)):
+        if isinstance(
+            v, (datetime.date, datetime.datetime, datetime.time, decimal.Decimal)
+        ):
             return '"%s"' % v
         if isinstance(v, str):
-            return '"%s"' % v.replace('%', '%%')
-        return '%r' % v
+            return '"%s"' % v.replace("%", "%%")
+        return "%r" % v
 
     negate = False
     while isinstance(condition, Not):
@@ -107,19 +121,25 @@ def prep_value(k, v):
     # restrict by string
     if isinstance(condition, str):
         columns.update(extract_column_names(condition))
-        return template % condition.strip().replace("%", "%%")  # escape %, see issue #376
+        return template % condition.strip().replace(
+            "%", "%%"
+        )  # escape %, see issue #376
 
     # restrict by AndList
     if isinstance(condition, AndList):
         # omit all conditions that evaluate to True
-        items = [item for item in (make_condition(query_expression, cond, columns)
-                 for cond in condition)
-                 if item is not True]
+        items = [
+            item
+            for item in (
+                make_condition(query_expression, cond, columns) for cond in condition
+            )
+            if item is not True
+        ]
         if any(item is False for item in items):
             return negate  # if any item is False, the whole thing is False
         if not items:
             return not negate  # and empty AndList is True
-        return template % ('(' + ') AND ('.join(items) + ')')
+        return template % ("(" + ") AND (".join(items) + ")")
 
     # restriction by dj.U evaluates to True
     if isinstance(condition, U):
@@ -135,20 +155,36 @@ def prep_value(k, v):
         if not common_attributes:
             return not negate  # no matching attributes -> evaluates to True
         columns.update(common_attributes)
-        return template % ('(' + ') AND ('.join(
-            '`%s`%s' % (k, ' IS NULL' if condition[k] is None
-                        else f'={prep_value(k, condition[k])}')
-            for k in common_attributes) + ')')
+        return template % (
+            "("
+            + ") AND (".join(
+                "`%s`%s"
+                % (
+                    k,
+                    " IS NULL"
+                    if condition[k] is None
+                    else f"={prep_value(k, condition[k])}",
+                )
+                for k in common_attributes
+            )
+            + ")"
+        )
 
     # restrict by a numpy record -- convert to an AndList of string equality conditions
     if isinstance(condition, numpy.void):
         common_attributes = set(condition.dtype.fields).intersection(
-            query_expression.heading.names)
+            query_expression.heading.names
+        )
         if not common_attributes:
-            return not negate   # no matching attributes -> evaluate to True
+            return not negate  # no matching attributes -> evaluate to True
         columns.update(common_attributes)
-        return template % ('(' + ') AND ('.join(
-            '`%s`=%s' % (k, prep_value(k, condition[k])) for k in common_attributes) + ')')
+        return template % (
+            "("
+            + ") AND (".join(
+                "`%s`=%s" % (k, prep_value(k, condition[k])) for k in common_attributes
+            )
+            + ")"
+        )
 
     # restrict by a QueryExpression subclass -- trigger instantiation and move on
     if inspect.isclass(condition) and issubclass(condition, QueryExpression):
@@ -163,18 +199,22 @@ def prep_value(k, v):
     if isinstance(condition, QueryExpression):
         if check_compatibility:
             assert_join_compatibility(query_expression, condition)
-        common_attributes = [q for q in condition.heading.names
-                             if q in query_expression.heading.names]
+        common_attributes = [
+            q for q in condition.heading.names if q in query_expression.heading.names
+        ]
         columns.update(common_attributes)
         if isinstance(condition, Aggregation):
             condition = condition.make_subquery()
         return (
             # without common attributes, any non-empty set matches everything
-            (not negate if condition else negate) if not common_attributes
-            else '({fields}) {not_}in ({subquery})'.format(
-                fields='`' + '`,`'.join(common_attributes) + '`',
+            (not negate if condition else negate)
+            if not common_attributes
+            else "({fields}) {not_}in ({subquery})".format(
+                fields="`" + "`,`".join(common_attributes) + "`",
                 not_="not " if negate else "",
-                subquery=condition.make_sql(common_attributes)))
+                subquery=condition.make_sql(common_attributes),
+            )
+        )
 
     # restrict by pandas.DataFrames
     if isinstance(condition, pandas.DataFrame):
@@ -184,12 +224,14 @@ def prep_value(k, v):
     try:
         or_list = [make_condition(query_expression, q, columns) for q in condition]
     except TypeError:
-        raise DataJointError('Invalid restriction type %r' % condition)
+        raise DataJointError("Invalid restriction type %r" % condition)
     else:
-        or_list = [item for item in or_list if item is not False]  # ignore False conditions
+        or_list = [
+            item for item in or_list if item is not False
+        ]  # ignore False conditions
         if any(item is True for item in or_list):  # if any item is True, entirely True
             return not negate
-        return template % ('(%s)' % ' OR '.join(or_list)) if or_list else negate
+        return template % ("(%s)" % " OR ".join(or_list)) if or_list else negate
 
 
 def extract_column_names(sql_expression):
@@ -205,21 +247,38 @@ def extract_column_names(sql_expression):
     result = set()
     s = sql_expression  # for terseness
     # remove escaped quotes
-    s = re.sub(r'(\\\")|(\\\')', '', s)
+    s = re.sub(r"(\\\")|(\\\')", "", s)
     # remove quoted text
     s = re.sub(r"'[^']*'", "", s)
-    s = re.sub(r'"[^"]*"', '', s)
+    s = re.sub(r'"[^"]*"', "", s)
     # find all tokens in back quotes and remove them
     result.update(re.findall(r"`([a-z][a-z_0-9]*)`", s))
-    s = re.sub(r"`[a-z][a-z_0-9]*`", '', s)
+    s = re.sub(r"`[a-z][a-z_0-9]*`", "", s)
     # remove space before parentheses
     s = re.sub(r"\s*\(", "(", s)
     # remove tokens followed by ( since they must be functions
     s = re.sub(r"(\b[a-z][a-z_0-9]*)\(", "(", s)
     remaining_tokens = set(re.findall(r"\b[a-z][a-z_0-9]*\b", s))
     # update result removing reserved words
-    result.update(remaining_tokens - {"is", "in", "between", "like", "and", "or", "null",
-                                      "not", "interval", "second", "minute", "hour", "day",
-                                      "month", "week", "year"
-                                      })
+    result.update(
+        remaining_tokens
+        - {
+            "is",
+            "in",
+            "between",
+            "like",
+            "and",
+            "or",
+            "null",
+            "not",
+            "interval",
+            "second",
+            "minute",
+            "hour",
+            "day",
+            "month",
+            "week",
+            "year",
+        }
+    )
     return result
diff --git a/datajoint/connection.py b/datajoint/connection.py
index 3daac4bac..2a93cbb17 100644
--- a/datajoint/connection.py
+++ b/datajoint/connection.py
@@ -1,6 +1,6 @@
 """
-This module contains the Connection class that manages the connection to the database,
- and the `conn` function that provides access to a persistent connection in datajoint.
+This module contains the Connection class that manages the connection to the database, and
+the ``conn`` function that provides access to a persistent connection in datajoint.
 """
 import warnings
 from contextlib import contextmanager
@@ -22,25 +22,29 @@
 
 
 def get_host_hook(host_input):
-    if '://' in host_input:
-        plugin_name = host_input.split('://')[0]
+    if "://" in host_input:
+        plugin_name = host_input.split("://")[0]
         try:
-            return connection_plugins[plugin_name]['object'].load().get_host(host_input)
+            return connection_plugins[plugin_name]["object"].load().get_host(host_input)
         except KeyError:
             raise errors.DataJointError(
-                "Connection plugin '{}' not found.".format(plugin_name))
+                "Connection plugin '{}' not found.".format(plugin_name)
+            )
     else:
         return host_input
 
 
 def connect_host_hook(connection_obj):
-    if '://' in connection_obj.conn_info['host_input']:
-        plugin_name = connection_obj.conn_info['host_input'].split('://')[0]
+    if "://" in connection_obj.conn_info["host_input"]:
+        plugin_name = connection_obj.conn_info["host_input"].split("://")[0]
         try:
-            connection_plugins[plugin_name]['object'].load().connect_host(connection_obj)
+            connection_plugins[plugin_name]["object"].load().connect_host(
+                connection_obj
+            )
         except KeyError:
             raise errors.DataJointError(
-                "Connection plugin '{}' not found.".format(plugin_name))
+                "Connection plugin '{}' not found.".format(plugin_name)
+            )
     else:
         connection_obj.connect()
 
@@ -48,24 +52,27 @@ def connect_host_hook(connection_obj):
 def translate_query_error(client_error, query):
     """
     Take client error and original query and return the corresponding DataJoint exception.
+
     :param client_error: the exception raised by the client interface
     :param query: sql query with placeholders
     :return: an instance of the corresponding subclass of datajoint.errors.DataJointError
     """
-    logger.debug('type: {}, args: {}'.format(type(client_error), client_error.args))
+    logger.debug("type: {}, args: {}".format(type(client_error), client_error.args))
 
     err, *args = client_error.args
 
     # Loss of connection errors
     if err in (0, "(0, '')"):
-        return errors.LostConnectionError('Server connection lost due to an interface error.', *args)
+        return errors.LostConnectionError(
+            "Server connection lost due to an interface error.", *args
+        )
     if err == 2006:
         return errors.LostConnectionError("Connection timed out", *args)
     if err == 2013:
         return errors.LostConnectionError("Server connection lost", *args)
     # Access errors
     if err in (1044, 1142):
-        return errors.AccessError('Insufficient privileges.', args[0],  query)
+        return errors.AccessError("Insufficient privileges.", args[0], query)
     # Integrity errors
     if err == 1062:
         return errors.DuplicateError(*args)
@@ -87,7 +94,9 @@ def translate_query_error(client_error, query):
     return client_error
 
 
-def conn(host=None, user=None, password=None, *, init_fun=None, reset=False, use_tls=None):
+def conn(
+    host=None, user=None, password=None, *, init_fun=None, reset=False, use_tls=None
+):
     """
     Returns a persistent connection object to be shared by multiple modules.
     If the connection is not yet established or reset=True, a new connection is set up.
@@ -100,28 +109,29 @@ def conn(host=None, user=None, password=None, *, init_fun=None, reset=False, use
     :param password: mysql password
     :param init_fun: initialization function
     :param reset: whether the connection should be reset or not
-    :param use_tls: TLS encryption option. Valid options are: True (required),
-                    False (required no TLS), None (TLS prefered, default),
-                    dict (Manually specify values per
-                    https://dev.mysql.com/doc/refman/5.7/en/connection-options.html
-                        #encrypted-connection-options).
+    :param use_tls: TLS encryption option. Valid options are: True (required), False
+        (required no TLS), None (TLS prefered, default), dict (Manually specify values per
+        https://dev.mysql.com/doc/refman/5.7/en/connection-options.html#encrypted-connection-options).
     """
-    if not hasattr(conn, 'connection') or reset:
-        host = host if host is not None else config['database.host']
-        user = user if user is not None else config['database.user']
-        password = password if password is not None else config['database.password']
+    if not hasattr(conn, "connection") or reset:
+        host = host if host is not None else config["database.host"]
+        user = user if user is not None else config["database.user"]
+        password = password if password is not None else config["database.password"]
         if user is None:  # pragma: no cover
             user = input("Please enter DataJoint username: ")
         if password is None:  # pragma: no cover
             password = getpass(prompt="Please enter DataJoint password: ")
-        init_fun = init_fun if init_fun is not None else config['connection.init_function']
-        use_tls = use_tls if use_tls is not None else config['database.use_tls']
+        init_fun = (
+            init_fun if init_fun is not None else config["connection.init_function"]
+        )
+        use_tls = use_tls if use_tls is not None else config["database.use_tls"]
         conn.connection = Connection(host, user, password, None, init_fun, use_tls)
     return conn.connection
 
 
 class EmulatedCursor:
     """acts like a cursor"""
+
     def __init__(self, data):
         self._data = data
         self._iter = iter(self._data)
@@ -160,17 +170,19 @@ class Connection:
 
     def __init__(self, host, user, password, port=None, init_fun=None, use_tls=None):
         host_input, host = (host, get_host_hook(host))
-        if ':' in host:
+        if ":" in host:
             # the port in the hostname overrides the port argument
-            host, port = host.split(':')
+            host, port = host.split(":")
             port = int(port)
         elif port is None:
-            port = config['database.port']
+            port = config["database.port"]
         self.conn_info = dict(host=host, port=port, user=user, passwd=password)
         if use_tls is not False:
-            self.conn_info['ssl'] = use_tls if isinstance(use_tls, dict) else {'ssl': {}}
-        self.conn_info['ssl_input'] = use_tls
-        self.conn_info['host_input'] = host_input
+            self.conn_info["ssl"] = (
+                use_tls if isinstance(use_tls, dict) else {"ssl": {}}
+            )
+        self.conn_info["ssl_input"] = use_tls
+        self.conn_info["host_input"] = host_input
         self.init_fun = init_fun
         print("Connecting {user}@{host}:{port}".format(**self.conn_info))
         self._conn = None
@@ -178,9 +190,9 @@ def __init__(self, host, user, password, port=None, init_fun=None, use_tls=None)
         connect_host_hook(self)
         if self.is_connected:
             logger.info("Connected {user}@{host}:{port}".format(**self.conn_info))
-            self.connection_id = self.query('SELECT connection_id()').fetchone()[0]
+            self.connection_id = self.query("SELECT connection_id()").fetchone()[0]
         else:
-            raise errors.LostConnectionError('Connection failed.')
+            raise errors.LostConnectionError("Connection failed.")
         self._in_transaction = False
         self.schemas = dict()
         self.dependencies = Dependencies(self)
@@ -191,29 +203,41 @@ def __eq__(self, other):
     def __repr__(self):
         connected = "connected" if self.is_connected else "disconnected"
         return "DataJoint connection ({connected}) {user}@{host}:{port}".format(
-            connected=connected, **self.conn_info)
+            connected=connected, **self.conn_info
+        )
 
     def connect(self):
-        """ Connect to the database server."""
+        """Connect to the database server."""
         with warnings.catch_warnings():
-            warnings.filterwarnings('ignore', '.*deprecated.*')
+            warnings.filterwarnings("ignore", ".*deprecated.*")
             try:
                 self._conn = client.connect(
                     init_command=self.init_fun,
                     sql_mode="NO_ZERO_DATE,NO_ZERO_IN_DATE,ERROR_FOR_DIVISION_BY_ZERO,"
-                             "STRICT_ALL_TABLES,NO_ENGINE_SUBSTITUTION,ONLY_FULL_GROUP_BY",
-                    charset=config['connection.charset'],
-                    **{k: v for k, v in self.conn_info.items()
-                       if k not in ['ssl_input', 'host_input']})
+                    "STRICT_ALL_TABLES,NO_ENGINE_SUBSTITUTION,ONLY_FULL_GROUP_BY",
+                    charset=config["connection.charset"],
+                    **{
+                        k: v
+                        for k, v in self.conn_info.items()
+                        if k not in ["ssl_input", "host_input"]
+                    }
+                )
             except client.err.InternalError:
                 self._conn = client.connect(
                     init_command=self.init_fun,
                     sql_mode="NO_ZERO_DATE,NO_ZERO_IN_DATE,ERROR_FOR_DIVISION_BY_ZERO,"
-                             "STRICT_ALL_TABLES,NO_ENGINE_SUBSTITUTION,ONLY_FULL_GROUP_BY",
-                    charset=config['connection.charset'],
-                    **{k: v for k, v in self.conn_info.items()
-                       if not(k in ['ssl_input', 'host_input'] or
-                              k == 'ssl' and self.conn_info['ssl_input'] is None)})
+                    "STRICT_ALL_TABLES,NO_ENGINE_SUBSTITUTION,ONLY_FULL_GROUP_BY",
+                    charset=config["connection.charset"],
+                    **{
+                        k: v
+                        for k, v in self.conn_info.items()
+                        if not (
+                            k in ["ssl_input", "host_input"]
+                            or k == "ssl"
+                            and self.conn_info["ssl_input"] is None
+                        )
+                    }
+                )
         self._conn.autocommit(True)
 
     def set_query_cache(self, query_cache=None):
@@ -222,15 +246,19 @@ def set_query_cache(self, query_cache=None):
         1. Only SELECT queries are allowed.
         2. The results of queries are cached under the path indicated by dj.config['query_cache']
         3. query_cache is a string that differentiates different cache states.
+
         :param query_cache: a string to initialize the hash for query results
         """
         self._query_cache = query_cache
 
     def purge_query_cache(self):
-        """ Purges all query cache. """
-        if 'query_cache' in config and isinstance(config['query_cache'], str) and \
-                pathlib.Path(config['query_cache']).is_dir():
-            path_iter = pathlib.Path(config['query_cache']).glob('**/*')
+        """Purges all query cache."""
+        if (
+            "query_cache" in config
+            and isinstance(config["query_cache"], str)
+            and pathlib.Path(config["query_cache"]).is_dir()
+        ):
+            path_iter = pathlib.Path(config["query_cache"]).glob("**/*")
             for path in path_iter:
                 path.unlink()
 
@@ -242,12 +270,12 @@ def register(self, schema):
         self.dependencies.clear()
 
     def ping(self):
-        """ Ping the connection or raises an exception if the connection is closed. """
+        """Ping the connection or raises an exception if the connection is closed."""
         self._conn.ping(reconnect=False)
 
     @property
     def is_connected(self):
-        """ Return true if the object is connected to the database server. """
+        """Return true if the object is connected to the database server."""
         try:
             self.ping()
         except:
@@ -265,9 +293,12 @@ def _execute_query(cursor, query, args, suppress_warnings):
         except client.err.Error as err:
             raise translate_query_error(err, query)
 
-    def query(self, query, args=(), *, as_dict=False, suppress_warnings=True, reconnect=None):
+    def query(
+        self, query, args=(), *, as_dict=False, suppress_warnings=True, reconnect=None
+    ):
         """
         Execute the specified query and return the tuple generator (cursor).
+
         :param query: SQL query
         :param args: additional arguments for the client.cursor
         :param as_dict: If as_dict is set to True, the returned cursor objects returns
@@ -278,21 +309,28 @@ def query(self, query, args=(), *, as_dict=False, suppress_warnings=True, reconn
         # check cache first:
         use_query_cache = bool(self._query_cache)
         if use_query_cache and not re.match(r"\s*(SELECT|SHOW)", query):
-            raise errors.DataJointError("Only SELECT queries are allowed when query caching is on.")
+            raise errors.DataJointError(
+                "Only SELECT queries are allowed when query caching is on."
+            )
         if use_query_cache:
-            if not config['query_cache']:
-                raise errors.DataJointError("Provide filepath dj.config['query_cache'] when using query caching.")
-            hash_ = uuid_from_buffer((str(self._query_cache) + re.sub(r'`\$\w+`', '', query)).encode() + pack(args))
-            cache_path = pathlib.Path(config['query_cache']) / str(hash_)
+            if not config["query_cache"]:
+                raise errors.DataJointError(
+                    "Provide filepath dj.config['query_cache'] when using query caching."
+                )
+            hash_ = uuid_from_buffer(
+                (str(self._query_cache) + re.sub(r"`\$\w+`", "", query)).encode()
+                + pack(args)
+            )
+            cache_path = pathlib.Path(config["query_cache"]) / str(hash_)
             try:
                 buffer = cache_path.read_bytes()
             except FileNotFoundError:
-                pass   # proceed to query the database
+                pass  # proceed to query the database
             else:
                 return EmulatedCursor(unpack(buffer))
 
         if reconnect is None:
-            reconnect = config['database.reconnect']
+            reconnect = config["database.reconnect"]
         logger.debug("Executing SQL:" + query[:query_log_max_length])
         cursor_class = client.cursors.DictCursor if as_dict else client.cursors.Cursor
         cursor = self._conn.cursor(cursor=cursor_class)
@@ -305,7 +343,9 @@ def query(self, query, args=(), *, as_dict=False, suppress_warnings=True, reconn
             connect_host_hook(self)
             if self._in_transaction:
                 self.cancel_transaction()
-                raise errors.LostConnectionError("Connection was lost during a transaction.")
+                raise errors.LostConnectionError(
+                    "Connection was lost during a transaction."
+                )
             logger.debug("Re-executing")
             cursor = self._conn.cursor(cursor=cursor_class)
             self._execute_query(cursor, query, args, suppress_warnings)
@@ -321,7 +361,7 @@ def get_user(self):
         """
         :return: the user name and host name provided by the client to the server.
         """
-        return self.query('SELECT user()').fetchone()[0]
+        return self.query("SELECT user()").fetchone()[0]
 
     # ---------- transaction processing
     @property
@@ -338,7 +378,7 @@ def start_transaction(self):
         """
         if self.in_transaction:
             raise errors.DataJointError("Nested connections are not supported.")
-        self.query('START TRANSACTION WITH CONSISTENT SNAPSHOT')
+        self.query("START TRANSACTION WITH CONSISTENT SNAPSHOT")
         self._in_transaction = True
         logger.info("Transaction started")
 
@@ -346,7 +386,7 @@ def cancel_transaction(self):
         """
         Cancels the current transaction and rolls back all changes made during the transaction.
         """
-        self.query('ROLLBACK')
+        self.query("ROLLBACK")
         self._in_transaction = False
         logger.info("Transaction cancelled. Rolling back ...")
 
@@ -355,7 +395,7 @@ def commit_transaction(self):
         Commit all changes made during the transaction and close it.
 
         """
-        self.query('COMMIT')
+        self.query("COMMIT")
         self._in_transaction = False
         logger.info("Transaction committed and closed.")
 
diff --git a/datajoint/declare.py b/datajoint/declare.py
index 93e7592de..0916f7bf3 100644
--- a/datajoint/declare.py
+++ b/datajoint/declare.py
@@ -9,42 +9,70 @@
 from .errors import DataJointError, _support_filepath_types, FILEPATH_FEATURE_SWITCH
 from .attribute_adapter import get_adapter
 
-UUID_DATA_TYPE = 'binary(16)'
+UUID_DATA_TYPE = "binary(16)"
 MAX_TABLE_NAME_LENGTH = 64
-CONSTANT_LITERALS = {'CURRENT_TIMESTAMP', 'NULL'}  # SQL literals to be used without quotes (case insensitive)
-EXTERNAL_TABLE_ROOT = '~external'
-
-TYPE_PATTERN = {k: re.compile(v, re.I) for k, v in dict(
-    INTEGER=r'((tiny|small|medium|big|)int|integer)(\s*\(.+\))?(\s+unsigned)?(\s+auto_increment)?|serial$',
-    DECIMAL=r'(decimal|numeric)(\s*\(.+\))?(\s+unsigned)?$',
-    FLOAT=r'(double|float|real)(\s*\(.+\))?(\s+unsigned)?$',
-    STRING=r'(var)?char\s*\(.+\)$',
-    ENUM=r'enum\s*\(.+\)$',
-    BOOL=r'bool(ean)?$',   # aliased to tinyint(1)
-    TEMPORAL=r'(date|datetime|time|timestamp|year)(\s*\(.+\))?$',
-    INTERNAL_BLOB=r'(tiny|small|medium|long|)blob$',
-    EXTERNAL_BLOB=r'blob@(?P<store>[a-z]\w*)$',
-    INTERNAL_ATTACH=r'attach$',
-    EXTERNAL_ATTACH=r'attach@(?P<store>[a-z]\w*)$',
-    FILEPATH=r'filepath@(?P<store>[a-z]\w*)$',
-    UUID=r'uuid$',
-    ADAPTED=r'<.+>$'
-).items()}
+CONSTANT_LITERALS = {
+    "CURRENT_TIMESTAMP",
+    "NULL",
+}  # SQL literals to be used without quotes (case insensitive)
+EXTERNAL_TABLE_ROOT = "~external"
+
+TYPE_PATTERN = {
+    k: re.compile(v, re.I)
+    for k, v in dict(
+        INTEGER=r"((tiny|small|medium|big|)int|integer)(\s*\(.+\))?(\s+unsigned)?(\s+auto_increment)?|serial$",
+        DECIMAL=r"(decimal|numeric)(\s*\(.+\))?(\s+unsigned)?$",
+        FLOAT=r"(double|float|real)(\s*\(.+\))?(\s+unsigned)?$",
+        STRING=r"(var)?char\s*\(.+\)$",
+        ENUM=r"enum\s*\(.+\)$",
+        BOOL=r"bool(ean)?$",  # aliased to tinyint(1)
+        TEMPORAL=r"(date|datetime|time|timestamp|year)(\s*\(.+\))?$",
+        INTERNAL_BLOB=r"(tiny|small|medium|long|)blob$",
+        EXTERNAL_BLOB=r"blob@(?P<store>[a-z][\-\w]*)$",
+        INTERNAL_ATTACH=r"attach$",
+        EXTERNAL_ATTACH=r"attach@(?P<store>[a-z][\-\w]*)$",
+        FILEPATH=r"filepath@(?P<store>[a-z][\-\w]*)$",
+        UUID=r"uuid$",
+        ADAPTED=r"<.+>$",
+    ).items()
+}
 
 # custom types are stored in attribute comment
-SPECIAL_TYPES = {'UUID', 'INTERNAL_ATTACH', 'EXTERNAL_ATTACH', 'EXTERNAL_BLOB', 'FILEPATH', 'ADAPTED'}
+SPECIAL_TYPES = {
+    "UUID",
+    "INTERNAL_ATTACH",
+    "EXTERNAL_ATTACH",
+    "EXTERNAL_BLOB",
+    "FILEPATH",
+    "ADAPTED",
+}
 NATIVE_TYPES = set(TYPE_PATTERN) - SPECIAL_TYPES
-EXTERNAL_TYPES = {'EXTERNAL_ATTACH', 'EXTERNAL_BLOB', 'FILEPATH'}  # data referenced by a UUID in external tables
-SERIALIZED_TYPES = {'EXTERNAL_ATTACH', 'INTERNAL_ATTACH', 'EXTERNAL_BLOB', 'INTERNAL_BLOB'}  # requires packing data
+EXTERNAL_TYPES = {
+    "EXTERNAL_ATTACH",
+    "EXTERNAL_BLOB",
+    "FILEPATH",
+}  # data referenced by a UUID in external tables
+SERIALIZED_TYPES = {
+    "EXTERNAL_ATTACH",
+    "INTERNAL_ATTACH",
+    "EXTERNAL_BLOB",
+    "INTERNAL_BLOB",
+}  # requires packing data
 
 assert set().union(SPECIAL_TYPES, EXTERNAL_TYPES, SERIALIZED_TYPES) <= set(TYPE_PATTERN)
 
 
 def match_type(attribute_type):
     try:
-        return next(category for category, pattern in TYPE_PATTERN.items() if pattern.match(attribute_type))
+        return next(
+            category
+            for category, pattern in TYPE_PATTERN.items()
+            if pattern.match(attribute_type)
+        )
     except StopIteration:
-        raise DataJointError("Unsupported attribute type {type}".format(type=attribute_type))
+        raise DataJointError(
+            "Unsupported attribute type {type}".format(type=attribute_type)
+        )
 
 
 logger = logging.getLogger(__name__)
@@ -53,48 +81,68 @@ def match_type(attribute_type):
 def build_foreign_key_parser_old():
     # old-style foreign key parser. Superseded by expression-based syntax. See issue #436
     # This will be deprecated in a future release.
-    left = pp.Literal('(').suppress()
-    right = pp.Literal(')').suppress()
-    attribute_name = pp.Word(pp.srange('[a-z]'), pp.srange('[a-z0-9_]'))
-    new_attrs = pp.Optional(left + pp.delimitedList(attribute_name) + right).setResultsName('new_attrs')
-    arrow = pp.Literal('->').suppress()
-    lbracket = pp.Literal('[').suppress()
-    rbracket = pp.Literal(']').suppress()
-    option = pp.Word(pp.srange('[a-zA-Z]'))
-    options = pp.Optional(lbracket + pp.delimitedList(option) + rbracket).setResultsName('options')
-    ref_table = pp.Word(pp.alphas, pp.alphanums + '._').setResultsName('ref_table')
-    ref_attrs = pp.Optional(left + pp.delimitedList(attribute_name) + right).setResultsName('ref_attrs')
+    left = pp.Literal("(").suppress()
+    right = pp.Literal(")").suppress()
+    attribute_name = pp.Word(pp.srange("[a-z]"), pp.srange("[a-z0-9_]"))
+    new_attrs = pp.Optional(
+        left + pp.delimitedList(attribute_name) + right
+    ).setResultsName("new_attrs")
+    arrow = pp.Literal("->").suppress()
+    lbracket = pp.Literal("[").suppress()
+    rbracket = pp.Literal("]").suppress()
+    option = pp.Word(pp.srange("[a-zA-Z]"))
+    options = pp.Optional(
+        lbracket + pp.delimitedList(option) + rbracket
+    ).setResultsName("options")
+    ref_table = pp.Word(pp.alphas, pp.alphanums + "._").setResultsName("ref_table")
+    ref_attrs = pp.Optional(
+        left + pp.delimitedList(attribute_name) + right
+    ).setResultsName("ref_attrs")
     return new_attrs + arrow + options + ref_table + ref_attrs
 
 
 def build_foreign_key_parser():
-    arrow = pp.Literal('->').suppress()
-    lbracket = pp.Literal('[').suppress()
-    rbracket = pp.Literal(']').suppress()
-    option = pp.Word(pp.srange('[a-zA-Z]'))
-    options = pp.Optional(lbracket + pp.delimitedList(option) + rbracket).setResultsName('options')
-    ref_table = pp.restOfLine.setResultsName('ref_table')
+    arrow = pp.Literal("->").suppress()
+    lbracket = pp.Literal("[").suppress()
+    rbracket = pp.Literal("]").suppress()
+    option = pp.Word(pp.srange("[a-zA-Z]"))
+    options = pp.Optional(
+        lbracket + pp.delimitedList(option) + rbracket
+    ).setResultsName("options")
+    ref_table = pp.restOfLine.setResultsName("ref_table")
     return arrow + options + ref_table
 
 
 def build_attribute_parser():
     quoted = pp.QuotedString('"') ^ pp.QuotedString("'")
-    colon = pp.Literal(':').suppress()
-    attribute_name = pp.Word(pp.srange('[a-z]'), pp.srange('[a-z0-9_]')).setResultsName('name')
-    data_type = (pp.Combine(pp.Word(pp.alphas) + pp.SkipTo("#", ignore=quoted))
-                 ^ pp.QuotedString('<', endQuoteChar='>', unquoteResults=False)).setResultsName('type')
-    default = pp.Literal('=').suppress() + pp.SkipTo(colon, ignore=quoted).setResultsName('default')
-    comment = pp.Literal('#').suppress() + pp.restOfLine.setResultsName('comment')
+    colon = pp.Literal(":").suppress()
+    attribute_name = pp.Word(pp.srange("[a-z]"), pp.srange("[a-z0-9_]")).setResultsName(
+        "name"
+    )
+    data_type = (
+        pp.Combine(pp.Word(pp.alphas) + pp.SkipTo("#", ignore=quoted))
+        ^ pp.QuotedString("<", endQuoteChar=">", unquoteResults=False)
+    ).setResultsName("type")
+    default = pp.Literal("=").suppress() + pp.SkipTo(
+        colon, ignore=quoted
+    ).setResultsName("default")
+    comment = pp.Literal("#").suppress() + pp.restOfLine.setResultsName("comment")
     return attribute_name + pp.Optional(default) + colon + data_type + comment
 
 
 def build_index_parser():
-    left = pp.Literal('(').suppress()
-    right = pp.Literal(')').suppress()
-    unique = pp.Optional(pp.CaselessKeyword('unique')).setResultsName('unique')
-    index = pp.CaselessKeyword('index').suppress()
-    attribute_name = pp.Word(pp.srange('[a-z]'), pp.srange('[a-z0-9_]'))
-    return unique + index + left + pp.delimitedList(attribute_name).setResultsName('attr_list') + right
+    left = pp.Literal("(").suppress()
+    right = pp.Literal(")").suppress()
+    unique = pp.Optional(pp.CaselessKeyword("unique")).setResultsName("unique")
+    index = pp.CaselessKeyword("index").suppress()
+    attribute_name = pp.Word(pp.srange("[a-z]"), pp.srange("[a-z0-9_]"))
+    return (
+        unique
+        + index
+        + left
+        + pp.delimitedList(attribute_name).setResultsName("attr_list")
+        + right
+    )
 
 
 foreign_key_parser_old = build_foreign_key_parser_old()
@@ -105,14 +153,17 @@ def build_index_parser():
 
 def is_foreign_key(line):
     """
+
     :param line: a line from the table definition
     :return: true if the line appears to be a foreign key definition
     """
-    arrow_position = line.find('->')
-    return arrow_position >= 0 and not any(c in line[:arrow_position] for c in '"#\'')
+    arrow_position = line.find("->")
+    return arrow_position >= 0 and not any(c in line[:arrow_position] for c in "\"#'")
 
 
-def compile_foreign_key(line, context, attributes, primary_key, attr_sql, foreign_key_sql, index_sql):
+def compile_foreign_key(
+    line, context, attributes, primary_key, attr_sql, foreign_key_sql, index_sql
+):
     """
     :param line: a line from a table definition
     :param context: namespace containing referenced objects
@@ -127,7 +178,7 @@ def compile_foreign_key(line, context, attributes, primary_key, attr_sql, foreig
     from .table import Table
     from .expression import QueryExpression
 
-    obsolete = False   # See issue #436.  Old style to be deprecated in a future release
+    obsolete = False  # See issue #436.  Old style to be deprecated in a future release
     try:
         result = foreign_key_parser.parseString(line)
     except pp.ParseException:
@@ -140,44 +191,66 @@ def compile_foreign_key(line, context, attributes, primary_key, attr_sql, foreig
     try:
         ref = eval(result.ref_table, context)
     except NameError if obsolete else Exception:
-        raise DataJointError('Foreign key reference %s could not be resolved' % result.ref_table)
+        raise DataJointError(
+            "Foreign key reference %s could not be resolved" % result.ref_table
+        )
 
     options = [opt.upper() for opt in result.options]
     for opt in options:  # check for invalid options
-        if opt not in {'NULLABLE', 'UNIQUE'}:
+        if opt not in {"NULLABLE", "UNIQUE"}:
             raise DataJointError('Invalid foreign key option "{opt}"'.format(opt=opt))
-    is_nullable = 'NULLABLE' in options
-    is_unique = 'UNIQUE' in options
+    is_nullable = "NULLABLE" in options
+    is_unique = "UNIQUE" in options
     if is_nullable and primary_key is not None:
-        raise DataJointError('Primary dependencies cannot be nullable in line "{line}"'.format(line=line))
+        raise DataJointError(
+            'Primary dependencies cannot be nullable in line "{line}"'.format(line=line)
+        )
 
     if obsolete:
         warnings.warn(
             'Line "{line}" uses obsolete syntax that will no longer be supported in datajoint 0.14. '
-            'For details, see issue #780 https://github.com/datajoint/datajoint-python/issues/780'.format(line=line))
+            "For details, see issue #780 https://github.com/datajoint/datajoint-python/issues/780".format(
+                line=line
+            )
+        )
         if not isinstance(ref, type) or not issubclass(ref, Table):
-            raise DataJointError('Foreign key reference %r must be a valid query' % result.ref_table)
+            raise DataJointError(
+                "Foreign key reference %r must be a valid query" % result.ref_table
+            )
 
     if isinstance(ref, type) and issubclass(ref, Table):
         ref = ref()
 
     # check that dependency is of a supported type
-    if (not isinstance(ref, QueryExpression) or len(ref.restriction) or
-            len(ref.support) != 1 or not isinstance(ref.support[0], str)):
-        raise DataJointError('Dependency "%s" is not supported (yet). Use a base table or its projection.' %
-                             result.ref_table)
+    if (
+        not isinstance(ref, QueryExpression)
+        or len(ref.restriction)
+        or len(ref.support) != 1
+        or not isinstance(ref.support[0], str)
+    ):
+        raise DataJointError(
+            'Dependency "%s" is not supported (yet). Use a base table or its projection.'
+            % result.ref_table
+        )
 
     if obsolete:
         # for backward compatibility with old-style dependency declarations.  See issue #436
         if not isinstance(ref, Table):
-            DataJointError('Dependency "%s" is not supported. Check documentation.' % result.ref_table)
+            DataJointError(
+                'Dependency "%s" is not supported. Check documentation.'
+                % result.ref_table
+            )
         if not all(r in ref.primary_key for r in result.ref_attrs):
             raise DataJointError('Invalid foreign key attributes in "%s"' % line)
         try:
-            raise DataJointError('Duplicate attributes "{attr}" in "{line}"'.format(
-                attr=next(attr for attr in result.new_attrs if attr in attributes), line=line))
+            raise DataJointError(
+                'Duplicate attributes "{attr}" in "{line}"'.format(
+                    attr=next(attr for attr in result.new_attrs if attr in attributes),
+                    line=line,
+                )
+            )
         except StopIteration:
-            pass   # the normal outcome
+            pass  # the normal outcome
 
         # Match the primary attributes of the referenced table to local attributes
         new_attrs = list(result.new_attrs)
@@ -186,7 +259,10 @@ def compile_foreign_key(line, context, attributes, primary_key, attr_sql, foreig
         # special case, the renamed attribute is implicit
         if new_attrs and not ref_attrs:
             if len(new_attrs) != 1:
-                raise DataJointError('Renamed foreign key must be mapped to the primary key in "%s"' % line)
+                raise DataJointError(
+                    'Renamed foreign key must be mapped to the primary key in "%s"'
+                    % line
+                )
             if len(ref.primary_key) == 1:
                 # if the primary key has one attribute, allow implicit renaming
                 ref_attrs = ref.primary_key
@@ -194,7 +270,10 @@ def compile_foreign_key(line, context, attributes, primary_key, attr_sql, foreig
                 # if only one primary key attribute remains, then allow implicit renaming
                 ref_attrs = [attr for attr in ref.primary_key if attr not in attributes]
                 if len(ref_attrs) != 1:
-                    raise DataJointError('Could not resolve which primary key attribute should be referenced in "%s"' % line)
+                    raise DataJointError(
+                        'Could not resolve which primary key attribute should be referenced in "%s"'
+                        % line
+                    )
 
         if len(new_attrs) != len(ref_attrs):
             raise DataJointError('Mismatched attributes in foreign key "%s"' % line)
@@ -210,26 +289,35 @@ def compile_foreign_key(line, context, attributes, primary_key, attr_sql, foreig
             if primary_key is not None:
                 primary_key.append(attr)
             attr_sql.append(
-                ref.heading[attr].sql.replace('NOT NULL ', '', int(is_nullable)))
+                ref.heading[attr].sql.replace("NOT NULL ", "", int(is_nullable))
+            )
 
     # declare the foreign key
     foreign_key_sql.append(
-        'FOREIGN KEY (`{fk}`) REFERENCES {ref} (`{pk}`) ON UPDATE CASCADE ON DELETE RESTRICT'.format(
-            fk='`,`'.join(ref.primary_key),
-            pk='`,`'.join(ref.heading[name].original_name for name in ref.primary_key),
-            ref=ref.support[0]))
+        "FOREIGN KEY (`{fk}`) REFERENCES {ref} (`{pk}`) ON UPDATE CASCADE ON DELETE RESTRICT".format(
+            fk="`,`".join(ref.primary_key),
+            pk="`,`".join(ref.heading[name].original_name for name in ref.primary_key),
+            ref=ref.support[0],
+        )
+    )
 
     # declare unique index
     if is_unique:
-        index_sql.append('UNIQUE INDEX ({attrs})'.format(attrs=','.join("`%s`" % attr for attr in ref.primary_key)))
+        index_sql.append(
+            "UNIQUE INDEX ({attrs})".format(
+                attrs=",".join("`%s`" % attr for attr in ref.primary_key)
+            )
+        )
 
 
 def prepare_declare(definition, context):
     # split definition into lines
-    definition = re.split(r'\s*\n\s*', definition.strip())
+    definition = re.split(r"\s*\n\s*", definition.strip())
     # check for optional table comment
-    table_comment = definition.pop(0)[1:].strip() if definition[0].startswith('#') else ''
-    if table_comment.startswith(':'):
+    table_comment = (
+        definition.pop(0)[1:].strip() if definition[0].startswith("#") else ""
+    )
+    if table_comment.startswith(":"):
         raise DataJointError('Table comment must not start with a colon ":"')
     in_key = True  # parse primary keys
     primary_key = []
@@ -240,15 +328,21 @@ def prepare_declare(definition, context):
     external_stores = []
 
     for line in definition:
-        if not line or line.startswith('#'):  # ignore additional comments
+        if not line or line.startswith("#"):  # ignore additional comments
             pass
-        elif line.startswith('---') or line.startswith('___'):
+        elif line.startswith("---") or line.startswith("___"):
             in_key = False  # start parsing dependent attributes
         elif is_foreign_key(line):
-            compile_foreign_key(line, context, attributes,
-                                primary_key if in_key else None,
-                                attribute_sql, foreign_key_sql, index_sql)
-        elif re.match(r'^(unique\s+)?index[^:]*$', line, re.I):   # index
+            compile_foreign_key(
+                line,
+                context,
+                attributes,
+                primary_key if in_key else None,
+                attribute_sql,
+                foreign_key_sql,
+                index_sql,
+            )
+        elif re.match(r"^(unique\s+)?index[^:]*$", line, re.I):  # index
             compile_index(line, index_sql)
         else:
             name, sql, store = compile_attribute(line, in_key, foreign_key_sql, context)
@@ -260,34 +354,55 @@ def prepare_declare(definition, context):
                 attributes.append(name)
                 attribute_sql.append(sql)
 
-    return table_comment, primary_key, attribute_sql, foreign_key_sql, index_sql, external_stores
+    return (
+        table_comment,
+        primary_key,
+        attribute_sql,
+        foreign_key_sql,
+        index_sql,
+        external_stores,
+    )
 
 
 def declare(full_table_name, definition, context):
     """
     Parse declaration and generate the SQL CREATE TABLE code
+
     :param full_table_name: full name of the table
     :param definition: DataJoint table definition
     :param context: dictionary of objects that might be referred to in the table
     :return: SQL CREATE TABLE statement, list of external stores used
     """
-    table_name = full_table_name.strip('`').split('.')[1]
+    table_name = full_table_name.strip("`").split(".")[1]
     if len(table_name) > MAX_TABLE_NAME_LENGTH:
         raise DataJointError(
-            'Table name `{name}` exceeds the max length of {max_length}'.format(
-                name=table_name,
-                max_length=MAX_TABLE_NAME_LENGTH))
-
-    table_comment, primary_key, attribute_sql, foreign_key_sql, index_sql, external_stores = prepare_declare(
-        definition, context)
+            "Table name `{name}` exceeds the max length of {max_length}".format(
+                name=table_name, max_length=MAX_TABLE_NAME_LENGTH
+            )
+        )
+
+    (
+        table_comment,
+        primary_key,
+        attribute_sql,
+        foreign_key_sql,
+        index_sql,
+        external_stores,
+    ) = prepare_declare(definition, context)
 
     if not primary_key:
-        raise DataJointError('Table must have a primary key')
+        raise DataJointError("Table must have a primary key")
 
     return (
-        'CREATE TABLE IF NOT EXISTS %s (\n' % full_table_name +
-        ',\n'.join(attribute_sql + ['PRIMARY KEY (`' + '`,`'.join(primary_key) + '`)'] + foreign_key_sql + index_sql) +
-        '\n) ENGINE=InnoDB, COMMENT "%s"' % table_comment), external_stores
+        "CREATE TABLE IF NOT EXISTS %s (\n" % full_table_name
+        + ",\n".join(
+            attribute_sql
+            + ["PRIMARY KEY (`" + "`,`".join(primary_key) + "`)"]
+            + foreign_key_sql
+            + index_sql
+        )
+        + '\n) ENGINE=InnoDB, COMMENT "%s"' % table_comment
+    ), external_stores
 
 
 def _make_attribute_alter(new, old, primary_key):
@@ -301,28 +416,32 @@ def _make_attribute_alter(new, old, primary_key):
     name_regexp = re.compile(r"^`(?P<name>\w+)`")
     original_regexp = re.compile(r'COMMENT "{\s*(?P<name>\w+)\s*}')
     matched = ((name_regexp.match(d), original_regexp.search(d)) for d in new)
-    new_names = dict((d.group('name'), n and n.group('name')) for d, n in matched)
-    old_names = [name_regexp.search(d).group('name') for d in old]
+    new_names = dict((d.group("name"), n and n.group("name")) for d, n in matched)
+    old_names = [name_regexp.search(d).group("name") for d in old]
 
     # verify that original names are only used once
     renamed = set()
     for v in new_names.values():
         if v:
             if v in renamed:
-                raise DataJointError('Alter attempted to rename attribute {%s} twice.' % v)
+                raise DataJointError(
+                    "Alter attempted to rename attribute {%s} twice." % v
+                )
             renamed.add(v)
 
     # verify that all renamed attributes existed in the old definition
     try:
         raise DataJointError(
             "Attribute {} does not exist in the original definition".format(
-                next(attr for attr in renamed if attr not in old_names)))
+                next(attr for attr in renamed if attr not in old_names)
+            )
+        )
     except StopIteration:
         pass
 
     # dropping attributes
     to_drop = [n for n in old_names if n not in renamed and n not in new_names]
-    sql = ['DROP `%s`' % n for n in to_drop]
+    sql = ["DROP `%s`" % n for n in to_drop]
     old_names = [name for name in old_names if name not in to_drop]
 
     # add or change attributes in order
@@ -339,12 +458,19 @@ def _make_attribute_alter(new, old, primary_key):
                     if idx >= 1 and old_names[idx - 1] != (prev[1] or prev[0]):
                         after = prev[0]
             if new_def not in old or after:
-                sql.append('{command} {new_def} {after}'.format(
-                    command=("ADD" if (old_name or new_name) not in old_names else
-                             "MODIFY" if not old_name else
-                             "CHANGE `%s`" % old_name),
-                    new_def=new_def,
-                    after="" if after is None else "AFTER `%s`" % after))
+                sql.append(
+                    "{command} {new_def} {after}".format(
+                        command=(
+                            "ADD"
+                            if (old_name or new_name) not in old_names
+                            else "MODIFY"
+                            if not old_name
+                            else "CHANGE `%s`" % old_name
+                        ),
+                        new_def=new_def,
+                        after="" if after is None else "AFTER `%s`" % after,
+                    )
+                )
         prev = new_name, old_name
 
     return sql
@@ -357,19 +483,31 @@ def alter(definition, old_definition, context):
     :param context: the context in which to evaluate foreign key definitions
     :return: string SQL ALTER command, list of new stores used for external storage
     """
-    table_comment, primary_key, attribute_sql, foreign_key_sql, index_sql, external_stores = prepare_declare(
-        definition, context)
-    table_comment_, primary_key_, attribute_sql_, foreign_key_sql_, index_sql_, external_stores_ = prepare_declare(
-        old_definition, context)
+    (
+        table_comment,
+        primary_key,
+        attribute_sql,
+        foreign_key_sql,
+        index_sql,
+        external_stores,
+    ) = prepare_declare(definition, context)
+    (
+        table_comment_,
+        primary_key_,
+        attribute_sql_,
+        foreign_key_sql_,
+        index_sql_,
+        external_stores_,
+    ) = prepare_declare(old_definition, context)
 
     # analyze differences between declarations
     sql = list()
     if primary_key != primary_key_:
-        raise NotImplementedError('table.alter cannot alter the primary key (yet).')
+        raise NotImplementedError("table.alter cannot alter the primary key (yet).")
     if foreign_key_sql != foreign_key_sql_:
-        raise NotImplementedError('table.alter cannot alter foreign keys (yet).')
+        raise NotImplementedError("table.alter cannot alter foreign keys (yet).")
     if index_sql != index_sql_:
-        raise NotImplementedError('table.alter cannot alter indexes (yet)')
+        raise NotImplementedError("table.alter cannot alter indexes (yet)")
     if attribute_sql != attribute_sql_:
         sql.extend(_make_attribute_alter(attribute_sql, attribute_sql_, primary_key))
     if table_comment != table_comment_:
@@ -379,9 +517,11 @@ def alter(definition, old_definition, context):
 
 def compile_index(line, index_sql):
     match = index_parser.parseString(line)
-    index_sql.append('{unique} index ({attrs})'.format(
-        unique=match.unique,
-        attrs=','.join('`%s`' % a for a in match.attr_list)))
+    index_sql.append(
+        "{unique} index ({attrs})".format(
+            unique=match.unique, attrs=",".join("`%s`" % a for a in match.attr_list)
+        )
+    )
 
 
 def substitute_special_type(match, category, foreign_key_sql, context):
@@ -391,36 +531,44 @@ def substitute_special_type(match, category, foreign_key_sql, context):
     :param foreign_key_sql: list of foreign key declarations to add to
     :param context: context for looking up user-defined attribute_type adapters
     """
-    if category == 'UUID':
-        match['type'] = UUID_DATA_TYPE
-    elif category == 'INTERNAL_ATTACH':
-        match['type'] = 'LONGBLOB'
+    if category == "UUID":
+        match["type"] = UUID_DATA_TYPE
+    elif category == "INTERNAL_ATTACH":
+        match["type"] = "LONGBLOB"
     elif category in EXTERNAL_TYPES:
-        if category == 'FILEPATH' and not _support_filepath_types():
-            raise DataJointError("""
+        if category == "FILEPATH" and not _support_filepath_types():
+            raise DataJointError(
+                """
             The filepath data type is disabled until complete validation.
             To turn it on as experimental feature, set the environment variable
             {env} = TRUE or upgrade datajoint.
-            """.format(env=FILEPATH_FEATURE_SWITCH))
-        match['store'] = match['type'].split('@', 1)[1]
-        match['type'] = UUID_DATA_TYPE
+            """.format(
+                    env=FILEPATH_FEATURE_SWITCH
+                )
+            )
+        match["store"] = match["type"].split("@", 1)[1]
+        match["type"] = UUID_DATA_TYPE
         foreign_key_sql.append(
             "FOREIGN KEY (`{name}`) REFERENCES `{{database}}`.`{external_table_root}_{store}` (`hash`) "
-            "ON UPDATE RESTRICT ON DELETE RESTRICT".format(external_table_root=EXTERNAL_TABLE_ROOT, **match))
-    elif category == 'ADAPTED':
-        adapter = get_adapter(context, match['type'])
-        match['type'] = adapter.attribute_type
-        category = match_type(match['type'])
+            "ON UPDATE RESTRICT ON DELETE RESTRICT".format(
+                external_table_root=EXTERNAL_TABLE_ROOT, **match
+            )
+        )
+    elif category == "ADAPTED":
+        adapter = get_adapter(context, match["type"])
+        match["type"] = adapter.attribute_type
+        category = match_type(match["type"])
         if category in SPECIAL_TYPES:
             # recursive redefinition from user-defined datatypes.
             substitute_special_type(match, category, foreign_key_sql, context)
     else:
-        assert False, 'Unknown special type'
+        assert False, "Unknown special type"
 
 
 def compile_attribute(line, in_key, foreign_key_sql, context):
     """
     Convert attribute definition from DataJoint format to SQL
+
     :param line: attribution line
     :param in_key: set to True if attribute is in primary key set
     :param foreign_key_sql: the list of foreign key declarations to add to
@@ -428,41 +576,67 @@ def compile_attribute(line, in_key, foreign_key_sql, context):
     :returns: (name, sql, is_external) -- attribute name and sql code for its declaration
     """
     try:
-        match = attribute_parser.parseString(line + '#', parseAll=True)
+        match = attribute_parser.parseString(line + "#", parseAll=True)
     except pp.ParseException as err:
-        raise DataJointError('Declaration error in position {pos} in line:\n  {line}\n{msg}'.format(
-            line=err.args[0], pos=err.args[1], msg=err.args[2]))
-    match['comment'] = match['comment'].rstrip('#')
-    if 'default' not in match:
-        match['default'] = ''
+        raise DataJointError(
+            "Declaration error in position {pos} in line:\n  {line}\n{msg}".format(
+                line=err.args[0], pos=err.args[1], msg=err.args[2]
+            )
+        )
+    match["comment"] = match["comment"].rstrip("#")
+    if "default" not in match:
+        match["default"] = ""
     match = {k: v.strip() for k, v in match.items()}
-    match['nullable'] = match['default'].lower() == 'null'
+    match["nullable"] = match["default"].lower() == "null"
 
-    if match['nullable']:
+    if match["nullable"]:
         if in_key:
-            raise DataJointError('Primary key attributes cannot be nullable in line "%s"' % line)
-        match['default'] = 'DEFAULT NULL'  # nullable attributes default to null
+            raise DataJointError(
+                'Primary key attributes cannot be nullable in line "%s"' % line
+            )
+        match["default"] = "DEFAULT NULL"  # nullable attributes default to null
     else:
-        if match['default']:
-            quote = (match['default'].split('(')[0].upper() not in CONSTANT_LITERALS
-                     and match['default'][0] not in '"\'')
-            match['default'] = 'NOT NULL DEFAULT ' + ('"%s"' if quote else "%s") % match['default']
+        if match["default"]:
+            quote = (
+                match["default"].split("(")[0].upper() not in CONSTANT_LITERALS
+                and match["default"][0] not in "\"'"
+            )
+            match["default"] = (
+                "NOT NULL DEFAULT " + ('"%s"' if quote else "%s") % match["default"]
+            )
         else:
-            match['default'] = 'NOT NULL'
+            match["default"] = "NOT NULL"
 
-    match['comment'] = match['comment'].replace('"', '\\"')   # escape double quotes in comment
+    match["comment"] = match["comment"].replace(
+        '"', '\\"'
+    )  # escape double quotes in comment
 
-    if match['comment'].startswith(':'):
-        raise DataJointError('An attribute comment must not start with a colon in comment "{comment}"'.format(**match))
+    if match["comment"].startswith(":"):
+        raise DataJointError(
+            'An attribute comment must not start with a colon in comment "{comment}"'.format(
+                **match
+            )
+        )
 
-    category = match_type(match['type'])
+    category = match_type(match["type"])
     if category in SPECIAL_TYPES:
-        match['comment'] = ':{type}:{comment}'.format(**match)  # insert custom type into comment
+        match["comment"] = ":{type}:{comment}".format(
+            **match
+        )  # insert custom type into comment
         substitute_special_type(match, category, foreign_key_sql, context)
 
-    if category in SERIALIZED_TYPES and match['default'] not in {'DEFAULT NULL', 'NOT NULL'}:
+    if category in SERIALIZED_TYPES and match["default"] not in {
+        "DEFAULT NULL",
+        "NOT NULL",
+    }:
         raise DataJointError(
-            'The default value for a blob or attachment attributes can only be NULL in:\n{line}'.format(line=line))
-
-    sql = ('`{name}` {type} {default}' + (' COMMENT "{comment}"' if match['comment'] else '')).format(**match)
-    return match['name'], sql, match.get('store')
+            "The default value for a blob or attachment attributes can only be NULL in:\n{line}".format(
+                line=line
+            )
+        )
+
+    sql = (
+        "`{name}` {type} {default}"
+        + (' COMMENT "{comment}"' if match["comment"] else "")
+    ).format(**match)
+    return match["name"], sql, match.get("store")
diff --git a/datajoint/dependencies.py b/datajoint/dependencies.py
index e5e94225d..96dc8f7f4 100644
--- a/datajoint/dependencies.py
+++ b/datajoint/dependencies.py
@@ -18,13 +18,13 @@ def unite_master_parts(lst):
     """
     for i in range(2, len(lst)):
         name = lst[i]
-        match = re.match(r'(?P<master>`\w+`.`#?\w+)__\w+`', name)
+        match = re.match(r"(?P<master>`\w+`.`#?\w+)__\w+`", name)
         if match:  # name is a part table
-            master = match.group('master')
-            for j in range(i-1, -1, -1):
-                if lst[j] == master + '`' or lst[j].startswith(master + '__'):
+            master = match.group("master")
+            for j in range(i - 1, -1, -1):
+                if lst[j] == master + "`" or lst[j].startswith(master + "__"):
                     # move from the ith position to the (j+1)th position
-                    lst[j+1:i+1] = [name] + lst[j+1:i]
+                    lst[j + 1 : i + 1] = [name] + lst[j + 1 : i]
                     break
     return lst
 
@@ -38,6 +38,7 @@ class Dependencies(nx.DiGraph):
     internally create objects with the expectation of empty constructors.
     See also: https://github.com/datajoint/datajoint-python/pull/443
     """
+
     def __init__(self, connection=None):
         self._conn = connection
         self._node_alias_count = itertools.count()
@@ -60,12 +61,16 @@ def load(self, force=True):
         self.clear()
 
         # load primary key info
-        keys = self._conn.query("""
+        keys = self._conn.query(
+            """
                 SELECT
                     concat('`', table_schema, '`.`', table_name, '`') as tab, column_name
                 FROM information_schema.key_column_usage
                 WHERE table_name not LIKE "~%%" AND table_schema in ('{schemas}') AND constraint_name="PRIMARY"
-                """.format(schemas="','".join(self._conn.schemas)))
+                """.format(
+                schemas="','".join(self._conn.schemas)
+            )
+        )
         pks = defaultdict(set)
         for key in keys:
             pks[key[0]].add(key[1])
@@ -75,7 +80,10 @@ def load(self, force=True):
             self.add_node(n, primary_key=pk)
 
         # load foreign keys
-        keys = ({k.lower(): v for k, v in elem.items()} for elem in self._conn.query("""
+        keys = (
+            {k.lower(): v for k, v in elem.items()}
+            for elem in self._conn.query(
+                """
         SELECT constraint_name,
             concat('`', table_schema, '`.`', table_name, '`') as referencing_table,
             concat('`', referenced_table_schema, '`.`',  referenced_table_name, '`') as referenced_table,
@@ -83,32 +91,44 @@ def load(self, force=True):
         FROM information_schema.key_column_usage
         WHERE referenced_table_name NOT LIKE "~%%" AND (referenced_table_schema in ('{schemas}') OR
             referenced_table_schema is not NULL AND table_schema in ('{schemas}'))
-        """.format(schemas="','".join(self._conn.schemas)), as_dict=True))
+        """.format(
+                    schemas="','".join(self._conn.schemas)
+                ),
+                as_dict=True,
+            )
+        )
         fks = defaultdict(lambda: dict(attr_map=dict()))
         for key in keys:
-            d = fks[(key['constraint_name'], key['referencing_table'], key['referenced_table'])]
-            d['referencing_table'] = key['referencing_table']
-            d['referenced_table'] = key['referenced_table']
-            d['attr_map'][key['column_name']] = key['referenced_column_name']
+            d = fks[
+                (
+                    key["constraint_name"],
+                    key["referencing_table"],
+                    key["referenced_table"],
+                )
+            ]
+            d["referencing_table"] = key["referencing_table"]
+            d["referenced_table"] = key["referenced_table"]
+            d["attr_map"][key["column_name"]] = key["referenced_column_name"]
 
         # add edges to the graph
         for fk in fks.values():
             props = dict(
-                primary=set(fk['attr_map']) <= set(pks[fk['referencing_table']]),
-                attr_map=fk['attr_map'],
-                aliased=any(k != v for k, v in fk['attr_map'].items()),
-                multi=set(fk['attr_map']) != set(pks[fk['referencing_table']]))
-            if not props['aliased']:
-                self.add_edge(fk['referenced_table'], fk['referencing_table'], **props)
+                primary=set(fk["attr_map"]) <= set(pks[fk["referencing_table"]]),
+                attr_map=fk["attr_map"],
+                aliased=any(k != v for k, v in fk["attr_map"].items()),
+                multi=set(fk["attr_map"]) != set(pks[fk["referencing_table"]]),
+            )
+            if not props["aliased"]:
+                self.add_edge(fk["referenced_table"], fk["referencing_table"], **props)
             else:
                 # for aliased dependencies, add an extra node in the format '1', '2', etc
-                alias_node = '%d' % next(self._node_alias_count)
+                alias_node = "%d" % next(self._node_alias_count)
                 self.add_node(alias_node)
-                self.add_edge(fk['referenced_table'], alias_node, **props)
-                self.add_edge(alias_node, fk['referencing_table'], **props)
+                self.add_edge(fk["referenced_table"], alias_node, **props)
+                self.add_edge(alias_node, fk["referencing_table"], **props)
 
         if not nx.is_directed_acyclic_graph(self):  # pragma: no cover
-            raise DataJointError('DataJoint can only work with acyclic dependencies')
+            raise DataJointError("DataJoint can only work with acyclic dependencies")
         self._loaded = True
 
     def parents(self, table_name, primary=None):
@@ -120,8 +140,11 @@ def parents(self, table_name, primary=None):
         :return: dict of tables referenced by the foreign keys of table
         """
         self.load(force=False)
-        return {p[0]: p[2] for p in self.in_edges(table_name, data=True)
-                if primary is None or p[2]['primary'] == primary}
+        return {
+            p[0]: p[2]
+            for p in self.in_edges(table_name, data=True)
+            if primary is None or p[2]["primary"] == primary
+        }
 
     def children(self, table_name, primary=None):
         """
@@ -132,8 +155,11 @@ def children(self, table_name, primary=None):
         :return: dict of tables referencing the table through foreign keys
         """
         self.load(force=False)
-        return {p[1]: p[2] for p in self.out_edges(table_name, data=True)
-                if primary is None or p[2]['primary'] == primary}
+        return {
+            p[1]: p[2]
+            for p in self.out_edges(table_name, data=True)
+            if primary is None or p[2]["primary"] == primary
+        }
 
     def descendants(self, full_table_name):
         """
@@ -141,10 +167,10 @@ def descendants(self, full_table_name):
         :return: all dependent tables sorted in topological order.  Self is included.
         """
         self.load(force=False)
-        nodes = self.subgraph(
-            nx.algorithms.dag.descendants(self, full_table_name))
-        return unite_master_parts([full_table_name] + list(
-            nx.algorithms.dag.topological_sort(nodes)))
+        nodes = self.subgraph(nx.algorithms.dag.descendants(self, full_table_name))
+        return unite_master_parts(
+            [full_table_name] + list(nx.algorithms.dag.topological_sort(nodes))
+        )
 
     def ancestors(self, full_table_name):
         """
@@ -152,7 +178,11 @@ def ancestors(self, full_table_name):
         :return: all dependent tables sorted in topological order.  Self is included.
         """
         self.load(force=False)
-        nodes = self.subgraph(
-            nx.algorithms.dag.ancestors(self, full_table_name))
-        return list(reversed(unite_master_parts(list(
-            nx.algorithms.dag.topological_sort(nodes)) + [full_table_name])))
+        nodes = self.subgraph(nx.algorithms.dag.ancestors(self, full_table_name))
+        return list(
+            reversed(
+                unite_master_parts(
+                    list(nx.algorithms.dag.topological_sort(nodes)) + [full_table_name]
+                )
+            )
+        )
diff --git a/datajoint/diagram.py b/datajoint/diagram.py
index fa03c123a..9fe9c95f9 100644
--- a/datajoint/diagram.py
+++ b/datajoint/diagram.py
@@ -9,12 +9,14 @@
 
 try:
     from matplotlib import pyplot as plt
+
     plot_active = True
 except:
     plot_active = False
 
 try:
     from networkx.drawing.nx_pydot import pydot_layout
+
     diagram_active = True
 except:
     diagram_active = False
@@ -31,21 +33,26 @@ class _AliasNode:
     """
     special class to indicate aliased foreign keys
     """
+
     pass
 
 
 def _get_tier(table_name):
-    if not table_name.startswith('`'):
+    if not table_name.startswith("`"):
         return _AliasNode
     else:
         try:
-            return next(tier for tier in user_table_classes
-                        if re.fullmatch(tier.tier_regexp, table_name.split('`')[-2]))
+            return next(
+                tier
+                for tier in user_table_classes
+                if re.fullmatch(tier.tier_regexp, table_name.split("`")[-2])
+            )
         except StopIteration:
             return None
 
 
 if not diagram_active:
+
     class Diagram:
         """
         Entity relationship diagram, currently disabled due to the lack of required packages: matplotlib and pygraphviz.
@@ -56,9 +63,12 @@ class Diagram:
         """
 
         def __init__(self, *args, **kwargs):
-            warnings.warn('Please install matplotlib and pygraphviz libraries to enable the Diagram feature.')
+            warnings.warn(
+                "Please install matplotlib and pygraphviz libraries to enable the Diagram feature."
+            )
 
 else:
+
     class Diagram(nx.DiGraph):
         """
         Entity relationship diagram.
@@ -81,6 +91,7 @@ class Diagram(nx.DiGraph):
         Note that diagram + 1 - 1  may differ from diagram - 1 + 1 and so forth.
         Only those tables that are loaded in the connection object are displayed
         """
+
         def __init__(self, source, context=None):
 
             if isinstance(source, Diagram):
@@ -105,7 +116,9 @@ def __init__(self, source, context=None):
                 try:
                     connection = source.schema.connection
                 except AttributeError:
-                    raise DataJointError('Could not find database connection in %s' % repr(source[0]))
+                    raise DataJointError(
+                        "Could not find database connection in %s" % repr(source[0])
+                    )
 
             # initialize graph from dependencies
             connection.dependencies.load()
@@ -122,43 +135,59 @@ def __init__(self, source, context=None):
                     try:
                         database = source.schema.database
                     except AttributeError:
-                        raise DataJointError('Cannot plot Diagram for %s' % repr(source))
+                        raise DataJointError(
+                            "Cannot plot Diagram for %s" % repr(source)
+                        )
                 for node in self:
-                    if node.startswith('`%s`' % database):
+                    if node.startswith("`%s`" % database):
                         self.nodes_to_show.add(node)
 
         @classmethod
         def from_sequence(cls, sequence):
             """
             The join Diagram for all objects in sequence
+
             :param sequence: a sequence (e.g. list, tuple)
             :return: Diagram(arg1) + ... + Diagram(argn)
             """
-            return functools.reduce(lambda x, y: x+y, map(Diagram, sequence))
+            return functools.reduce(lambda x, y: x + y, map(Diagram, sequence))
 
         def add_parts(self):
             """
             Adds to the diagram the part tables of tables already included in the diagram
             :return:
             """
+
             def is_part(part, master):
                 """
                 :param part:  `database`.`table_name`
                 :param master:   `database`.`table_name`
                 :return: True if part is part of master.
                 """
-                part = [s.strip('`') for s in part.split('.')]
-                master = [s.strip('`') for s in master.split('.')]
-                return master[0] == part[0] and master[1] + '__' == part[1][:len(master[1])+2]
+                part = [s.strip("`") for s in part.split(".")]
+                master = [s.strip("`") for s in master.split(".")]
+                return (
+                    master[0] == part[0]
+                    and master[1] + "__" == part[1][: len(master[1]) + 2]
+                )
 
             self = Diagram(self)  # copy
-            self.nodes_to_show.update(n for n in self.nodes() if any(is_part(n, m) for m in self.nodes_to_show))
+            self.nodes_to_show.update(
+                n
+                for n in self.nodes()
+                if any(is_part(n, m) for m in self.nodes_to_show)
+            )
             return self
 
         def topological_sort(self):
-            """ :return:  list of nodes in topological order """
-            return unite_master_parts(list(nx.algorithms.dag.topological_sort(
-                nx.DiGraph(self).subgraph(self.nodes_to_show))))
+            """:return:  list of nodes in topological order"""
+            return unite_master_parts(
+                list(
+                    nx.algorithms.dag.topological_sort(
+                        nx.DiGraph(self).subgraph(self.nodes_to_show)
+                    )
+                )
+            )
 
         def __add__(self, arg):
             """
@@ -166,7 +195,7 @@ def __add__(self, arg):
             :return: Union of the diagrams when arg is another Diagram
                      or an expansion downstream when arg is a positive integer.
             """
-            self = Diagram(self)   # copy
+            self = Diagram(self)  # copy
             try:
                 self.nodes_to_show.update(arg.nodes_to_show)
             except AttributeError:
@@ -174,11 +203,17 @@ def __add__(self, arg):
                     self.nodes_to_show.add(arg.full_table_name)
                 except AttributeError:
                     for i in range(arg):
-                        new = nx.algorithms.boundary.node_boundary(self, self.nodes_to_show)
+                        new = nx.algorithms.boundary.node_boundary(
+                            self, self.nodes_to_show
+                        )
                         if not new:
                             break
                         # add nodes referenced by aliased nodes
-                        new.update(nx.algorithms.boundary.node_boundary(self, (a for a in new if a.isdigit())))
+                        new.update(
+                            nx.algorithms.boundary.node_boundary(
+                                self, (a for a in new if a.isdigit())
+                            )
+                        )
                         self.nodes_to_show.update(new)
             return self
 
@@ -188,7 +223,7 @@ def __sub__(self, arg):
             :return: Difference of the diagrams when arg is another Diagram or
                      an expansion upstream when arg is a positive integer.
             """
-            self = Diagram(self)   # copy
+            self = Diagram(self)  # copy
             try:
                 self.nodes_to_show.difference_update(arg.nodes_to_show)
             except AttributeError:
@@ -197,11 +232,17 @@ def __sub__(self, arg):
                 except AttributeError:
                     for i in range(arg):
                         graph = nx.DiGraph(self).reverse()
-                        new = nx.algorithms.boundary.node_boundary(graph, self.nodes_to_show)
+                        new = nx.algorithms.boundary.node_boundary(
+                            graph, self.nodes_to_show
+                        )
                         if not new:
                             break
                         # add nodes referenced by aliased nodes
-                        new.update(nx.algorithms.boundary.node_boundary(graph, (a for a in new if a.isdigit())))
+                        new.update(
+                            nx.algorithms.boundary.node_boundary(
+                                graph, (a for a in new if a.isdigit())
+                            )
+                        )
                         self.nodes_to_show.update(new)
             return self
 
@@ -211,7 +252,7 @@ def __mul__(self, arg):
             :param arg: another Diagram
             :return: a new Diagram comprising nodes that are present in both operands.
             """
-            self = Diagram(self)   # copy
+            self = Diagram(self)  # copy
             self.nodes_to_show.intersection_update(arg.nodes_to_show)
             return self
 
@@ -223,28 +264,39 @@ def _make_graph(self):
             # attributes
             for name in self.nodes_to_show:
                 foreign_attributes = set(
-                    attr for p in self.in_edges(name, data=True)
-                    for attr in p[2]['attr_map'] if p[2]['primary'])
-                self.nodes[name]['distinguished'] = (
-                        'primary_key' in self.nodes[name] and
-                        foreign_attributes < self.nodes[name]['primary_key'])
+                    attr
+                    for p in self.in_edges(name, data=True)
+                    for attr in p[2]["attr_map"]
+                    if p[2]["primary"]
+                )
+                self.nodes[name]["distinguished"] = (
+                    "primary_key" in self.nodes[name]
+                    and foreign_attributes < self.nodes[name]["primary_key"]
+                )
             # include aliased nodes that are sandwiched between two displayed nodes
-            gaps = set(nx.algorithms.boundary.node_boundary(
-                self, self.nodes_to_show)).intersection(
-                    nx.algorithms.boundary.node_boundary(nx.DiGraph(self).reverse(),
-                                                         self.nodes_to_show))
+            gaps = set(
+                nx.algorithms.boundary.node_boundary(self, self.nodes_to_show)
+            ).intersection(
+                nx.algorithms.boundary.node_boundary(
+                    nx.DiGraph(self).reverse(), self.nodes_to_show
+                )
+            )
             nodes = self.nodes_to_show.union(a for a in gaps if a.isdigit)
             # construct subgraph and rename nodes to class names
             graph = nx.DiGraph(nx.DiGraph(self).subgraph(nodes))
-            nx.set_node_attributes(graph, name='node_type', values={n: _get_tier(n)
-                                                                    for n in graph})
+            nx.set_node_attributes(
+                graph, name="node_type", values={n: _get_tier(n) for n in graph}
+            )
             # relabel nodes to class names
-            mapping = {node: lookup_class_name(node, self.context) or node
-                       for node in graph.nodes()}
+            mapping = {
+                node: lookup_class_name(node, self.context) or node
+                for node in graph.nodes()
+            }
             new_names = [mapping.values()]
             if len(new_names) > len(set(new_names)):
                 raise DataJointError(
-                    'Some classes have identical names. The Diagram cannot be plotted.')
+                    "Some classes have identical names. The Diagram cannot be plotted."
+                )
             nx.relabel_nodes(graph, mapping, copy=False)
             return graph
 
@@ -253,64 +305,125 @@ def make_dot(self):
             graph = self._make_graph()
             graph.nodes()
 
-            scale = 1.2   # scaling factor for fonts and boxes
+            scale = 1.2  # scaling factor for fonts and boxes
             label_props = {  # http://matplotlib.org/examples/color/named_colors.html
-                None: dict(shape='circle', color="#FFFF0040", fontcolor='yellow', fontsize=round(scale*8),
-                           size=0.4*scale, fixed=False),
-                _AliasNode: dict(shape='circle', color="#FF880080", fontcolor='#FF880080', fontsize=round(scale*0),
-                                 size=0.05*scale, fixed=True),
-                Manual: dict(shape='box', color="#00FF0030", fontcolor='darkgreen', fontsize=round(scale*10),
-                             size=0.4*scale, fixed=False),
-                Lookup: dict(shape='plaintext', color='#00000020', fontcolor='black', fontsize=round(scale*8),
-                             size=0.4*scale, fixed=False),
-                Computed: dict(shape='ellipse', color='#FF000020', fontcolor='#7F0000A0', fontsize=round(scale*10),
-                               size=0.3*scale, fixed=True),
-                Imported: dict(shape='ellipse', color='#00007F40', fontcolor='#00007FA0', fontsize=round(scale*10),
-                               size=0.4*scale, fixed=False),
-                Part: dict(shape='plaintext', color='#0000000', fontcolor='black', fontsize=round(scale*8),
-                           size=0.1*scale, fixed=False)}
-            node_props = {node: label_props[d['node_type']] for node, d in dict(graph.nodes(data=True)).items()}
+                None: dict(
+                    shape="circle",
+                    color="#FFFF0040",
+                    fontcolor="yellow",
+                    fontsize=round(scale * 8),
+                    size=0.4 * scale,
+                    fixed=False,
+                ),
+                _AliasNode: dict(
+                    shape="circle",
+                    color="#FF880080",
+                    fontcolor="#FF880080",
+                    fontsize=round(scale * 0),
+                    size=0.05 * scale,
+                    fixed=True,
+                ),
+                Manual: dict(
+                    shape="box",
+                    color="#00FF0030",
+                    fontcolor="darkgreen",
+                    fontsize=round(scale * 10),
+                    size=0.4 * scale,
+                    fixed=False,
+                ),
+                Lookup: dict(
+                    shape="plaintext",
+                    color="#00000020",
+                    fontcolor="black",
+                    fontsize=round(scale * 8),
+                    size=0.4 * scale,
+                    fixed=False,
+                ),
+                Computed: dict(
+                    shape="ellipse",
+                    color="#FF000020",
+                    fontcolor="#7F0000A0",
+                    fontsize=round(scale * 10),
+                    size=0.3 * scale,
+                    fixed=True,
+                ),
+                Imported: dict(
+                    shape="ellipse",
+                    color="#00007F40",
+                    fontcolor="#00007FA0",
+                    fontsize=round(scale * 10),
+                    size=0.4 * scale,
+                    fixed=False,
+                ),
+                Part: dict(
+                    shape="plaintext",
+                    color="#0000000",
+                    fontcolor="black",
+                    fontsize=round(scale * 8),
+                    size=0.1 * scale,
+                    fixed=False,
+                ),
+            }
+            node_props = {
+                node: label_props[d["node_type"]]
+                for node, d in dict(graph.nodes(data=True)).items()
+            }
 
             dot = nx.drawing.nx_pydot.to_pydot(graph)
             for node in dot.get_nodes():
-                node.set_shape('circle')
+                node.set_shape("circle")
                 name = node.get_name().strip('"')
                 props = node_props[name]
-                node.set_fontsize(props['fontsize'])
-                node.set_fontcolor(props['fontcolor'])
-                node.set_shape(props['shape'])
-                node.set_fontname('arial')
-                node.set_fixedsize('shape' if props['fixed'] else False)
-                node.set_width(props['size'])
-                node.set_height(props['size'])
-                if name.split('.')[0] in self.context:
+                node.set_fontsize(props["fontsize"])
+                node.set_fontcolor(props["fontcolor"])
+                node.set_shape(props["shape"])
+                node.set_fontname("arial")
+                node.set_fixedsize("shape" if props["fixed"] else False)
+                node.set_width(props["size"])
+                node.set_height(props["size"])
+                if name.split(".")[0] in self.context:
                     cls = eval(name, self.context)
-                    assert(issubclass(cls, Table))
-                    description = cls().describe(context=self.context, printout=False).split('\n')
+                    assert issubclass(cls, Table)
+                    description = (
+                        cls().describe(context=self.context, printout=False).split("\n")
+                    )
                     description = (
-                        '-'*30 if q.startswith('---') else q.replace('->', '&#8594;') if '->' in q else q.split(':')[0]
-                        for q in description if not q.startswith('#'))
-                    node.set_tooltip('&#13;'.join(description))
-                node.set_label("<<u>"+name+"</u>>" if node.get('distinguished') == 'True' else name)
-                node.set_color(props['color'])
-                node.set_style('filled')
+                        "-" * 30
+                        if q.startswith("---")
+                        else q.replace("->", "&#8594;")
+                        if "->" in q
+                        else q.split(":")[0]
+                        for q in description
+                        if not q.startswith("#")
+                    )
+                    node.set_tooltip("&#13;".join(description))
+                node.set_label(
+                    "<<u>" + name + "</u>>"
+                    if node.get("distinguished") == "True"
+                    else name
+                )
+                node.set_color(props["color"])
+                node.set_style("filled")
 
             for edge in dot.get_edges():
                 # see https://graphviz.org/doc/info/attrs.html
                 src = edge.get_source().strip('"')
                 dest = edge.get_destination().strip('"')
                 props = graph.get_edge_data(src, dest)
-                edge.set_color('#00000040')
-                edge.set_style('solid' if props['primary'] else 'dashed')
-                master_part = graph.nodes[dest]['node_type'] is Part and dest.startswith(src+'.')
+                edge.set_color("#00000040")
+                edge.set_style("solid" if props["primary"] else "dashed")
+                master_part = graph.nodes[dest][
+                    "node_type"
+                ] is Part and dest.startswith(src + ".")
                 edge.set_weight(3 if master_part else 1)
-                edge.set_arrowhead('none')
-                edge.set_penwidth(.75 if props['multi'] else 2)
+                edge.set_arrowhead("none")
+                edge.set_penwidth(0.75 if props["multi"] else 2)
 
             return dot
 
         def make_svg(self):
             from IPython.display import SVG
+
             return SVG(self.make_dot().create_svg())
 
         def make_png(self):
@@ -328,26 +441,26 @@ def _repr_svg_(self):
         def draw(self):
             if plot_active:
                 plt.imshow(self.make_image())
-                plt.gca().axis('off')
+                plt.gca().axis("off")
                 plt.show()
             else:
                 raise DataJointError("pyplot was not imported")
 
         def save(self, filename, format=None):
             if format is None:
-                if filename.lower().endswith('.png'):
-                    format = 'png'
-                elif filename.lower().endswith('.svg'):
-                    format = 'svg'
-            if format.lower() == 'png':
-                with open(filename, 'wb') as f:
+                if filename.lower().endswith(".png"):
+                    format = "png"
+                elif filename.lower().endswith(".svg"):
+                    format = "svg"
+            if format.lower() == "png":
+                with open(filename, "wb") as f:
                     f.write(self.make_png().getbuffer().tobytes())
-            elif format.lower() == 'svg':
-                with open(filename, 'w') as f:
+            elif format.lower() == "svg":
+                with open(filename, "w") as f:
                     f.write(self.make_svg().data)
             else:
-                raise DataJointError('Unsupported file format')
+                raise DataJointError("Unsupported file format")
 
         @staticmethod
         def _layout(graph, **kwargs):
-            return pydot_layout(graph, prog='dot', **kwargs)
+            return pydot_layout(graph, prog="dot", **kwargs)
diff --git a/datajoint/errors.py b/datajoint/errors.py
index fe0ffc539..427e8d1ad 100644
--- a/datajoint/errors.py
+++ b/datajoint/errors.py
@@ -15,17 +15,26 @@ class DataJointError(Exception):
     """
     Base class for errors specific to DataJoint internal operation.
     """
+
     def __init__(self, *args):
         from .plugin import connection_plugins, type_plugins
-        self.__cause__ = PluginWarning(
-            'Unverified DataJoint plugin detected.') if any([any(
-                [not plugins[k]['verified'] for k in plugins])
-                for plugins in [connection_plugins, type_plugins]
-                if plugins]) else None
+
+        self.__cause__ = (
+            PluginWarning("Unverified DataJoint plugin detected.")
+            if any(
+                [
+                    any([not plugins[k]["verified"] for k in plugins])
+                    for plugins in [connection_plugins, type_plugins]
+                    if plugins
+                ]
+            )
+            else None
+        )
 
     def suggest(self, *args):
         """
         regenerate the exception with additional arguments
+
         :param args: addition arguments
         :return: a new exception of the same type with the additional arguments
         """
diff --git a/datajoint/expression.py b/datajoint/expression.py
index 624f1122b..c3c385b22 100644
--- a/datajoint/expression.py
+++ b/datajoint/expression.py
@@ -7,8 +7,14 @@
 from .errors import DataJointError
 from .fetch import Fetch, Fetch1
 from .preview import preview, repr_html
-from .condition import AndList, Not, \
-    make_condition, assert_join_compatibility, extract_column_names, PromiscuousOperand
+from .condition import (
+    AndList,
+    Not,
+    make_condition,
+    assert_join_compatibility,
+    extract_column_names,
+    PromiscuousOperand,
+)
 from .declare import CONSTANT_LITERALS
 
 logger = logging.getLogger(__name__)
@@ -35,6 +41,7 @@ class QueryExpression:
         2. A projection is applied remapping remapped attributes
         3. Subclasses: Join, Aggregation, and Union have additional specific rules.
     """
+
     _restriction = None
     _restriction_attributes = None
     _left = []  # list of booleans True for left joins, False for inner joins
@@ -50,36 +57,36 @@ class QueryExpression:
 
     @property
     def connection(self):
-        """ a dj.Connection object """
+        """a dj.Connection object"""
         assert self._connection is not None
         return self._connection
 
     @property
     def support(self):
-        """ A list of table names or subqueries to from the FROM clause """
+        """A list of table names or subqueries to from the FROM clause"""
         assert self._support is not None
         return self._support
 
     @property
     def heading(self):
-        """ a dj.Heading object, reflects the effects of the projection operator .proj """
+        """a dj.Heading object, reflects the effects of the projection operator .proj"""
         return self._heading
 
     @property
     def original_heading(self):
-        """ a dj.Heading object reflecting the attributes before projection """
+        """a dj.Heading object reflecting the attributes before projection"""
         return self._original_heading or self.heading
 
     @property
     def restriction(self):
-        """ a AndList object of restrictions applied to input to produce the result """
+        """a AndList object of restrictions applied to input to produce the result"""
         if self._restriction is None:
             self._restriction = AndList()
         return self._restriction
 
     @property
     def restriction_attributes(self):
-        """ the set of attribute names invoked in the WHERE clause """
+        """the set of attribute names invoked in the WHERE clause"""
         if self._restriction_attributes is None:
             self._restriction_attributes = set()
         return self._restriction_attributes
@@ -88,36 +95,45 @@ def restriction_attributes(self):
     def primary_key(self):
         return self.heading.primary_key
 
-    _subquery_alias_count = count()    # count for alias names used in the FROM clause
+    _subquery_alias_count = count()  # count for alias names used in the FROM clause
 
     def from_clause(self):
-        support = ('(' + src.make_sql() + ') as `$%x`' % next(
-            self._subquery_alias_count) if isinstance(src, QueryExpression)
-            else src for src in self.support)
+        support = (
+            "(" + src.make_sql() + ") as `$%x`" % next(self._subquery_alias_count)
+            if isinstance(src, QueryExpression)
+            else src
+            for src in self.support
+        )
         clause = next(support)
         for s, left in zip(support, self._left):
-            clause += ' NATURAL{left} JOIN {clause}'.format(
-                left=" LEFT" if left else "",
-                clause=s)
+            clause += " NATURAL{left} JOIN {clause}".format(
+                left=" LEFT" if left else "", clause=s
+            )
         return clause
 
     def where_clause(self):
-        return '' if not self.restriction else ' WHERE (%s)' % ')AND('.join(
-            str(s) for s in self.restriction)
+        return (
+            ""
+            if not self.restriction
+            else " WHERE (%s)" % ")AND(".join(str(s) for s in self.restriction)
+        )
 
     def make_sql(self, fields=None):
         """
         Make the SQL SELECT statement.
+
         :param fields: used to explicitly set the select attributes
         """
-        return 'SELECT {distinct}{fields} FROM {from_}{where}'.format(
+        return "SELECT {distinct}{fields} FROM {from_}{where}".format(
             distinct="DISTINCT " if self._distinct else "",
             fields=self.heading.as_sql(fields or self.heading.names),
-            from_=self.from_clause(), where=self.where_clause())
+            from_=self.from_clause(),
+            where=self.where_clause(),
+        )
 
     # --------- query operators -----------
     def make_subquery(self):
-        """ create a new SELECT statement where self is the FROM clause """
+        """create a new SELECT statement where self is the FROM clause"""
         result = QueryExpression()
         result._connection = self.connection
         result._support = [self]
@@ -175,19 +191,24 @@ def restrict(self, restriction):
             return self  # restriction has no effect, return the same object
         # check that all attributes in condition are present in the query
         try:
-            raise DataJointError("Attribute `%s` is not found in query." % next(
-                attr for attr in attributes if attr not in self.heading.names))
+            raise DataJointError(
+                "Attribute `%s` is not found in query."
+                % next(attr for attr in attributes if attr not in self.heading.names)
+            )
         except StopIteration:
             pass  # all ok
         # If the new condition uses any new attributes, a subquery is required.
         # However, Aggregation's HAVING statement works fine with aliased attributes.
         need_subquery = isinstance(self, Union) or (
-                not isinstance(self, Aggregation) and self.heading.new_attributes)
+            not isinstance(self, Aggregation) and self.heading.new_attributes
+        )
         if need_subquery:
             result = self.make_subquery()
         else:
             result = copy.copy(self)
-            result._restriction = AndList(self.restriction)  # copy to preserve the original
+            result._restriction = AndList(
+                self.restriction
+            )  # copy to preserve the original
         result.restriction.append(new_condition)
         result.restriction_attributes.update(attributes)
         return result
@@ -266,14 +287,21 @@ def join(self, other, semantic_check=True, left=False):
         # needs subquery if self's FROM clause has common attributes with other's FROM clause
         need_subquery1 = need_subquery2 = bool(
             (set(self.original_heading.names) & set(other.original_heading.names))
-            - join_attributes)
+            - join_attributes
+        )
         # need subquery if any of the join attributes are derived
-        need_subquery1 = (need_subquery1 or isinstance(self, Aggregation) or
-                          any(n in self.heading.new_attributes for n in join_attributes)
-                          or isinstance(self, Union))
-        need_subquery2 = (need_subquery2 or isinstance(other, Aggregation) or
-                          any(n in other.heading.new_attributes for n in join_attributes)
-                          or isinstance(self, Union))
+        need_subquery1 = (
+            need_subquery1
+            or isinstance(self, Aggregation)
+            or any(n in self.heading.new_attributes for n in join_attributes)
+            or isinstance(self, Union)
+        )
+        need_subquery2 = (
+            need_subquery2
+            or isinstance(other, Aggregation)
+            or any(n in other.heading.new_attributes for n in join_attributes)
+            or isinstance(self, Union)
+        )
         if need_subquery1:
             self = self.make_subquery()
         if need_subquery2:
@@ -296,6 +324,7 @@ def __add__(self, other):
     def proj(self, *attributes, **named_attributes):
         """
         Projection operator.
+
         :param attributes:  attributes to be included in the result. (The primary key is already included).
         :param named_attributes: new attributes computed or renamed from existing attributes.
         :return: the projected expression.
@@ -314,66 +343,115 @@ def proj(self, *attributes, **named_attributes):
         Each attribute name can only be used once.
         """
         # new attributes in parentheses are included again with the new name without removing original
-        duplication_pattern = re.compile(fr'^\s*\(\s*(?!{"|".join(CONSTANT_LITERALS)})(?P<name>[a-zA-Z_]\w*)\s*\)\s*$')
+        duplication_pattern = re.compile(
+            rf'^\s*\(\s*(?!{"|".join(CONSTANT_LITERALS)})(?P<name>[a-zA-Z_]\w*)\s*\)\s*$'
+        )
         # attributes without parentheses renamed
-        rename_pattern = re.compile(fr'^\s*(?!{"|".join(CONSTANT_LITERALS)})(?P<name>[a-zA-Z_]\w*)\s*$')
-        replicate_map = {k: m.group('name')
-                         for k, m in ((k, duplication_pattern.match(v)) for k, v in named_attributes.items()) if m}
-        rename_map = {k: m.group('name')
-                      for k, m in ((k, rename_pattern.match(v)) for k, v in named_attributes.items()) if m}
-        compute_map = {k: v for k, v in named_attributes.items()
-                       if not duplication_pattern.match(v) and not rename_pattern.match(v)}
+        rename_pattern = re.compile(
+            rf'^\s*(?!{"|".join(CONSTANT_LITERALS)})(?P<name>[a-zA-Z_]\w*)\s*$'
+        )
+        replicate_map = {
+            k: m.group("name")
+            for k, m in (
+                (k, duplication_pattern.match(v)) for k, v in named_attributes.items()
+            )
+            if m
+        }
+        rename_map = {
+            k: m.group("name")
+            for k, m in (
+                (k, rename_pattern.match(v)) for k, v in named_attributes.items()
+            )
+            if m
+        }
+        compute_map = {
+            k: v
+            for k, v in named_attributes.items()
+            if not duplication_pattern.match(v) and not rename_pattern.match(v)
+        }
         attributes = set(attributes)
         # include primary key
         attributes.update((k for k in self.primary_key if k not in rename_map.values()))
         # include all secondary attributes with Ellipsis
         if Ellipsis in attributes:
             attributes.discard(Ellipsis)
-            attributes.update((a for a in self.heading.secondary_attributes
-                               if a not in attributes and a not in rename_map.values()))
+            attributes.update(
+                (
+                    a
+                    for a in self.heading.secondary_attributes
+                    if a not in attributes and a not in rename_map.values()
+                )
+            )
         try:
-            raise DataJointError("%s is not a valid data type for an attribute in .proj" % next(
-                a for a in attributes if not isinstance(a, str)))
+            raise DataJointError(
+                "%s is not a valid data type for an attribute in .proj"
+                % next(a for a in attributes if not isinstance(a, str))
+            )
         except StopIteration:
             pass  # normal case
         # remove excluded attributes, specified as `-attr'
-        excluded = set(a for a in attributes if a.strip().startswith('-'))
+        excluded = set(a for a in attributes if a.strip().startswith("-"))
         attributes.difference_update(excluded)
-        excluded = set(a.lstrip('-').strip() for a in excluded)
+        excluded = set(a.lstrip("-").strip() for a in excluded)
         attributes.difference_update(excluded)
         try:
-            raise DataJointError("Cannot exclude primary key attribute %s", next(
-                a for a in excluded if a in self.primary_key))
+            raise DataJointError(
+                "Cannot exclude primary key attribute %s",
+                next(a for a in excluded if a in self.primary_key),
+            )
         except StopIteration:
             pass  # all ok
         # check that all attributes exist in heading
         try:
             raise DataJointError(
-                'Attribute `%s` not found.' % next(a for a in attributes if a not in self.heading.names))
+                "Attribute `%s` not found."
+                % next(a for a in attributes if a not in self.heading.names)
+            )
         except StopIteration:
             pass  # all ok
 
         # check that all mentioned names are present in heading
         mentions = attributes.union(replicate_map.values()).union(rename_map.values())
         try:
-            raise DataJointError("Attribute '%s' not found." % next(a for a in mentions if not self.heading.names))
+            raise DataJointError(
+                "Attribute '%s' not found."
+                % next(a for a in mentions if not self.heading.names)
+            )
         except StopIteration:
             pass  # all ok
 
         # check that newly created attributes do not clash with any other selected attributes
         try:
-            raise DataJointError("Attribute `%s` already exists" % next(
-                a for a in rename_map if a in attributes.union(compute_map).union(replicate_map)))
+            raise DataJointError(
+                "Attribute `%s` already exists"
+                % next(
+                    a
+                    for a in rename_map
+                    if a in attributes.union(compute_map).union(replicate_map)
+                )
+            )
         except StopIteration:
             pass  # all ok
         try:
-            raise DataJointError("Attribute `%s` already exists" % next(
-                a for a in compute_map if a in attributes.union(rename_map).union(replicate_map)))
+            raise DataJointError(
+                "Attribute `%s` already exists"
+                % next(
+                    a
+                    for a in compute_map
+                    if a in attributes.union(rename_map).union(replicate_map)
+                )
+            )
         except StopIteration:
             pass  # all ok
         try:
-            raise DataJointError("Attribute `%s` already exists" % next(
-                a for a in replicate_map if a in attributes.union(rename_map).union(compute_map)))
+            raise DataJointError(
+                "Attribute `%s` already exists"
+                % next(
+                    a
+                    for a in replicate_map
+                    if a in attributes.union(rename_map).union(compute_map)
+                )
+            )
         except StopIteration:
             pass  # all ok
 
@@ -383,21 +461,29 @@ def proj(self, *attributes, **named_attributes):
         used.update(replicate_map.values())
         used.intersection_update(self.heading.names)
         need_subquery = isinstance(self, Union) or any(
-            self.heading[name].attribute_expression is not None for name in used)
+            self.heading[name].attribute_expression is not None for name in used
+        )
         if not need_subquery and self.restriction:
             # need a subquery if the restriction applies to attributes that have been renamed
-            need_subquery = any(name in self.restriction_attributes for name in self.heading.new_attributes)
+            need_subquery = any(
+                name in self.restriction_attributes
+                for name in self.heading.new_attributes
+            )
 
         result = self.make_subquery() if need_subquery else copy.copy(self)
         result._original_heading = result.original_heading
         result._heading = result.heading.select(
-            attributes, rename_map=dict(**rename_map, **replicate_map), compute_map=compute_map)
+            attributes,
+            rename_map=dict(**rename_map, **replicate_map),
+            compute_map=compute_map,
+        )
         return result
 
     def aggr(self, group, *attributes, keep_all_rows=False, **named_attributes):
         """
         Aggregation of the type U('attr1','attr2').aggr(group, computation="QueryExpression")
         has the primary key ('attr1','attr2') and performs aggregation computations for all matching elements of `group`.
+
         :param group:  The query expression to be aggregated.
         :param keep_all_rows: True=keep all the rows from self. False=keep only rows that match entries in group.
         :param named_attributes: computations of the form new_attribute="sql expression on attributes of group"
@@ -408,8 +494,9 @@ def aggr(self, group, *attributes, keep_all_rows=False, **named_attributes):
             attributes = set(attributes)
             attributes.discard(Ellipsis)
             attributes.update(self.heading.secondary_attributes)
-        return Aggregation.create(
-            self, group=group, keep_all_rows=keep_all_rows).proj(*attributes, **named_attributes)
+        return Aggregation.create(self, group=group, keep_all_rows=keep_all_rows).proj(
+            *attributes, **named_attributes
+        )
 
     aggregate = aggr  # alias for aggr
 
@@ -426,6 +513,7 @@ def head(self, limit=25, **fetch_kwargs):
         """
         shortcut to fetch the first few entries from query expression.
         Equivalent to fetch(order_by="KEY", limit=25)
+
         :param limit:  number of entries
         :param fetch_kwargs: kwargs for fetch
         :return: query result
@@ -436,6 +524,7 @@ def tail(self, limit=25, **fetch_kwargs):
         """
         shortcut to fetch the last few entries from query expression.
         Equivalent to fetch(order_by="KEY DESC", limit=25)[::-1]
+
         :param limit:  number of entries
         :param fetch_kwargs: kwargs for fetch
         :return: query result
@@ -445,27 +534,39 @@ def tail(self, limit=25, **fetch_kwargs):
     def __len__(self):
         """:return: number of elements in the result set e.g. ``len(q1)``."""
         return self.connection.query(
-            'SELECT {select_} FROM {from_}{where}'.format(
-                select_=('count(*)' if any(self._left)
-                         else 'count(DISTINCT {fields})'.format(fields=self.heading.as_sql(
-                            self.primary_key, include_aliases=False))),
+            "SELECT {select_} FROM {from_}{where}".format(
+                select_=(
+                    "count(*)"
+                    if any(self._left)
+                    else "count(DISTINCT {fields})".format(
+                        fields=self.heading.as_sql(
+                            self.primary_key, include_aliases=False
+                        )
+                    )
+                ),
                 from_=self.from_clause(),
-                where=self.where_clause())).fetchone()[0]
+                where=self.where_clause(),
+            )
+        ).fetchone()[0]
 
     def __bool__(self):
         """
         :return: True if the result is not empty. Equivalent to len(self) > 0 but often
             faster e.g. ``bool(q1)``.
         """
-        return bool(self.connection.query(
-            'SELECT EXISTS(SELECT 1 FROM {from_}{where})'.format(
-                from_=self.from_clause(),
-                where=self.where_clause())).fetchone()[0])
+        return bool(
+            self.connection.query(
+                "SELECT EXISTS(SELECT 1 FROM {from_}{where})".format(
+                    from_=self.from_clause(), where=self.where_clause()
+                )
+            ).fetchone()[0]
+        )
 
     def __contains__(self, item):
         """
         returns True if the restriction in item matches any entries in self
             e.g. ``restriction in q1``.
+
         :param item: any restriction
         (item in query_expression) is equivalent to bool(query_expression & item) but may be
         executed more efficiently.
@@ -479,7 +580,7 @@ def __iter__(self):
         :param self: iterator-compatible QueryExpression object
         """
         self._iter_only_key = all(v.in_key for v in self.heading.attributes.values())
-        self._iter_keys = self.fetch('KEY')
+        self._iter_keys = self.fetch("KEY")
         return self
 
     def __next__(self):
@@ -495,8 +596,10 @@ def __next__(self):
             key = self._iter_keys.pop(0)
         except AttributeError:
             # self._iter_keys is missing because __iter__ has not been called.
-            raise TypeError("A QueryExpression object is not an iterator. "
-                            "Use iter(obj) to create an iterator.")
+            raise TypeError(
+                "A QueryExpression object is not an iterator. "
+                "Use iter(obj) to create an iterator."
+            )
         except IndexError:
             raise StopIteration
         else:
@@ -516,12 +619,12 @@ def cursor(self, offset=0, limit=None, order_by=None, as_dict=False):
         :return: query cursor
         """
         if offset and limit is None:
-            raise DataJointError('limit is required when offset is set')
+            raise DataJointError("limit is required when offset is set")
         sql = self.make_sql()
         if order_by is not None:
-            sql += ' ORDER BY ' + ', '.join(order_by)
+            sql += " ORDER BY " + ", ".join(order_by)
         if limit is not None:
-            sql += ' LIMIT %d' % limit + (' OFFSET %d' % offset if offset else "")
+            sql += " LIMIT %d" % limit + (" OFFSET %d" % offset if offset else "")
         logger.debug(sql)
         return self.connection.query(sql, as_dict=as_dict)
 
@@ -533,14 +636,18 @@ def __repr__(self):
         :type self: :class:`QueryExpression`
         :rtype: str
         """
-        return super().__repr__() if config['loglevel'].lower() == 'debug' else self.preview()
+        return (
+            super().__repr__()
+            if config["loglevel"].lower() == "debug"
+            else self.preview()
+        )
 
     def preview(self, limit=None, width=None):
-        """ :return: a string of preview of the contents of the query. """
+        """:return: a string of preview of the contents of the query."""
         return preview(self, limit, width)
 
     def _repr_html_(self):
-        """ :return: HTML to display table in Jupyter notebook. """
+        """:return: HTML to display table in Jupyter notebook."""
         return repr_html(self)
 
 
@@ -553,20 +660,23 @@ class Aggregation(QueryExpression):
     Aggregation is used QueryExpression.aggr and U.aggr.
     Aggregation is a private class in DataJoint, not exposed to users.
     """
-    _left_restrict = None   # the pre-GROUP BY conditions for the WHERE clause
+
+    _left_restrict = None  # the pre-GROUP BY conditions for the WHERE clause
     _subquery_alias_count = count()
 
     @classmethod
     def create(cls, arg, group, keep_all_rows=False):
         if inspect.isclass(group) and issubclass(group, QueryExpression):
-            group = group()   # instantiate if a class
+            group = group()  # instantiate if a class
         assert isinstance(group, QueryExpression)
         if keep_all_rows and len(group.support) > 1 or group.heading.new_attributes:
             group = group.make_subquery()  # subquery if left joining a join
         join = arg.join(group, left=keep_all_rows)  # reuse the join logic
         result = cls()
         result._connection = join.connection
-        result._heading = join.heading.set_primary_key(arg.primary_key)  # use left operand's primary key
+        result._heading = join.heading.set_primary_key(
+            arg.primary_key
+        )  # use left operand's primary key
         result._support = join.support
         result._left = join._left
         result._left_restrict = join.restriction  # WHERE clause applied before GROUP BY
@@ -575,37 +685,51 @@ def create(cls, arg, group, keep_all_rows=False):
         return result
 
     def where_clause(self):
-        return '' if not self._left_restrict else ' WHERE (%s)' % ')AND('.join(
-            str(s) for s in self._left_restrict)
+        return (
+            ""
+            if not self._left_restrict
+            else " WHERE (%s)" % ")AND(".join(str(s) for s in self._left_restrict)
+        )
 
     def make_sql(self, fields=None):
         fields = self.heading.as_sql(fields or self.heading.names)
         assert self._grouping_attributes or not self.restriction
         distinct = set(self.heading.names) == set(self.primary_key)
-        return 'SELECT {distinct}{fields} FROM {from_}{where}{group_by}'.format(
+        return "SELECT {distinct}{fields} FROM {from_}{where}{group_by}".format(
             distinct="DISTINCT " if distinct else "",
             fields=fields,
             from_=self.from_clause(),
             where=self.where_clause(),
-            group_by="" if not self.primary_key else (
-                " GROUP BY `%s`" % '`,`'.join(self._grouping_attributes) +
-                ("" if not self.restriction else ' HAVING (%s)' % ')AND('.join(self.restriction))))
+            group_by=""
+            if not self.primary_key
+            else (
+                " GROUP BY `%s`" % "`,`".join(self._grouping_attributes)
+                + (
+                    ""
+                    if not self.restriction
+                    else " HAVING (%s)" % ")AND(".join(self.restriction)
+                )
+            ),
+        )
 
     def __len__(self):
         return self.connection.query(
-            'SELECT count(1) FROM ({subquery}) `${alias:x}`'.format(
-                subquery=self.make_sql(),
-                alias=next(self._subquery_alias_count))).fetchone()[0]
+            "SELECT count(1) FROM ({subquery}) `${alias:x}`".format(
+                subquery=self.make_sql(), alias=next(self._subquery_alias_count)
+            )
+        ).fetchone()[0]
 
     def __bool__(self):
-        return bool(self.connection.query(
-            'SELECT EXISTS({sql})'.format(sql=self.make_sql())))
+        return bool(
+            self.connection.query("SELECT EXISTS({sql})".format(sql=self.make_sql()))
+        )
 
 
 class Union(QueryExpression):
     """
     Union is the private DataJoint class that implements the union operator.
     """
+
     __count = count()
 
     @classmethod
@@ -614,15 +738,22 @@ def create(cls, arg1, arg2):
             arg2 = arg2()  # instantiate if a class
         if not isinstance(arg2, QueryExpression):
             raise DataJointError(
-                "A QueryExpression can only be unioned with another QueryExpression")
+                "A QueryExpression can only be unioned with another QueryExpression"
+            )
         if arg1.connection != arg2.connection:
             raise DataJointError(
-                "Cannot operate on QueryExpressions originating from different connections.")
+                "Cannot operate on QueryExpressions originating from different connections."
+            )
         if set(arg1.primary_key) != set(arg2.primary_key):
-            raise DataJointError("The operands of a union must share the same primary key.")
-        if set(arg1.heading.secondary_attributes) & set(arg2.heading.secondary_attributes):
             raise DataJointError(
-                "The operands of a union must not share any secondary attributes.")
+                "The operands of a union must share the same primary key."
+            )
+        if set(arg1.heading.secondary_attributes) & set(
+            arg2.heading.secondary_attributes
+        ):
+            raise DataJointError(
+                "The operands of a union must not share any secondary attributes."
+            )
         result = cls()
         result._connection = arg1.connection
         result._heading = arg1.heading.join(arg2.heading)
@@ -631,38 +762,51 @@ def create(cls, arg1, arg2):
 
     def make_sql(self):
         arg1, arg2 = self._support
-        if not arg1.heading.secondary_attributes and not arg2.heading.secondary_attributes:
+        if (
+            not arg1.heading.secondary_attributes
+            and not arg2.heading.secondary_attributes
+        ):
             # no secondary attributes: use UNION DISTINCT
             fields = arg1.primary_key
-            return ("SELECT * FROM (({sql1}) UNION ({sql2})) as `_u{alias}`".format(
-                sql1=arg1.make_sql() if isinstance(arg1, Union) else arg1.make_sql(fields),
-                sql2=arg2.make_sql() if isinstance(arg2, Union) else arg2.make_sql(fields),
-                alias=next(self.__count)
-                ))
+            return "SELECT * FROM (({sql1}) UNION ({sql2})) as `_u{alias}`".format(
+                sql1=arg1.make_sql()
+                if isinstance(arg1, Union)
+                else arg1.make_sql(fields),
+                sql2=arg2.make_sql()
+                if isinstance(arg2, Union)
+                else arg2.make_sql(fields),
+                alias=next(self.__count),
+            )
         # with secondary attributes, use union of left join with antijoin
         fields = self.heading.names
         sql1 = arg1.join(arg2, left=True).make_sql(fields)
-        sql2 = (arg2 - arg1).proj(
-            ..., **{k: 'NULL' for k in arg1.heading.secondary_attributes}).make_sql(fields)
+        sql2 = (
+            (arg2 - arg1)
+            .proj(..., **{k: "NULL" for k in arg1.heading.secondary_attributes})
+            .make_sql(fields)
+        )
         return "({sql1})  UNION ({sql2})".format(sql1=sql1, sql2=sql2)
 
     def from_clause(self):
-        """ The union does not use a FROM clause """
+        """The union does not use a FROM clause"""
         assert False
 
     def where_clause(self):
-        """ The union does not use a WHERE clause """
+        """The union does not use a WHERE clause"""
         assert False
 
     def __len__(self):
         return self.connection.query(
-            'SELECT count(1) FROM ({subquery}) `${alias:x}`'.format(
+            "SELECT count(1) FROM ({subquery}) `${alias:x}`".format(
                 subquery=self.make_sql(),
-                alias=next(QueryExpression._subquery_alias_count))).fetchone()[0]
+                alias=next(QueryExpression._subquery_alias_count),
+            )
+        ).fetchone()[0]
 
     def __bool__(self):
-        return bool(self.connection.query(
-            'SELECT EXISTS({sql})'.format(sql=self.make_sql())))
+        return bool(
+            self.connection.query("SELECT EXISTS({sql})".format(sql=self.make_sql()))
+        )
 
 
 class U:
@@ -725,9 +869,9 @@ def primary_key(self):
 
     def __and__(self, other):
         if inspect.isclass(other) and issubclass(other, QueryExpression):
-            other = other()   # instantiate if a class
+            other = other()  # instantiate if a class
         if not isinstance(other, QueryExpression):
-            raise DataJointError('Set U can only be restricted with a QueryExpression.')
+            raise DataJointError("Set U can only be restricted with a QueryExpression.")
         result = copy.copy(other)
         result._distinct = True
         result._heading = result.heading.set_primary_key(self.primary_key)
@@ -744,36 +888,42 @@ def join(self, other, left=False):
         :return: a copy of the other query expression with the primary key extended.
         """
         if inspect.isclass(other) and issubclass(other, QueryExpression):
-            other = other()   # instantiate if a class
+            other = other()  # instantiate if a class
         if not isinstance(other, QueryExpression):
-            raise DataJointError('Set U can only be joined with a QueryExpression.')
+            raise DataJointError("Set U can only be joined with a QueryExpression.")
         try:
             raise DataJointError(
-                'Attribute `%s` not found' % next(k for k in self.primary_key
-                                                  if k not in other.heading.names))
+                "Attribute `%s` not found"
+                % next(k for k in self.primary_key if k not in other.heading.names)
+            )
         except StopIteration:
             pass  # all ok
         result = copy.copy(other)
         result._heading = result.heading.set_primary_key(
-            other.primary_key + [k for k in self.primary_key
-                                 if k not in other.primary_key])
+            other.primary_key
+            + [k for k in self.primary_key if k not in other.primary_key]
+        )
         return result
 
     def __mul__(self, other):
-        """ shorthand for join """
+        """shorthand for join"""
         return self.join(other)
 
     def aggr(self, group, **named_attributes):
         """
         Aggregation of the type U('attr1','attr2').aggr(group, computation="QueryExpression")
         has the primary key ('attr1','attr2') and performs aggregation computations for all matching elements of `group`.
+
         :param group:  The query expression to be aggregated.
         :param named_attributes: computations of the form new_attribute="sql expression on attributes of group"
         :return: The derived query expression
         """
-        if named_attributes.get('keep_all_rows', False):
+        if named_attributes.get("keep_all_rows", False):
             raise DataJointError(
-                'Cannot set keep_all_rows=True when aggregating on a universal set.')
-        return Aggregation.create(self, group=group,  keep_all_rows=False).proj(**named_attributes)
+                "Cannot set keep_all_rows=True when aggregating on a universal set."
+            )
+        return Aggregation.create(self, group=group, keep_all_rows=False).proj(
+            **named_attributes
+        )
 
     aggregate = aggr  # alias for aggr
diff --git a/datajoint/external.py b/datajoint/external.py
index 62120e306..c04cc40c4 100644
--- a/datajoint/external.py
+++ b/datajoint/external.py
@@ -10,15 +10,22 @@
 from . import s3
 from .utils import safe_write, safe_copy
 
-CACHE_SUBFOLDING = (2, 2)   # (2, 2) means  "0123456789abcd" will be saved as "01/23/0123456789abcd"
-SUPPORT_MIGRATED_BLOBS = True   # support blobs migrated from datajoint 0.11.*
+CACHE_SUBFOLDING = (
+    2,
+    2,
+)  # (2, 2) means  "0123456789abcd" will be saved as "01/23/0123456789abcd"
+SUPPORT_MIGRATED_BLOBS = True  # support blobs migrated from datajoint 0.11.*
 
 
 def subfold(name, folds):
     """
     subfolding for external storage:   e.g.  subfold('aBCdefg', (2, 3))  -->  ['ab','cde']
     """
-    return (name[:folds[0]].lower(),) + subfold(name[folds[0]:], folds[1:]) if folds else ()
+    return (
+        (name[: folds[0]].lower(),) + subfold(name[folds[0] :], folds[1:])
+        if folds
+        else ()
+    )
 
 
 class ExternalTable(Table):
@@ -26,24 +33,29 @@ class ExternalTable(Table):
     The table tracking externally stored objects.
     Declare as ExternalTable(connection, database)
     """
+
     def __init__(self, connection, store, database):
         self.store = store
         self.spec = config.get_store_spec(store)
         self._s3 = None
         self.database = database
         self._connection = connection
-        self._heading = Heading(table_info=dict(
-            conn=connection,
-            database=database,
-            table_name=self.table_name,
-            context=None))
+        self._heading = Heading(
+            table_info=dict(
+                conn=connection,
+                database=database,
+                table_name=self.table_name,
+                context=None,
+            )
+        )
         self._support = [self.full_table_name]
         if not self.is_declared:
             self.declare()
         self._s3 = None
-        if self.spec['protocol'] == 'file' and not Path(self.spec['location']).is_dir():
-            raise FileNotFoundError('Inaccessible local directory %s' %
-                                    self.spec['location']) from None
+        if self.spec["protocol"] == "file" and not Path(self.spec["location"]).is_dir():
+            raise FileNotFoundError(
+                "Inaccessible local directory %s" % self.spec["location"]
+            ) from None
 
     @property
     def definition(self):
@@ -60,7 +72,9 @@ def definition(self):
 
     @property
     def table_name(self):
-        return '{external_table_root}_{store}'.format(external_table_root=EXTERNAL_TABLE_ROOT, store=self.store)
+        return "{external_table_root}_{store}".format(
+            external_table_root=EXTERNAL_TABLE_ROOT, store=self.store
+        )
 
     @property
     def s3(self):
@@ -73,60 +87,66 @@ def s3(self):
     def _make_external_filepath(self, relative_filepath):
         """resolve the complete external path based on the relative path"""
         # Strip root
-        if self.spec['protocol'] == 's3':
-            posix_path = PurePosixPath(PureWindowsPath(self.spec['location']))
-            location_path = Path(
-                    *posix_path.parts[1:]) if len(
-                    self.spec['location']) > 0 and any(
-                    case in posix_path.parts[0] for case in (
-                        '\\', ':')) else Path(posix_path)
+        if self.spec["protocol"] == "s3":
+            posix_path = PurePosixPath(PureWindowsPath(self.spec["location"]))
+            location_path = (
+                Path(*posix_path.parts[1:])
+                if len(self.spec["location"]) > 0
+                and any(case in posix_path.parts[0] for case in ("\\", ":"))
+                else Path(posix_path)
+            )
             return PurePosixPath(location_path, relative_filepath)
         # Preserve root
-        elif self.spec['protocol'] == 'file':
-            return PurePosixPath(Path(self.spec['location']), relative_filepath)
+        elif self.spec["protocol"] == "file":
+            return PurePosixPath(Path(self.spec["location"]), relative_filepath)
         else:
             assert False
 
-    def _make_uuid_path(self, uuid, suffix=''):
+    def _make_uuid_path(self, uuid, suffix=""):
         """create external path based on the uuid hash"""
-        return self._make_external_filepath(PurePosixPath(
-            self.database, '/'.join(subfold(uuid.hex, self.spec['subfolding'])), uuid.hex).with_suffix(suffix))
+        return self._make_external_filepath(
+            PurePosixPath(
+                self.database,
+                "/".join(subfold(uuid.hex, self.spec["subfolding"])),
+                uuid.hex,
+            ).with_suffix(suffix)
+        )
 
     def _upload_file(self, local_path, external_path, metadata=None):
-        if self.spec['protocol'] == 's3':
+        if self.spec["protocol"] == "s3":
             self.s3.fput(local_path, external_path, metadata)
-        elif self.spec['protocol'] == 'file':
+        elif self.spec["protocol"] == "file":
             safe_copy(local_path, external_path, overwrite=True)
         else:
             assert False
 
     def _download_file(self, external_path, download_path):
-        if self.spec['protocol'] == 's3':
+        if self.spec["protocol"] == "s3":
             self.s3.fget(external_path, download_path)
-        elif self.spec['protocol'] == 'file':
+        elif self.spec["protocol"] == "file":
             safe_copy(external_path, download_path)
         else:
             assert False
 
     def _upload_buffer(self, buffer, external_path):
-        if self.spec['protocol'] == 's3':
+        if self.spec["protocol"] == "s3":
             self.s3.put(external_path, buffer)
-        elif self.spec['protocol'] == 'file':
+        elif self.spec["protocol"] == "file":
             safe_write(external_path, buffer)
         else:
             assert False
 
     def _download_buffer(self, external_path):
-        if self.spec['protocol'] == 's3':
+        if self.spec["protocol"] == "s3":
             return self.s3.get(external_path)
-        if self.spec['protocol'] == 'file':
+        if self.spec["protocol"] == "file":
             return Path(external_path).read_bytes()
         assert False
 
     def _remove_external_file(self, external_path):
-        if self.spec['protocol'] == 's3':
+        if self.spec["protocol"] == "s3":
             self.s3.remove_object(external_path)
-        elif self.spec['protocol'] == 'file':
+        elif self.spec["protocol"] == "file":
             try:
                 Path(external_path).unlink()
             except FileNotFoundError:
@@ -136,9 +156,9 @@ def exists(self, external_filepath):
         """
         :return: True if the external file is accessible
         """
-        if self.spec['protocol'] == 's3':
+        if self.spec["protocol"] == "s3":
             return self.s3.exists(external_filepath)
-        if self.spec['protocol'] == 'file':
+        if self.spec["protocol"] == "file":
             return Path(external_filepath).is_file()
         assert False
 
@@ -154,7 +174,10 @@ def put(self, blob):
         self.connection.query(
             "INSERT INTO {tab} (hash, size) VALUES (%s, {size}) ON DUPLICATE KEY "
             "UPDATE timestamp=CURRENT_TIMESTAMP".format(
-                tab=self.full_table_name, size=len(blob)), args=(uuid.bytes,))
+                tab=self.full_table_name, size=len(blob)
+            ),
+            args=(uuid.bytes,),
+        )
         return uuid
 
     def get(self, uuid):
@@ -165,7 +188,7 @@ def get(self, uuid):
             return None
         # attempt to get object from cache
         blob = None
-        cache_folder = config.get('cache', None)
+        cache_folder = config.get("cache", None)
         if cache_folder:
             try:
                 cache_path = Path(cache_folder, *subfold(uuid.hex, CACHE_SUBFOLDING))
@@ -181,10 +204,14 @@ def get(self, uuid):
                 if not SUPPORT_MIGRATED_BLOBS:
                     raise
                 # blobs migrated from datajoint 0.11 are stored at explicitly defined filepaths
-                relative_filepath, contents_hash = (self & {'hash': uuid}).fetch1('filepath', 'contents_hash')
+                relative_filepath, contents_hash = (self & {"hash": uuid}).fetch1(
+                    "filepath", "contents_hash"
+                )
                 if relative_filepath is None:
                     raise
-                blob = self._download_buffer(self._make_external_filepath(relative_filepath))
+                blob = self._download_buffer(
+                    self._make_external_filepath(relative_filepath)
+                )
             if cache_folder:
                 cache_path.mkdir(parents=True, exist_ok=True)
                 safe_write(cache_path / uuid.hex, blob)
@@ -194,25 +221,29 @@ def get(self, uuid):
 
     def upload_attachment(self, local_path):
         attachment_name = Path(local_path).name
-        uuid = uuid_from_file(local_path, init_string=attachment_name + '\0')
-        external_path = self._make_uuid_path(uuid, '.' + attachment_name)
+        uuid = uuid_from_file(local_path, init_string=attachment_name + "\0")
+        external_path = self._make_uuid_path(uuid, "." + attachment_name)
         self._upload_file(local_path, external_path)
         # insert tracking info
-        self.connection.query("""
+        self.connection.query(
+            """
         INSERT INTO {tab} (hash, size, attachment_name)
         VALUES (%s, {size}, "{attachment_name}")
         ON DUPLICATE KEY UPDATE timestamp=CURRENT_TIMESTAMP""".format(
                 tab=self.full_table_name,
                 size=Path(local_path).stat().st_size,
-                attachment_name=attachment_name), args=[uuid.bytes])
+                attachment_name=attachment_name,
+            ),
+            args=[uuid.bytes],
+        )
         return uuid
 
     def get_attachment_name(self, uuid):
-        return (self & {'hash': uuid}).fetch1('attachment_name')
+        return (self & {"hash": uuid}).fetch1("attachment_name")
 
     def download_attachment(self, uuid, attachment_name, download_path):
-        """ save attachment from memory buffer into the save_path """
-        external_path = self._make_uuid_path(uuid, '.' + attachment_name)
+        """save attachment from memory buffer into the save_path"""
+        external_path = self._make_uuid_path(uuid, "." + attachment_name)
         self._download_file(external_path, download_path)
 
     # --- FILEPATH ---
@@ -225,46 +256,75 @@ def upload_filepath(self, local_filepath):
         """
         local_filepath = Path(local_filepath)
         try:
-            relative_filepath = str(local_filepath.relative_to(self.spec['stage']).as_posix())
+            relative_filepath = str(
+                local_filepath.relative_to(self.spec["stage"]).as_posix()
+            )
         except ValueError:
-            raise DataJointError('The path {path} is not in stage {stage}'.format(
-                path=local_filepath.parent, **self.spec))
-        uuid = uuid_from_buffer(init_string=relative_filepath)  # hash relative path, not contents
+            raise DataJointError(
+                "The path {path} is not in stage {stage}".format(
+                    path=local_filepath.parent, **self.spec
+                )
+            )
+        uuid = uuid_from_buffer(
+            init_string=relative_filepath
+        )  # hash relative path, not contents
         contents_hash = uuid_from_file(local_filepath)
 
         # check if the remote file already exists and verify that it matches
-        check_hash = (self & {'hash': uuid}).fetch('contents_hash')
+        check_hash = (self & {"hash": uuid}).fetch("contents_hash")
         if check_hash:
             # the tracking entry exists, check that it's the same file as before
             if contents_hash != check_hash[0]:
                 raise DataJointError(
-                    "A different version of '{file}' has already been placed.".format(file=relative_filepath))
+                    "A different version of '{file}' has already been placed.".format(
+                        file=relative_filepath
+                    )
+                )
         else:
             # upload the file and create its tracking entry
-            self._upload_file(local_filepath, self._make_external_filepath(relative_filepath),
-                              metadata={'contents_hash': str(contents_hash)})
+            self._upload_file(
+                local_filepath,
+                self._make_external_filepath(relative_filepath),
+                metadata={"contents_hash": str(contents_hash)},
+            )
             self.connection.query(
                 "INSERT INTO {tab} (hash, size, filepath, contents_hash) VALUES (%s, {size}, '{filepath}', %s)".format(
-                    tab=self.full_table_name, size=Path(local_filepath).stat().st_size,
-                    filepath=relative_filepath), args=(uuid.bytes, contents_hash.bytes))
+                    tab=self.full_table_name,
+                    size=Path(local_filepath).stat().st_size,
+                    filepath=relative_filepath,
+                ),
+                args=(uuid.bytes, contents_hash.bytes),
+            )
         return uuid
 
     def download_filepath(self, filepath_hash):
         """
         sync a file from external store to the local stage
+
         :param filepath_hash: The hash (UUID) of the relative_path
         :return: hash (UUID) of the contents of the downloaded file or Nones
         """
         if filepath_hash is not None:
-            relative_filepath, contents_hash = (self & {'hash': filepath_hash}).fetch1('filepath', 'contents_hash')
+            relative_filepath, contents_hash = (self & {"hash": filepath_hash}).fetch1(
+                "filepath", "contents_hash"
+            )
             external_path = self._make_external_filepath(relative_filepath)
-            local_filepath = Path(self.spec['stage']).absolute() / relative_filepath
-            file_exists = Path(local_filepath).is_file() and uuid_from_file(local_filepath) == contents_hash
+            local_filepath = Path(self.spec["stage"]).absolute() / relative_filepath
+            file_exists = (
+                Path(local_filepath).is_file()
+                and uuid_from_file(local_filepath) == contents_hash
+            )
             if not file_exists:
                 self._download_file(external_path, local_filepath)
                 checksum = uuid_from_file(local_filepath)
-                if checksum != contents_hash:  # this should never happen without outside interference
-                    raise DataJointError("'{file}' downloaded but did not pass checksum'".format(file=local_filepath))
+                if (
+                    checksum != contents_hash
+                ):  # this should never happen without outside interference
+                    raise DataJointError(
+                        "'{file}' downloaded but did not pass checksum'".format(
+                            file=local_filepath
+                        )
+                    )
             return str(local_filepath), contents_hash
 
     # --- UTILITIES ---
@@ -274,54 +334,80 @@ def references(self):
         """
         :return: generator of referencing table names and their referencing columns
         """
-        return ({k.lower(): v for k, v in elem.items()} for elem in self.connection.query("""
+        return (
+            {k.lower(): v for k, v in elem.items()}
+            for elem in self.connection.query(
+                """
         SELECT concat('`', table_schema, '`.`', table_name, '`') as referencing_table, column_name
         FROM information_schema.key_column_usage
         WHERE referenced_table_name="{tab}" and referenced_table_schema="{db}"
-        """.format(tab=self.table_name, db=self.database), as_dict=True))
+        """.format(
+                    tab=self.table_name, db=self.database
+                ),
+                as_dict=True,
+            )
+        )
 
     def fetch_external_paths(self, **fetch_kwargs):
         """
         generate complete external filepaths from the query.
         Each element is a tuple: (uuid, path)
+
         :param fetch_kwargs: keyword arguments to pass to fetch
         """
         fetch_kwargs.update(as_dict=True)
         paths = []
-        for item in self.fetch('hash', 'attachment_name', 'filepath', **fetch_kwargs):
-            if item['attachment_name']:
+        for item in self.fetch("hash", "attachment_name", "filepath", **fetch_kwargs):
+            if item["attachment_name"]:
                 # attachments
-                path = self._make_uuid_path(item['hash'], '.' + item['attachment_name'])
-            elif item['filepath']:
+                path = self._make_uuid_path(item["hash"], "." + item["attachment_name"])
+            elif item["filepath"]:
                 # external filepaths
-                path = self._make_external_filepath(item['filepath'])
+                path = self._make_external_filepath(item["filepath"])
             else:
                 # blobs
-                path = self._make_uuid_path(item['hash'])
-            paths.append((item['hash'], path))
+                path = self._make_uuid_path(item["hash"])
+            paths.append((item["hash"], path))
         return paths
 
     def unused(self):
         """
         query expression for unused hashes
+
         :return: self restricted to elements that are not in use by any tables in the schema
         """
-        return self - [FreeTable(self.connection, ref['referencing_table']).proj(hash=ref['column_name'])
-                       for ref in self.references]
+        return self - [
+            FreeTable(self.connection, ref["referencing_table"]).proj(
+                hash=ref["column_name"]
+            )
+            for ref in self.references
+        ]
 
     def used(self):
         """
         query expression for used hashes
+
         :return: self restricted to elements that in use by tables in the schema
         """
-        return self & [FreeTable(self.connection, ref['referencing_table']).proj(hash=ref['column_name'])
-                       for ref in self.references]
-
-    def delete(self, *, delete_external_files=None, limit=None, display_progress=True, errors_as_string=True):
+        return self & [
+            FreeTable(self.connection, ref["referencing_table"]).proj(
+                hash=ref["column_name"]
+            )
+            for ref in self.references
+        ]
+
+    def delete(
+        self,
+        *,
+        delete_external_files=None,
+        limit=None,
+        display_progress=True,
+        errors_as_string=True
+    ):
         """
-        :param delete_external_files: True or False. If False, only the tracking info is removed from the
-        external store table but the external files remain intact. If True, then the external files
-        themselves are deleted too.
+
+        :param delete_external_files: True or False. If False, only the tracking info is removed from the external
+                store table but the external files remain intact. If True, then the external files themselves are deleted too.
         :param errors_as_string: If True any errors returned when deleting from external files will be strings
         :param limit: (integer) limit the number of items to delete
         :param display_progress: if True, display progress as files are cleaned up
@@ -330,7 +416,8 @@ def delete(self, *, delete_external_files=None, limit=None, display_progress=Tru
         if delete_external_files not in (True, False):
             raise DataJointError(
                 "The delete_external_files argument must be set to either "
-                "True or False in delete()")
+                "True or False in delete()"
+            )
 
         if not delete_external_files:
             self.unused().delete_quick()
@@ -341,20 +428,25 @@ def delete(self, *, delete_external_files=None, limit=None, display_progress=Tru
             # delete items one by one, close to transaction-safe
             error_list = []
             for uuid, external_path in items:
-                row = (self & {'hash': uuid}).fetch()
+                row = (self & {"hash": uuid}).fetch()
                 if row.size:
                     try:
-                        (self & {'hash': uuid}).delete_quick()
+                        (self & {"hash": uuid}).delete_quick()
                     except Exception:
-                        pass   # if delete failed, do not remove the external file
+                        pass  # if delete failed, do not remove the external file
                     else:
                         try:
                             self._remove_external_file(external_path)
                         except Exception as error:
                             # adding row back into table after failed delete
                             self.insert1(row[0], skip_duplicates=True)
-                            error_list.append((uuid, external_path,
-                                               str(error) if errors_as_string else error))
+                            error_list.append(
+                                (
+                                    uuid,
+                                    external_path,
+                                    str(error) if errors_as_string else error,
+                                )
+                            )
             return error_list
 
 
@@ -365,25 +457,33 @@ class ExternalMapping(Mapping):
         e = ExternalMapping(schema)
         external_table = e[store]
     """
+
     def __init__(self, schema):
         self.schema = schema
         self._tables = {}
 
     def __repr__(self):
-        return ("External file tables for schema `{schema}`:\n    ".format(schema=self.schema.database)
-                + "\n    ".join('"{store}" {protocol}:{location}'.format(
-                    store=k, **v.spec) for k, v in self.items()))
+        return "External file tables for schema `{schema}`:\n    ".format(
+            schema=self.schema.database
+        ) + "\n    ".join(
+            '"{store}" {protocol}:{location}'.format(store=k, **v.spec)
+            for k, v in self.items()
+        )
 
     def __getitem__(self, store):
         """
         Triggers the creation of an external table.
         Should only be used when ready to save or read from external storage.
+
         :param store: the name of the store
         :return: the ExternalTable object for the store
         """
         if store not in self._tables:
             self._tables[store] = ExternalTable(
-                connection=self.schema.connection, store=store, database=self.schema.database)
+                connection=self.schema.connection,
+                store=store,
+                database=self.schema.database,
+            )
         return self._tables[store]
 
     def __len__(self):
diff --git a/datajoint/fetch.py b/datajoint/fetch.py
index caf17d5ac..936624400 100644
--- a/datajoint/fetch.py
+++ b/datajoint/fetch.py
@@ -18,11 +18,12 @@ class key:
     object that allows requesting the primary key as an argument in expression.fetch()
     The string "KEY" can be used instead of the class key
     """
+
     pass
 
 
 def is_key(attr):
-    return attr is key or attr == 'KEY'
+    return attr is key or attr == "KEY"
 
 
 def to_dicts(recarray):
@@ -45,7 +46,11 @@ def _get(connection, attr, data, squeeze, download_path):
     if data is None:
         return
 
-    extern = connection.schemas[attr.database].external[attr.store] if attr.is_external else None
+    extern = (
+        connection.schemas[attr.database].external[attr.store]
+        if attr.is_external
+        else None
+    )
 
     # apply attribute adapter if present
     adapt = attr.adapter.get if attr.adapter else lambda x: x
@@ -60,20 +65,33 @@ def _get(connection, attr, data, squeeze, download_path):
         # 3. if exists and checksum passes then return the local filepath
         # 4. Otherwise, download the remote file and return the new filepath
         _uuid = uuid.UUID(bytes=data) if attr.is_external else None
-        attachment_name = (extern.get_attachment_name(_uuid) if attr.is_external
-                           else data.split(b"\0", 1)[0].decode())
+        attachment_name = (
+            extern.get_attachment_name(_uuid)
+            if attr.is_external
+            else data.split(b"\0", 1)[0].decode()
+        )
         local_filepath = Path(download_path) / attachment_name
         if local_filepath.is_file():
-            attachment_checksum = _uuid if attr.is_external else hash.uuid_from_buffer(data)
-            if attachment_checksum == hash.uuid_from_file(local_filepath, init_string=attachment_name + '\0'):
-                return adapt(str(local_filepath))  # checksum passed, no need to download again
+            attachment_checksum = (
+                _uuid if attr.is_external else hash.uuid_from_buffer(data)
+            )
+            if attachment_checksum == hash.uuid_from_file(
+                local_filepath, init_string=attachment_name + "\0"
+            ):
+                return adapt(
+                    str(local_filepath)
+                )  # checksum passed, no need to download again
             # generate the next available alias filename
             for n in itertools.count():
-                f = local_filepath.parent / (local_filepath.stem + '_%04x' % n + local_filepath.suffix)
+                f = local_filepath.parent / (
+                    local_filepath.stem + "_%04x" % n + local_filepath.suffix
+                )
                 if not f.is_file():
                     local_filepath = f
                     break
-                if attachment_checksum == hash.uuid_from_file(f, init_string=attachment_name + '\0'):
+                if attachment_checksum == hash.uuid_from_file(
+                    f, init_string=attachment_name + "\0"
+                ):
                     return adapt(str(f))  # checksum passed, no need to download again
         # Save attachment
         if attr.is_external:
@@ -83,9 +101,18 @@ def _get(connection, attr, data, squeeze, download_path):
             safe_write(local_filepath, data.split(b"\0", 1)[1])
         return adapt(str(local_filepath))  # download file from remote store
 
-    return adapt(uuid.UUID(bytes=data) if attr.uuid else (
-            blob.unpack(extern.get(uuid.UUID(bytes=data)) if attr.is_external else data, squeeze=squeeze)
-            if attr.is_blob else data))
+    return adapt(
+        uuid.UUID(bytes=data)
+        if attr.uuid
+        else (
+            blob.unpack(
+                extern.get(uuid.UUID(bytes=data)) if attr.is_external else data,
+                squeeze=squeeze,
+            )
+            if attr.is_blob
+            else data
+        )
+    )
 
 
 def _flatten_attribute_list(primary_key, attrs):
@@ -95,10 +122,10 @@ def _flatten_attribute_list(primary_key, attrs):
     :return: generator of attributes where "KEY" is replaces with its component attributes
     """
     for a in attrs:
-        if re.match(r'^\s*KEY(\s+[aA][Ss][Cc])?\s*$', a):
+        if re.match(r"^\s*KEY(\s+[aA][Ss][Cc])?\s*$", a):
             yield from primary_key
-        elif re.match(r'^\s*KEY\s+[Dd][Ee][Ss][Cc]\s*$', a):
-            yield from (q + ' DESC' for q in primary_key)
+        elif re.match(r"^\s*KEY\s+[Dd][Ee][Ss][Cc]\s*$", a):
+            yield from (q + " DESC" for q in primary_key)
         else:
             yield a
 
@@ -106,31 +133,39 @@ def _flatten_attribute_list(primary_key, attrs):
 class Fetch:
     """
     A fetch object that handles retrieving elements from the table expression.
+
     :param expression: the QueryExpression object to fetch from.
     """
 
     def __init__(self, expression):
         self._expression = expression
 
-    def __call__(self, *attrs, offset=None, limit=None, order_by=None, format=None, as_dict=None,
-                 squeeze=False, download_path='.'):
+    def __call__(
+        self,
+        *attrs,
+        offset=None,
+        limit=None,
+        order_by=None,
+        format=None,
+        as_dict=None,
+        squeeze=False,
+        download_path="."
+    ):
         """
         Fetches the expression results from the database into an np.array or list of dictionaries and
         unpacks blob attributes.
-        :param attrs: zero or more attributes to fetch. If not provided, the call will return
-        all attributes of this relation. If provided, returns tuples with an entry for each attribute.
+
+        :param attrs: zero or more attributes to fetch. If not provided, the call will return all attributes of this
+                        relation. If provided, returns tuples with an entry for each attribute.
         :param offset: the number of tuples to skip in the returned result
         :param limit: the maximum number of tuples to return
-        :param order_by: a single attribute or the list of attributes to order the results.
-                No ordering should be assumed if order_by=None.
-                To reverse the order, add DESC to the attribute name or names: e.g. ("age DESC", "frequency")
-                To order by primary key, use "KEY" or "KEY DESC"
-        :param format: Effective when as_dict=None and when attrs is empty
-                None: default from config['fetch_format'] or 'array' if not configured
-                "array": use numpy.key_array
-                "frame": output pandas.DataFrame. .
-        :param as_dict: returns a list of dictionaries instead of a record array.
-                Defaults to False for .fetch() and to True for .fetch('KEY')
+        :param order_by: a single attribute or the list of attributes to order the results. No ordering should be assumed
+                        if order_by=None. To reverse the order, add DESC to the attribute name or names: e.g. ("age DESC",
+                        "frequency") To order by primary key, use "KEY" or "KEY DESC"
+        :param format: Effective when as_dict=None and when attrs is empty None: default from config['fetch_format'] or
+                        'array' if not configured "array": use numpy.key_array "frame": output pandas.DataFrame. .
+        :param as_dict: returns a list of dictionaries instead of a record array. Defaults to False for .fetch() and to
+                        True for .fetch('KEY')
         :param squeeze:  if True, remove extra dimensions from arrays
         :param download_path: for fetches that download data, e.g. attachments
         :return: the contents of the relation in the form of a structured numpy.array or a dict list
@@ -140,68 +175,109 @@ def __call__(self, *attrs, offset=None, limit=None, order_by=None, format=None,
             if isinstance(order_by, str):
                 order_by = [order_by]
             # expand "KEY" or "KEY DESC"
-            order_by = list(_flatten_attribute_list(self._expression.primary_key, order_by))
+            order_by = list(
+                _flatten_attribute_list(self._expression.primary_key, order_by)
+            )
 
         attrs_as_dict = as_dict and attrs
         if attrs_as_dict:
             # absorb KEY into attrs and prepare to return attributes as dict (issue #595)
             if any(is_key(k) for k in attrs):
                 attrs = list(self._expression.primary_key) + [
-                    a for a in attrs if a not in self._expression.primary_key]
+                    a for a in attrs if a not in self._expression.primary_key
+                ]
         if as_dict is None:
             as_dict = bool(attrs)  # default to True for "KEY" and False otherwise
         # format should not be specified with attrs or is_dict=True
         if format is not None and (as_dict or attrs):
-            raise DataJointError('Cannot specify output format when as_dict=True or '
-                                 'when attributes are selected to be fetched separately.')
+            raise DataJointError(
+                "Cannot specify output format when as_dict=True or "
+                "when attributes are selected to be fetched separately."
+            )
         if format not in {None, "array", "frame"}:
             raise DataJointError(
-                'Fetch output format must be in '
-                '{{"array", "frame"}} but "{}" was given'.format(format))
+                "Fetch output format must be in "
+                '{{"array", "frame"}} but "{}" was given'.format(format)
+            )
 
         if not (attrs or as_dict) and format is None:
-            format = config['fetch_format']  # default to array
+            format = config["fetch_format"]  # default to array
             if format not in {"array", "frame"}:
                 raise DataJointError(
                     'Invalid entry "{}" in datajoint.config["fetch_format"]: '
-                    'use "array" or "frame"'.format(format))
+                    'use "array" or "frame"'.format(format)
+                )
 
         if limit is None and offset is not None:
-            warnings.warn('Offset set, but no limit. Setting limit to a large number. '
-                          'Consider setting a limit explicitly.')
+            warnings.warn(
+                "Offset set, but no limit. Setting limit to a large number. "
+                "Consider setting a limit explicitly."
+            )
             limit = 8000000000  # just a very large number to effect no limit
 
-        get = partial(_get, self._expression.connection,
-                      squeeze=squeeze, download_path=download_path)
+        get = partial(
+            _get,
+            self._expression.connection,
+            squeeze=squeeze,
+            download_path=download_path,
+        )
         if attrs:  # a list of attributes provided
             attributes = [a for a in attrs if not is_key(a)]
             ret = self._expression.proj(*attributes)
             ret = ret.fetch(
-                offset=offset, limit=limit, order_by=order_by,
-                as_dict=False, squeeze=squeeze, download_path=download_path,
-                format='array')
+                offset=offset,
+                limit=limit,
+                order_by=order_by,
+                as_dict=False,
+                squeeze=squeeze,
+                download_path=download_path,
+                format="array",
+            )
             if attrs_as_dict:
-                ret = [{k: v for k, v in zip(ret.dtype.names, x) if k in attrs} for x in ret]
+                ret = [
+                    {k: v for k, v in zip(ret.dtype.names, x) if k in attrs}
+                    for x in ret
+                ]
             else:
-                return_values = [list(
-                    (to_dicts if as_dict else lambda x: x)(ret[self._expression.primary_key]))
-                                 if is_key(attribute) else ret[attribute]
-                                 for attribute in attrs]
+                return_values = [
+                    list(
+                        (to_dicts if as_dict else lambda x: x)(
+                            ret[self._expression.primary_key]
+                        )
+                    )
+                    if is_key(attribute)
+                    else ret[attribute]
+                    for attribute in attrs
+                ]
                 ret = return_values[0] if len(attrs) == 1 else return_values
         else:  # fetch all attributes as a numpy.record_array or pandas.DataFrame
             cur = self._expression.cursor(
-                as_dict=as_dict, limit=limit, offset=offset, order_by=order_by)
+                as_dict=as_dict, limit=limit, offset=offset, order_by=order_by
+            )
             heading = self._expression.heading
             if as_dict:
-                ret = [dict((name, get(heading[name], d[name]))
-                            for name in heading.names) for d in cur]
+                ret = [
+                    dict((name, get(heading[name], d[name])) for name in heading.names)
+                    for d in cur
+                ]
             else:
                 ret = list(cur.fetchall())
-                record_type = (heading.as_dtype if not ret else np.dtype(
-                    [(name, type(value))   # use the first element to determine blob type
-                        if heading[name].is_blob and isinstance(value, numbers.Number)
-                        else (name, heading.as_dtype[name])
-                        for value, name in zip(ret[0], heading.as_dtype.names)]))
+                record_type = (
+                    heading.as_dtype
+                    if not ret
+                    else np.dtype(
+                        [
+                            (
+                                name,
+                                type(value),
+                            )  # use the first element to determine blob type
+                            if heading[name].is_blob
+                            and isinstance(value, numbers.Number)
+                            else (name, heading.as_dtype[name])
+                            for value, name in zip(ret[0], heading.as_dtype.names)
+                        ]
+                    )
+                )
                 try:
                     ret = np.array(ret, dtype=record_type)
                 except Exception as e:
@@ -217,12 +293,14 @@ def __call__(self, *attrs, offset=None, limit=None, order_by=None, format=None,
 class Fetch1:
     """
     Fetch object for fetching the result of a query yielding one row.
+
     :param expression: a query expression to fetch from.
     """
+
     def __init__(self, expression):
         self._expression = expression
 
-    def __call__(self, *attrs, squeeze=False, download_path='.'):
+    def __call__(self, *attrs, squeeze=False, download_path="."):
         """
         Fetches the result of a query expression that yields one entry.
 
@@ -245,20 +323,36 @@ def __call__(self, *attrs, squeeze=False, download_path='.'):
             cur = self._expression.cursor(as_dict=True)
             ret = cur.fetchone()
             if not ret or cur.fetchone():
-                raise DataJointError('fetch1 requires exactly one tuple in the input set.')
-            ret = dict((name, _get(self._expression.connection, heading[name], ret[name],
-                                   squeeze=squeeze, download_path=download_path))
-                       for name in heading.names)
+                raise DataJointError(
+                    "fetch1 requires exactly one tuple in the input set."
+                )
+            ret = dict(
+                (
+                    name,
+                    _get(
+                        self._expression.connection,
+                        heading[name],
+                        ret[name],
+                        squeeze=squeeze,
+                        download_path=download_path,
+                    ),
+                )
+                for name in heading.names
+            )
         else:  # fetch some attributes, return as tuple
             attributes = [a for a in attrs if not is_key(a)]
             result = self._expression.proj(*attributes).fetch(
-                squeeze=squeeze, download_path=download_path, format="array")
+                squeeze=squeeze, download_path=download_path, format="array"
+            )
             if len(result) != 1:
                 raise DataJointError(
-                    'fetch1 should only return one tuple. %d tuples found' % len(result))
+                    "fetch1 should only return one tuple. %d tuples found" % len(result)
+                )
             return_values = tuple(
                 next(to_dicts(result[self._expression.primary_key]))
-                if is_key(attribute) else result[attribute][0]
-                for attribute in attrs)
+                if is_key(attribute)
+                else result[attribute][0]
+                for attribute in attrs
+            )
             ret = return_values[0] if len(attrs) == 1 else return_values
         return ret
diff --git a/datajoint/heading.py b/datajoint/heading.py
index 076a2204e..027ad77f7 100644
--- a/datajoint/heading.py
+++ b/datajoint/heading.py
@@ -4,36 +4,62 @@
 import re
 import logging
 from .errors import DataJointError, _support_filepath_types, FILEPATH_FEATURE_SWITCH
-from .declare import UUID_DATA_TYPE, SPECIAL_TYPES, TYPE_PATTERN, EXTERNAL_TYPES, NATIVE_TYPES
+from .declare import (
+    UUID_DATA_TYPE,
+    SPECIAL_TYPES,
+    TYPE_PATTERN,
+    EXTERNAL_TYPES,
+    NATIVE_TYPES,
+)
 from .attribute_adapter import get_adapter, AttributeAdapter
 
 
 logger = logging.getLogger(__name__)
 
-default_attribute_properties = dict(    # these default values are set in computed attributes
-    name=None, type='expression', in_key=False, nullable=False, default=None, comment='calculated attribute',
-    autoincrement=False, numeric=None, string=None, uuid=False, is_blob=False, is_attachment=False, is_filepath=False,
-    is_external=False, adapter=None,
-    store=None, unsupported=False, attribute_expression=None, database=None, dtype=object)
-
-
-class Attribute(namedtuple('_Attribute', default_attribute_properties)):
+default_attribute_properties = (
+    dict(  # these default values are set in computed attributes
+        name=None,
+        type="expression",
+        in_key=False,
+        nullable=False,
+        default=None,
+        comment="calculated attribute",
+        autoincrement=False,
+        numeric=None,
+        string=None,
+        uuid=False,
+        is_blob=False,
+        is_attachment=False,
+        is_filepath=False,
+        is_external=False,
+        adapter=None,
+        store=None,
+        unsupported=False,
+        attribute_expression=None,
+        database=None,
+        dtype=object,
+    )
+)
+
+
+class Attribute(namedtuple("_Attribute", default_attribute_properties)):
     """
     Properties of a table column (attribute)
     """
+
     def todict(self):
         """Convert namedtuple to dict."""
         return dict((name, self[i]) for i, name in enumerate(self._fields))
 
     @property
     def sql_type(self):
-        """ :return: datatype (as string) in database. In most cases, it is the same as self.type """
+        """:return: datatype (as string) in database. In most cases, it is the same as self.type"""
         return UUID_DATA_TYPE if self.uuid else self.type
 
     @property
     def sql_comment(self):
-        """ :return: full comment for the SQL declaration. Includes custom type specification """
-        return (':uuid:' if self.uuid else '') + self.comment
+        """:return: full comment for the SQL declaration. Includes custom type specification"""
+        return (":uuid:" if self.uuid else "") + self.comment
 
     @property
     def sql(self):
@@ -41,17 +67,19 @@ def sql(self):
         Convert primary key attribute tuple into its SQL CREATE TABLE clause.
         Default values are not reflected.
         This is used for declaring foreign keys in referencing tables
+
         :return: SQL code for attribute declaration
         """
         return '`{name}` {type} NOT NULL COMMENT "{comment}"'.format(
-            name=self.name, type=self.sql_type, comment=self.sql_comment)
+            name=self.name, type=self.sql_type, comment=self.sql_comment
+        )
 
     @property
     def original_name(self):
         if self.attribute_expression is None:
             return self.name
-        assert self.attribute_expression.startswith('`')
-        return self.attribute_expression.strip('`')
+        assert self.attribute_expression.startswith("`")
+        return self.attribute_expression.strip("`")
 
 
 class Heading:
@@ -63,14 +91,18 @@ class Heading:
 
     def __init__(self, attribute_specs=None, table_info=None):
         """
+
         :param attribute_specs: a list of dicts with the same keys as Attribute
         :param table_info: a dict with information to load the heading from the database
         """
         self.indexes = None
         self.table_info = table_info
         self._table_status = None
-        self._attributes = None if attribute_specs is None else dict(
-            (q['name'], Attribute(**q)) for q in attribute_specs)
+        self._attributes = (
+            None
+            if attribute_specs is None
+            else dict((q["name"], Attribute(**q)) for q in attribute_specs)
+        )
 
     def __len__(self):
         return 0 if self.attributes is None else len(self.attributes)
@@ -86,7 +118,7 @@ def table_status(self):
     @property
     def attributes(self):
         if self._attributes is None:
-            self._init_from_database()   # lazy loading from database
+            self._init_from_database()  # lazy loading from database
         return self._attributes
 
     @property
@@ -107,11 +139,17 @@ def blobs(self):
 
     @property
     def non_blobs(self):
-        return [k for k, v in self.attributes.items() if not v.is_blob and not v.is_attachment and not v.is_filepath]
+        return [
+            k
+            for k, v in self.attributes.items()
+            if not v.is_blob and not v.is_attachment and not v.is_filepath
+        ]
 
     @property
     def new_attributes(self):
-        return [k for k, v in self.attributes.items() if v.attribute_expression is not None]
+        return [
+            k for k, v in self.attributes.items() if v.attribute_expression is not None
+        ]
 
     def __getitem__(self, name):
         """shortcut to the attribute"""
@@ -122,16 +160,18 @@ def __repr__(self):
         :return:  heading representation in DataJoint declaration format but without foreign key expansion
         """
         in_key = True
-        ret = ''
+        ret = ""
         if self._table_status is not None:
-            ret += '# ' + self.table_status['comment'] + '\n'
+            ret += "# " + self.table_status["comment"] + "\n"
         for v in self.attributes.values():
             if in_key and not v.in_key:
-                ret += '---\n'
+                ret += "---\n"
                 in_key = False
-            ret += '%-20s : %-28s # %s\n' % (
-                v.name if v.default is None else '%s=%s' % (v.name, v.default),
-                '%s%s' % (v.type, 'auto_increment' if v.autoincrement else ''), v.comment)
+            ret += "%-20s : %-28s # %s\n" % (
+                v.name if v.default is None else "%s=%s" % (v.name, v.default),
+                "%s%s" % (v.type, "auto_increment" if v.autoincrement else ""),
+                v.comment,
+            )
         return ret
 
     @property
@@ -143,185 +183,260 @@ def as_dtype(self):
         """
         represent the heading as a numpy dtype
         """
-        return np.dtype(dict(
-            names=self.names,
-            formats=[v.dtype for v in self.attributes.values()]))
+        return np.dtype(
+            dict(names=self.names, formats=[v.dtype for v in self.attributes.values()])
+        )
 
     def as_sql(self, fields, include_aliases=True):
         """
         represent heading as the SQL SELECT clause.
         """
-        return ','.join(
-            '`%s`' % name if self.attributes[name].attribute_expression is None
-            else self.attributes[name].attribute_expression + (' as `%s`' % name if include_aliases else '')
-            for name in fields)
+        return ",".join(
+            "`%s`" % name
+            if self.attributes[name].attribute_expression is None
+            else self.attributes[name].attribute_expression
+            + (" as `%s`" % name if include_aliases else "")
+            for name in fields
+        )
 
     def __iter__(self):
         return iter(self.attributes)
 
     def _init_from_database(self):
-        """ initialize heading from an existing database table. """
+        """initialize heading from an existing database table."""
         conn, database, table_name, context = (
-            self.table_info[k] for k in ('conn', 'database', 'table_name', 'context'))
-        info = conn.query('SHOW TABLE STATUS FROM `{database}` WHERE name="{table_name}"'.format(
-            table_name=table_name, database=database), as_dict=True).fetchone()
+            self.table_info[k] for k in ("conn", "database", "table_name", "context")
+        )
+        info = conn.query(
+            'SHOW TABLE STATUS FROM `{database}` WHERE name="{table_name}"'.format(
+                table_name=table_name, database=database
+            ),
+            as_dict=True,
+        ).fetchone()
         if info is None:
-            if table_name == '~log':
-                logger.warning('Could not create the ~log table')
+            if table_name == "~log":
+                logger.warning("Could not create the ~log table")
                 return
-            raise DataJointError('The table `{database}`.`{table_name}` is not defined.'.format(
-                table_name=table_name, database=database))
+            raise DataJointError(
+                "The table `{database}`.`{table_name}` is not defined.".format(
+                    table_name=table_name, database=database
+                )
+            )
         self._table_status = {k.lower(): v for k, v in info.items()}
         cur = conn.query(
-            'SHOW FULL COLUMNS FROM `{table_name}` IN `{database}`'.format(
-                table_name=table_name, database=database), as_dict=True)
+            "SHOW FULL COLUMNS FROM `{table_name}` IN `{database}`".format(
+                table_name=table_name, database=database
+            ),
+            as_dict=True,
+        )
 
         attributes = cur.fetchall()
 
         rename_map = {
-            'Field': 'name',
-            'Type': 'type',
-            'Null': 'nullable',
-            'Default': 'default',
-            'Key': 'in_key',
-            'Comment': 'comment'}
+            "Field": "name",
+            "Type": "type",
+            "Null": "nullable",
+            "Default": "default",
+            "Key": "in_key",
+            "Comment": "comment",
+        }
 
-        fields_to_drop = ('Privileges', 'Collation')
+        fields_to_drop = ("Privileges", "Collation")
 
         # rename and drop attributes
-        attributes = [{rename_map[k] if k in rename_map else k: v
-                       for k, v in x.items() if k not in fields_to_drop}
-                      for x in attributes]
+        attributes = [
+            {
+                rename_map[k] if k in rename_map else k: v
+                for k, v in x.items()
+                if k not in fields_to_drop
+            }
+            for x in attributes
+        ]
         numeric_types = {
-            ('float', False): np.float64,
-            ('float', True): np.float64,
-            ('double', False): np.float64,
-            ('double', True): np.float64,
-            ('tinyint', False): np.int64,
-            ('tinyint', True): np.int64,
-            ('smallint', False): np.int64,
-            ('smallint', True): np.int64,
-            ('mediumint', False): np.int64,
-            ('mediumint', True): np.int64,
-            ('int', False): np.int64,
-            ('int', True): np.int64,
-            ('bigint', False): np.int64,
-            ('bigint', True): np.uint64}
-
-        sql_literals = ['CURRENT_TIMESTAMP']
+            ("float", False): np.float64,
+            ("float", True): np.float64,
+            ("double", False): np.float64,
+            ("double", True): np.float64,
+            ("tinyint", False): np.int64,
+            ("tinyint", True): np.int64,
+            ("smallint", False): np.int64,
+            ("smallint", True): np.int64,
+            ("mediumint", False): np.int64,
+            ("mediumint", True): np.int64,
+            ("int", False): np.int64,
+            ("int", True): np.int64,
+            ("bigint", False): np.int64,
+            ("bigint", True): np.uint64,
+        }
+
+        sql_literals = ["CURRENT_TIMESTAMP"]
 
         # additional attribute properties
         for attr in attributes:
 
             attr.update(
-                in_key=(attr['in_key'] == 'PRI'),
+                in_key=(attr["in_key"] == "PRI"),
                 database=database,
-                nullable=attr['nullable'] == 'YES',
-                autoincrement=bool(re.search(r'auto_increment', attr['Extra'], flags=re.I)),
-                numeric=any(TYPE_PATTERN[t].match(attr['type']) for t in ('DECIMAL', 'INTEGER', 'FLOAT')),
-                string=any(TYPE_PATTERN[t].match(attr['type']) for t in ('ENUM', 'TEMPORAL', 'STRING')),
-                is_blob=bool(TYPE_PATTERN['INTERNAL_BLOB'].match(attr['type'])),
-                uuid=False, is_attachment=False, is_filepath=False, adapter=None,
-                store=None, is_external=False, attribute_expression=None)
-
-            if any(TYPE_PATTERN[t].match(attr['type']) for t in ('INTEGER', 'FLOAT')):
-                attr['type'] = re.sub(r'\(\d+\)', '', attr['type'], count=1)  # strip size off integers and floats
-            attr['unsupported'] = not any((attr['is_blob'], attr['numeric'], attr['numeric']))
-            attr.pop('Extra')
+                nullable=attr["nullable"] == "YES",
+                autoincrement=bool(
+                    re.search(r"auto_increment", attr["Extra"], flags=re.I)
+                ),
+                numeric=any(
+                    TYPE_PATTERN[t].match(attr["type"])
+                    for t in ("DECIMAL", "INTEGER", "FLOAT")
+                ),
+                string=any(
+                    TYPE_PATTERN[t].match(attr["type"])
+                    for t in ("ENUM", "TEMPORAL", "STRING")
+                ),
+                is_blob=bool(TYPE_PATTERN["INTERNAL_BLOB"].match(attr["type"])),
+                uuid=False,
+                is_attachment=False,
+                is_filepath=False,
+                adapter=None,
+                store=None,
+                is_external=False,
+                attribute_expression=None,
+            )
+
+            if any(TYPE_PATTERN[t].match(attr["type"]) for t in ("INTEGER", "FLOAT")):
+                attr["type"] = re.sub(
+                    r"\(\d+\)", "", attr["type"], count=1
+                )  # strip size off integers and floats
+            attr["unsupported"] = not any(
+                (attr["is_blob"], attr["numeric"], attr["numeric"])
+            )
+            attr.pop("Extra")
 
             # process custom DataJoint types
-            special = re.match(r':(?P<type>[^:]+):(?P<comment>.*)', attr['comment'])
+            special = re.match(r":(?P<type>[^:]+):(?P<comment>.*)", attr["comment"])
             if special:
                 special = special.groupdict()
                 attr.update(special)
             # process adapted attribute types
-            if special and TYPE_PATTERN['ADAPTED'].match(attr['type']):
-                assert context is not None, 'Declaration context is not set'
-                adapter_name = special['type']
+            if special and TYPE_PATTERN["ADAPTED"].match(attr["type"]):
+                assert context is not None, "Declaration context is not set"
+                adapter_name = special["type"]
                 try:
                     attr.update(adapter=get_adapter(context, adapter_name))
                 except DataJointError:
                     # if no adapter, then delay the error until the first invocation
                     attr.update(adapter=AttributeAdapter())
                 else:
-                    attr.update(type=attr['adapter'].attribute_type)
-                    if not any(r.match(attr['type']) for r in TYPE_PATTERN.values()):
+                    attr.update(type=attr["adapter"].attribute_type)
+                    if not any(r.match(attr["type"]) for r in TYPE_PATTERN.values()):
                         raise DataJointError(
                             "Invalid attribute type '{type}' in adapter object <{adapter_name}>.".format(
-                                adapter_name=adapter_name, **attr))
-                    special = not any(TYPE_PATTERN[c].match(attr['type']) for c in NATIVE_TYPES)
+                                adapter_name=adapter_name, **attr
+                            )
+                        )
+                    special = not any(
+                        TYPE_PATTERN[c].match(attr["type"]) for c in NATIVE_TYPES
+                    )
 
             if special:
                 try:
-                    category = next(c for c in SPECIAL_TYPES if TYPE_PATTERN[c].match(attr['type']))
+                    category = next(
+                        c for c in SPECIAL_TYPES if TYPE_PATTERN[c].match(attr["type"])
+                    )
                 except StopIteration:
-                    if attr['type'].startswith('external'):
-                        url = "https://docs.datajoint.io/python/admin/5-blob-config.html" \
-                              "#migration-between-datajoint-v0-11-and-v0-12"
-                        raise DataJointError('Legacy datatype `{type}`. Migrate your external stores to '
-                                             'datajoint 0.12: {url}'.format(url=url, **attr))
-                    raise DataJointError('Unknown attribute type `{type}`'.format(**attr))
-                if category == 'FILEPATH' and not _support_filepath_types():
-                    raise DataJointError("""
+                    if attr["type"].startswith("external"):
+                        url = (
+                            "https://docs.datajoint.io/python/admin/5-blob-config.html"
+                            "#migration-between-datajoint-v0-11-and-v0-12"
+                        )
+                        raise DataJointError(
+                            "Legacy datatype `{type}`. Migrate your external stores to "
+                            "datajoint 0.12: {url}".format(url=url, **attr)
+                        )
+                    raise DataJointError(
+                        "Unknown attribute type `{type}`".format(**attr)
+                    )
+                if category == "FILEPATH" and not _support_filepath_types():
+                    raise DataJointError(
+                        """
                         The filepath data type is disabled until complete validation.
                         To turn it on as experimental feature, set the environment variable
                         {env} = TRUE or upgrade datajoint.
-                        """.format(env=FILEPATH_FEATURE_SWITCH))
+                        """.format(
+                            env=FILEPATH_FEATURE_SWITCH
+                        )
+                    )
                 attr.update(
                     unsupported=False,
-                    is_attachment=category in ('INTERNAL_ATTACH', 'EXTERNAL_ATTACH'),
-                    is_filepath=category == 'FILEPATH',
+                    is_attachment=category in ("INTERNAL_ATTACH", "EXTERNAL_ATTACH"),
+                    is_filepath=category == "FILEPATH",
                     # INTERNAL_BLOB is not a custom type but is included for completeness
-                    is_blob=category in ('INTERNAL_BLOB', 'EXTERNAL_BLOB'),
-                    uuid=category == 'UUID',
+                    is_blob=category in ("INTERNAL_BLOB", "EXTERNAL_BLOB"),
+                    uuid=category == "UUID",
                     is_external=category in EXTERNAL_TYPES,
-                    store=attr['type'].split('@')[1] if category in EXTERNAL_TYPES else None)
-
-            if attr['in_key'] and any((attr['is_blob'], attr['is_attachment'], attr['is_filepath'])):
-                raise DataJointError('Blob, attachment, or filepath attributes are not allowed in the primary key')
-
-            if attr['string'] and attr['default'] is not None and attr['default'] not in sql_literals:
-                attr['default'] = '"%s"' % attr['default']
-
-            if attr['nullable']:   # nullable fields always default to null
-                attr['default'] = 'null'
+                    store=attr["type"].split("@")[1]
+                    if category in EXTERNAL_TYPES
+                    else None,
+                )
+
+            if attr["in_key"] and any(
+                (attr["is_blob"], attr["is_attachment"], attr["is_filepath"])
+            ):
+                raise DataJointError(
+                    "Blob, attachment, or filepath attributes are not allowed in the primary key"
+                )
+
+            if (
+                attr["string"]
+                and attr["default"] is not None
+                and attr["default"] not in sql_literals
+            ):
+                attr["default"] = '"%s"' % attr["default"]
+
+            if attr["nullable"]:  # nullable fields always default to null
+                attr["default"] = "null"
 
             # fill out dtype. All floats and non-nullable integers are turned into specific dtypes
-            attr['dtype'] = object
-            if attr['numeric'] and not attr['adapter']:
-                is_integer = TYPE_PATTERN['INTEGER'].match(attr['type'])
-                is_float = TYPE_PATTERN['FLOAT'].match(attr['type'])
-                if is_integer and not attr['nullable'] or is_float:
-                    is_unsigned = bool(re.match('sunsigned', attr['type'], flags=re.I))
-                    t = re.sub(r'\(.*\)', '', attr['type'])    # remove parentheses
-                    t = re.sub(r' unsigned$', '', t)   # remove unsigned
-                    assert (t, is_unsigned) in numeric_types, 'dtype not found for type %s' % t
-                    attr['dtype'] = numeric_types[(t, is_unsigned)]
-
-            if attr['adapter']:
+            attr["dtype"] = object
+            if attr["numeric"] and not attr["adapter"]:
+                is_integer = TYPE_PATTERN["INTEGER"].match(attr["type"])
+                is_float = TYPE_PATTERN["FLOAT"].match(attr["type"])
+                if is_integer and not attr["nullable"] or is_float:
+                    is_unsigned = bool(re.match("sunsigned", attr["type"], flags=re.I))
+                    t = re.sub(r"\(.*\)", "", attr["type"])  # remove parentheses
+                    t = re.sub(r" unsigned$", "", t)  # remove unsigned
+                    assert (t, is_unsigned) in numeric_types, (
+                        "dtype not found for type %s" % t
+                    )
+                    attr["dtype"] = numeric_types[(t, is_unsigned)]
+
+            if attr["adapter"]:
                 # restore adapted type name
-                attr['type'] = adapter_name
+                attr["type"] = adapter_name
 
-        self._attributes = dict(((q['name'], Attribute(**q)) for q in attributes))
+        self._attributes = dict(((q["name"], Attribute(**q)) for q in attributes))
 
         # Read and tabulate secondary indexes
         keys = defaultdict(dict)
-        for item in conn.query('SHOW KEYS FROM `{db}`.`{tab}`'.format(db=database, tab=table_name), as_dict=True):
-            if item['Key_name'] != 'PRIMARY':
-                keys[item['Key_name']][item['Seq_in_index']] = dict(
-                    column=item['Column_name'],
-                    unique=(item['Non_unique'] == 0),
-                    nullable=item['Null'].lower() == 'yes')
+        for item in conn.query(
+            "SHOW KEYS FROM `{db}`.`{tab}`".format(db=database, tab=table_name),
+            as_dict=True,
+        ):
+            if item["Key_name"] != "PRIMARY":
+                keys[item["Key_name"]][item["Seq_in_index"]] = dict(
+                    column=item["Column_name"],
+                    unique=(item["Non_unique"] == 0),
+                    nullable=item["Null"].lower() == "yes",
+                )
         self.indexes = {
-            tuple(item[k]['column'] for k in sorted(item.keys())):
-                dict(unique=item[1]['unique'],
-                     nullable=any(v['nullable'] for v in item.values()))
-            for item in keys.values()}
+            tuple(item[k]["column"] for k in sorted(item.keys())): dict(
+                unique=item[1]["unique"],
+                nullable=any(v["nullable"] for v in item.values()),
+            )
+            for item in keys.values()
+        }
 
     def select(self, select_list, rename_map=None, compute_map=None):
         """
         derive a new heading by selecting, renaming, or computing attributes.
         In relational algebra these operators are known as project, rename, and extend.
+
         :param select_list:  the full list of existing attributes to include
         :param rename_map:  dictionary of renamed attributes: keys=new names, values=old names
         :param compute_map: a direction of computed attributes
@@ -333,11 +448,21 @@ def select(self, select_list, rename_map=None, compute_map=None):
         for name in self.attributes:
             if name in select_list:
                 copy_attrs.append(self.attributes[name].todict())
-            copy_attrs.extend((
-                dict(self.attributes[old_name].todict(), name=new_name, attribute_expression='`%s`' % old_name)
-                for new_name, old_name in rename_map.items() if old_name == name))
-        compute_attrs = (dict(default_attribute_properties, name=new_name, attribute_expression=expr)
-                         for new_name, expr in compute_map.items())
+            copy_attrs.extend(
+                (
+                    dict(
+                        self.attributes[old_name].todict(),
+                        name=new_name,
+                        attribute_expression="`%s`" % old_name,
+                    )
+                    for new_name, old_name in rename_map.items()
+                    if old_name == name
+                )
+            )
+        compute_attrs = (
+            dict(default_attribute_properties, name=new_name, attribute_expression=expr)
+            for new_name, expr in compute_map.items()
+        )
         return Heading(chain(copy_attrs, compute_attrs))
 
     def join(self, other):
@@ -346,23 +471,49 @@ def join(self, other):
         It assumes that self and other are headings that share no common dependent attributes.
         """
         return Heading(
-            [self.attributes[name].todict() for name in self.primary_key] +
-            [other.attributes[name].todict() for name in other.primary_key if name not in self.primary_key] +
-            [self.attributes[name].todict() for name in self.secondary_attributes if name not in other.primary_key] +
-            [other.attributes[name].todict() for name in other.secondary_attributes if name not in self.primary_key])
+            [self.attributes[name].todict() for name in self.primary_key]
+            + [
+                other.attributes[name].todict()
+                for name in other.primary_key
+                if name not in self.primary_key
+            ]
+            + [
+                self.attributes[name].todict()
+                for name in self.secondary_attributes
+                if name not in other.primary_key
+            ]
+            + [
+                other.attributes[name].todict()
+                for name in other.secondary_attributes
+                if name not in self.primary_key
+            ]
+        )
 
     def set_primary_key(self, primary_key):
         """
         Create a new heading with the specified primary key.
         This low-level method performs no error checking.
         """
-        return Heading(chain(
-            (dict(self.attributes[name].todict(), in_key=True) for name in primary_key),
-            (dict(self.attributes[name].todict(), in_key=False) for name in self.names if name not in primary_key)))
+        return Heading(
+            chain(
+                (
+                    dict(self.attributes[name].todict(), in_key=True)
+                    for name in primary_key
+                ),
+                (
+                    dict(self.attributes[name].todict(), in_key=False)
+                    for name in self.names
+                    if name not in primary_key
+                ),
+            )
+        )
 
     def make_subquery_heading(self):
         """
         Create a new heading with removed attribute sql_expressions.
         Used by subqueries, which resolve the sql_expressions.
         """
-        return Heading(dict(v.todict(), attribute_expression=None) for v in self.attributes.values())
+        return Heading(
+            dict(v.todict(), attribute_expression=None)
+            for v in self.attributes.values()
+        )
diff --git a/datajoint/jobs.py b/datajoint/jobs.py
index 571270931..8aedded42 100644
--- a/datajoint/jobs.py
+++ b/datajoint/jobs.py
@@ -7,22 +7,22 @@
 from .heading import Heading
 
 ERROR_MESSAGE_LENGTH = 2047
-TRUNCATION_APPENDIX = '...truncated'
+TRUNCATION_APPENDIX = "...truncated"
 
 
 class JobTable(Table):
     """
     A base relation with no definition. Allows reserving jobs
     """
+
     def __init__(self, conn, database):
         self.database = database
         self._connection = conn
-        self._heading = Heading(table_info=dict(
-            conn=conn,
-            database=database,
-            table_name=self.table_name,
-            context=None
-        ))
+        self._heading = Heading(
+            table_info=dict(
+                conn=conn, database=database, table_name=self.table_name, context=None
+            )
+        )
         self._support = [self.full_table_name]
 
         self._definition = """    # job reservation table for `{database}`
@@ -38,7 +38,9 @@ def __init__(self, conn, database):
         pid=0  :int unsigned  # system process id
         connection_id = 0  : bigint unsigned          # connection_id()
         timestamp=CURRENT_TIMESTAMP  :timestamp   # automatic timestamp
-        """.format(database=database, error_message_length=ERROR_MESSAGE_LENGTH)
+        """.format(
+            database=database, error_message_length=ERROR_MESSAGE_LENGTH
+        )
         if not self.is_declared:
             self.declare()
         self._user = self.connection.get_user()
@@ -49,7 +51,7 @@ def definition(self):
 
     @property
     def table_name(self):
-        return '~jobs'
+        return "~jobs"
 
     def delete(self):
         """bypass interactive prompts and dependencies"""
@@ -63,6 +65,7 @@ def reserve(self, table_name, key):
         """
         Reserve a job for computation.  When a job is reserved, the job table contains an entry for the
         job key, identified by its hash. When jobs are completed, the entry is removed.
+
         :param table_name: `database`.`table_name`
         :param key: the dict of the job's primary key
         :return: True if reserved job successfully. False = the jobs is already taken
@@ -70,12 +73,13 @@ def reserve(self, table_name, key):
         job = dict(
             table_name=table_name,
             key_hash=key_hash(key),
-            status='reserved',
+            status="reserved",
             host=platform.node(),
             pid=os.getpid(),
             connection_id=self.connection.connection_id,
             key=key,
-            user=self._user)
+            user=self._user,
+        )
         try:
             with config(enable_python_native_blobs=True):
                 self.insert1(job, ignore_extra_fields=True)
@@ -86,6 +90,7 @@ def reserve(self, table_name, key):
     def complete(self, table_name, key):
         """
         Log a completed job.  When a job is completed, its reservation entry is deleted.
+
         :param table_name: `database`.`table_name`
         :param key: the dict of the job's primary key
         """
@@ -96,13 +101,17 @@ def error(self, table_name, key, error_message, error_stack=None):
         """
         Log an error message.  The job reservation is replaced with an error entry.
         if an error occurs, leave an entry describing the problem
+
         :param table_name: `database`.`table_name`
         :param key: the dict of the job's primary key
         :param error_message: string error message
         :param error_stack: stack trace
         """
         if len(error_message) > ERROR_MESSAGE_LENGTH:
-            error_message = error_message[:ERROR_MESSAGE_LENGTH-len(TRUNCATION_APPENDIX)] + TRUNCATION_APPENDIX
+            error_message = (
+                error_message[: ERROR_MESSAGE_LENGTH - len(TRUNCATION_APPENDIX)]
+                + TRUNCATION_APPENDIX
+            )
         with config(enable_python_native_blobs=True):
             self.insert1(
                 dict(
@@ -115,5 +124,8 @@ def error(self, table_name, key, error_message, error_stack=None):
                     user=self._user,
                     key=key,
                     error_message=error_message,
-                    error_stack=error_stack),
-                replace=True, ignore_extra_fields=True)
+                    error_stack=error_stack,
+                ),
+                replace=True,
+                ignore_extra_fields=True,
+            )
diff --git a/datajoint/migrate.py b/datajoint/migrate.py
index 445bc317c..41654b68e 100644
--- a/datajoint/migrate.py
+++ b/datajoint/migrate.py
@@ -7,29 +7,40 @@
 def migrate_dj011_external_blob_storage_to_dj012(migration_schema, store):
     """
     Utility function to migrate external blob data from 0.11 to 0.12.
+
     :param migration_schema: string of target schema to be migrated
     :param store: string of target dj.config['store'] to be migrated
     """
     if not isinstance(migration_schema, str):
         raise ValueError(
-            'Expected type {} for migration_schema, not {}.'.format(
-                str, type(migration_schema)))
+            "Expected type {} for migration_schema, not {}.".format(
+                str, type(migration_schema)
+            )
+        )
 
     do_migration = False
-    do_migration = user_choice(
+    do_migration = (
+        user_choice(
             """
 Warning: Ensure the following are completed before proceeding.
 - Appropriate backups have been taken,
 - Any existing DJ 0.11.X connections are suspended, and
 - External config has been updated to new dj.config['stores'] structure.
 Proceed?
-            """, default='no') == 'yes'
+            """,
+            default="no",
+        )
+        == "yes"
+    )
     if do_migration:
         _migrate_dj011_blob(dj.Schema(migration_schema), store)
-        print('Migration completed for schema: {}, store: {}.'.format(
-                migration_schema, store))
+        print(
+            "Migration completed for schema: {}, store: {}.".format(
+                migration_schema, store
+            )
+        )
         return
-    print('No migration performed.')
+    print("No migration performed.")
 
 
 def _migrate_dj011_blob(schema, default_store):
@@ -38,34 +49,44 @@ def _migrate_dj011_blob(schema, default_store):
     LEGACY_HASH_SIZE = 43
 
     legacy_external = dj.FreeTable(
-        schema.connection,
-        '`{db}`.`~external`'.format(db=schema.database))
+        schema.connection, "`{db}`.`~external`".format(db=schema.database)
+    )
 
     # get referencing tables
-    refs = [{k.lower(): v for k, v in elem.items()} for elem in query("""
+    refs = [
+        {k.lower(): v for k, v in elem.items()}
+        for elem in query(
+            """
     SELECT concat('`', table_schema, '`.`', table_name, '`')
             as referencing_table, column_name, constraint_name
     FROM information_schema.key_column_usage
     WHERE referenced_table_name="{tab}" and referenced_table_schema="{db}"
     """.format(
-        tab=legacy_external.table_name,
-        db=legacy_external.database), as_dict=True).fetchall()]
+                tab=legacy_external.table_name, db=legacy_external.database
+            ),
+            as_dict=True,
+        ).fetchall()
+    ]
 
     for ref in refs:
         # get comment
         column = query(
-            'SHOW FULL COLUMNS FROM {referencing_table}'
-            'WHERE Field="{column_name}"'.format(
-                **ref), as_dict=True).fetchone()
+            "SHOW FULL COLUMNS FROM {referencing_table}"
+            'WHERE Field="{column_name}"'.format(**ref),
+            as_dict=True,
+        ).fetchone()
 
         store, comment = re.match(
-            r':external(-(?P<store>.+))?:(?P<comment>.*)',
-            column['Comment']).group('store', 'comment')
+            r":external(-(?P<store>.+))?:(?P<comment>.*)", column["Comment"]
+        ).group("store", "comment")
 
         # get all the hashes from the reference
-        hashes = {x[0] for x in query(
-            'SELECT `{column_name}` FROM {referencing_table}'.format(
-                **ref))}
+        hashes = {
+            x[0]
+            for x in query(
+                "SELECT `{column_name}` FROM {referencing_table}".format(**ref)
+            )
+        }
 
         # sanity check make sure that store suffixes match
         if store is None:
@@ -77,55 +98,69 @@ def _migrate_dj011_blob(schema, default_store):
         ext = schema.external[store or default_store]
 
         # add the new-style reference field
-        temp_suffix = 'tempsub'
+        temp_suffix = "tempsub"
 
         try:
-            query("""ALTER TABLE {referencing_table}
+            query(
+                """ALTER TABLE {referencing_table}
                 ADD COLUMN `{column_name}_{temp_suffix}` {type} DEFAULT NULL
             COMMENT ":blob@{store}:{comment}"
             """.format(
-                type=dj.declare.UUID_DATA_TYPE,
-                temp_suffix=temp_suffix,
-                store=(store or default_store), comment=comment, **ref))
+                    type=dj.declare.UUID_DATA_TYPE,
+                    temp_suffix=temp_suffix,
+                    store=(store or default_store),
+                    comment=comment,
+                    **ref
+                )
+            )
         except:
-            print('Column already added')
+            print("Column already added")
             pass
 
-        for _hash, size in zip(*legacy_external.fetch('hash', 'size')):
+        for _hash, size in zip(*legacy_external.fetch("hash", "size")):
             if _hash in hashes:
                 relative_path = str(Path(schema.database, _hash).as_posix())
                 uuid = dj.hash.uuid_from_buffer(init_string=relative_path)
                 external_path = ext._make_external_filepath(relative_path)
-                if ext.spec['protocol'] == 's3':
-                    contents_hash = dj.hash.uuid_from_buffer(ext._download_buffer(external_path))
+                if ext.spec["protocol"] == "s3":
+                    contents_hash = dj.hash.uuid_from_buffer(
+                        ext._download_buffer(external_path)
+                    )
                 else:
                     contents_hash = dj.hash.uuid_from_file(external_path)
-                ext.insert1(dict(
-                    filepath=relative_path,
-                    size=size,
-                    contents_hash=contents_hash,
-                    hash=uuid
-                ), skip_duplicates=True)
+                ext.insert1(
+                    dict(
+                        filepath=relative_path,
+                        size=size,
+                        contents_hash=contents_hash,
+                        hash=uuid,
+                    ),
+                    skip_duplicates=True,
+                )
 
                 query(
-                    'UPDATE {referencing_table} '
-                    'SET `{column_name}_{temp_suffix}`=%s '
-                    'WHERE `{column_name}` = "{_hash}"'
-                    .format(
-                        _hash=_hash,
-                        temp_suffix=temp_suffix, **ref), uuid.bytes)
+                    "UPDATE {referencing_table} "
+                    "SET `{column_name}_{temp_suffix}`=%s "
+                    'WHERE `{column_name}` = "{_hash}"'.format(
+                        _hash=_hash, temp_suffix=temp_suffix, **ref
+                    ),
+                    uuid.bytes,
+                )
 
         # check that all have been copied
         check = query(
-            'SELECT * FROM {referencing_table} '
-            'WHERE `{column_name}` IS NOT NULL'
-            '  AND `{column_name}_{temp_suffix}` IS NULL'
-            .format(temp_suffix=temp_suffix, **ref)).fetchall()
+            "SELECT * FROM {referencing_table} "
+            "WHERE `{column_name}` IS NOT NULL"
+            "  AND `{column_name}_{temp_suffix}` IS NULL".format(
+                temp_suffix=temp_suffix, **ref
+            )
+        ).fetchall()
 
-        assert len(check) == 0, 'Some hashes havent been migrated'
+        assert len(check) == 0, "Some hashes havent been migrated"
 
         # drop old foreign key, rename, and create new foreign key
-        query("""
+        query(
+            """
             ALTER TABLE {referencing_table}
             DROP FOREIGN KEY `{constraint_name}`,
             DROP COLUMN `{column_name}`,
@@ -138,20 +173,30 @@ def _migrate_dj011_blob(schema, default_store):
                 temp_suffix=temp_suffix,
                 ext_table_name=ext.full_table_name,
                 type=dj.declare.UUID_DATA_TYPE,
-                store=(store or default_store), comment=comment, **ref))
+                store=(store or default_store),
+                comment=comment,
+                **ref
+            )
+        )
 
     # Drop the old external table but make sure it's no longer referenced
     # get referencing tables
-    refs = [{k.lower(): v for k, v in elem.items()} for elem in query("""
+    refs = [
+        {k.lower(): v for k, v in elem.items()}
+        for elem in query(
+            """
     SELECT concat('`', table_schema, '`.`', table_name, '`') as
         referencing_table, column_name, constraint_name
     FROM information_schema.key_column_usage
     WHERE referenced_table_name="{tab}" and referenced_table_schema="{db}"
     """.format(
-        tab=legacy_external.table_name,
-        db=legacy_external.database), as_dict=True).fetchall()]
+                tab=legacy_external.table_name, db=legacy_external.database
+            ),
+            as_dict=True,
+        ).fetchall()
+    ]
 
-    assert not refs, 'Some references still exist'
+    assert not refs, "Some references still exist"
 
     # drop old external table
     legacy_external.drop_quick()
diff --git a/datajoint/plugin.py b/datajoint/plugin.py
index d82e457d1..96f388089 100644
--- a/datajoint/plugin.py
+++ b/datajoint/plugin.py
@@ -7,32 +7,35 @@
 
 def _update_error_stack(plugin_name):
     try:
-        base_name = 'datajoint'
+        base_name = "datajoint"
         base_meta = pkg_resources.get_distribution(base_name)
         plugin_meta = pkg_resources.get_distribution(plugin_name)
 
         data = hash_pkg(pkgpath=str(Path(plugin_meta.module_path, plugin_name)))
-        signature = plugin_meta.get_metadata('{}.sig'.format(plugin_name))
-        pubkey_path = str(Path(base_meta.egg_info, '{}.pub'.format(base_name)))
+        signature = plugin_meta.get_metadata("{}.sig".format(plugin_name))
+        pubkey_path = str(Path(base_meta.egg_info, "{}.pub".format(base_name)))
         verify(pubkey_path=pubkey_path, data=data, signature=signature)
-        print('DataJoint verified plugin `{}` detected.'.format(plugin_name))
+        print("DataJoint verified plugin `{}` detected.".format(plugin_name))
         return True
     except (FileNotFoundError, InvalidSignature):
-        print('Unverified plugin `{}` detected.'.format(plugin_name))
+        print("Unverified plugin `{}` detected.".format(plugin_name))
         return False
 
 
 def _import_plugins(category):
     return {
-            entry_point.name: dict(object=entry_point,
-                                   verified=_update_error_stack(
-                                        entry_point.module_name.split('.')[0]))
-            for entry_point
-            in pkg_resources.iter_entry_points('datajoint_plugins.{}'.format(category))
-            if 'plugin' not in config or category not in config['plugin'] or
-            entry_point.module_name.split('.')[0] in config['plugin'][category]
-        }
+        entry_point.name: dict(
+            object=entry_point,
+            verified=_update_error_stack(entry_point.module_name.split(".")[0]),
+        )
+        for entry_point in pkg_resources.iter_entry_points(
+            "datajoint_plugins.{}".format(category)
+        )
+        if "plugin" not in config
+        or category not in config["plugin"]
+        or entry_point.module_name.split(".")[0] in config["plugin"][category]
+    }
 
 
-connection_plugins = _import_plugins('connection')
-type_plugins = _import_plugins('datatype')
+connection_plugins = _import_plugins("connection")
+type_plugins = _import_plugins("datatype")
diff --git a/datajoint/preview.py b/datajoint/preview.py
index f3daeebf5..f761cf533 100644
--- a/datajoint/preview.py
+++ b/datajoint/preview.py
@@ -7,33 +7,52 @@ def preview(query_expression, limit, width):
     heading = query_expression.heading
     rel = query_expression.proj(*heading.non_blobs)
     if limit is None:
-        limit = config['display.limit']
+        limit = config["display.limit"]
     if width is None:
-        width = config['display.width']
+        width = config["display.width"]
     tuples = rel.fetch(limit=limit + 1, format="array")
     has_more = len(tuples) > limit
     tuples = tuples[:limit]
     columns = heading.names
-    widths = {f: min(max([len(f)] +
-                         [len(str(e)) for e in tuples[f]] if f in tuples.dtype.names else [len('=BLOB=')]) + 4, width) for f
-              in columns}
-    templates = {f: '%%-%d.%ds' % (widths[f], widths[f]) for f in columns}
+    widths = {
+        f: min(
+            max(
+                [len(f)] + [len(str(e)) for e in tuples[f]]
+                if f in tuples.dtype.names
+                else [len("=BLOB=")]
+            )
+            + 4,
+            width,
+        )
+        for f in columns
+    }
+    templates = {f: "%%-%d.%ds" % (widths[f], widths[f]) for f in columns}
     return (
-            ' '.join([templates[f] % ('*' + f if f in rel.primary_key else f) for f in columns]) + '\n' +
-            ' '.join(['+' + '-' * (widths[column] - 2) + '+' for column in columns]) + '\n' +
-            '\n'.join(' '.join(templates[f] % (tup[f] if f in tup.dtype.names else '=BLOB=')
-                               for f in columns) for tup in tuples) +
-            ('\n   ...\n' if has_more else '\n') +
-            (' (Total: %d)\n' % len(rel) if config['display.show_tuple_count'] else ''))
+        " ".join(
+            [templates[f] % ("*" + f if f in rel.primary_key else f) for f in columns]
+        )
+        + "\n"
+        + " ".join(["+" + "-" * (widths[column] - 2) + "+" for column in columns])
+        + "\n"
+        + "\n".join(
+            " ".join(
+                templates[f] % (tup[f] if f in tup.dtype.names else "=BLOB=")
+                for f in columns
+            )
+            for tup in tuples
+        )
+        + ("\n   ...\n" if has_more else "\n")
+        + (" (Total: %d)\n" % len(rel) if config["display.show_tuple_count"] else "")
+    )
 
 
 def repr_html(query_expression):
     heading = query_expression.heading
     rel = query_expression.proj(*heading.non_blobs)
     info = heading.table_status
-    tuples = rel.fetch(limit=config['display.limit'] + 1, format='array')
-    has_more = len(tuples) > config['display.limit']
-    tuples = tuples[0:config['display.limit']]
+    tuples = rel.fetch(limit=config["display.limit"] + 1, format="array")
+    has_more = len(tuples) > config["display.limit"]
+    tuples = tuples[0 : config["display.limit"]]
 
     css = """
     <style type="text/css">
@@ -100,14 +119,31 @@ def repr_html(query_expression):
         {count}</div>
         """.format(
         css=css,
-        title="" if info is None else "<b>%s</b>" % info['comment'],
-        head='</th><th>'.join(
-            head_template.format(column=c, comment=heading.attributes[c].comment,
-                                 primary='primary' if c in query_expression.primary_key else 'nonprimary') for c in
-            heading.names),
-        ellipsis='<p>...</p>' if has_more else '',
-        body='</tr><tr>'.join(
-            ['\n'.join(['<td>%s</td>' % (tup[name] if name in tup.dtype.names else '=BLOB=')
-                        for name in heading.names])
-             for tup in tuples]),
-        count=('<p>Total: %d</p>' % len(rel)) if config['display.show_tuple_count'] else '')
+        title="" if info is None else "<b>%s</b>" % info["comment"],
+        head="</th><th>".join(
+            head_template.format(
+                column=c,
+                comment=heading.attributes[c].comment,
+                primary="primary"
+                if c in query_expression.primary_key
+                else "nonprimary",
+            )
+            for c in heading.names
+        ),
+        ellipsis="<p>...</p>" if has_more else "",
+        body="</tr><tr>".join(
+            [
+                "\n".join(
+                    [
+                        "<td>%s</td>"
+                        % (tup[name] if name in tup.dtype.names else "=BLOB=")
+                        for name in heading.names
+                    ]
+                )
+                for tup in tuples
+            ]
+        ),
+        count=("<p>Total: %d</p>" % len(rel))
+        if config["display.show_tuple_count"]
+        else "",
+    )
diff --git a/datajoint/s3.py b/datajoint/s3.py
index e26cf1329..1979af912 100644
--- a/datajoint/s3.py
+++ b/datajoint/s3.py
@@ -2,7 +2,7 @@
 AWS S3 operations
 """
 from io import BytesIO
-import minio   # https://docs.minio.io/docs/python-client-api-reference
+import minio  # https://docs.minio.io/docs/python-client-api-reference
 import urllib3
 import warnings
 import uuid
@@ -17,8 +17,18 @@ class Folder:
     """
     A Folder instance manipulates a flat folder of objects within an S3-compatible object store
     """
-    def __init__(self, endpoint, bucket, access_key, secret_key, *, secure=False,
-                 proxy_server=None, **_):
+
+    def __init__(
+        self,
+        endpoint,
+        bucket,
+        access_key,
+        secret_key,
+        *,
+        secure=False,
+        proxy_server=None,
+        **_
+    ):
         # from https://docs.min.io/docs/python-client-api-reference
         self.client = minio.Minio(
             endpoint,
@@ -26,77 +36,84 @@ def __init__(self, endpoint, bucket, access_key, secret_key, *, secure=False,
             secret_key=secret_key,
             secure=secure,
             http_client=(
-                urllib3.ProxyManager(proxy_server,
-                                     timeout=urllib3.Timeout.DEFAULT_TIMEOUT,
-                                     cert_reqs="CERT_REQUIRED",
-                                     retries=urllib3.Retry(total=5,
-                                                           backoff_factor=0.2,
-                                                           status_forcelist=[500, 502, 503,
-                                                                             504]))
-                if proxy_server else None),
-            )
+                urllib3.ProxyManager(
+                    proxy_server,
+                    timeout=urllib3.Timeout.DEFAULT_TIMEOUT,
+                    cert_reqs="CERT_REQUIRED",
+                    retries=urllib3.Retry(
+                        total=5,
+                        backoff_factor=0.2,
+                        status_forcelist=[500, 502, 503, 504],
+                    ),
+                )
+                if proxy_server
+                else None
+            ),
+        )
         self.bucket = bucket
         if not self.client.bucket_exists(bucket):
-            raise errors.BucketInaccessible('Inaccessible s3 bucket %s' % bucket)
+            raise errors.BucketInaccessible("Inaccessible s3 bucket %s" % bucket)
 
     def put(self, name, buffer):
-        logger.debug('put: {}:{}'.format(self.bucket, name))
+        logger.debug("put: {}:{}".format(self.bucket, name))
         return self.client.put_object(
-            self.bucket, str(name), BytesIO(buffer), length=len(buffer))
+            self.bucket, str(name), BytesIO(buffer), length=len(buffer)
+        )
 
     def fput(self, local_file, name, metadata=None):
-        logger.debug('fput: {} -> {}:{}'.format(self.bucket, local_file, name))
+        logger.debug("fput: {} -> {}:{}".format(self.bucket, local_file, name))
         return self.client.fput_object(
-            self.bucket, str(name), str(local_file), metadata=metadata)
+            self.bucket, str(name), str(local_file), metadata=metadata
+        )
 
     def get(self, name):
-        logger.debug('get: {}:{}'.format(self.bucket, name))
+        logger.debug("get: {}:{}".format(self.bucket, name))
         try:
             return self.client.get_object(self.bucket, str(name)).data
         except minio.error.S3Error as e:
-            if e.code == 'NoSuchKey':
-                raise errors.MissingExternalFile('Missing s3 key %s' % name)
+            if e.code == "NoSuchKey":
+                raise errors.MissingExternalFile("Missing s3 key %s" % name)
             else:
                 raise e
 
     def fget(self, name, local_filepath):
         """get file from object name to local filepath"""
-        logger.debug('fget: {}:{}'.format(self.bucket, name))
+        logger.debug("fget: {}:{}".format(self.bucket, name))
         name = str(name)
         stat = self.client.stat_object(self.bucket, name)
-        meta = {k.lower().lstrip('x-amz-meta'): v for k, v in stat.metadata.items()}
+        meta = {k.lower().lstrip("x-amz-meta"): v for k, v in stat.metadata.items()}
         data = self.client.get_object(self.bucket, name)
         local_filepath = Path(local_filepath)
         local_filepath.parent.mkdir(parents=True, exist_ok=True)
-        with local_filepath.open('wb') as f:
+        with local_filepath.open("wb") as f:
             for d in data.stream(1 << 16):
                 f.write(d)
-        if 'contents_hash' in meta:
-            return uuid.UUID(meta['contents_hash'])
+        if "contents_hash" in meta:
+            return uuid.UUID(meta["contents_hash"])
 
     def exists(self, name):
-        logger.debug('exists: {}:{}'.format(self.bucket, name))
+        logger.debug("exists: {}:{}".format(self.bucket, name))
         try:
             self.client.stat_object(self.bucket, str(name))
         except minio.error.S3Error as e:
-            if e.code == 'NoSuchKey':
+            if e.code == "NoSuchKey":
                 return False
             else:
                 raise e
         return True
 
     def get_size(self, name):
-        logger.debug('get_size: {}:{}'.format(self.bucket, name))
+        logger.debug("get_size: {}:{}".format(self.bucket, name))
         try:
             return self.client.stat_object(self.bucket, str(name)).size
         except minio.error.S3Error as e:
-            if e.code == 'NoSuchKey':
+            if e.code == "NoSuchKey":
                 raise errors.MissingExternalFile
             raise e
 
     def remove_object(self, name):
-        logger.debug('remove_object: {}:{}'.format(self.bucket, name))
+        logger.debug("remove_object: {}:{}".format(self.bucket, name))
         try:
             self.client.remove_object(self.bucket, str(name))
         except minio.error.MinioException:
-            raise errors.DataJointError('Failed to delete %s from s3 storage' % name)
+            raise errors.DataJointError("Failed to delete %s from s3 storage" % name)
diff --git a/datajoint/schemas.py b/datajoint/schemas.py
index ab2fc03af..3f89bd31e 100644
--- a/datajoint/schemas.py
+++ b/datajoint/schemas.py
@@ -23,6 +23,7 @@ def ordered_dir(class_):
     """
     List (most) attributes of the class including inherited ones, similar to `dir` build-in function,
     but respects order of attribute declaration as much as possible.
+
     :param class_: class to list members for
     :return: a list of attributes declared in class_ and its superclasses
     """
@@ -38,8 +39,16 @@ class Schema:
     It also specifies the namespace `context` in which other UserTable classes are defined.
     """
 
-    def __init__(self, schema_name=None, context=None, *, connection=None, create_schema=True,
-                 create_tables=True, add_objects=None):
+    def __init__(
+        self,
+        schema_name=None,
+        context=None,
+        *,
+        connection=None,
+        create_schema=True,
+        create_tables=True,
+        add_objects=None
+    ):
         """
         Associate database schema `schema_name`. If the schema does not exist, attempt to
         create it on the server.
@@ -71,11 +80,19 @@ def __init__(self, schema_name=None, context=None, *, connection=None, create_sc
     def is_activated(self):
         return self.database is not None
 
-    def activate(self, schema_name=None, *, connection=None, create_schema=None,
-                 create_tables=None, add_objects=None):
+    def activate(
+        self,
+        schema_name=None,
+        *,
+        connection=None,
+        create_schema=None,
+        create_tables=None,
+        add_objects=None
+    ):
         """
         Associate database schema `schema_name`. If the schema does not exist, attempt to
         create it on the server.
+
         :param schema_name: the database schema to associate.
             schema_name=None is used to assert that the schema has already been activated.
         :param connection: Connection object. Defaults to datajoint.conn().
@@ -93,7 +110,10 @@ def activate(self, schema_name=None, *, connection=None, create_schema=None,
             if self.database == schema_name:  # already activated
                 return
             raise DataJointError(
-                "The schema is already activated for schema {db}.".format(db=self.database))
+                "The schema is already activated for schema {db}.".format(
+                    db=self.database
+                )
+            )
         if connection is not None:
             self.connection = connection
         if self.connection is None:
@@ -109,17 +129,23 @@ def activate(self, schema_name=None, *, connection=None, create_schema=None,
             if not self.create_schema or not self.database:
                 raise DataJointError(
                     "Database `{name}` has not yet been declared. "
-                    "Set argument create_schema=True to create it.".format(name=schema_name))
+                    "Set argument create_schema=True to create it.".format(
+                        name=schema_name
+                    )
+                )
             # create database
             logger.info("Creating schema `{name}`.".format(name=schema_name))
             try:
-                self.connection.query("CREATE DATABASE `{name}`".format(name=schema_name))
+                self.connection.query(
+                    "CREATE DATABASE `{name}`".format(name=schema_name)
+                )
             except AccessError:
                 raise DataJointError(
                     "Schema `{name}` does not exist and could not be created. "
-                    "Check permissions.".format(name=schema_name))
+                    "Check permissions.".format(name=schema_name)
+                )
             else:
-                self.log('created')
+                self.log("created")
         self.connection.register(self)
 
         # decorate all tables already decorated
@@ -131,17 +157,22 @@ def activate(self, schema_name=None, *, connection=None, create_schema=None,
     def _assert_exists(self, message=None):
         if not self.exists:
             raise DataJointError(
-                message or "Schema `{db}` has not been created.".format(db=self.database))
+                message
+                or "Schema `{db}` has not been created.".format(db=self.database)
+            )
 
     def __call__(self, cls, *, context=None):
         """
         Binds the supplied class to a schema. This is intended to be used as a decorator.
+
         :param cls: class to decorate.
         :param context: supplied when called from spawn_missing_classes
         """
         context = context or self.context or inspect.currentframe().f_back.f_locals
         if issubclass(cls, Part):
-            raise DataJointError('The schema decorator should not be applied to Part relations')
+            raise DataJointError(
+                "The schema decorator should not be applied to Part relations"
+            )
         if self.is_activated():
             self._decorate_master(cls, context)
         else:
@@ -150,10 +181,13 @@ def __call__(self, cls, *, context=None):
 
     def _decorate_master(self, cls, context):
         """
+
         :param cls: the master class to process
         :param context: the class' declaration context
         """
-        self._decorate_table(cls, context=dict(context, self=cls, **{cls.__name__: cls}))
+        self._decorate_table(
+            cls, context=dict(context, self=cls, **{cls.__name__: cls})
+        )
         # Process part tables
         for part in ordered_dir(cls):
             if part[0].isupper():
@@ -161,8 +195,12 @@ def _decorate_master(self, cls, context):
                 if inspect.isclass(part) and issubclass(part, Part):
                     part._master = cls
                     # allow addressing master by name or keyword 'master'
-                    self._decorate_table(part, context=dict(
-                        context, master=cls, self=part, **{cls.__name__: cls}))
+                    self._decorate_table(
+                        part,
+                        context=dict(
+                            context, master=cls, self=part, **{cls.__name__: cls}
+                        ),
+                    )
 
     def _decorate_table(self, table_class, context, assert_declared=False):
         """
@@ -170,11 +208,14 @@ def _decorate_table(self, table_class, context, assert_declared=False):
         """
         table_class.database = self.database
         table_class._connection = self.connection
-        table_class._heading = Heading(table_info=dict(
-            conn=self.connection,
-            database=self.database,
-            table_name=table_class.table_name,
-            context=context))
+        table_class._heading = Heading(
+            table_info=dict(
+                conn=self.connection,
+                database=self.database,
+                table_name=table_class.table_name,
+                context=context,
+            )
+        )
         table_class._support = [table_class.full_table_name]
         table_class.declaration_context = context
 
@@ -183,23 +224,34 @@ def _decorate_table(self, table_class, context, assert_declared=False):
         is_declared = instance.is_declared
         if not is_declared:
             if not self.create_tables or assert_declared:
-                raise DataJointError('Table `%s` not declared' % instance.table_name)
+                raise DataJointError("Table `%s` not declared" % instance.table_name)
             instance.declare(context)
             self.connection.dependencies.clear()
         is_declared = is_declared or instance.is_declared
 
         # add table definition to the doc string
         if isinstance(table_class.definition, str):
-            table_class.__doc__ = (table_class.__doc__ or "") + "\nTable definition:\n\n" + table_class.definition
+            table_class.__doc__ = (
+                (table_class.__doc__ or "")
+                + "\nTable definition:\n\n"
+                + table_class.definition
+            )
 
         # fill values in Lookup tables from their contents property
-        if isinstance(instance, Lookup) and hasattr(instance, 'contents') and is_declared:
+        if (
+            isinstance(instance, Lookup)
+            and hasattr(instance, "contents")
+            and is_declared
+        ):
             contents = list(instance.contents)
             if len(contents) > len(instance):
                 if instance.heading.has_autoincrement:
-                    warnings.warn(('Contents has changed but cannot be inserted because '
-                                  '{table} has autoincrement.').format(
-                        table=instance.__class__.__name__))
+                    warnings.warn(
+                        (
+                            "Contents has changed but cannot be inserted because "
+                            "{table} has autoincrement."
+                        ).format(table=instance.__class__.__name__)
+                    )
                 else:
                     instance.insert(contents, skip_duplicates=True)
 
@@ -211,7 +263,7 @@ def log(self):
         return self._log
 
     def __repr__(self):
-        return 'Schema `{name}`\n'.format(name=self.database)
+        return "Schema `{name}`\n".format(name=self.database)
 
     @property
     def size_on_disk(self):
@@ -219,16 +271,22 @@ def size_on_disk(self):
         :return: size of the entire schema in bytes
         """
         self._assert_exists()
-        return int(self.connection.query(
-            """
+        return int(
+            self.connection.query(
+                """
             SELECT SUM(data_length + index_length)
             FROM information_schema.tables WHERE table_schema='{db}'
-            """.format(db=self.database)).fetchone()[0])
+            """.format(
+                    db=self.database
+                )
+            ).fetchone()[0]
+        )
 
     def spawn_missing_classes(self, context=None):
         """
         Creates the appropriate python user relation classes from tables in the schema and places them
         in the context.
+
         :param context: alternative context to place the missing classes into, e.g. locals()
         """
         self._assert_exists()
@@ -241,30 +299,44 @@ def spawn_missing_classes(self, context=None):
                 context = frame.f_locals
                 del frame
         tables = [
-            row[0] for row in self.connection.query('SHOW TABLES in `%s`' % self.database)
-            if lookup_class_name('`{db}`.`{tab}`'.format(db=self.database, tab=row[0]), context, 0) is None]
+            row[0]
+            for row in self.connection.query("SHOW TABLES in `%s`" % self.database)
+            if lookup_class_name(
+                "`{db}`.`{tab}`".format(db=self.database, tab=row[0]), context, 0
+            )
+            is None
+        ]
         master_classes = (Lookup, Manual, Imported, Computed)
         part_tables = []
         for table_name in tables:
             class_name = to_camel_case(table_name)
             if class_name not in context:
                 try:
-                    cls = next(cls for cls in master_classes if re.fullmatch(cls.tier_regexp, table_name))
+                    cls = next(
+                        cls
+                        for cls in master_classes
+                        if re.fullmatch(cls.tier_regexp, table_name)
+                    )
                 except StopIteration:
                     if re.fullmatch(Part.tier_regexp, table_name):
                         part_tables.append(table_name)
                 else:
                     # declare and decorate master relation classes
-                    context[class_name] = self(type(class_name, (cls,), dict()), context=context)
+                    context[class_name] = self(
+                        type(class_name, (cls,), dict()), context=context
+                    )
 
         # attach parts to masters
         for table_name in part_tables:
             groups = re.fullmatch(Part.tier_regexp, table_name).groupdict()
-            class_name = to_camel_case(groups['part'])
+            class_name = to_camel_case(groups["part"])
             try:
-                master_class = context[to_camel_case(groups['master'])]
+                master_class = context[to_camel_case(groups["master"])]
             except KeyError:
-                raise DataJointError('The table %s does not follow DataJoint naming conventions' % table_name)
+                raise DataJointError(
+                    "The table %s does not follow DataJoint naming conventions"
+                    % table_name
+                )
             part_class = type(class_name, (Part,), dict(definition=...))
             part_class._master = master_class
             self._decorate_table(part_class, context=context, assert_declared=True)
@@ -275,19 +347,34 @@ def drop(self, force=False):
         Drop the associated schema if it exists
         """
         if not self.exists:
-            logger.info("Schema named `{database}` does not exist. Doing nothing.".format(
-                database=self.database))
-        elif (not config['safemode'] or
-              force or
-              user_choice("Proceed to delete entire schema `%s`?" % self.database, default='no') == 'yes'):
+            logger.info(
+                "Schema named `{database}` does not exist. Doing nothing.".format(
+                    database=self.database
+                )
+            )
+        elif (
+            not config["safemode"]
+            or force
+            or user_choice(
+                "Proceed to delete entire schema `%s`?" % self.database, default="no"
+            )
+            == "yes"
+        ):
             logger.info("Dropping `{database}`.".format(database=self.database))
             try:
-                self.connection.query("DROP DATABASE `{database}`".format(database=self.database))
-                logger.info("Schema `{database}` was dropped successfully.".format(database=self.database))
+                self.connection.query(
+                    "DROP DATABASE `{database}`".format(database=self.database)
+                )
+                logger.info(
+                    "Schema `{database}` was dropped successfully.".format(
+                        database=self.database
+                    )
+                )
             except AccessError:
                 raise AccessError(
                     "An attempt to drop schema `{database}` "
-                    "has failed. Check permissions.".format(database=self.database))
+                    "has failed. Check permissions.".format(database=self.database)
+                )
 
     @property
     def exists(self):
@@ -296,15 +383,19 @@ def exists(self):
         """
         if self.database is None:
             raise DataJointError("Schema must be activated first.")
-        return bool(self.connection.query(
-            "SELECT schema_name "
-            "FROM information_schema.schemata "
-            "WHERE schema_name = '{database}'".format(database=self.database)).rowcount)
+        return bool(
+            self.connection.query(
+                "SELECT schema_name "
+                "FROM information_schema.schemata "
+                "WHERE schema_name = '{database}'".format(database=self.database)
+            ).rowcount
+        )
 
     @property
     def jobs(self):
         """
         schema.jobs provides a view of the job reservation table for the schema
+
         :return: jobs table
         """
         self._assert_exists()
@@ -321,60 +412,84 @@ def save(self, python_filename=None):
         """
         Generate the code for a module that recreates the schema.
         This method is in preparation for a future release and is not officially supported.
+
         :return: a string containing the body of a complete Python module defining this schema.
         """
         self._assert_exists()
         module_count = itertools.count()
         # add virtual modules for referenced modules with names vmod0, vmod1, ...
-        module_lookup = collections.defaultdict(lambda: 'vmod' + str(next(module_count)))
+        module_lookup = collections.defaultdict(
+            lambda: "vmod" + str(next(module_count))
+        )
         db = self.database
 
         def make_class_definition(table):
             tier = _get_tier(table).__name__
-            class_name = table.split('.')[1].strip('`')
-            indent = ''
-            if tier == 'Part':
-                class_name = class_name.split('__')[-1]
-                indent += '    '
+            class_name = table.split(".")[1].strip("`")
+            indent = ""
+            if tier == "Part":
+                class_name = class_name.split("__")[-1]
+                indent += "    "
             class_name = to_camel_case(class_name)
 
             def replace(s):
                 d, tabs = s.group(1), s.group(2)
-                return ('' if d == db else (module_lookup[d]+'.')) + '.'.join(
-                    to_camel_case(tab) for tab in tabs.lstrip('__').split('__'))
+                return ("" if d == db else (module_lookup[d] + ".")) + ".".join(
+                    to_camel_case(tab) for tab in tabs.lstrip("__").split("__")
+                )
 
-            return ('' if tier == 'Part' else '\n@schema\n') + (
-                '{indent}class {class_name}(dj.{tier}):\n'
+            return ("" if tier == "Part" else "\n@schema\n") + (
+                "{indent}class {class_name}(dj.{tier}):\n"
                 '{indent}    definition = """\n'
-                '{indent}    {defi}"""').format(
-                    class_name=class_name,
-                    indent=indent,
-                    tier=tier,
-                    defi=re.sub(r'`([^`]+)`.`([^`]+)`', replace,
-                                FreeTable(self.connection, table).describe(printout=False)
-                                ).replace('\n', '\n    ' + indent))
+                '{indent}    {defi}"""'
+            ).format(
+                class_name=class_name,
+                indent=indent,
+                tier=tier,
+                defi=re.sub(
+                    r"`([^`]+)`.`([^`]+)`",
+                    replace,
+                    FreeTable(self.connection, table).describe(printout=False),
+                ).replace("\n", "\n    " + indent),
+            )
 
         diagram = Diagram(self)
-        body = '\n\n'.join(make_class_definition(table) for table in diagram.topological_sort())
-        python_code = '\n\n'.join((
-            '"""This module was auto-generated by datajoint from an existing schema"""',
-            "import datajoint as dj\n\nschema = dj.Schema('{db}')".format(db=db),
-            '\n'.join("{module} = dj.VirtualModule('{module}', '{schema_name}')".format(module=v, schema_name=k)
-                      for k, v in module_lookup.items()), body))
+        body = "\n\n".join(
+            make_class_definition(table) for table in diagram.topological_sort()
+        )
+        python_code = "\n\n".join(
+            (
+                '"""This module was auto-generated by datajoint from an existing schema"""',
+                "import datajoint as dj\n\nschema = dj.Schema('{db}')".format(db=db),
+                "\n".join(
+                    "{module} = dj.VirtualModule('{module}', '{schema_name}')".format(
+                        module=v, schema_name=k
+                    )
+                    for k, v in module_lookup.items()
+                ),
+                body,
+            )
+        )
         if python_filename is None:
             return python_code
-        with open(python_filename, 'wt') as f:
+        with open(python_filename, "wt") as f:
             f.write(python_code)
 
     def list_tables(self):
         """
         Return a list of all tables in the schema except tables with ~ in first character such
         as ~logs and ~job
+
         :return: A list of table names from the database schema.
         """
-        return [t for d, t in (full_t.replace('`', '').split('.')
-                               for full_t in Diagram(self).topological_sort())
-                if d == self.database]
+        return [
+            t
+            for d, t in (
+                full_t.replace("`", "").split(".")
+                for full_t in Diagram(self).topological_sort()
+            )
+            if d == self.database
+        ]
 
 
 class VirtualModule(types.ModuleType):
@@ -382,11 +497,21 @@ class VirtualModule(types.ModuleType):
     A virtual module imitates a Python module representing a DataJoint schema from table definitions in the database.
     It declares the schema objects and a class for each table.
     """
-    def __init__(self, module_name, schema_name, *, create_schema=False,
-                 create_tables=False, connection=None, add_objects=None):
+
+    def __init__(
+        self,
+        module_name,
+        schema_name,
+        *,
+        create_schema=False,
+        create_tables=False,
+        connection=None,
+        add_objects=None
+    ):
         """
         Creates a python module with the given name from the name of a schema on the server and
         automatically adds classes to it corresponding to the tables in the schema.
+
         :param module_name: displayed module name
         :param schema_name: name of the database in mysql
         :param create_schema: if True, create the schema on the database server
@@ -396,20 +521,29 @@ def __init__(self, module_name, schema_name, *, create_schema=False,
         :return: the python module containing classes from the schema object and the table classes
         """
         super(VirtualModule, self).__init__(name=module_name)
-        _schema = Schema(schema_name, create_schema=create_schema,
-                         create_tables=create_tables, connection=connection)
+        _schema = Schema(
+            schema_name,
+            create_schema=create_schema,
+            create_tables=create_tables,
+            connection=connection,
+        )
         if add_objects:
             self.__dict__.update(add_objects)
-        self.__dict__['schema'] = _schema
+        self.__dict__["schema"] = _schema
         _schema.spawn_missing_classes(context=self.__dict__)
 
 
 def list_schemas(connection=None):
     """
+
     :param connection: a dj.Connection object
     :return: list of all accessible schemas on the server
     """
-    return [r[0] for r in (connection or conn()).query(
-        'SELECT schema_name '
-        'FROM information_schema.schemata '
-        'WHERE schema_name <> "information_schema"')]
+    return [
+        r[0]
+        for r in (connection or conn()).query(
+            "SELECT schema_name "
+            "FROM information_schema.schemata "
+            'WHERE schema_name <> "information_schema"'
+        )
+    ]
diff --git a/datajoint/settings.py b/datajoint/settings.py
index 75f265f87..a0b59e0a5 100644
--- a/datajoint/settings.py
+++ b/datajoint/settings.py
@@ -10,51 +10,53 @@
 from enum import Enum
 from .errors import DataJointError
 
-LOCALCONFIG = 'dj_local_conf.json'
-GLOBALCONFIG = '.datajoint_config.json'
+LOCALCONFIG = "dj_local_conf.json"
+GLOBALCONFIG = ".datajoint_config.json"
 # subfolding for external storage in filesystem.
 # 2, 2 means that file abcdef is stored as /ab/cd/abcdef
 DEFAULT_SUBFOLDING = (2, 2)
 
 validators = collections.defaultdict(lambda: lambda value: True)
-validators['database.port'] = lambda a: isinstance(a, int)
+validators["database.port"] = lambda a: isinstance(a, int)
 
-Role = Enum('Role', 'manual lookup imported computed job')
+Role = Enum("Role", "manual lookup imported computed job")
 role_to_prefix = {
-    Role.manual: '',
-    Role.lookup: '#',
-    Role.imported: '_',
-    Role.computed: '__',
-    Role.job: '~'
+    Role.manual: "",
+    Role.lookup: "#",
+    Role.imported: "_",
+    Role.computed: "__",
+    Role.job: "~",
 }
 prefix_to_role = dict(zip(role_to_prefix.values(), role_to_prefix))
 
-default = dict({
-    'database.host': 'localhost',
-    'database.password': None,
-    'database.user': None,
-    'database.port': 3306,
-    'database.reconnect': True,
-    'connection.init_function': None,
-    'connection.charset': '',   # pymysql uses '' as default
-    'loglevel': 'INFO',
-    'safemode': True,
-    'fetch_format': 'array',
-    'display.limit': 12,
-    'display.width': 14,
-    'display.show_tuple_count': True,
-    'database.use_tls': None,
-    'enable_python_native_blobs': True,  # python-native/dj0 encoding support
-})
+default = dict(
+    {
+        "database.host": "localhost",
+        "database.password": None,
+        "database.user": None,
+        "database.port": 3306,
+        "database.reconnect": True,
+        "connection.init_function": None,
+        "connection.charset": "",  # pymysql uses '' as default
+        "loglevel": "INFO",
+        "safemode": True,
+        "fetch_format": "array",
+        "display.limit": 12,
+        "display.width": 14,
+        "display.show_tuple_count": True,
+        "database.use_tls": None,
+        "enable_python_native_blobs": True,  # python-native/dj0 encoding support
+    }
+)
 
 logger = logging.getLogger(__name__)
 log_levels = {
-    'INFO': logging.INFO,
-    'WARNING': logging.WARNING,
-    'CRITICAL': logging.CRITICAL,
-    'DEBUG': logging.DEBUG,
-    'ERROR': logging.ERROR,
-    None:  logging.NOTSET
+    "INFO": logging.INFO,
+    "WARNING": logging.WARNING,
+    "CRITICAL": logging.CRITICAL,
+    "DEBUG": logging.DEBUG,
+    "ERROR": logging.ERROR,
+    None: logging.NOTSET,
 }
 
 
@@ -95,22 +97,24 @@ def __len__(self):
     def save(self, filename, verbose=False):
         """
         Saves the settings in JSON format to the given file path.
+
         :param filename: filename of the local JSON settings file.
         :param verbose: report having saved the settings file
         """
-        with open(filename, 'w') as fid:
+        with open(filename, "w") as fid:
             json.dump(self._conf, fid, indent=4)
         if verbose:
-            print('Saved settings in ' + filename)
+            print("Saved settings in " + filename)
 
     def load(self, filename):
         """
         Updates the setting from config file in JSON format.
+
         :param filename: filename of the local JSON settings file. If None, the local config file is used.
         """
         if filename is None:
             filename = LOCALCONFIG
-        with open(filename, 'r') as fid:
+        with open(filename, "r") as fid:
             self._conf.update(json.load(fid))
 
     def save_local(self, verbose=False):
@@ -123,40 +127,72 @@ def save_global(self, verbose=False):
         """
         saves the settings in the global config file
         """
-        self.save(os.path.expanduser(os.path.join('~', GLOBALCONFIG)), verbose)
+        self.save(os.path.expanduser(os.path.join("~", GLOBALCONFIG)), verbose)
 
     def get_store_spec(self, store):
         """
         find configuration of external stores for blobs and attachments
         """
         try:
-            spec = self['stores'][store]
+            spec = self["stores"][store]
         except KeyError:
-            raise DataJointError('Storage {store} is requested but not configured'.format(store=store))
+            raise DataJointError(
+                "Storage {store} is requested but not configured".format(store=store)
+            )
 
-        spec['subfolding'] = spec.get('subfolding', DEFAULT_SUBFOLDING)
+        spec["subfolding"] = spec.get("subfolding", DEFAULT_SUBFOLDING)
         spec_keys = {  # REQUIRED in uppercase and allowed in lowercase
-            'file': ('PROTOCOL', 'LOCATION', 'subfolding', 'stage'),
-            's3': ('PROTOCOL', 'ENDPOINT', 'BUCKET', 'ACCESS_KEY', 'SECRET_KEY', 'LOCATION',
-                   'secure', 'subfolding', 'stage', 'proxy_server')}
+            "file": ("PROTOCOL", "LOCATION", "subfolding", "stage"),
+            "s3": (
+                "PROTOCOL",
+                "ENDPOINT",
+                "BUCKET",
+                "ACCESS_KEY",
+                "SECRET_KEY",
+                "LOCATION",
+                "secure",
+                "subfolding",
+                "stage",
+                "proxy_server",
+            ),
+        }
 
         try:
-            spec_keys = spec_keys[spec.get('protocol', '').lower()]
+            spec_keys = spec_keys[spec.get("protocol", "").lower()]
         except KeyError:
             raise DataJointError(
-                'Missing or invalid protocol in dj.config["stores"]["{store}"]'.format(store=store))
+                'Missing or invalid protocol in dj.config["stores"]["{store}"]'.format(
+                    store=store
+                )
+            )
 
         # check that all required keys are present in spec
         try:
-            raise DataJointError('dj.config["stores"]["{store}"] is missing "{k}"'.format(
-                store=store, k=next(k.lower() for k in spec_keys if k.isupper() and k.lower() not in spec)))
+            raise DataJointError(
+                'dj.config["stores"]["{store}"] is missing "{k}"'.format(
+                    store=store,
+                    k=next(
+                        k.lower()
+                        for k in spec_keys
+                        if k.isupper() and k.lower() not in spec
+                    ),
+                )
+            )
         except StopIteration:
             pass
 
         # check that only allowed keys are present in spec
         try:
-            raise DataJointError('Invalid key "{k}" in dj.config["stores"]["{store}"]'.format(
-                store=store, k=next(k for k in spec if k.upper() not in spec_keys and k.lower() not in spec_keys)))
+            raise DataJointError(
+                'Invalid key "{k}" in dj.config["stores"]["{store}"]'.format(
+                    store=store,
+                    k=next(
+                        k
+                        for k in spec
+                        if k.upper() not in spec_keys and k.lower() not in spec_keys
+                    ),
+                )
+            )
         except StopIteration:
             pass  # no invalid keys
 
@@ -178,7 +214,7 @@ def __call__(self, **kwargs):
         try:
             backup = self.instance
             self.instance = Config.__Config(self.instance._conf)
-            new = {k.replace('__', '.'): v for k, v in kwargs.items()}
+            new = {k.replace("__", "."): v for k, v in kwargs.items()}
             self.instance._conf.update(new)
             yield self
         except:
@@ -204,16 +240,20 @@ def __getitem__(self, key):
             return self._conf[key]
 
         def __setitem__(self, key, value):
-            logger.log(logging.INFO, u"Setting {0:s} to {1:s}".format(str(key), str(value)))
+            logger.log(
+                logging.INFO, "Setting {0:s} to {1:s}".format(str(key), str(value))
+            )
             if validators[key](value):
                 self._conf[key] = value
             else:
-                raise DataJointError(u'Validator for {0:s} did not pass'.format(key))
+                raise DataJointError("Validator for {0:s} did not pass".format(key))
 
 
 # Load configuration from file
 config = Config()
-config_files = (os.path.expanduser(n) for n in (LOCALCONFIG, os.path.join('~', GLOBALCONFIG)))
+config_files = (
+    os.path.expanduser(n) for n in (LOCALCONFIG, os.path.join("~", GLOBALCONFIG))
+)
 try:
     config_file = next(n for n in config_files if os.path.exists(n))
 except StopIteration:
@@ -222,12 +262,29 @@ def __setitem__(self, key, value):
     config.load(config_file)
 
 # override login credentials with environment variables
-mapping = {k: v for k, v in zip(
-    ('database.host', 'database.user', 'database.password',
-     'external.aws_access_key_id', 'external.aws_secret_access_key',),
-    map(os.getenv, ('DJ_HOST', 'DJ_USER', 'DJ_PASS',
-                    'DJ_AWS_ACCESS_KEY_ID', 'DJ_AWS_SECRET_ACCESS_KEY',)))
-           if v is not None}
+mapping = {
+    k: v
+    for k, v in zip(
+        (
+            "database.host",
+            "database.user",
+            "database.password",
+            "external.aws_access_key_id",
+            "external.aws_secret_access_key",
+        ),
+        map(
+            os.getenv,
+            (
+                "DJ_HOST",
+                "DJ_USER",
+                "DJ_PASS",
+                "DJ_AWS_ACCESS_KEY_ID",
+                "DJ_AWS_SECRET_ACCESS_KEY",
+            ),
+        ),
+    )
+    if v is not None
+}
 config.update(mapping)
 
-logger.setLevel(log_levels[config['loglevel']])
+logger.setLevel(log_levels[config["loglevel"]])
diff --git a/datajoint/table.py b/datajoint/table.py
index 587de6aef..2a61a9b4d 100644
--- a/datajoint/table.py
+++ b/datajoint/table.py
@@ -16,8 +16,13 @@
 from . import blob
 from .utils import user_choice, get_master
 from .heading import Heading
-from .errors import (DuplicateError, AccessError, DataJointError, UnknownAttributeError,
-                     IntegrityError)
+from .errors import (
+    DuplicateError,
+    AccessError,
+    DataJointError,
+    UnknownAttributeError,
+    IntegrityError,
+)
 from .version import __version__ as version
 
 logger = logging.getLogger(__name__)
@@ -26,9 +31,11 @@
     r"[\w\s:]*\((?P<child>`[^`]+`.`[^`]+`), "
     r"CONSTRAINT (?P<name>`[^`]+`) "
     r"(FOREIGN KEY \((?P<fk_attrs>[^)]+)\) "
-    r"REFERENCES (?P<parent>`[^`]+`(\.`[^`]+`)?) \((?P<pk_attrs>[^)]+)\)[\s\w]+\))?")
+    r"REFERENCES (?P<parent>`[^`]+`(\.`[^`]+`)?) \((?P<pk_attrs>[^)]+)\)[\s\w]+\))?"
+)
 
-constraint_info_query = ' '.join("""
+constraint_info_query = " ".join(
+    """
     SELECT
         COLUMN_NAME as fk_attrs,
         CONCAT('`', REFERENCED_TABLE_SCHEMA, '`.`', REFERENCED_TABLE_NAME, '`') as parent,
@@ -36,11 +43,13 @@
     FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
     WHERE
         CONSTRAINT_NAME = %s AND TABLE_SCHEMA = %s AND TABLE_NAME = %s;
-    """.split())
+    """.split()
+)
 
 
 class _RenameMap(tuple):
-    """ for internal use """
+    """for internal use"""
+
     pass
 
 
@@ -67,17 +76,21 @@ def table_name(self):
     @property
     def definition(self):
         raise NotImplementedError(
-            'Subclasses of Table must implement the `definition` property')
+            "Subclasses of Table must implement the `definition` property"
+        )
 
     def declare(self, context=None):
         """
         Declare the table in the schema based on self.definition.
+
         :param context: the context for foreign key resolution. If None, foreign keys are
             not allowed.
         """
         if self.connection.in_transaction:
-            raise DataJointError('Cannot declare new tables inside a transaction, '
-                                 'e.g. from inside a populate/make call')
+            raise DataJointError(
+                "Cannot declare new tables inside a transaction, "
+                "e.g. from inside a populate/make call"
+            )
         sql, external_stores = declare(self.full_table_name, self.definition, context)
         sql = sql.format(database=self.database)
         try:
@@ -89,7 +102,7 @@ def declare(self, context=None):
             # skip if no create privilege
             pass
         else:
-            self._log('Declared ' + self.full_table_name)
+            self._log("Declared " + self.full_table_name)
 
     def alter(self, prompt=True, context=None):
         """
@@ -97,8 +110,9 @@ def alter(self, prompt=True, context=None):
         """
         if self.connection.in_transaction:
             raise DataJointError(
-                'Cannot update table declaration inside a transaction, '
-                'e.g. from inside a populate/make call')
+                "Cannot update table declaration inside a transaction, "
+                "e.g. from inside a populate/make call"
+            )
         if context is None:
             frame = inspect.currentframe().f_back
             context = dict(frame.f_globals, **frame.f_locals)
@@ -107,10 +121,12 @@ def alter(self, prompt=True, context=None):
         sql, external_stores = alter(self.definition, old_definition, context)
         if not sql:
             if prompt:
-                print('Nothing to alter.')
+                print("Nothing to alter.")
         else:
-            sql = "ALTER TABLE {tab}\n\t".format(tab=self.full_table_name) + ",\n\t".join(sql)
-            if not prompt or user_choice(sql + '\n\nExecute?') == 'yes':
+            sql = "ALTER TABLE {tab}\n\t".format(
+                tab=self.full_table_name
+            ) + ",\n\t".join(sql)
+            if not prompt or user_choice(sql + "\n\nExecute?") == "yes":
                 try:
                     # declare all external tables before declaring main table
                     for store in external_stores:
@@ -121,10 +137,12 @@ def alter(self, prompt=True, context=None):
                     pass
                 else:
                     # reset heading
-                    self.__class__._heading = Heading(table_info=self.heading.table_info)
+                    self.__class__._heading = Heading(
+                        table_info=self.heading.table_info
+                    )
                     if prompt:
-                        print('Table altered')
-                    self._log('Altered ' + self.full_table_name)
+                        print("Table altered")
+                    self._log("Altered " + self.full_table_name)
 
     def from_clause(self):
         """
@@ -136,10 +154,13 @@ def get_select_fields(self, select_fields=None):
         """
         :return: the selected attributes from the SQL SELECT statement.
         """
-        return '*' if select_fields is None else self.heading.project(select_fields).as_sql
+        return (
+            "*" if select_fields is None else self.heading.project(select_fields).as_sql
+        )
 
     def parents(self, primary=None, as_objects=False, foreign_key_info=False):
         """
+
         :param primary: if None, then all parents are returned. If True, then only foreign keys composed of
             primary key attributes are considered.  If False, return foreign keys including at least one
             secondary attribute.
@@ -149,8 +170,10 @@ def parents(self, primary=None, as_objects=False, foreign_key_info=False):
             with (optional) foreign key information.
         """
         get_edge = self.connection.dependencies.parents
-        nodes = [next(iter(get_edge(name).items())) if name.isdigit() else (name, props)
-                 for name, props in get_edge(self.full_table_name, primary).items()]
+        nodes = [
+            next(iter(get_edge(name).items())) if name.isdigit() else (name, props)
+            for name, props in get_edge(self.full_table_name, primary).items()
+        ]
         if as_objects:
             nodes = [(FreeTable(self.connection, name), props) for name, props in nodes]
         if not foreign_key_info:
@@ -159,6 +182,7 @@ def parents(self, primary=None, as_objects=False, foreign_key_info=False):
 
     def children(self, primary=None, as_objects=False, foreign_key_info=False):
         """
+
         :param primary: if None, then all children are returned. If True, then only foreign keys composed of
             primary key attributes are considered.  If False, return foreign keys including at least one
             secondary attribute.
@@ -168,8 +192,10 @@ def children(self, primary=None, as_objects=False, foreign_key_info=False):
             with (optional) foreign key information.
         """
         get_edge = self.connection.dependencies.children
-        nodes = [next(iter(get_edge(name).items())) if name.isdigit() else (name, props)
-                 for name, props in get_edge(self.full_table_name, primary).items()]
+        nodes = [
+            next(iter(get_edge(name).items())) if name.isdigit() else (name, props)
+            for name, props in get_edge(self.full_table_name, primary).items()
+        ]
         if as_objects:
             nodes = [(FreeTable(self.connection, name), props) for name, props in nodes]
         if not foreign_key_info:
@@ -178,27 +204,39 @@ def children(self, primary=None, as_objects=False, foreign_key_info=False):
 
     def descendants(self, as_objects=False):
         """
+
         :param as_objects: False - a list of table names; True - a list of table objects.
         :return: list of tables descendants in topological order.
         """
-        return [FreeTable(self.connection, node) if as_objects else node
-                for node in self.connection.dependencies.descendants(self.full_table_name) if not node.isdigit()]
+        return [
+            FreeTable(self.connection, node) if as_objects else node
+            for node in self.connection.dependencies.descendants(self.full_table_name)
+            if not node.isdigit()
+        ]
 
     def ancestors(self, as_objects=False):
         """
+
         :param as_objects: False - a list of table names; True - a list of table objects.
         :return: list of tables ancestors in topological order.
         """
-        return [FreeTable(self.connection, node) if as_objects else node
-                for node in self.connection.dependencies.ancestors(self.full_table_name) if not node.isdigit()]
+        return [
+            FreeTable(self.connection, node) if as_objects else node
+            for node in self.connection.dependencies.ancestors(self.full_table_name)
+            if not node.isdigit()
+        ]
 
     def parts(self, as_objects=False):
         """
         return part tables either as entries in a dict with foreign key informaiton or a list of objects
+
         :param as_objects: if False (default), the output is a dict describing the foreign keys. If True, return table objects.
         """
-        nodes = [node for node in self.connection.dependencies.nodes
-                 if not node.isdigit() and node.startswith(self.full_table_name[:-1] + '__')]
+        nodes = [
+            node
+            for node in self.connection.dependencies.nodes
+            if not node.isdigit() and node.startswith(self.full_table_name[:-1] + "__")
+        ]
         return [FreeTable(self.connection, c) for c in nodes] if as_objects else nodes
 
     @property
@@ -206,9 +244,14 @@ def is_declared(self):
         """
         :return: True is the table is declared in the schema.
         """
-        return self.connection.query(
-            'SHOW TABLES in `{database}` LIKE "{table_name}"'.format(
-                database=self.database, table_name=self.table_name)).rowcount > 0
+        return (
+            self.connection.query(
+                'SHOW TABLES in `{database}` LIKE "{table_name}"'.format(
+                    database=self.database, table_name=self.table_name
+                )
+            ).rowcount
+            > 0
+        )
 
     @property
     def full_table_name(self):
@@ -220,7 +263,11 @@ def full_table_name(self):
     @property
     def _log(self):
         if self._log_ is None:
-            self._log_ = Log(self.connection, database=self.database, skip_logging=self.table_name.startswith('~'))
+            self._log_ = Log(
+                self.connection,
+                database=self.database,
+                skip_logging=self.table_name.startswith("~"),
+            )
         return self._log_
 
     @property
@@ -229,41 +276,53 @@ def external(self):
 
     def update1(self, row):
         """
-        update1 updates one existing entry in the table.
-        Caution: In DataJoint the primary modes for data manipulation is to ``insert`` and ``delete``
-        entire records since referential integrity works on the level of records, not fields.
-        Therefore, updates are reserved for corrective operations outside of main workflow.
-        Use UPDATE methods sparingly with full awareness of potential violations of assumptions.
+        ``update1`` updates one existing entry in the table.
+        Caution: In DataJoint the primary modes for data manipulation is to ``insert`` and
+        ``delete`` entire records since referential integrity works on the level of records,
+        not fields. Therefore, updates are reserved for corrective operations outside of main
+        workflow. Use UPDATE methods sparingly with full awareness of potential violations of
+        assumptions.
 
         :param row: a ``dict`` containing the primary key values and the attributes to update.
-        Setting an attribute value to None will reset it to the default value (if any)
+            Setting an attribute value to None will reset it to the default value (if any).
+
         The primary key attributes must always be provided.
+
         Examples:
+
         >>> table.update1({'id': 1, 'value': 3})  # update value in record with id=1
         >>> table.update1({'id': 1, 'value': None})  # reset value to default
         """
         # argument validations
         if not isinstance(row, collections.abc.Mapping):
-            raise DataJointError('The argument of update1 must be dict-like.')
+            raise DataJointError("The argument of update1 must be dict-like.")
         if not set(row).issuperset(self.primary_key):
-            raise DataJointError('The argument of update1 must supply all primary key values.')
+            raise DataJointError(
+                "The argument of update1 must supply all primary key values."
+            )
         try:
-            raise DataJointError('Attribute `%s` not found.' %
-                                 next(k for k in row if k not in self.heading.names))
+            raise DataJointError(
+                "Attribute `%s` not found."
+                % next(k for k in row if k not in self.heading.names)
+            )
         except StopIteration:
             pass  # ok
         if len(self.restriction):
-            raise DataJointError('Update cannot be applied to a restricted table.')
+            raise DataJointError("Update cannot be applied to a restricted table.")
         key = {k: row[k] for k in self.primary_key}
         if len(self & key) != 1:
-            raise DataJointError('Update entry must exist.')
+            raise DataJointError("Update entry must exist.")
         # UPDATE query
-        row = [self.__make_placeholder(k, v) for k, v in row.items()
-               if k not in self.primary_key]
+        row = [
+            self.__make_placeholder(k, v)
+            for k, v in row.items()
+            if k not in self.primary_key
+        ]
         query = "UPDATE {table} SET {assignments} WHERE {where}".format(
             table=self.full_table_name,
-            assignments=",".join('`%s`=%s' % r[:2] for r in row),
-            where=make_condition(self, key, set()))
+            assignments=",".join("`%s`=%s" % r[:2] for r in row),
+            where=make_condition(self, key, set()),
+        )
         self.connection.query(query, args=list(r[2] for r in row if r[2] is not None))
 
     def insert1(self, row, **kwargs):
@@ -271,25 +330,34 @@ def insert1(self, row, **kwargs):
         Insert one data record into the table. For ``kwargs``, see ``insert()``.
 
         :param row: a numpy record, a dict-like object, or an ordered sequence to be inserted
-        as one row.
+            as one row.
         """
         self.insert((row,), **kwargs)
 
-    def insert(self, rows, replace=False, skip_duplicates=False, ignore_extra_fields=False,
-               allow_direct_insert=None):
+    def insert(
+        self,
+        rows,
+        replace=False,
+        skip_duplicates=False,
+        ignore_extra_fields=False,
+        allow_direct_insert=None,
+    ):
         """
         Insert a collection of rows.
+
         :param rows: An iterable where an element is a numpy record, a dict-like object, a
             pandas.DataFrame, a sequence, or a query expression with the same heading as self.
         :param replace: If True, replaces the existing tuple.
         :param skip_duplicates: If True, silently skip duplicate inserts.
         :param ignore_extra_fields: If False, fields that are not in the heading raise error.
-        :param allow_direct_insert: applies only in auto-populated tables.
-    If False (default), insert are allowed only from inside the make callback.
-        Example::
-        >>> relation.insert([
-        >>>     dict(subject_id=7, species="mouse", date_of_birth="2014-09-01"),
-        >>>     dict(subject_id=8, species="mouse", date_of_birth="2014-09-02")])
+        :param allow_direct_insert: applies only in auto-populated tables. If False (default),
+            insert are allowed only from inside the make callback.
+
+        Example:
+
+            >>> relation.insert([
+            >>>     dict(subject_id=7, species="mouse", date_of_birth="2014-09-01"),
+            >>>     dict(subject_id=8, species="mouse", date_of_birth="2014-09-02")])
         """
         if isinstance(rows, pandas.DataFrame):
             # drop 'extra' synthetic index for 1-field index case -
@@ -299,11 +367,12 @@ def insert(self, rows, replace=False, skip_duplicates=False, ignore_extra_fields
             ).to_records(index=False)
 
         # prohibit direct inserts into auto-populated tables
-        if not allow_direct_insert and not getattr(self, '_allow_insert', True):
+        if not allow_direct_insert and not getattr(self, "_allow_insert", True):
             raise DataJointError(
-                'Inserts into an auto-populated table can only be done inside '
-                'its make method during a populate call.'
-                ' To override, set keyword argument allow_direct_insert=True.')
+                "Inserts into an auto-populated table can only be done inside "
+                "its make method during a populate call."
+                " To override, set keyword argument allow_direct_insert=True."
+            )
 
         if inspect.isclass(rows) and issubclass(rows, QueryExpression):
             rows = rows()  # instantiate if a class
@@ -313,51 +382,81 @@ def insert(self, rows, replace=False, skip_duplicates=False, ignore_extra_fields
                 try:
                     raise DataJointError(
                         "Attribute %s not found. To ignore extra attributes in insert, "
-                        "set ignore_extra_fields=True." % next(
-                            name for name in rows.heading if name not in self.heading))
+                        "set ignore_extra_fields=True."
+                        % next(
+                            name for name in rows.heading if name not in self.heading
+                        )
+                    )
                 except StopIteration:
                     pass
             fields = list(name for name in rows.heading if name in self.heading)
-            query = '{command} INTO {table} ({fields}) {select}{duplicate}'.format(
-                command='REPLACE' if replace else 'INSERT',
-                fields='`' + '`,`'.join(fields) + '`',
+            query = "{command} INTO {table} ({fields}) {select}{duplicate}".format(
+                command="REPLACE" if replace else "INSERT",
+                fields="`" + "`,`".join(fields) + "`",
                 table=self.full_table_name,
                 select=rows.make_sql(fields),
-                duplicate=(' ON DUPLICATE KEY UPDATE `{pk}`={table}.`{pk}`'.format(
-                    table=self.full_table_name, pk=self.primary_key[0])
-                           if skip_duplicates else ''))
+                duplicate=(
+                    " ON DUPLICATE KEY UPDATE `{pk}`={table}.`{pk}`".format(
+                        table=self.full_table_name, pk=self.primary_key[0]
+                    )
+                    if skip_duplicates
+                    else ""
+                ),
+            )
             self.connection.query(query)
             return
 
         field_list = []  # collects the field list from first row (passed by reference)
-        rows = list(self.__make_row_to_insert(row, field_list, ignore_extra_fields) for row in rows)
+        rows = list(
+            self.__make_row_to_insert(row, field_list, ignore_extra_fields)
+            for row in rows
+        )
         if rows:
             try:
                 query = "{command} INTO {destination}(`{fields}`) VALUES {placeholders}{duplicate}".format(
-                    command='REPLACE' if replace else 'INSERT',
+                    command="REPLACE" if replace else "INSERT",
                     destination=self.from_clause(),
-                    fields='`,`'.join(field_list),
-                    placeholders=','.join('(' + ','.join(row['placeholders']) + ')' for row in rows),
-                    duplicate=(' ON DUPLICATE KEY UPDATE `{pk}`=`{pk}`'.format(pk=self.primary_key[0])
-                               if skip_duplicates else ''))
-                self.connection.query(query, args=list(
-                    itertools.chain.from_iterable(
-                        (v for v in r['values'] if v is not None) for r in rows)))
+                    fields="`,`".join(field_list),
+                    placeholders=",".join(
+                        "(" + ",".join(row["placeholders"]) + ")" for row in rows
+                    ),
+                    duplicate=(
+                        " ON DUPLICATE KEY UPDATE `{pk}`=`{pk}`".format(
+                            pk=self.primary_key[0]
+                        )
+                        if skip_duplicates
+                        else ""
+                    ),
+                )
+                self.connection.query(
+                    query,
+                    args=list(
+                        itertools.chain.from_iterable(
+                            (v for v in r["values"] if v is not None) for r in rows
+                        )
+                    ),
+                )
             except UnknownAttributeError as err:
                 raise err.suggest(
-                    'To ignore extra fields in insert, set ignore_extra_fields=True')
+                    "To ignore extra fields in insert, set ignore_extra_fields=True"
+                )
             except DuplicateError as err:
                 raise err.suggest(
-                    'To ignore duplicate entries in insert, set skip_duplicates=True')
+                    "To ignore duplicate entries in insert, set skip_duplicates=True"
+                )
 
     def delete_quick(self, get_count=False):
         """
         Deletes the table without cascading and without user prompt.
         If this table has populated dependent tables, this will fail.
         """
-        query = 'DELETE FROM ' + self.full_table_name + self.where_clause()
+        query = "DELETE FROM " + self.full_table_name + self.where_clause()
         self.connection.query(query)
-        count = self.connection.query("SELECT ROW_COUNT()").fetchone()[0] if get_count else None
+        count = (
+            self.connection.query("SELECT ROW_COUNT()").fetchone()[0]
+            if get_count
+            else None
+        )
         self._log(query[:255])
         return count
 
@@ -365,11 +464,9 @@ def delete(self, transaction=True, safemode=None, force_parts=False):
         """
         Deletes the contents of the table and its dependent tables, recursively.
 
-        :param transaction: if True, use the entire delete becomes an atomic transaction.
-            This is the default and recommended behavior. Set to False if this delete is nested
-            within another transaction.
-        :param safemode: If True, prohibit nested transactions and prompt to confirm. Default
-            is dj.config['safemode'].
+        :param transaction: if True, use the entire delete becomes an atomic transaction. This is the default and
+                            recommended behavior. Set to False if this delete is nested within another transaction.
+        :param safemode: If True, prohibit nested transactions and prompt to confirm. Default is dj.config['safemode'].
         :param force_parts: Delete from parts even when not deleting from their masters.
         :return: number of deleted rows (excluding those from dependent tables)
         """
@@ -383,44 +480,69 @@ def cascade(table):
                     delete_count = table.delete_quick(get_count=True)
                 except IntegrityError as error:
                     match = foreign_key_error_regexp.match(error.args[0]).groupdict()
-                    if "`.`" not in match['child']:  # if schema name missing, use table
-                        match['child'] = '{}.{}'.format(table.full_table_name.split(".")[0],
-                                                        match['child'])
-                    if match['pk_attrs'] is not None:  # fully matched, adjusting the keys
-                        match['fk_attrs'] = [k.strip('`') for k in match['fk_attrs'].split(',')]
-                        match['pk_attrs'] = [k.strip('`') for k in match['pk_attrs'].split(',')]
+                    if "`.`" not in match["child"]:  # if schema name missing, use table
+                        match["child"] = "{}.{}".format(
+                            table.full_table_name.split(".")[0], match["child"]
+                        )
+                    if (
+                        match["pk_attrs"] is not None
+                    ):  # fully matched, adjusting the keys
+                        match["fk_attrs"] = [
+                            k.strip("`") for k in match["fk_attrs"].split(",")
+                        ]
+                        match["pk_attrs"] = [
+                            k.strip("`") for k in match["pk_attrs"].split(",")
+                        ]
                     else:  # only partially matched, querying with constraint to determine keys
-                        match['fk_attrs'], match['parent'], match['pk_attrs'] = list(map(
-                            list, zip(*table.connection.query(constraint_info_query, args=(
-                                match['name'].strip('`'),
-                                *[_.strip('`') for _ in match['child'].split('`.`')]
-                            )).fetchall())))
-                        match['parent'] = match['parent'][0]
+                        match["fk_attrs"], match["parent"], match["pk_attrs"] = list(
+                            map(
+                                list,
+                                zip(
+                                    *table.connection.query(
+                                        constraint_info_query,
+                                        args=(
+                                            match["name"].strip("`"),
+                                            *[
+                                                _.strip("`")
+                                                for _ in match["child"].split("`.`")
+                                            ],
+                                        ),
+                                    ).fetchall()
+                                ),
+                            )
+                        )
+                        match["parent"] = match["parent"][0]
 
                     # Restrict child by table if
                     #   1. if table's restriction attributes are not in child's primary key
                     #   2. if child renames any attributes
                     # Otherwise restrict child by table's restriction.
-                    child = FreeTable(table.connection, match['child'])
-                    if set(table.restriction_attributes) <= set(child.primary_key) and \
-                            match['fk_attrs'] == match['pk_attrs']:
+                    child = FreeTable(table.connection, match["child"])
+                    if (
+                        set(table.restriction_attributes) <= set(child.primary_key)
+                        and match["fk_attrs"] == match["pk_attrs"]
+                    ):
                         child._restriction = table._restriction
-                    elif match['fk_attrs'] != match['pk_attrs']:
-                        child &= table.proj(**dict(zip(match['fk_attrs'],
-                                                       match['pk_attrs'])))
+                    elif match["fk_attrs"] != match["pk_attrs"]:
+                        child &= table.proj(
+                            **dict(zip(match["fk_attrs"], match["pk_attrs"]))
+                        )
                     else:
                         child &= table.proj()
                     cascade(child)
                 else:
                     deleted.add(table.full_table_name)
-                    print("Deleting {count} rows from {table}".format(
-                        count=delete_count, table=table.full_table_name))
+                    print(
+                        "Deleting {count} rows from {table}".format(
+                            count=delete_count, table=table.full_table_name
+                        )
+                    )
                     break
             else:
-                raise DataJointError('Exceeded maximum number of delete attempts.')
+                raise DataJointError("Exceeded maximum number of delete attempts.")
             return delete_count
 
-        safemode = config['safemode'] if safemode is None else safemode
+        safemode = config["safemode"] if safemode is None else safemode
 
         # Start transaction
         if transaction:
@@ -432,7 +554,8 @@ def cascade(table):
                 else:
                     raise DataJointError(
                         "Delete cannot use a transaction within an ongoing transaction. "
-                        "Set transaction=False or safemode=False).")
+                        "Set transaction=False or safemode=False)."
+                    )
 
         # Cascading delete
         try:
@@ -450,26 +573,27 @@ def cascade(table):
                     if transaction:
                         self.connection.cancel_transaction()
                     raise DataJointError(
-                        'Attempt to delete part table {part} before deleting from '
-                        'its master {master} first.'.format(part=part, master=master))
+                        "Attempt to delete part table {part} before deleting from "
+                        "its master {master} first.".format(part=part, master=master)
+                    )
 
         # Confirm and commit
         if delete_count == 0:
             if safemode:
-                print('Nothing to delete.')
+                print("Nothing to delete.")
             if transaction:
                 self.connection.cancel_transaction()
         else:
-            if not safemode or user_choice("Commit deletes?", default='no') == 'yes':
+            if not safemode or user_choice("Commit deletes?", default="no") == "yes":
                 if transaction:
                     self.connection.commit_transaction()
                 if safemode:
-                    print('Deletes committed.')
+                    print("Deletes committed.")
             else:
                 if transaction:
                     self.connection.cancel_transaction()
                 if safemode:
-                    print('Deletes cancelled')
+                    print("Deletes cancelled")
         return delete_count
 
     def drop_quick(self):
@@ -478,12 +602,14 @@ def drop_quick(self):
         If the table has any dependent table(s), this call will fail with an error.
         """
         if self.is_declared:
-            query = 'DROP TABLE %s' % self.full_table_name
+            query = "DROP TABLE %s" % self.full_table_name
             self.connection.query(query)
             logger.info("Dropped table %s" % self.full_table_name)
             self._log(query[:255])
         else:
-            logger.info("Nothing to drop: table %s is not declared" % self.full_table_name)
+            logger.info(
+                "Nothing to drop: table %s is not declared" % self.full_table_name
+            )
 
     def drop(self):
         """
@@ -491,29 +617,35 @@ def drop(self):
         User is prompted for confirmation if config['safemode'] is set to True.
         """
         if self.restriction:
-            raise DataJointError('A table with an applied restriction cannot be dropped.'
-                                 ' Call drop() on the unrestricted Table.')
+            raise DataJointError(
+                "A table with an applied restriction cannot be dropped."
+                " Call drop() on the unrestricted Table."
+            )
         self.connection.dependencies.load()
         do_drop = True
-        tables = [table for table in self.connection.dependencies.descendants(
-            self.full_table_name) if not table.isdigit()]
+        tables = [
+            table
+            for table in self.connection.dependencies.descendants(self.full_table_name)
+            if not table.isdigit()
+        ]
 
         # avoid dropping part tables without their masters: See issue #374
         for part in tables:
             master = get_master(part)
             if master and master not in tables:
                 raise DataJointError(
-                    'Attempt to drop part table {part} before dropping '
-                    'its master. Drop {master} first.'.format(part=part, master=master))
+                    "Attempt to drop part table {part} before dropping "
+                    "its master. Drop {master} first.".format(part=part, master=master)
+                )
 
-        if config['safemode']:
+        if config["safemode"]:
             for table in tables:
-                print(table, '(%d tuples)' % len(FreeTable(self.connection, table)))
-            do_drop = user_choice("Proceed?", default='no') == 'yes'
+                print(table, "(%d tuples)" % len(FreeTable(self.connection, table)))
+            do_drop = user_choice("Proceed?", default="no") == "yes"
         if do_drop:
             for table in reversed(tables):
                 FreeTable(self.connection, table).drop_quick()
-            print('Tables dropped.  Restart kernel.')
+            print("Tables dropped.  Restart kernel.")
 
     @property
     def size_on_disk(self):
@@ -522,11 +654,16 @@ def size_on_disk(self):
         """
         ret = self.connection.query(
             'SHOW TABLE STATUS FROM `{database}` WHERE NAME="{table}"'.format(
-                database=self.database, table=self.table_name), as_dict=True).fetchone()
-        return ret['Data_length'] + ret['Index_length']
+                database=self.database, table=self.table_name
+            ),
+            as_dict=True,
+        ).fetchone()
+        return ret["Data_length"] + ret["Index_length"]
 
     def show_definition(self):
-        raise AttributeError('show_definition is deprecated. Use the describe method instead.')
+        raise AttributeError(
+            "show_definition is deprecated. Use the describe method instead."
+        )
 
     def describe(self, context=None, printout=True):
         """
@@ -540,108 +677,129 @@ def describe(self, context=None, printout=True):
             self.connection.dependencies.load()
         parents = self.parents(foreign_key_info=True)
         in_key = True
-        definition = ('# ' + self.heading.table_status['comment'] + '\n'
-                      if self.heading.table_status['comment'] else '')
+        definition = (
+            "# " + self.heading.table_status["comment"] + "\n"
+            if self.heading.table_status["comment"]
+            else ""
+        )
         attributes_thus_far = set()
         attributes_declared = set()
         indexes = self.heading.indexes.copy()
         for attr in self.heading.attributes.values():
             if in_key and not attr.in_key:
-                definition += '---\n'
+                definition += "---\n"
                 in_key = False
             attributes_thus_far.add(attr.name)
             do_include = True
             for parent_name, fk_props in parents:
-                if attr.name in fk_props['attr_map']:
+                if attr.name in fk_props["attr_map"]:
                     do_include = False
-                    if attributes_thus_far.issuperset(fk_props['attr_map']):
+                    if attributes_thus_far.issuperset(fk_props["attr_map"]):
                         # foreign key properties
                         try:
-                            index_props = indexes.pop(tuple(fk_props['attr_map']))
+                            index_props = indexes.pop(tuple(fk_props["attr_map"]))
                         except KeyError:
-                            index_props = ''
+                            index_props = ""
                         else:
                             index_props = [k for k, v in index_props.items() if v]
-                            index_props = ' [{}]'.format(', '.join(index_props)) if index_props else ''
+                            index_props = (
+                                " [{}]".format(", ".join(index_props))
+                                if index_props
+                                else ""
+                            )
 
-                        if not fk_props['aliased']:
+                        if not fk_props["aliased"]:
                             # simple foreign key
-                            definition += '->{props} {class_name}\n'.format(
+                            definition += "->{props} {class_name}\n".format(
                                 props=index_props,
-                                class_name=lookup_class_name(parent_name, context) or parent_name)
+                                class_name=lookup_class_name(parent_name, context)
+                                or parent_name,
+                            )
                         else:
                             # projected foreign key
-                            definition += '->{props} {class_name}.proj({proj_list})\n'.format(
-                                props=index_props,
-                                class_name=lookup_class_name(parent_name, context) or parent_name,
-                                proj_list=','.join(
-                                    '{}="{}"'.format(attr, ref)
-                                    for attr, ref in fk_props['attr_map'].items() if ref != attr))
-                            attributes_declared.update(fk_props['attr_map'])
+                            definition += (
+                                "->{props} {class_name}.proj({proj_list})\n".format(
+                                    props=index_props,
+                                    class_name=lookup_class_name(parent_name, context)
+                                    or parent_name,
+                                    proj_list=",".join(
+                                        '{}="{}"'.format(attr, ref)
+                                        for attr, ref in fk_props["attr_map"].items()
+                                        if ref != attr
+                                    ),
+                                )
+                            )
+                            attributes_declared.update(fk_props["attr_map"])
             if do_include:
                 attributes_declared.add(attr.name)
-                definition += '%-20s : %-28s %s\n' % (
-                    attr.name if attr.default is None else '%s=%s' % (attr.name, attr.default),
-                    '%s%s' % (attr.type, ' auto_increment' if attr.autoincrement else ''),
-                    '# ' + attr.comment if attr.comment else '')
+                definition += "%-20s : %-28s %s\n" % (
+                    attr.name
+                    if attr.default is None
+                    else "%s=%s" % (attr.name, attr.default),
+                    "%s%s"
+                    % (attr.type, " auto_increment" if attr.autoincrement else ""),
+                    "# " + attr.comment if attr.comment else "",
+                )
         # add remaining indexes
         for k, v in indexes.items():
-            definition += '{unique}INDEX ({attrs})\n'.format(
-                unique='UNIQUE ' if v['unique'] else '',
-                attrs=', '.join(k))
+            definition += "{unique}INDEX ({attrs})\n".format(
+                unique="UNIQUE " if v["unique"] else "", attrs=", ".join(k)
+            )
         if printout:
             print(definition)
         return definition
 
     def _update(self, attrname, value=None):
         """
-            This is a deprecated function to be removed in datajoint 0.14.
-            Use ``.update1`` instead.
-
-            Updates a field in one existing tuple. self must be restricted to exactly one entry.
-            In DataJoint the principal way of updating data is to delete and re-insert the
-            entire record and updates are reserved for corrective actions.
-            This is because referential integrity is observed on the level of entire
-            records rather than individual attributes.
-
-            Safety constraints:
-               1. self must be restricted to exactly one tuple
-               2. the update attribute must not be in primary key
-
-            Example:
-            >>> (v2p.Mice() & key)._update('mouse_dob', '2011-01-01')
-            >>> (v2p.Mice() & key)._update( 'lens')   # set the value to NULL
+        This is a deprecated function to be removed in datajoint 0.14.
+        Use ``.update1`` instead.
+
+        Updates a field in one existing tuple. self must be restricted to exactly one entry.
+        In DataJoint the principal way of updating data is to delete and re-insert the
+        entire record and updates are reserved for corrective actions.
+        This is because referential integrity is observed on the level of entire
+        records rather than individual attributes.
+
+        Safety constraints:
+           1. self must be restricted to exactly one tuple
+           2. the update attribute must not be in primary key
+
+        Example:
+        >>> (v2p.Mice() & key)._update('mouse_dob', '2011-01-01')
+        >>> (v2p.Mice() & key)._update( 'lens')   # set the value to NULL
         """
         warnings.warn(
-            '`_update` is a deprecated function to be removed in datajoint 0.14. '
-            'Use `.update1` instead.')
+            "`_update` is a deprecated function to be removed in datajoint 0.14. "
+            "Use `.update1` instead."
+        )
         if len(self) != 1:
-            raise DataJointError('Update is only allowed on one tuple at a time')
+            raise DataJointError("Update is only allowed on one tuple at a time")
         if attrname not in self.heading:
-            raise DataJointError('Invalid attribute name')
+            raise DataJointError("Invalid attribute name")
         if attrname in self.heading.primary_key:
-            raise DataJointError('Cannot update a key value.')
+            raise DataJointError("Cannot update a key value.")
 
         attr = self.heading[attrname]
 
         if attr.is_blob:
             value = blob.pack(value)
-            placeholder = '%s'
+            placeholder = "%s"
         elif attr.numeric:
             if value is None or np.isnan(float(value)):  # nans are turned into NULLs
-                placeholder = 'NULL'
+                placeholder = "NULL"
                 value = None
             else:
-                placeholder = '%s'
+                placeholder = "%s"
                 value = str(int(value) if isinstance(value, bool) else value)
         else:
-            placeholder = '%s' if value is not None else 'NULL'
+            placeholder = "%s" if value is not None else "NULL"
         command = "UPDATE {full_table_name} SET `{attrname}`={placeholder} {where_clause}".format(
             full_table_name=self.from_clause(),
             attrname=attrname,
             placeholder=placeholder,
-            where_clause=self.where_clause())
-        self.connection.query(command, args=(value, ) if value is not None else ())
+            where_clause=self.where_clause(),
+        )
+        self.connection.query(command, args=(value,) if value is not None else ())
 
     # --- private helper functions ----
     def __make_placeholder(self, name, value, ignore_extra_fields=False):
@@ -649,6 +807,7 @@ def __make_placeholder(self, name, value, ignore_extra_fields=False):
         For a given attribute `name` with `value`, return its processed value or value placeholder
         as a string to be included in the query and the value, if any, to be submitted for
         processing by mysql API.
+
         :param name:  name of attribute to be inserted
         :param value: value of attribute to be inserted
         """
@@ -657,31 +816,45 @@ def __make_placeholder(self, name, value, ignore_extra_fields=False):
         attr = self.heading[name]
         if attr.adapter:
             value = attr.adapter.put(value)
-        if value is None or (attr.numeric and (value == '' or np.isnan(float(value)))):
+        if value is None or (attr.numeric and (value == "" or np.isnan(float(value)))):
             # set default value
-            placeholder, value = 'DEFAULT', None
+            placeholder, value = "DEFAULT", None
         else:  # not NULL
-            placeholder = '%s'
+            placeholder = "%s"
             if attr.uuid:
                 if not isinstance(value, uuid.UUID):
                     try:
                         value = uuid.UUID(value)
                     except (AttributeError, ValueError):
                         raise DataJointError(
-                            'badly formed UUID value {v} for attribute `{n}`'.format(v=value,
-                                                                                     n=name))
+                            "badly formed UUID value {v} for attribute `{n}`".format(
+                                v=value, n=name
+                            )
+                        )
                 value = value.bytes
             elif attr.is_blob:
                 value = blob.pack(value)
-                value = self.external[attr.store].put(value).bytes if attr.is_external else value
+                value = (
+                    self.external[attr.store].put(value).bytes
+                    if attr.is_external
+                    else value
+                )
             elif attr.is_attachment:
                 attachment_path = Path(value)
                 if attr.is_external:
                     # value is hash of contents
-                    value = self.external[attr.store].upload_attachment(attachment_path).bytes
+                    value = (
+                        self.external[attr.store]
+                        .upload_attachment(attachment_path)
+                        .bytes
+                    )
                 else:
                     # value is filename + contents
-                    value = str.encode(attachment_path.name) + b'\0' + attachment_path.read_bytes()
+                    value = (
+                        str.encode(attachment_path.name)
+                        + b"\0"
+                        + attachment_path.read_bytes()
+                    )
             elif attr.is_filepath:
                 value = self.external[attr.store].upload_filepath(value).bytes
             elif attr.numeric:
@@ -691,6 +864,7 @@ def __make_placeholder(self, name, value, ignore_extra_fields=False):
     def __make_row_to_insert(self, row, field_list, ignore_extra_fields):
         """
         Helper function for insert and update
+
         :param row:  A tuple to insert
         :return: a dict with fields 'names', 'placeholders', 'values'
         """
@@ -698,88 +872,117 @@ def __make_row_to_insert(self, row, field_list, ignore_extra_fields):
         def check_fields(fields):
             """
             Validates that all items in `fields` are valid attributes in the heading
+
             :param fields: field names of a tuple
             """
             if not field_list:
                 if not ignore_extra_fields:
                     for field in fields:
                         if field not in self.heading:
-                            raise KeyError(u'`{0:s}` is not in the table heading'.format(field))
+                            raise KeyError(
+                                "`{0:s}` is not in the table heading".format(field)
+                            )
             elif set(field_list) != set(fields).intersection(self.heading.names):
-                raise DataJointError('Attempt to insert rows with different fields.')
+                raise DataJointError("Attempt to insert rows with different fields.")
 
         if isinstance(row, np.void):  # np.array
             check_fields(row.dtype.fields)
-            attributes = [self.__make_placeholder(name, row[name], ignore_extra_fields)
-                          for name in self.heading if name in row.dtype.fields]
+            attributes = [
+                self.__make_placeholder(name, row[name], ignore_extra_fields)
+                for name in self.heading
+                if name in row.dtype.fields
+            ]
         elif isinstance(row, collections.abc.Mapping):  # dict-based
             check_fields(row)
-            attributes = [self.__make_placeholder(name, row[name], ignore_extra_fields)
-                          for name in self.heading if name in row]
+            attributes = [
+                self.__make_placeholder(name, row[name], ignore_extra_fields)
+                for name in self.heading
+                if name in row
+            ]
         else:  # positional
             try:
                 if len(row) != len(self.heading):
                     raise DataJointError(
-                        'Invalid insert argument. Incorrect number of attributes: '
-                        '{given} given; {expected} expected'.format(
-                            given=len(row), expected=len(self.heading)))
+                        "Invalid insert argument. Incorrect number of attributes: "
+                        "{given} given; {expected} expected".format(
+                            given=len(row), expected=len(self.heading)
+                        )
+                    )
             except TypeError:
-                raise DataJointError('Datatype %s cannot be inserted' % type(row))
+                raise DataJointError("Datatype %s cannot be inserted" % type(row))
             else:
-                attributes = [self.__make_placeholder(name, value, ignore_extra_fields)
-                              for name, value in zip(self.heading, row)]
+                attributes = [
+                    self.__make_placeholder(name, value, ignore_extra_fields)
+                    for name, value in zip(self.heading, row)
+                ]
         if ignore_extra_fields:
             attributes = [a for a in attributes if a is not None]
 
-        assert len(attributes), 'Empty tuple'
-        row_to_insert = dict(zip(('names', 'placeholders', 'values'), zip(*attributes)))
+        assert len(attributes), "Empty tuple"
+        row_to_insert = dict(zip(("names", "placeholders", "values"), zip(*attributes)))
         if not field_list:
             # first row sets the composition of the field list
-            field_list.extend(row_to_insert['names'])
+            field_list.extend(row_to_insert["names"])
         else:
             #  reorder attributes in row_to_insert to match field_list
-            order = list(row_to_insert['names'].index(field) for field in field_list)
-            row_to_insert['names'] = list(row_to_insert['names'][i] for i in order)
-            row_to_insert['placeholders'] = list(row_to_insert['placeholders'][i] for i in order)
-            row_to_insert['values'] = list(row_to_insert['values'][i] for i in order)
+            order = list(row_to_insert["names"].index(field) for field in field_list)
+            row_to_insert["names"] = list(row_to_insert["names"][i] for i in order)
+            row_to_insert["placeholders"] = list(
+                row_to_insert["placeholders"][i] for i in order
+            )
+            row_to_insert["values"] = list(row_to_insert["values"][i] for i in order)
         return row_to_insert
 
 
 def lookup_class_name(name, context, depth=3):
     """
     given a table name in the form `schema_name`.`table_name`, find its class in the context.
+
     :param name: `schema_name`.`table_name`
     :param context: dictionary representing the namespace
     :param depth: search depth into imported modules, helps avoid infinite recursion.
     :return: class name found in the context or None if not found
     """
     # breadth-first search
-    nodes = [dict(context=context, context_name='', depth=depth)]
+    nodes = [dict(context=context, context_name="", depth=depth)]
     while nodes:
         node = nodes.pop(0)
-        for member_name, member in node['context'].items():
-            if not member_name.startswith('_'):  # skip IPython's implicit variables
+        for member_name, member in node["context"].items():
+            if not member_name.startswith("_"):  # skip IPython's implicit variables
                 if inspect.isclass(member) and issubclass(member, Table):
-                    if member.full_table_name == name:   # found it!
-                        return '.'.join([node['context_name'],  member_name]).lstrip('.')
+                    if member.full_table_name == name:  # found it!
+                        return ".".join([node["context_name"], member_name]).lstrip(".")
                     try:  # look for part tables
                         parts = member.__dict__
                     except AttributeError:
                         pass  # not a UserTable -- cannot have part tables.
                     else:
-                        for part in (getattr(member, p) for p in parts
-                                     if p[0].isupper() and hasattr(member, p)):
-                            if inspect.isclass(part) and issubclass(part, Table) and \
-                                    part.full_table_name == name:
-                                return '.'.join([node['context_name'],
-                                                 member_name, part.__name__]).lstrip('.')
-                elif node['depth'] > 0 and inspect.ismodule(member) and \
-                        member.__name__ != 'datajoint':
+                        for part in (
+                            getattr(member, p)
+                            for p in parts
+                            if p[0].isupper() and hasattr(member, p)
+                        ):
+                            if (
+                                inspect.isclass(part)
+                                and issubclass(part, Table)
+                                and part.full_table_name == name
+                            ):
+                                return ".".join(
+                                    [node["context_name"], member_name, part.__name__]
+                                ).lstrip(".")
+                elif (
+                    node["depth"] > 0
+                    and inspect.ismodule(member)
+                    and member.__name__ != "datajoint"
+                ):
                     try:
                         nodes.append(
-                            dict(context=dict(inspect.getmembers(member)),
-                                 context_name=node['context_name'] + '.' + member_name,
-                                 depth=node['depth']-1))
+                            dict(
+                                context=dict(inspect.getmembers(member)),
+                                context_name=node["context_name"] + "." + member_name,
+                                depth=node["depth"] - 1,
+                            )
+                        )
                     except ImportError:
                         pass  # could not import, so do not attempt
     return None
@@ -789,42 +992,52 @@ class FreeTable(Table):
     """
     A base relation without a dedicated class. Each instance is associated with a table
     specified by full_table_name.
+
     :param conn:  a dj.Connection object
     :param full_table_name: in format `database`.`table_name`
     """
+
     def __init__(self, conn, full_table_name):
-        self.database, self._table_name = (s.strip('`') for s in full_table_name.split('.'))
+        self.database, self._table_name = (
+            s.strip("`") for s in full_table_name.split(".")
+        )
         self._connection = conn
         self._support = [full_table_name]
-        self._heading = Heading(table_info=dict(
-            conn=conn,
-            database=self.database,
-            table_name=self.table_name,
-            context=None))
+        self._heading = Heading(
+            table_info=dict(
+                conn=conn,
+                database=self.database,
+                table_name=self.table_name,
+                context=None,
+            )
+        )
 
     def __repr__(self):
-        return "FreeTable(`%s`.`%s`)\n" % (self.database, self._table_name) + super().__repr__()
+        return (
+            "FreeTable(`%s`.`%s`)\n" % (self.database, self._table_name)
+            + super().__repr__()
+        )
 
 
 class Log(Table):
     """
     The log table for each schema.
     Instances are callable.  Calls log the time and identifying information along with the event.
+
     :param skip_logging: if True, then log entry is skipped by default. See __call__
     """
 
-    _table_name = '~log'
+    _table_name = "~log"
 
     def __init__(self, conn, database, skip_logging=False):
         self.database = database
         self.skip_logging = skip_logging
         self._connection = conn
-        self._heading = Heading(table_info=dict(
-            conn=conn,
-            database=database,
-            table_name=self.table_name,
-            context=None
-        ))
+        self._heading = Heading(
+            table_info=dict(
+                conn=conn, database=database, table_name=self.table_name, context=None
+            )
+        )
         self._support = [self.full_table_name]
 
         self._definition = """    # event logging table for `{database}`
@@ -835,7 +1048,9 @@ def __init__(self, conn, database, skip_logging=False):
         user     :varchar(255)                    # user@host
         host=""  :varchar(255)                    # system hostname
         event="" :varchar(255)                    # event message
-        """.format(database=database)
+        """.format(
+            database=database
+        )
 
         super().__init__()
 
@@ -850,23 +1065,30 @@ def definition(self):
 
     def __call__(self, event, skip_logging=None):
         """
+
         :param event: string to write into the log table
         :param skip_logging: If True then do not log. If None, then use self.skip_logging
         """
         skip_logging = self.skip_logging if skip_logging is None else skip_logging
         if not skip_logging:
             try:
-                self.insert1(dict(
-                    user=self._user,
-                    version=version + 'py',
-                    host=platform.uname().node,
-                    event=event), skip_duplicates=True, ignore_extra_fields=True)
+                self.insert1(
+                    dict(
+                        user=self._user,
+                        version=version + "py",
+                        host=platform.uname().node,
+                        event=event,
+                    ),
+                    skip_duplicates=True,
+                    ignore_extra_fields=True,
+                )
             except DataJointError:
-                logger.info('could not log event in table ~log')
+                logger.info("could not log event in table ~log")
 
     def delete(self):
         """
         bypass interactive prompts and cascading dependencies
+
         :return: number of deleted items
         """
         return self.delete_quick(get_count=True)
diff --git a/datajoint/user_tables.py b/datajoint/user_tables.py
index 76cea5fbf..3c8a186e2 100644
--- a/datajoint/user_tables.py
+++ b/datajoint/user_tables.py
@@ -7,16 +7,39 @@
 from .utils import from_camel_case, ClassProperty
 from .errors import DataJointError
 
-_base_regexp = r'[a-z][a-z0-9]*(_[a-z][a-z0-9]*)*'
+_base_regexp = r"[a-z][a-z0-9]*(_[a-z][a-z0-9]*)*"
 
 # attributes that trigger instantiation of user classes
 
 
 supported_class_attrs = {
-    'key_source', 'describe', 'alter', 'heading', 'populate', 'progress', 'primary_key',
-    'proj', 'aggr', 'join', 'fetch', 'fetch1', 'head', 'tail',
-    'descendants', 'ancestors', 'parts', 'parents', 'children',
-    'insert', 'insert1', 'update1', 'drop', 'drop_quick', 'delete', 'delete_quick'}
+    "key_source",
+    "describe",
+    "alter",
+    "heading",
+    "populate",
+    "progress",
+    "primary_key",
+    "proj",
+    "aggr",
+    "join",
+    "fetch",
+    "fetch1",
+    "head",
+    "tail",
+    "descendants",
+    "ancestors",
+    "parts",
+    "parents",
+    "children",
+    "insert",
+    "insert1",
+    "update1",
+    "drop",
+    "drop_quick",
+    "delete",
+    "delete_quick",
+}
 
 
 class TableMeta(type):
@@ -24,10 +47,14 @@ class TableMeta(type):
     TableMeta subclasses allow applying some instance methods and properties directly
     at class level. For example, this allows Table.fetch() instead of Table().fetch().
     """
+
     def __getattribute__(cls, name):
         # trigger instantiation for supported class attrs
-        return (cls().__getattribute__(name) if name in supported_class_attrs
-                else super().__getattribute__(name))
+        return (
+            cls().__getattribute__(name)
+            if name in supported_class_attrs
+            else super().__getattribute__(name)
+        )
 
     def __and__(cls, arg):
         return cls() & arg
@@ -59,6 +86,7 @@ class UserTable(Table, metaclass=TableMeta):
     A subclass of UserTable is a dedicated class interfacing a base relation.
     UserTable is initialized by the decorator generated by schema().
     """
+
     # set by @schema
     _connection = None
     _heading = None
@@ -74,7 +102,8 @@ def definition(self):
         :return: a string containing the table definition using the DataJoint DDL.
         """
         raise NotImplementedError(
-            'Subclasses of Table must implement the property "definition"')
+            'Subclasses of Table must implement the property "definition"'
+        )
 
     @ClassProperty
     def connection(cls):
@@ -86,7 +115,7 @@ def table_name(cls):
         :return: the table name of the table formatted for mysql.
         """
         if cls._prefix is None:
-            raise AttributeError('Class prefix is not defined!')
+            raise AttributeError("Class prefix is not defined!")
         return cls._prefix + from_camel_case(cls.__name__)
 
     @ClassProperty
@@ -95,8 +124,9 @@ def full_table_name(cls):
             # for derived classes only
             if cls.database is None:
                 raise DataJointError(
-                    'Class %s is not properly declared (schema decorator not applied?)' %
-                    cls.__name__)
+                    "Class %s is not properly declared (schema decorator not applied?)"
+                    % cls.__name__
+                )
             return r"`{0:s}`.`{1:s}`".format(cls.database, cls.table_name)
 
 
@@ -104,8 +134,9 @@ class Manual(UserTable):
     """
     Inherit from this class if the table's values are entered manually.
     """
-    _prefix = r''
-    tier_regexp = r'(?P<manual>' + _prefix + _base_regexp + ')'
+
+    _prefix = r""
+    tier_regexp = r"(?P<manual>" + _prefix + _base_regexp + ")"
 
 
 class Lookup(UserTable):
@@ -114,8 +145,11 @@ class Lookup(UserTable):
     currently equivalent to defining the table as Manual and serves semantic
     purposes only.
     """
-    _prefix = '#'
-    tier_regexp = r'(?P<lookup>' + _prefix + _base_regexp.replace('TIER', 'lookup') + ')'
+
+    _prefix = "#"
+    tier_regexp = (
+        r"(?P<lookup>" + _prefix + _base_regexp.replace("TIER", "lookup") + ")"
+    )
 
 
 class Imported(UserTable, AutoPopulate):
@@ -123,8 +157,9 @@ class Imported(UserTable, AutoPopulate):
     Inherit from this class if the table's values are imported from external data sources.
     The inherited class must at least provide the function `_make_tuples`.
     """
-    _prefix = '_'
-    tier_regexp = r'(?P<imported>' + _prefix + _base_regexp + ')'
+
+    _prefix = "_"
+    tier_regexp = r"(?P<imported>" + _prefix + _base_regexp + ")"
 
 
 class Computed(UserTable, AutoPopulate):
@@ -132,8 +167,9 @@ class Computed(UserTable, AutoPopulate):
     Inherit from this class if the table's values are computed from other relations in the schema.
     The inherited class must at least provide the function `_make_tuples`.
     """
-    _prefix = '__'
-    tier_regexp = r'(?P<computed>' + _prefix + _base_regexp + ')'
+
+    _prefix = "__"
+    tier_regexp = r"(?P<computed>" + _prefix + _base_regexp + ")"
 
 
 class Part(UserTable):
@@ -147,9 +183,15 @@ class Part(UserTable):
     _connection = None
     _master = None
 
-    tier_regexp = r'(?P<master>' + '|'.join(
-        [c.tier_regexp for c in (Manual, Lookup, Imported, Computed)]
-    ) + r'){1,1}' + '__' + r'(?P<part>' + _base_regexp + ')'
+    tier_regexp = (
+        r"(?P<master>"
+        + "|".join([c.tier_regexp for c in (Manual, Lookup, Imported, Computed)])
+        + r"){1,1}"
+        + "__"
+        + r"(?P<part>"
+        + _base_regexp
+        + ")"
+    )
 
     @ClassProperty
     def connection(cls):
@@ -157,8 +199,11 @@ def connection(cls):
 
     @ClassProperty
     def full_table_name(cls):
-        return None if cls.database is None or cls.table_name is None else r"`{0:s}`.`{1:s}`".format(
-            cls.database, cls.table_name)
+        return (
+            None
+            if cls.database is None or cls.table_name is None
+            else r"`{0:s}`.`{1:s}`".format(cls.database, cls.table_name)
+        )
 
     @ClassProperty
     def master(cls):
@@ -166,7 +211,11 @@ def master(cls):
 
     @ClassProperty
     def table_name(cls):
-        return None if cls.master is None else cls.master.table_name + '__' + from_camel_case(cls.__name__)
+        return (
+            None
+            if cls.master is None
+            else cls.master.table_name + "__" + from_camel_case(cls.__name__)
+        )
 
     def delete(self, force=False):
         """
@@ -176,7 +225,8 @@ def delete(self, force=False):
             super().delete(force_parts=True)
         else:
             raise DataJointError(
-                'Cannot delete from a Part directly. Delete from master instead')
+                "Cannot delete from a Part directly. Delete from master instead"
+            )
 
     def drop(self, force=False):
         """
@@ -185,4 +235,6 @@ def drop(self, force=False):
         if force:
             super().drop()
         else:
-            raise DataJointError('Cannot drop a Part directly.  Delete from master instead')
+            raise DataJointError(
+                "Cannot drop a Part directly.  Delete from master instead"
+            )
diff --git a/datajoint/utils.py b/datajoint/utils.py
index 057e7e820..adf160918 100644
--- a/datajoint/utils.py
+++ b/datajoint/utils.py
@@ -17,16 +17,19 @@ def __get__(self, obj, owner):
 def user_choice(prompt, choices=("yes", "no"), default=None):
     """
     Prompts the user for confirmation.  The default value, if any, is capitalized.
+
     :param prompt: Information to display to the user.
     :param choices: an iterable of possible choices.
     :param default: default choice
     :return: the user's choice
     """
     assert default is None or default in choices
-    choice_list = ', '.join((choice.title() if choice == default else choice for choice in choices))
+    choice_list = ", ".join(
+        (choice.title() if choice == default else choice for choice in choices)
+    )
     response = None
     while response not in choices:
-        response = input(prompt + ' [' + choice_list + ']: ')
+        response = input(prompt + " [" + choice_list + "]: ")
         response = response.lower() if response else default
     return response
 
@@ -46,13 +49,14 @@ def get_master(full_table_name: str) -> str:
     :return: Supposed master full table name or empty string if not a part table name.
     :rtype: str
     """
-    match = re.match(r'(?P<master>`\w+`.`\w+)__(?P<part>\w+)`', full_table_name)
-    return match['master'] + '`' if match else ''
+    match = re.match(r"(?P<master>`\w+`.`\w+)__(?P<part>\w+)`", full_table_name)
+    return match["master"] + "`" if match else ""
 
 
 def to_camel_case(s):
     """
     Convert names with under score (_) separation into camel case names.
+
     :param s: string in under_score notation
     :returns: string in CamelCase notation
     Example:
@@ -62,12 +66,13 @@ def to_camel_case(s):
     def to_upper(match):
         return match.group(0)[-1].upper()
 
-    return re.sub(r'(^|[_\W])+[a-zA-Z]', to_upper, s)
+    return re.sub(r"(^|[_\W])+[a-zA-Z]", to_upper, s)
 
 
 def from_camel_case(s):
     """
     Convert names in camel case into underscore (_) separated names
+
     :param s: string in CamelCase notation
     :returns: string in under_score notation
     Example:
@@ -75,24 +80,26 @@ def from_camel_case(s):
     """
 
     def convert(match):
-        return ('_' if match.groups()[0] else '') + match.group(0).lower()
+        return ("_" if match.groups()[0] else "") + match.group(0).lower()
 
-    if not re.match(r'[A-Z][a-zA-Z0-9]*', s):
+    if not re.match(r"[A-Z][a-zA-Z0-9]*", s):
         raise DataJointError(
-            'ClassName must be alphanumeric in CamelCase, begin with a capital letter')
-    return re.sub(r'(\B[A-Z])|(\b[A-Z])', convert, s)
+            "ClassName must be alphanumeric in CamelCase, begin with a capital letter"
+        )
+    return re.sub(r"(\B[A-Z])|(\b[A-Z])", convert, s)
 
 
 def safe_write(filepath, blob):
     """
     A two-step write.
+
     :param filename: full path
     :param blob: binary data
     """
     filepath = Path(filepath)
     if not filepath.is_file():
         filepath.parent.mkdir(parents=True, exist_ok=True)
-        temp_file = filepath.with_suffix(filepath.suffix + '.saving')
+        temp_file = filepath.with_suffix(filepath.suffix + ".saving")
         temp_file.write_bytes(blob)
         temp_file.rename(filepath)
 
@@ -104,7 +111,7 @@ def safe_copy(src, dest, overwrite=False):
     src, dest = Path(src), Path(dest)
     if not (dest.exists() and src.samefile(dest)) and (overwrite or not dest.is_file()):
         dest.parent.mkdir(parents=True, exist_ok=True)
-        temp_file = dest.with_suffix(dest.suffix + '.copying')
+        temp_file = dest.with_suffix(dest.suffix + ".copying")
         shutil.copyfile(str(src), str(temp_file))
         temp_file.rename(dest)
 
@@ -113,16 +120,16 @@ def parse_sql(filepath):
     """
     yield SQL statements from an SQL file
     """
-    delimiter = ';'
+    delimiter = ";"
     statement = []
-    with Path(filepath).open('rt') as f:
+    with Path(filepath).open("rt") as f:
         for line in f:
             line = line.strip()
-            if not line.startswith('--') and len(line) > 1:
-                if line.startswith('delimiter'):
+            if not line.startswith("--") and len(line) > 1:
+                if line.startswith("delimiter"):
                     delimiter = line.split()[1]
                 else:
                     statement.append(line)
                     if line.endswith(delimiter):
-                        yield ' '.join(statement)
+                        yield " ".join(statement)
                         statement = []
diff --git a/datajoint/version.py b/datajoint/version.py
index 6253805eb..16c0e8ac1 100644
--- a/datajoint/version.py
+++ b/datajoint/version.py
@@ -1,3 +1,3 @@
-__version__ = "0.13.3"
+__version__ = "0.13.5"
 
 assert len(__version__) <= 10  # The log table limits version to the 10 characters
diff --git a/docs-api/Makefile b/docs-api/Makefile
new file mode 100644
index 000000000..d0c3cbf10
--- /dev/null
+++ b/docs-api/Makefile
@@ -0,0 +1,20 @@
+# Minimal makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line, and also
+# from the environment for the first two.
+SPHINXOPTS    ?=
+SPHINXBUILD   ?= sphinx-build
+SOURCEDIR     = source
+BUILDDIR      = build
+
+# Put it first so that "make" without argument is like "make help".
+help:
+	@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
+
+.PHONY: help Makefile
+
+# Catch-all target: route all unknown targets to Sphinx using the new
+# "make mode" option.  $(O) is meant as a shortcut for $(SPHINXOPTS).
+%: Makefile
+	@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
diff --git a/docs-api/apt_requirements.txt b/docs-api/apt_requirements.txt
new file mode 100644
index 000000000..ac08cdcf9
--- /dev/null
+++ b/docs-api/apt_requirements.txt
@@ -0,0 +1,1 @@
+make
\ No newline at end of file
diff --git a/docs-api/docker-compose.yaml b/docs-api/docker-compose.yaml
new file mode 100644
index 000000000..4124b9139
--- /dev/null
+++ b/docs-api/docker-compose.yaml
@@ -0,0 +1,27 @@
+# HOST_UID=1000 docker-compose -f ./docs-api/docker-compose.yaml up --build
+# docker exec -it docs-api_docs-builder_1 bash -c "pip install -U /main/datajoint-python && rm -R build && make html"
+version: '2.4'
+services:
+  # example how to build
+  docs-builder:
+    # build: .
+    image: datajoint/djbase:py3.9-debian
+    user: ${HOST_UID}:anaconda
+    working_dir: /main/docs-api
+    volumes:
+      - .:/main/docs-api
+      - ./pip_requirements.txt:/tmp/pip_requirements.txt
+      - ./apt_requirements.txt:/tmp/apt_requirements.txt
+      - ../datajoint:/main/datajoint-python/datajoint:ro
+      - ../setup.py:/main/datajoint-python/setup.py:ro
+      - ../requirements.txt:/main/datajoint-python/requirements.txt
+    command:
+      - bash
+      - -c
+      - |
+        pip install /main/datajoint-python
+        make html
+        chmod -R o=rwx /main/docs-api/build/html
+        # tail -f /dev/null
+        # sphinx-quickstart 
+        # sphinx-apidoc -o ./source ../datajoint-python/datajoint
diff --git a/docs-api/make.bat b/docs-api/make.bat
new file mode 100644
index 000000000..747ffb7b3
--- /dev/null
+++ b/docs-api/make.bat
@@ -0,0 +1,35 @@
+@ECHO OFF
+
+pushd %~dp0
+
+REM Command file for Sphinx documentation
+
+if "%SPHINXBUILD%" == "" (
+	set SPHINXBUILD=sphinx-build
+)
+set SOURCEDIR=source
+set BUILDDIR=build
+
+%SPHINXBUILD% >NUL 2>NUL
+if errorlevel 9009 (
+	echo.
+	echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
+	echo.installed, then set the SPHINXBUILD environment variable to point
+	echo.to the full path of the 'sphinx-build' executable. Alternatively you
+	echo.may add the Sphinx directory to PATH.
+	echo.
+	echo.If you don't have Sphinx installed, grab it from
+	echo.https://www.sphinx-doc.org/
+	exit /b 1
+)
+
+if "%1" == "" goto help
+
+%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
+goto end
+
+:help
+%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
+
+:end
+popd
diff --git a/docs-api/pip_requirements.txt b/docs-api/pip_requirements.txt
new file mode 100644
index 000000000..856b3f4cb
--- /dev/null
+++ b/docs-api/pip_requirements.txt
@@ -0,0 +1,2 @@
+sphinx
+sphinx_rtd_theme
\ No newline at end of file
diff --git a/docs-api/source/conf.py b/docs-api/source/conf.py
new file mode 100644
index 000000000..2b114821e
--- /dev/null
+++ b/docs-api/source/conf.py
@@ -0,0 +1,51 @@
+# Configuration file for the Sphinx documentation builder.
+#
+# This file only contains a selection of the most common options. For a full
+# list see the documentation:
+# https://www.sphinx-doc.org/en/master/usage/configuration.html
+
+# -- Path setup --------------------------------------------------------------
+
+# 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
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+#
+import os
+import sys
+sys.path.insert(0, os.path.abspath('../datajoint'))
+
+
+# -- Project information -----------------------------------------------------
+
+project = 'DataJoint API Docs'
+copyright = '2022, DataJoint contributors'
+author = 'DataJoint contributors'
+
+
+# -- General configuration ---------------------------------------------------
+
+# 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.autodoc']
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['_templates']
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+# This pattern also affects html_static_path and html_extra_path.
+exclude_patterns = []
+
+
+# -- Options for HTML output -------------------------------------------------
+
+# The theme to use for HTML and HTML Help pages.  See the documentation for
+# a list of builtin themes.
+#
+html_theme = 'sphinx_rtd_theme'
+
+# 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']
\ No newline at end of file
diff --git a/docs-api/source/index.rst b/docs-api/source/index.rst
new file mode 100644
index 000000000..b733b02f8
--- /dev/null
+++ b/docs-api/source/index.rst
@@ -0,0 +1,28 @@
+.. DataJoint API Docs documentation master file, created by
+   sphinx-quickstart on Tue Apr 26 22:08:07 2022.
+   You can adapt this file completely to your liking, but it should at least
+   contain the root `toctree` directive.
+
+Welcome to DataJoint's API Documentation!
+=========================================
+
+datajoint module
+----------------
+
+.. automodule:: datajoint
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+.. toctree::
+   :maxdepth: 2
+   :caption: Submodules:
+   
+   submodules
+
+Indices and tables
+==================
+
+* :ref:`genindex`
+* :ref:`modindex`
+* :ref:`search`
diff --git a/docs-api/source/submodules.rst b/docs-api/source/submodules.rst
new file mode 100644
index 000000000..45105d3dd
--- /dev/null
+++ b/docs-api/source/submodules.rst
@@ -0,0 +1,207 @@
+datajoint.admin module
+----------------------
+
+.. automodule:: datajoint.admin
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+datajoint.attribute\_adapter module
+-----------------------------------
+
+.. automodule:: datajoint.attribute_adapter
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+datajoint.autopopulate module
+-----------------------------
+
+.. automodule:: datajoint.autopopulate
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+datajoint.blob module
+---------------------
+
+.. automodule:: datajoint.blob
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+datajoint.condition module
+--------------------------
+
+.. automodule:: datajoint.condition
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+datajoint.connection module
+---------------------------
+
+.. automodule:: datajoint.connection
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+datajoint.declare module
+------------------------
+
+.. automodule:: datajoint.declare
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+datajoint.dependencies module
+-----------------------------
+
+.. automodule:: datajoint.dependencies
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+datajoint.diagram module
+------------------------
+
+.. automodule:: datajoint.diagram
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+datajoint.errors module
+-----------------------
+
+.. automodule:: datajoint.errors
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+datajoint.expression module
+---------------------------
+
+.. automodule:: datajoint.expression
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+datajoint.external module
+-------------------------
+
+.. automodule:: datajoint.external
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+datajoint.fetch module
+----------------------
+
+.. automodule:: datajoint.fetch
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+datajoint.hash module
+---------------------
+
+.. automodule:: datajoint.hash
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+datajoint.heading module
+------------------------
+
+.. automodule:: datajoint.heading
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+datajoint.jobs module
+---------------------
+
+.. automodule:: datajoint.jobs
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+datajoint.migrate module
+------------------------
+
+.. automodule:: datajoint.migrate
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+datajoint.plugin module
+-----------------------
+
+.. automodule:: datajoint.plugin
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+datajoint.preview module
+------------------------
+
+.. automodule:: datajoint.preview
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+datajoint.s3 module
+-------------------
+
+.. automodule:: datajoint.s3
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+datajoint.schemas module
+------------------------
+
+.. automodule:: datajoint.schemas
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+datajoint.settings module
+-------------------------
+
+.. automodule:: datajoint.settings
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+datajoint.table module
+----------------------
+
+.. automodule:: datajoint.table
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+datajoint.user\_tables module
+-----------------------------
+
+.. automodule:: datajoint.user_tables
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+datajoint.utils module
+----------------------
+
+.. automodule:: datajoint.utils
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+datajoint.version module
+------------------------
+
+.. automodule:: datajoint.version
+   :members:
+   :undoc-members:
+   :show-inheritance:
diff --git a/docs-parts/intro/Releases_lang1.rst b/docs-parts/intro/Releases_lang1.rst
index 286096437..219ab8ae7 100644
--- a/docs-parts/intro/Releases_lang1.rst
+++ b/docs-parts/intro/Releases_lang1.rst
@@ -1,3 +1,16 @@
+0.13.5 -- May 19, 2022
+----------------------
+* Update - Import ABC from collections.abc for Python 3.10 compatibility
+* Bugfix - Fix multiprocessing value error (#1013) PR #1026
+
+0.13.4 -- March 28, 2022
+----------------------
+* Add - Allow reading blobs produced by legacy 32-bit compiled mYm library for matlab. PR #995
+* Bugfix - Add missing ``jobs`` argument for multiprocessing PR #997
+* Add - Test for multiprocessing PR #1008
+* Bugfix - Fix external store key name doesn't allow '-' (#1005) PR #1006
+* Add - Adopted black formatting into code base PR #998
+
 0.13.3 -- Feb 9, 2022
 ----------------------
 * Bugfix - Fix error in listing ancestors, descendants with part tables.
diff --git a/local-docker-compose.yml b/local-docker-compose.yml
index 19a8c7ae2..057bec111 100644
--- a/local-docker-compose.yml
+++ b/local-docker-compose.yml
@@ -34,7 +34,7 @@ services:
       interval: 1s
   fakeservices.datajoint.io:
     <<: *net
-    image: datajoint/nginx:v0.0.19
+    image: datajoint/nginx:v0.1.1
     environment:
     - ADD_db_TYPE=DATABASE
     - ADD_db_ENDPOINT=db:3306
@@ -82,7 +82,7 @@ services:
       - -c
       - |
         set -e
-        pip install --user nose nose-cov coveralls flake8 ptvsd
+        pip install --user nose nose-cov coveralls flake8 ptvsd black
         pip install -e .
         pip freeze | grep datajoint
         ## You may run the below tests once sh'ed into container i.e. docker exec -it datajoint-python_app_1 sh
@@ -90,7 +90,8 @@ services:
         # nosetests -vs --tests=tests.test_external_class:test_insert_and_fetch; #run specific basic test
         # nosetests -vs --tests=tests.test_fetch:TestFetch.test_getattribute_for_fetch1; #run specific Class test
         # flake8 datajoint --count --select=E9,F63,F7,F82 --show-source --statistics
-        # flake8 --ignore=E121,E123,E126,E226,E24,E704,W503,W504,E722,F401,W605 datajoint --count --max-complexity=62 --max-line-length=127 --statistics
+        # flake8 --ignore=E203,E722,F401,W503 datajoint --count --max-complexity=62 --max-line-length=127 --statistics
+        # black datajoint --check -v
         ## Interactive Jupyter Notebook environment
         jupyter notebook &
         ## Remote debugger


Test patch:
diff --git a/test_requirements.txt b/test_requirements.txt
index 0ed24a620..373bb1a8e 100644
--- a/test_requirements.txt
+++ b/test_requirements.txt
@@ -1,4 +1,4 @@
 nose
 nose-cov
 coveralls
-faker
+faker
\ No newline at end of file
diff --git a/tests/__init__.py b/tests/__init__.py
index 6b802e332..e1795e12d 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -17,57 +17,62 @@
 import shutil
 from datajoint.utils import parse_sql
 
-__author__ = 'Edgar Walker, Fabian Sinz, Dimitri Yatsenko, Raphael Guzman'
+__author__ = "Edgar Walker, Fabian Sinz, Dimitri Yatsenko, Raphael Guzman"
 
 # turn on verbose logging
 logging.basicConfig(level=logging.DEBUG)
 
-__all__ = ['__author__', 'PREFIX', 'CONN_INFO']
+__all__ = ["__author__", "PREFIX", "CONN_INFO"]
 
 # Connection for testing
 CONN_INFO = dict(
-    host=environ.get('DJ_TEST_HOST', 'fakeservices.datajoint.io'),
-    user=environ.get('DJ_TEST_USER', 'datajoint'),
-    password=environ.get('DJ_TEST_PASSWORD', 'datajoint'))
+    host=environ.get("DJ_TEST_HOST", "fakeservices.datajoint.io"),
+    user=environ.get("DJ_TEST_USER", "datajoint"),
+    password=environ.get("DJ_TEST_PASSWORD", "datajoint"),
+)
 
 CONN_INFO_ROOT = dict(
-    host=environ.get('DJ_HOST', 'fakeservices.datajoint.io'),
-    user=environ.get('DJ_USER', 'root'),
-    password=environ.get('DJ_PASS', 'simple'))
+    host=environ.get("DJ_HOST", "fakeservices.datajoint.io"),
+    user=environ.get("DJ_USER", "root"),
+    password=environ.get("DJ_PASS", "simple"),
+)
 
 S3_CONN_INFO = dict(
-    endpoint=environ.get('S3_ENDPOINT', 'fakeservices.datajoint.io'),
-    access_key=environ.get('S3_ACCESS_KEY', 'datajoint'),
-    secret_key=environ.get('S3_SECRET_KEY', 'datajoint'),
-    bucket=environ.get('S3_BUCKET', 'datajoint.test'))
-
-S3_MIGRATE_BUCKET = [path.name for path in Path(
-        Path(__file__).resolve().parent,
-        'external-legacy-data', 's3').iterdir()][0]
+    endpoint=environ.get("S3_ENDPOINT", "fakeservices.datajoint.io"),
+    access_key=environ.get("S3_ACCESS_KEY", "datajoint"),
+    secret_key=environ.get("S3_SECRET_KEY", "datajoint"),
+    bucket=environ.get("S3_BUCKET", "datajoint.test"),
+)
+
+S3_MIGRATE_BUCKET = [
+    path.name
+    for path in Path(
+        Path(__file__).resolve().parent, "external-legacy-data", "s3"
+    ).iterdir()
+][0]
 
 # Prefix for all databases used during testing
-PREFIX = environ.get('DJ_TEST_DB_PREFIX', 'djtest')
+PREFIX = environ.get("DJ_TEST_DB_PREFIX", "djtest")
 conn_root = dj.conn(**CONN_INFO_ROOT)
 
 # Initialize httpClient with relevant timeout.
 httpClient = urllib3.PoolManager(
-            timeout=30,
-            cert_reqs='CERT_REQUIRED',
-            ca_certs=certifi.where(),
-            retries=urllib3.Retry(
-                total=3,
-                backoff_factor=0.2,
-                status_forcelist=[500, 502, 503, 504]
-            )
-        )
+    timeout=30,
+    cert_reqs="CERT_REQUIRED",
+    ca_certs=certifi.where(),
+    retries=urllib3.Retry(
+        total=3, backoff_factor=0.2, status_forcelist=[500, 502, 503, 504]
+    ),
+)
 
 # Initialize minioClient with an endpoint and access/secret keys.
 minioClient = minio.Minio(
-    S3_CONN_INFO['endpoint'],
-    access_key=S3_CONN_INFO['access_key'],
-    secret_key=S3_CONN_INFO['secret_key'],
+    S3_CONN_INFO["endpoint"],
+    access_key=S3_CONN_INFO["access_key"],
+    secret_key=S3_CONN_INFO["secret_key"],
     secure=True,
-    http_client=httpClient)
+    http_client=httpClient,
+)
 
 
 def setup_package():
@@ -75,92 +80,112 @@ def setup_package():
     Package-level unit test setup
     Turns off safemode
     """
-    dj.config['safemode'] = False
+    dj.config["safemode"] = False
 
     # Create MySQL users
-    if LooseVersion(conn_root.query(
-            "select @@version;").fetchone()[0]) >= LooseVersion('8.0.0'):
+    if LooseVersion(conn_root.query("select @@version;").fetchone()[0]) >= LooseVersion(
+        "8.0.0"
+    ):
         # create user if necessary on mysql8
-        conn_root.query("""
+        conn_root.query(
+            """
                 CREATE USER IF NOT EXISTS 'datajoint'@'%%'
                 IDENTIFIED BY 'datajoint';
-                """)
-        conn_root.query("""
+                """
+        )
+        conn_root.query(
+            """
                 CREATE USER IF NOT EXISTS 'djview'@'%%'
                 IDENTIFIED BY 'djview';
-                """)
-        conn_root.query("""
+                """
+        )
+        conn_root.query(
+            """
                 CREATE USER IF NOT EXISTS 'djssl'@'%%'
                 IDENTIFIED BY 'djssl'
                 REQUIRE SSL;
-                """)
-        conn_root.query(
-            "GRANT ALL PRIVILEGES ON `djtest%%`.* TO 'datajoint'@'%%';")
-        conn_root.query(
-            "GRANT SELECT ON `djtest%%`.* TO 'djview'@'%%';")
-        conn_root.query(
-            "GRANT SELECT ON `djtest%%`.* TO 'djssl'@'%%';")
+                """
+        )
+        conn_root.query("GRANT ALL PRIVILEGES ON `djtest%%`.* TO 'datajoint'@'%%';")
+        conn_root.query("GRANT SELECT ON `djtest%%`.* TO 'djview'@'%%';")
+        conn_root.query("GRANT SELECT ON `djtest%%`.* TO 'djssl'@'%%';")
     else:
         # grant permissions. For MySQL 5.7 this also automatically creates user
         # if not exists
-        conn_root.query("""
+        conn_root.query(
+            """
             GRANT ALL PRIVILEGES ON `djtest%%`.* TO 'datajoint'@'%%'
             IDENTIFIED BY 'datajoint';
-            """)
+            """
+        )
         conn_root.query(
             "GRANT SELECT ON `djtest%%`.* TO 'djview'@'%%' IDENTIFIED BY 'djview';"
-            )
-        conn_root.query("""
+        )
+        conn_root.query(
+            """
             GRANT SELECT ON `djtest%%`.* TO 'djssl'@'%%'
             IDENTIFIED BY 'djssl'
             REQUIRE SSL;
-            """)
+            """
+        )
 
     # Add old MySQL
-    source = Path(
-        Path(__file__).resolve().parent,
-        'external-legacy-data')
+    source = Path(Path(__file__).resolve().parent, "external-legacy-data")
     db_name = "djtest_blob_migrate"
     db_file = "v0_11.sql"
-    conn_root.query("""
+    conn_root.query(
+        """
         CREATE DATABASE IF NOT EXISTS {};
-        """.format(db_name))
+        """.format(
+            db_name
+        )
+    )
 
-    statements = parse_sql(Path(source,db_file))
+    statements = parse_sql(Path(source, db_file))
     for s in statements:
         conn_root.query(s)
 
     # Add old S3
-    source = Path(
-        Path(__file__).resolve().parent,
-        'external-legacy-data', 's3')
+    source = Path(Path(__file__).resolve().parent, "external-legacy-data", "s3")
     region = "us-east-1"
     try:
         minioClient.make_bucket(S3_MIGRATE_BUCKET, location=region)
     except minio.error.S3Error as e:
-        if e.code != 'BucketAlreadyOwnedByYou':
+        if e.code != "BucketAlreadyOwnedByYou":
             raise e
 
-    pathlist = Path(source).glob('**/*')
+    pathlist = Path(source).glob("**/*")
     for path in pathlist:
         if os.path.isfile(str(path)) and ".sql" not in str(path):
             minioClient.fput_object(
-                    S3_MIGRATE_BUCKET, str(Path(
-                        os.path.relpath(str(path), str(Path(source, S3_MIGRATE_BUCKET))))
-                                .as_posix()), str(path))
+                S3_MIGRATE_BUCKET,
+                str(
+                    Path(
+                        os.path.relpath(str(path), str(Path(source, S3_MIGRATE_BUCKET)))
+                    ).as_posix()
+                ),
+                str(path),
+            )
     # Add S3
     try:
-        minioClient.make_bucket(S3_CONN_INFO['bucket'], location=region)
+        minioClient.make_bucket(S3_CONN_INFO["bucket"], location=region)
     except minio.error.S3Error as e:
-        if e.code != 'BucketAlreadyOwnedByYou':
+        if e.code != "BucketAlreadyOwnedByYou":
             raise e
 
     # Add old File Content
     try:
         shutil.copytree(
-            str(Path(Path(__file__).resolve().parent,
-            'external-legacy-data','file','temp')),
-            str(Path(os.path.expanduser('~'),'temp')))
+            str(
+                Path(
+                    Path(__file__).resolve().parent,
+                    "external-legacy-data",
+                    "file",
+                    "temp",
+                )
+            ),
+            str(Path(os.path.expanduser("~"), "temp")),
+        )
     except FileExistsError:
         pass
 
@@ -172,31 +197,34 @@ def teardown_package():
     To deal with possible foreign key constraints, it will unset
     and then later reset FOREIGN_KEY_CHECKS flag
     """
-    conn_root.query('SET FOREIGN_KEY_CHECKS=0')
+    conn_root.query("SET FOREIGN_KEY_CHECKS=0")
     cur = conn_root.query('SHOW DATABASES LIKE "{}\_%%"'.format(PREFIX))
     for db in cur.fetchall():
-        conn_root.query('DROP DATABASE `{}`'.format(db[0]))
-    conn_root.query('SET FOREIGN_KEY_CHECKS=1')
+        conn_root.query("DROP DATABASE `{}`".format(db[0]))
+    conn_root.query("SET FOREIGN_KEY_CHECKS=1")
     if os.path.exists("dj_local_conf.json"):
         remove("dj_local_conf.json")
 
     # Remove created users
-    conn_root.query('DROP USER `datajoint`')
-    conn_root.query('DROP USER `djview`')
-    conn_root.query('DROP USER `djssl`')
+    conn_root.query("DROP USER `datajoint`")
+    conn_root.query("DROP USER `djview`")
+    conn_root.query("DROP USER `djssl`")
 
     # Remove old S3
-    objs = list(minioClient.list_objects(
-            S3_MIGRATE_BUCKET, recursive=True))
-    objs = [minioClient.remove_object(S3_MIGRATE_BUCKET,
-            o.object_name.encode('utf-8')) for o in objs]
+    objs = list(minioClient.list_objects(S3_MIGRATE_BUCKET, recursive=True))
+    objs = [
+        minioClient.remove_object(S3_MIGRATE_BUCKET, o.object_name.encode("utf-8"))
+        for o in objs
+    ]
     minioClient.remove_bucket(S3_MIGRATE_BUCKET)
 
     # Remove S3
-    objs = list(minioClient.list_objects(S3_CONN_INFO['bucket'], recursive=True))
-    objs = [minioClient.remove_object(S3_CONN_INFO['bucket'],
-            o.object_name.encode('utf-8')) for o in objs]
-    minioClient.remove_bucket(S3_CONN_INFO['bucket'])
+    objs = list(minioClient.list_objects(S3_CONN_INFO["bucket"], recursive=True))
+    objs = [
+        minioClient.remove_object(S3_CONN_INFO["bucket"], o.object_name.encode("utf-8"))
+        for o in objs
+    ]
+    minioClient.remove_bucket(S3_CONN_INFO["bucket"])
 
     # Remove old File Content
-    shutil.rmtree(str(Path(os.path.expanduser('~'),'temp')))
+    shutil.rmtree(str(Path(os.path.expanduser("~"), "temp")))
diff --git a/tests/schema.py b/tests/schema.py
index ccb5fcf31..dafd481da 100644
--- a/tests/schema.py
+++ b/tests/schema.py
@@ -8,7 +8,7 @@
 import inspect
 from . import PREFIX, CONN_INFO
 
-schema = dj.Schema(PREFIX + '_test1', connection=dj.conn(**CONN_INFO))
+schema = dj.Schema(PREFIX + "_test1", connection=dj.conn(**CONN_INFO))
 
 
 @schema
@@ -16,12 +16,13 @@ class TTest(dj.Lookup):
     """
     doc string
     """
+
     definition = """
     key   :   int     # key
     ---
     value   :   int     # value
     """
-    contents = [(k, 2*k) for k in range(10)]
+    contents = [(k, 2 * k) for k in range(10)]
 
 
 @schema
@@ -58,6 +59,7 @@ class TTestExtra(dj.Manual):
     """
     clone of Test but with an extra field
     """
+
     definition = TTest.definition + "\nextra : int # extra int\n"
 
 
@@ -66,6 +68,7 @@ class TTestNoExtra(dj.Manual):
     """
     clone of Test but with no extra fields
     """
+
     definition = TTest.definition
 
 
@@ -87,7 +90,15 @@ class User(dj.Lookup):
     definition = """      # lab members
     username: varchar(12)
     """
-    contents = [['Jake'], ['Cathryn'], ['Shan'], ['Fabian'], ['Edgar'], ['George'], ['Dimitri']]
+    contents = [
+        ["Jake"],
+        ["Cathryn"],
+        ["Shan"],
+        ["Fabian"],
+        ["Edgar"],
+        ["George"],
+        ["Dimitri"],
+    ]
 
 
 @schema
@@ -103,10 +114,11 @@ class Subject(dj.Lookup):
     """
 
     contents = [
-        [1551, '1551', 'mouse', '2015-04-01', 'genetically engineered super mouse'],
-        [10, 'Curious George', 'monkey', '2008-06-30', ''],
-        [1552, '1552', 'mouse', '2015-06-15', ''],
-        [1553, '1553', 'mouse', '2016-07-01', '']]
+        [1551, "1551", "mouse", "2015-04-01", "genetically engineered super mouse"],
+        [10, "Curious George", "monkey", "2008-06-30", ""],
+        [1552, "1552", "mouse", "2015-06-15", ""],
+        [1553, "1553", "mouse", "2016-07-01", ""],
+    ]
 
 
 @schema
@@ -118,12 +130,13 @@ class Language(dj.Lookup):
     language    : varchar(40) # language
     """
     contents = [
-        ('Fabian', 'English'),
-        ('Edgar', 'English'),
-        ('Dimitri', 'English'),
-        ('Dimitri', 'Ukrainian'),
-        ('Fabian', 'German'),
-        ('Edgar', 'Japanese')]
+        ("Fabian", "English"),
+        ("Edgar", "English"),
+        ("Dimitri", "English"),
+        ("Dimitri", "Ukrainian"),
+        ("Fabian", "German"),
+        ("Edgar", "Japanese"),
+    ]
 
 
 @schema
@@ -146,14 +159,21 @@ def make(self, key):
         populate with random data
         """
         from datetime import date, timedelta
-        users = [None, None] + list(User().fetch()['username'])
-        random.seed('Amazing Seed')
+
+        users = [None, None] + list(User().fetch()["username"])
+        random.seed("Amazing Seed")
         self.insert(
-            dict(key,
-                 experiment_id=experiment_id,
-                 experiment_date=(date.today() - timedelta(random.expovariate(1 / 30))).isoformat(),
-                 username=random.choice(users))
-            for experiment_id in range(self.fake_experiments_per_subject))
+            dict(
+                key,
+                experiment_id=experiment_id,
+                experiment_date=(
+                    date.today() - timedelta(random.expovariate(1 / 30))
+                ).isoformat(),
+                username=random.choice(users),
+            )
+            for experiment_id in range(self.fake_experiments_per_subject)
+        )
+
 
 @schema
 class Trial(dj.Imported):
@@ -173,15 +193,16 @@ class Condition(dj.Part):
         """
 
     def make(self, key):
-        """ populate with random data (pretend reading from raw files) """
-        random.seed('Amazing Seed')
+        """populate with random data (pretend reading from raw files)"""
+        random.seed("Amazing Seed")
         trial = self.Condition()
         for trial_id in range(10):
-            key['trial_id'] = trial_id
+            key["trial_id"] = trial_id
             self.insert1(dict(key, start_time=random.random() * 1e9))
-            trial.insert(dict(key,
-                              cond_idx=cond_idx,
-                              orientation=random.random()*360) for cond_idx in range(30))
+            trial.insert(
+                dict(key, cond_idx=cond_idx, orientation=random.random() * 360)
+                for cond_idx in range(30)
+            )
 
 
 @schema
@@ -207,17 +228,20 @@ def _make_tuples(self, key):
         populate with random data
         """
         random.seed(str(key))
-        row = dict(key,
-                   sampling_frequency=6000,
-                   duration=np.minimum(2, random.expovariate(1)))
+        row = dict(
+            key, sampling_frequency=6000, duration=np.minimum(2, random.expovariate(1))
+        )
         self.insert1(row)
-        number_samples = int(row['duration'] * row['sampling_frequency'] + 0.5)
+        number_samples = int(row["duration"] * row["sampling_frequency"] + 0.5)
         sub = self.Channel()
         sub.insert(
-            dict(key,
-                 channel=channel,
-                 voltage=np.float32(np.random.randn(number_samples)))
-            for channel in range(2))
+            dict(
+                key,
+                channel=channel,
+                voltage=np.float32(np.random.randn(number_samples)),
+            )
+            for channel in range(2)
+        )
 
 
 @schema
@@ -274,7 +298,7 @@ class SigTermTable(dj.Computed):
     """
 
     def make(self, key):
-        raise SystemExit('SIGTERM received')
+        raise SystemExit("SIGTERM received")
 
 
 @schema
@@ -285,8 +309,11 @@ class DjExceptionName(dj.Lookup):
 
     @property
     def contents(self):
-        return [[member_name] for member_name, member_type in inspect.getmembers(dj.errors)
-                if inspect.isclass(member_type) and issubclass(member_type, Exception)]
+        return [
+            [member_name]
+            for member_name, member_type in inspect.getmembers(dj.errors)
+            if inspect.isclass(member_type) and issubclass(member_type, Exception)
+        ]
 
 
 @schema
@@ -296,7 +323,7 @@ class ErrorClass(dj.Computed):
     """
 
     def make(self, key):
-        exception_name = key['dj_exception_name']
+        exception_name = key["dj_exception_name"]
         raise getattr(dj.errors, exception_name)
 
 
@@ -354,7 +381,7 @@ class Parent(dj.Lookup):
     ---
     name: varchar(30)
     """
-    contents = [(1, 'Joe')]
+    contents = [(1, "Joe")]
 
 
 @schema
@@ -365,19 +392,21 @@ class Child(dj.Lookup):
     ---
     name: varchar(30)
     """
-    contents = [(1, 12, 'Dan')]
+    contents = [(1, 12, "Dan")]
+
 
 # Related to issue #886 (8), #883 (5)
 @schema
 class ComplexParent(dj.Lookup):
-    definition = '\n'.join(['parent_id_{}: int'.format(i+1) for i in range(8)])
+    definition = "\n".join(["parent_id_{}: int".format(i + 1) for i in range(8)])
     contents = [tuple(i for i in range(8))]
 
 
 @schema
 class ComplexChild(dj.Lookup):
-    definition = '\n'.join(['-> ComplexParent'] + ['child_id_{}: int'.format(i+1)
-                                                   for i in range(1)])
+    definition = "\n".join(
+        ["-> ComplexParent"] + ["child_id_{}: int".format(i + 1) for i in range(1)]
+    )
     contents = [tuple(i for i in range(9))]
 
 
@@ -390,9 +419,9 @@ class SubjectA(dj.Lookup):
     sex : enum('M', 'F', 'U')
     """
     contents = [
-        ('mouse1', '2020-09-01', 'M'),
-        ('mouse2', '2020-03-19', 'F'),
-        ('mouse3', '2020-08-23', 'F')
+        ("mouse1", "2020-09-01", "M"),
+        ("mouse2", "2020-03-19", "F"),
+        ("mouse3", "2020-08-23", "F"),
     ]
 
 
@@ -405,10 +434,10 @@ class SessionA(dj.Lookup):
     session_dir=''  : varchar(32)
     """
     contents = [
-        ('mouse1', '2020-12-01 12:32:34', ''),
-        ('mouse1', '2020-12-02 12:32:34', ''),
-        ('mouse1', '2020-12-03 12:32:34', ''),
-        ('mouse1', '2020-12-04 12:32:34', '')
+        ("mouse1", "2020-12-01 12:32:34", ""),
+        ("mouse1", "2020-12-02 12:32:34", ""),
+        ("mouse1", "2020-12-03 12:32:34", ""),
+        ("mouse1", "2020-12-04 12:32:34", ""),
     ]
 
 
@@ -420,10 +449,10 @@ class SessionStatusA(dj.Lookup):
     status: enum('in_training', 'trained_1a', 'trained_1b', 'ready4ephys')
     """
     contents = [
-        ('mouse1', '2020-12-01 12:32:34', 'in_training'),
-        ('mouse1', '2020-12-02 12:32:34', 'trained_1a'),
-        ('mouse1', '2020-12-03 12:32:34', 'trained_1b'),
-        ('mouse1', '2020-12-04 12:32:34', 'ready4ephys'),
+        ("mouse1", "2020-12-01 12:32:34", "in_training"),
+        ("mouse1", "2020-12-02 12:32:34", "trained_1a"),
+        ("mouse1", "2020-12-03 12:32:34", "trained_1b"),
+        ("mouse1", "2020-12-04 12:32:34", "ready4ephys"),
     ]
 
 
@@ -434,10 +463,10 @@ class SessionDateA(dj.Lookup):
     session_date:  date
     """
     contents = [
-        ('mouse1', '2020-12-01'),
-        ('mouse1', '2020-12-02'),
-        ('mouse1', '2020-12-03'),
-        ('mouse1', '2020-12-04')
+        ("mouse1", "2020-12-01"),
+        ("mouse1", "2020-12-02"),
+        ("mouse1", "2020-12-03"),
+        ("mouse1", "2020-12-04"),
     ]
 
 
@@ -449,3 +478,12 @@ class Stimulus(dj.Lookup):
     contrast: int
     brightness: int
     """
+
+
+@schema
+class Longblob(dj.Manual):
+    definition = """
+    id: int
+    ---
+    data: longblob
+    """
diff --git a/tests/schema_adapted.py b/tests/schema_adapted.py
index 1260f4431..8f966b876 100644
--- a/tests/schema_adapted.py
+++ b/tests/schema_adapted.py
@@ -8,15 +8,14 @@
 from . import PREFIX, CONN_INFO, S3_CONN_INFO
 
 stores_config = {
-    'repo_s3': dict(
-        S3_CONN_INFO,
-        protocol='s3',
-        location='adapted/repo',
-        stage=tempfile.mkdtemp())}
+    "repo-s3": dict(
+        S3_CONN_INFO, protocol="s3", location="adapted/repo", stage=tempfile.mkdtemp()
+    )
+}
 
-dj.config['stores'] = stores_config
+dj.config["stores"] = stores_config
 
-schema_name = PREFIX + '_test_custom_datatype'
+schema_name = PREFIX + "_test_custom_datatype"
 schema = dj.schema(schema_name, connection=dj.conn(**CONN_INFO))
 
 
@@ -25,7 +24,7 @@
 
 class GraphAdapter(dj.AttributeAdapter):
 
-    attribute_type = 'longblob'  # this is how the attribute will be declared
+    attribute_type = "longblob"  # this is how the attribute will be declared
 
     @staticmethod
     def get(obj):
@@ -51,6 +50,7 @@ class Connectivity(dj.Manual):
     conn_graph = null : <graph>
     """
 
+
 errors._switch_filepath_types(True)
 
 
@@ -59,7 +59,7 @@ class LayoutToFilepath(dj.AttributeAdapter):
     An adapted data type that saves a graph layout into fixed filepath
     """
 
-    attribute_type = 'filepath@repo_s3'
+    attribute_type = "filepath@repo-s3"
 
     @staticmethod
     def get(path):
@@ -68,7 +68,7 @@ def get(path):
 
     @staticmethod
     def put(layout):
-        path = Path(dj.config['stores']['repo_s3']['stage'], 'layout.json')
+        path = Path(dj.config["stores"]["repo-s3"]["stage"], "layout.json")
         with open(str(path), "w") as f:
             json.dump(layout, f)
         return path
@@ -86,5 +86,6 @@ class Layout(dj.Manual):
     layout: <layout_to_filepath>
     """
 
+
 errors._switch_filepath_types(False)
 errors._switch_adapted_types(False)  # disable again
diff --git a/tests/schema_advanced.py b/tests/schema_advanced.py
index 9c00b3a8e..a3c2d0d64 100644
--- a/tests/schema_advanced.py
+++ b/tests/schema_advanced.py
@@ -1,7 +1,7 @@
 import datajoint as dj
 from . import PREFIX, CONN_INFO
 
-schema = dj.Schema(PREFIX + '_advanced', locals(), connection=dj.conn(**CONN_INFO))
+schema = dj.Schema(PREFIX + "_advanced", locals(), connection=dj.conn(**CONN_INFO))
 
 
 @schema
@@ -17,23 +17,26 @@ def fill(self):
         """
         fill fake names from www.fakenamegenerator.com
         """
-        self.insert((
-            (0, "May K. Hall", "F"),
-            (1, "Jeffrey E. Gillen", "M"),
-            (2, "Hanna R. Walters", "F"),
-            (3, "Russel S. James", "M"),
-            (4, "Robbin J. Fletcher", "F"),
-            (5, "Wade J. Sullivan", "M"),
-            (6, "Dorothy J. Chen", "F"),
-            (7, "Michael L. Kowalewski", "M"),
-            (8, "Kimberly J. Stringer", "F"),
-            (9, "Mark G. Hair", "M"),
-            (10, "Mary R. Thompson", "F"),
-            (11, "Graham C. Gilpin", "M"),
-            (12, "Nelda T. Ruggeri", "F"),
-            (13, "Bryan M. Cummings", "M"),
-            (14, "Sara C. Le", "F"),
-            (15, "Myron S. Jaramillo", "M")))
+        self.insert(
+            (
+                (0, "May K. Hall", "F"),
+                (1, "Jeffrey E. Gillen", "M"),
+                (2, "Hanna R. Walters", "F"),
+                (3, "Russel S. James", "M"),
+                (4, "Robbin J. Fletcher", "F"),
+                (5, "Wade J. Sullivan", "M"),
+                (6, "Dorothy J. Chen", "F"),
+                (7, "Michael L. Kowalewski", "M"),
+                (8, "Kimberly J. Stringer", "F"),
+                (9, "Mark G. Hair", "M"),
+                (10, "Mary R. Thompson", "F"),
+                (11, "Graham C. Gilpin", "M"),
+                (12, "Nelda T. Ruggeri", "F"),
+                (13, "Bryan M. Cummings", "M"),
+                (14, "Sara C. Le", "F"),
+                (15, "Myron S. Jaramillo", "M"),
+            )
+        )
 
 
 @schema
@@ -46,16 +49,44 @@ class Parent(dj.Manual):
     """
 
     def fill(self):
-
         def make_parent(pid, parent):
-            return dict(person_id=pid,
-                        parent=parent,
-                        parent_sex=(Person & {'person_id': parent}).fetch1('sex'))
-
-        self.insert(make_parent(*r) for r in (
-            (0, 2), (0, 3), (1, 4), (1, 5), (2, 4), (2, 5), (3, 4), (3, 7), (4, 7),
-            (4, 8), (5, 9), (5, 10), (6, 9), (6, 10), (7, 11), (7, 12), (8, 11), (8, 14),
-            (9, 11), (9, 12), (10, 13), (10, 14), (11, 14), (11, 15), (12, 14), (12, 15)))
+            return dict(
+                person_id=pid,
+                parent=parent,
+                parent_sex=(Person & {"person_id": parent}).fetch1("sex"),
+            )
+
+        self.insert(
+            make_parent(*r)
+            for r in (
+                (0, 2),
+                (0, 3),
+                (1, 4),
+                (1, 5),
+                (2, 4),
+                (2, 5),
+                (3, 4),
+                (3, 7),
+                (4, 7),
+                (4, 8),
+                (5, 9),
+                (5, 10),
+                (6, 9),
+                (6, 10),
+                (7, 11),
+                (7, 12),
+                (8, 11),
+                (8, 14),
+                (9, 11),
+                (9, 12),
+                (10, 13),
+                (10, 14),
+                (11, 14),
+                (11, 15),
+                (12, 14),
+                (12, 15),
+            )
+        )
 
 
 @schema
diff --git a/tests/schema_empty.py b/tests/schema_empty.py
index c6cfd4121..f6ebf6278 100644
--- a/tests/schema_empty.py
+++ b/tests/schema_empty.py
@@ -4,9 +4,9 @@
 
 import datajoint as dj
 from . import PREFIX, CONN_INFO
-from . import schema as _ # make sure that the other tables are defined
+from . import schema as _  # make sure that the other tables are defined
 
-schema = dj.Schema(PREFIX + '_test1', locals(), connection=dj.conn(**CONN_INFO))
+schema = dj.Schema(PREFIX + "_test1", locals(), connection=dj.conn(**CONN_INFO))
 
 
 @schema
@@ -14,4 +14,5 @@ class Ephys(dj.Imported):
     definition = """  # This is already declare in ./schema.py
     """
 
-schema.spawn_missing_classes()    # load the rest of the classes
\ No newline at end of file
+
+schema.spawn_missing_classes()  # load the rest of the classes
diff --git a/tests/schema_external.py b/tests/schema_external.py
index 7db44bc49..4d16ff544 100644
--- a/tests/schema_external.py
+++ b/tests/schema_external.py
@@ -8,41 +8,26 @@
 from . import PREFIX, CONN_INFO, S3_CONN_INFO
 import numpy as np
 
-schema = dj.Schema(PREFIX + '_extern', connection=dj.conn(**CONN_INFO))
+schema = dj.Schema(PREFIX + "_extern", connection=dj.conn(**CONN_INFO))
 
 
 stores_config = {
-
-    'raw': dict(
-        protocol='file',
-        location=tempfile.mkdtemp()),
-
-    'repo': dict(
-        stage=tempfile.mkdtemp(),
-        protocol='file',
-        location=tempfile.mkdtemp()),
-
-    'repo_s3': dict(
-        S3_CONN_INFO,
-        protocol='s3',
-        location='dj/repo',
-        stage=tempfile.mkdtemp()),
-
-    'local': dict(
-        protocol='file',
-        location=tempfile.mkdtemp(),
-        subfolding=(1, 1)),
-
-    'share': dict(
-        S3_CONN_INFO,
-        protocol='s3',
-        location='dj/store/repo',
-        subfolding=(2, 4))
+    "raw": dict(protocol="file", location=tempfile.mkdtemp()),
+    "repo": dict(
+        stage=tempfile.mkdtemp(), protocol="file", location=tempfile.mkdtemp()
+    ),
+    "repo-s3": dict(
+        S3_CONN_INFO, protocol="s3", location="dj/repo", stage=tempfile.mkdtemp()
+    ),
+    "local": dict(protocol="file", location=tempfile.mkdtemp(), subfolding=(1, 1)),
+    "share": dict(
+        S3_CONN_INFO, protocol="s3", location="dj/store/repo", subfolding=(2, 4)
+    ),
 }
 
-dj.config['stores'] = stores_config
+dj.config["stores"] = stores_config
 
-dj.config['cache'] = tempfile.mkdtemp()
+dj.config["cache"] = tempfile.mkdtemp()
 
 
 @schema
@@ -78,9 +63,7 @@ class Dimension(dj.Lookup):
     ---
     dimensions  : blob
     """
-    contents = (
-        [0, [100, 50]],
-        [1, [3, 4, 8, 6]])
+    contents = ([0, [100, 50]], [1, [3, 4, 8, 6]])
 
 
 @schema
@@ -95,8 +78,8 @@ class Image(dj.Computed):
     """
 
     def make(self, key):
-        np.random.seed(key['seed'])
-        img = np.random.rand(*(Dimension() & key).fetch1('dimensions'))
+        np.random.seed(key["seed"])
+        img = np.random.rand(*(Dimension() & key).fetch1("dimensions"))
         self.insert1(dict(key, img=img, neg=-img.astype(np.float32)))
 
 
@@ -130,7 +113,8 @@ class FilepathS3(dj.Manual):
     # table for file management 
     fnum : int 
     ---
-    img : filepath@repo_s3  # managed files 
+    img : filepath@repo-s3  # managed files 
     """
 
+
 dj.errors._switch_filepath_types(False)
diff --git a/tests/schema_simple.py b/tests/schema_simple.py
index 9263fa01a..559486a8e 100644
--- a/tests/schema_simple.py
+++ b/tests/schema_simple.py
@@ -13,7 +13,7 @@
 import numpy as np
 from datetime import date, timedelta
 
-schema = dj.Schema(PREFIX + '_relational', locals(), connection=dj.conn(**CONN_INFO))
+schema = dj.Schema(PREFIX + "_relational", locals(), connection=dj.conn(**CONN_INFO))
 
 
 @schema
@@ -22,7 +22,7 @@ class IJ(dj.Lookup):
     i  : int
     j  : int
     """
-    contents = list(dict(i=i, j=j+2) for i in range(3) for j in range(3))
+    contents = list(dict(i=i, j=j + 2) for i in range(3) for j in range(3))
 
 
 @schema
@@ -31,7 +31,7 @@ class JI(dj.Lookup):
     j  : int
     i  : int
     """
-    contents = list(dict(i=i+1, j=j) for i in range(3) for j in range(3))
+    contents = list(dict(i=i + 1, j=j) for i in range(3) for j in range(3))
 
 
 @schema
@@ -67,12 +67,15 @@ def make(self, key):
         random.seed(str(key))
         sub = B.C()
         for i in range(4):
-            key['id_b'] = i
+            key["id_b"] = i
             mu = random.normalvariate(0, 10)
             sigma = random.lognormvariate(0, 4)
             n = random.randint(0, 10)
             self.insert1(dict(key, mu=mu, sigma=sigma, n=n))
-            sub.insert(dict(key, id_c=j, value=random.normalvariate(mu, sigma)) for j in range(n))
+            sub.insert(
+                dict(key, id_c=j, value=random.normalvariate(mu, sigma))
+                for j in range(n)
+            )
 
 
 @schema
@@ -97,7 +100,7 @@ class D(dj.Computed):
     def _make_tuples(self, key):
         # make reference to a random tuple from L
         random.seed(str(key))
-        lookup = list(L().fetch('KEY'))
+        lookup = list(L().fetch("KEY"))
         self.insert(dict(key, id_d=i, **random.choice(lookup)) for i in range(4))
 
 
@@ -120,11 +123,15 @@ class F(dj.Part):
 
     def make(self, key):
         random.seed(str(key))
-        self.insert1(dict(key, **random.choice(list(L().fetch('KEY')))))
+        self.insert1(dict(key, **random.choice(list(L().fetch("KEY")))))
         sub = E.F()
-        references = list((B.C() & key).fetch('KEY'))
+        references = list((B.C() & key).fetch("KEY"))
         random.shuffle(references)
-        sub.insert(dict(key, id_f=i, **ref) for i, ref in enumerate(references) if random.getrandbits(1))
+        sub.insert(
+            dict(key, id_f=i, **ref)
+            for i, ref in enumerate(references)
+            if random.getrandbits(1)
+        )
 
 
 @schema
@@ -135,6 +142,7 @@ class F(dj.Manual):
     date=null: date
     """
 
+
 @schema
 class DataA(dj.Lookup):
     definition = """
@@ -199,9 +207,10 @@ def populate_random(self, n=10):
             profile = fake.profile()
             with self.connection.transaction:
                 self.insert1(profile, ignore_extra_fields=True)
-                for url in profile['website']:
+                for url in profile["website"]:
                     self.Website().insert1(
-                        dict(ssn=profile['ssn'], url_hash=Website().insert1_url(url)))
+                        dict(ssn=profile["ssn"], url_hash=Website().insert1_url(url))
+                    )
 
 
 @schema
@@ -215,8 +224,9 @@ class TTestUpdate(dj.Lookup):
     """
 
     contents = [
-        (0, 'my_string', 0.0, np.random.randn(10, 2)),
-        (1, 'my_other_string', 1.0, np.random.randn(20, 1))]
+        (0, "my_string", 0.0, np.random.randn(10, 2)),
+        (1, "my_other_string", 1.0, np.random.randn(20, 1)),
+    ]
 
 
 @schema
@@ -233,9 +243,11 @@ class ArgmaxTest(dj.Lookup):
     @property
     def contents(self):
         n = self.n
-        yield from zip(range(n ** 2),
-                       itertools.chain(*itertools.repeat(tuple(map(chr, range(100, 100 + n))), n)),
-                       np.random.rand(n ** 2))
+        yield from zip(
+            range(n**2),
+            itertools.chain(*itertools.repeat(tuple(map(chr, range(100, 100 + n))), n)),
+            np.random.rand(n**2),
+        )
 
 
 @schema
@@ -267,4 +279,4 @@ class OutfitPiece(dj.Part, dj.Lookup):
         -> OutfitLaunch
         piece: varchar(20)
         """
-        contents = [(0, 'jeans'), (0, 'sneakers'), (0, 'polo')]
+        contents = [(0, "jeans"), (0, "sneakers"), (0, "polo")]
diff --git a/tests/schema_university.py b/tests/schema_university.py
index bb2675f8d..619ea459f 100644
--- a/tests/schema_university.py
+++ b/tests/schema_university.py
@@ -96,10 +96,18 @@ class LetterGrade(dj.Lookup):
     points : decimal(3,2)
     """
     contents = [
-        ['A', 4.00], ['A-', 3.67],
-        ['B+', 3.33], ['B', 3.00], ['B-', 2.67],
-        ['C+', 2.33], ['C', 2.00], ['C-', 1.67],
-        ['D+', 1.33], ['D', 1.00], ['F', 0.00]]
+        ["A", 4.00],
+        ["A-", 3.67],
+        ["B+", 3.33],
+        ["B", 3.00],
+        ["B-", 2.67],
+        ["C+", 2.33],
+        ["C", 2.00],
+        ["C-", 1.67],
+        ["D+", 1.33],
+        ["D", 1.00],
+        ["F", 0.00],
+    ]
 
 
 @schema
@@ -108,4 +116,4 @@ class Grade(dj.Manual):
     -> Enroll
     ---
     -> LetterGrade
-    """
\ No newline at end of file
+    """
diff --git a/tests/schema_uuid.py b/tests/schema_uuid.py
index ba44894fb..8aeff5cb5 100644
--- a/tests/schema_uuid.py
+++ b/tests/schema_uuid.py
@@ -2,9 +2,9 @@
 import datajoint as dj
 from . import PREFIX, CONN_INFO
 
-schema = dj.Schema(PREFIX + '_test1', connection=dj.conn(**CONN_INFO))
+schema = dj.Schema(PREFIX + "_test1", connection=dj.conn(**CONN_INFO))
 
-top_level_namespace_id = uuid.UUID('00000000-0000-0000-0000-000000000000')
+top_level_namespace_id = uuid.UUID("00000000-0000-0000-0000-000000000000")
 
 
 @schema
@@ -27,7 +27,9 @@ class Topic(dj.Manual):
 
     def add(self, topic):
         """add a new topic with a its UUID"""
-        self.insert1(dict(topic_id=uuid.uuid5(top_level_namespace_id, topic), topic=topic))
+        self.insert1(
+            dict(topic_id=uuid.uuid5(top_level_namespace_id, topic), topic=topic)
+        )
 
 
 @schema
@@ -39,8 +41,10 @@ class Item(dj.Computed):
     word : varchar(8000)
     """
 
-    key_source = Topic    # test key source that is not instantiated
+    key_source = Topic  # test key source that is not instantiated
 
     def make(self, key):
-        for word in ('Habenula', 'Hippocampus', 'Hypothalamus', 'Hypophysis'):
-            self.insert1(dict(key, word=word, item_id=uuid.uuid5(key['topic_id'], word)))
+        for word in ("Habenula", "Hippocampus", "Hypothalamus", "Hypophysis"):
+            self.insert1(
+                dict(key, word=word, item_id=uuid.uuid5(key["topic_id"], word))
+            )
diff --git a/tests/test_adapted_attributes.py b/tests/test_adapted_attributes.py
index 17acc897d..34a2d73c6 100644
--- a/tests/test_adapted_attributes.py
+++ b/tests/test_adapted_attributes.py
@@ -8,9 +8,14 @@
 
 def test_adapted_type(c=adapted.Connectivity()):
     dj.errors._switch_adapted_types(True)
-    graphs = [nx.lollipop_graph(4, 2), nx.star_graph(5), nx.barbell_graph(3, 1), nx.cycle_graph(5)]
+    graphs = [
+        nx.lollipop_graph(4, 2),
+        nx.star_graph(5),
+        nx.barbell_graph(3, 1),
+        nx.cycle_graph(5),
+    ]
     c.insert((i, g) for i, g in enumerate(graphs))
-    returned_graphs = c.fetch('conn_graph', order_by='connid')
+    returned_graphs = c.fetch("conn_graph", order_by="connid")
     for g1, g2 in zip(graphs, returned_graphs):
         assert_true(isinstance(g2, nx.Graph))
         assert_equal(len(g1.edges), len(g2.edges))
@@ -29,12 +34,12 @@ def test_adapted_filepath_type():
     c.delete()
     c.insert1((0, nx.lollipop_graph(4, 2)))
 
-    layout = nx.spring_layout(c.fetch1('conn_graph'))
+    layout = nx.spring_layout(c.fetch1("conn_graph"))
     # make json friendly
     layout = {str(k): [round(r, ndigits=4) for r in v] for k, v in layout.items()}
     t = adapted.Layout()
     t.insert1((0, layout))
-    result = t.fetch1('layout')
+    result = t.fetch1("layout")
     assert_dict_equal(result, layout)
 
     t.delete()
@@ -43,6 +48,7 @@ def test_adapted_filepath_type():
     dj.errors._switch_filepath_types(False)
     dj.errors._switch_adapted_types(False)
 
+
 # test spawned classes
 local_schema = dj.Schema(adapted.schema_name)
 local_schema.spawn_missing_classes()
@@ -51,9 +57,14 @@ def test_adapted_filepath_type():
 def test_adapted_spawned():
     dj.errors._switch_adapted_types(True)
     c = Connectivity()  # a spawned class
-    graphs = [nx.lollipop_graph(4, 2), nx.star_graph(5), nx.barbell_graph(3, 1), nx.cycle_graph(5)]
+    graphs = [
+        nx.lollipop_graph(4, 2),
+        nx.star_graph(5),
+        nx.barbell_graph(3, 1),
+        nx.cycle_graph(5),
+    ]
     c.insert((i, g) for i, g in enumerate(graphs))
-    returned_graphs = c.fetch('conn_graph', order_by='connid')
+    returned_graphs = c.fetch("conn_graph", order_by="connid")
     for g1, g2 in zip(graphs, returned_graphs):
         assert_true(isinstance(g2, nx.Graph))
         assert_equal(len(g1.edges), len(g2.edges))
@@ -63,16 +74,23 @@ def test_adapted_spawned():
 
 
 # test with virtual module
-virtual_module = dj.VirtualModule('virtual_module', adapted.schema_name, add_objects={'graph': graph})
+virtual_module = dj.VirtualModule(
+    "virtual_module", adapted.schema_name, add_objects={"graph": graph}
+)
 
 
 def test_adapted_virtual():
     dj.errors._switch_adapted_types(True)
     c = virtual_module.Connectivity()
-    graphs = [nx.lollipop_graph(4, 2), nx.star_graph(5), nx.barbell_graph(3, 1), nx.cycle_graph(5)]
+    graphs = [
+        nx.lollipop_graph(4, 2),
+        nx.star_graph(5),
+        nx.barbell_graph(3, 1),
+        nx.cycle_graph(5),
+    ]
     c.insert((i, g) for i, g in enumerate(graphs))
-    c.insert1({'connid': 100})  # test work with NULLs
-    returned_graphs = c.fetch('conn_graph', order_by='connid')
+    c.insert1({"connid": 100})  # test work with NULLs
+    returned_graphs = c.fetch("conn_graph", order_by="connid")
     for g1, g2 in zip_longest(graphs, returned_graphs):
         if g1 is None:
             assert_true(g2 is None)
diff --git a/tests/test_aggr_regressions.py b/tests/test_aggr_regressions.py
index 7947e6f18..18ed0ba84 100644
--- a/tests/test_aggr_regressions.py
+++ b/tests/test_aggr_regressions.py
@@ -8,7 +8,8 @@
 from . import PREFIX, CONN_INFO
 import uuid
 from .schema_uuid import Topic, Item, top_level_namespace_id
-schema = dj.Schema(PREFIX + '_aggr_regress', connection=dj.conn(**CONN_INFO))
+
+schema = dj.Schema(PREFIX + "_aggr_regress", connection=dj.conn(**CONN_INFO))
 
 # --------------- ISSUE 386 -------------------
 # Issue 386 resulted from the loss of aggregated attributes when the aggregation was used as the restrictor
@@ -21,7 +22,7 @@ class R(dj.Lookup):
     definition = """
     r : char(1)
     """
-    contents = zip('ABCDFGHIJKLMNOPQRST')
+    contents = zip("ABCDFGHIJKLMNOPQRST")
 
 
 @schema
@@ -29,7 +30,7 @@ class Q(dj.Lookup):
     definition = """
     -> R
     """
-    contents = zip('ABCDFGH')
+    contents = zip("ABCDFGH")
 
 
 @schema
@@ -38,33 +39,35 @@ class S(dj.Lookup):
     -> R
     s : int
     """
-    contents = itertools.product('ABCDF', range(10))
+    contents = itertools.product("ABCDF", range(10))
 
 
 def test_issue386():
-    result = R.aggr(S, n='count(*)') & 'n=10'
+    result = R.aggr(S, n="count(*)") & "n=10"
     result = Q & result
     result.fetch()
 
+
 # ---------------- ISSUE 449 ------------------
 # Issue 449 arises from incorrect group by attributes after joining with a dj.U()
 
 
 def test_issue449():
-    result = dj.U('n') * R.aggr(S, n='max(s)')
+    result = dj.U("n") * R.aggr(S, n="max(s)")
     result.fetch()
 
 
 # ---------------- ISSUE 484 -----------------
 # Issue 484
 def test_issue484():
-    q = dj.U().aggr(S, n='max(s)')
-    n = q.fetch('n')
-    n = q.fetch1('n')
-    q = dj.U().aggr(S, n='avg(s)')
-    result = dj.U().aggr(q, m='max(n)')
+    q = dj.U().aggr(S, n="max(s)")
+    n = q.fetch("n")
+    n = q.fetch1("n")
+    q = dj.U().aggr(S, n="avg(s)")
+    result = dj.U().aggr(q, m="max(n)")
     result.fetch()
 
+
 # ---------------  ISSUE 558 ------------------
 #  Issue 558 resulted from the fact that DataJoint saves subqueries and often combines a restriction followed
 #  by a projection into a single SELECT statement, which in several unusual cases produces unexpected results.
@@ -96,22 +99,23 @@ class X(dj.Lookup):
 
 
 def test_issue558_part1():
-    q = (A-B).proj(id2='3')
+    q = (A - B).proj(id2="3")
     assert_equal(len(A - B), len(q))
 
 
 def test_issue558_part2():
     d = dict(id=3, id2=5)
-    assert_equal(len(X & d), len((X & d).proj(id2='3')))
+    assert_equal(len(X & d), len((X & d).proj(id2="3")))
 
 
 def test_left_join_len():
-    Topic().add('jeff')
+    Topic().add("jeff")
     Item.populate()
-    Topic().add('jeff2')
-    Topic().add('jeff3')
-    q = Topic.join(Item - dict(topic_id=uuid.uuid5(top_level_namespace_id, 'jeff')),
-                   left=True)
+    Topic().add("jeff2")
+    Topic().add("jeff3")
+    q = Topic.join(
+        Item - dict(topic_id=uuid.uuid5(top_level_namespace_id, "jeff")), left=True
+    )
     qf = q.fetch()
     assert len(q) == len(qf)
 
@@ -120,11 +124,18 @@ def test_union_join():
     # https://github.com/datajoint/datajoint-python/issues/930
     A.insert(zip([100, 200, 300, 400, 500, 600]))
     B.insert([(100, 11), (200, 22), (300, 33), (400, 44)])
-    q1 = B & 'id < 300'
-    q2 = B & 'id > 300'
-
-    expected_data = [{'id': 0, 'id2': 5}, {'id': 1, 'id2': 6}, {'id': 2, 'id2': 7},
-                     {'id': 3, 'id2': 8}, {'id': 4, 'id2': 9}, {'id': 100, 'id2': 11},
-                     {'id': 200, 'id2': 22}, {'id': 400, 'id2': 44}]
+    q1 = B & "id < 300"
+    q2 = B & "id > 300"
+
+    expected_data = [
+        {"id": 0, "id2": 5},
+        {"id": 1, "id2": 6},
+        {"id": 2, "id2": 7},
+        {"id": 3, "id2": 8},
+        {"id": 4, "id2": 9},
+        {"id": 100, "id2": 11},
+        {"id": 200, "id2": 22},
+        {"id": 400, "id2": 44},
+    ]
 
     assert ((q1 + q2) * A).fetch(as_dict=True) == expected_data
diff --git a/tests/test_alter.py b/tests/test_alter.py
index 7a40ee822..c234d2bb2 100644
--- a/tests/test_alter.py
+++ b/tests/test_alter.py
@@ -29,13 +29,19 @@ class Experiment(dj.Imported):
 
 
 def test_alter():
-    original = schema.connection.query("SHOW CREATE TABLE " + Experiment.full_table_name).fetchone()[1]
+    original = schema.connection.query(
+        "SHOW CREATE TABLE " + Experiment.full_table_name
+    ).fetchone()[1]
     Experiment.definition = Experiment.definition1
     Experiment.alter(prompt=False)
-    altered = schema.connection.query("SHOW CREATE TABLE " + Experiment.full_table_name).fetchone()[1]
+    altered = schema.connection.query(
+        "SHOW CREATE TABLE " + Experiment.full_table_name
+    ).fetchone()[1]
     assert_not_equal(original, altered)
     Experiment.definition = Experiment.original_definition
     Experiment().alter(prompt=False)
-    restored = schema.connection.query("SHOW CREATE TABLE " + Experiment.full_table_name).fetchone()[1]
+    restored = schema.connection.query(
+        "SHOW CREATE TABLE " + Experiment.full_table_name
+    ).fetchone()[1]
     assert_not_equal(altered, restored)
     assert_equal(original, restored)
diff --git a/tests/test_attach.py b/tests/test_attach.py
index ebe866f0c..32ade5672 100644
--- a/tests/test_attach.py
+++ b/tests/test_attach.py
@@ -7,59 +7,62 @@
 
 
 def test_attach_attributes():
-    """ test saving files in attachments """
+    """test saving files in attachments"""
     # create a mock file
     table = Attach()
     source_folder = tempfile.mkdtemp()
     for i in range(2):
-        attach1 = Path(source_folder, 'attach1.img')
+        attach1 = Path(source_folder, "attach1.img")
         data1 = os.urandom(100)
-        with attach1.open('wb') as f:
+        with attach1.open("wb") as f:
             f.write(data1)
-        attach2 = Path(source_folder, 'attach2.txt')
+        attach2 = Path(source_folder, "attach2.txt")
         data2 = os.urandom(200)
-        with attach2.open('wb') as f:
+        with attach2.open("wb") as f:
             f.write(data2)
         table.insert1(dict(attach=i, img=attach1, txt=attach2))
 
     download_folder = Path(tempfile.mkdtemp())
-    keys, path1, path2 = table.fetch("KEY", 'img', 'txt', download_path=download_folder, order_by="KEY")
+    keys, path1, path2 = table.fetch(
+        "KEY", "img", "txt", download_path=download_folder, order_by="KEY"
+    )
 
     # verify that different attachment are renamed if their filenames collide
     assert_not_equal(path1[0], path2[0])
     assert_not_equal(path1[0], path1[1])
     assert_equal(Path(path1[0]).parent, download_folder)
-    with Path(path1[-1]).open('rb') as f:
+    with Path(path1[-1]).open("rb") as f:
         check1 = f.read()
-    with Path(path2[-1]).open('rb') as f:
+    with Path(path2[-1]).open("rb") as f:
         check2 = f.read()
     assert_equal(data1, check1)
     assert_equal(data2, check2)
 
     # verify that existing files are not duplicated if their filename matches issue #592
-    p1, p2 = (Attach & keys[0]).fetch1('img', 'txt', download_path=download_folder)
+    p1, p2 = (Attach & keys[0]).fetch1("img", "txt", download_path=download_folder)
     assert_equal(p1, path1[0])
     assert_equal(p2, path2[0])
 
 
 def test_return_string():
-    """ test returning string on fetch """
+    """test returning string on fetch"""
     # create a mock file
     table = Attach()
     source_folder = tempfile.mkdtemp()
 
-    attach1 = Path(source_folder, 'attach1.img')
+    attach1 = Path(source_folder, "attach1.img")
     data1 = os.urandom(100)
-    with attach1.open('wb') as f:
+    with attach1.open("wb") as f:
         f.write(data1)
-    attach2 = Path(source_folder, 'attach2.txt')
+    attach2 = Path(source_folder, "attach2.txt")
     data2 = os.urandom(200)
-    with attach2.open('wb') as f:
+    with attach2.open("wb") as f:
         f.write(data2)
     table.insert1(dict(attach=2, img=attach1, txt=attach2))
 
     download_folder = Path(tempfile.mkdtemp())
     keys, path1, path2 = table.fetch(
-            "KEY", 'img', 'txt', download_path=download_folder, order_by="KEY")
+        "KEY", "img", "txt", download_path=download_folder, order_by="KEY"
+    )
 
     assert_true(isinstance(path1[0], str))
diff --git a/tests/test_autopopulate.py b/tests/test_autopopulate.py
index 1875a6743..7683e4dd0 100644
--- a/tests/test_autopopulate.py
+++ b/tests/test_autopopulate.py
@@ -8,6 +8,7 @@ class TestPopulate:
     """
     Test base relations: insert, delete
     """
+
     def setUp(self):
         self.user = schema.User()
         self.subject = schema.Subject()
@@ -26,19 +27,21 @@ def tearDown(self):
 
     def test_populate(self):
         # test simple populate
-        assert_true(self.subject, 'root tables are empty')
-        assert_false(self.experiment, 'table already filled?')
+        assert_true(self.subject, "root tables are empty")
+        assert_false(self.experiment, "table already filled?")
         self.experiment.populate()
-        assert_true(len(self.experiment) ==
-                    len(self.subject)*self.experiment.fake_experiments_per_subject)
+        assert_true(
+            len(self.experiment)
+            == len(self.subject) * self.experiment.fake_experiments_per_subject
+        )
 
         # test restricted populate
-        assert_false(self.trial, 'table already filled?')
-        restriction = self.subject.proj(animal='subject_id').fetch('KEY')[0]
+        assert_false(self.trial, "table already filled?")
+        restriction = self.subject.proj(animal="subject_id").fetch("KEY")[0]
         d = self.trial.connection.dependencies
         d.load()
         self.trial.populate(restriction)
-        assert_true(self.trial, 'table was not populated')
+        assert_true(self.trial, "table was not populated")
         key_source = self.trial.key_source
         assert_equal(len(key_source & self.trial), len(key_source & restriction))
         assert_equal(len(key_source - self.trial), len(key_source - restriction))
@@ -51,22 +54,40 @@ def test_populate(self):
         assert_true(self.channel)
 
     def test_allow_direct_insert(self):
-        assert_true(self.subject, 'root tables are empty')
-        key = self.subject.fetch('KEY', limit=1)[0]
-        key['experiment_id'] = 1000
-        key['experiment_date'] = '2018-10-30'
+        assert_true(self.subject, "root tables are empty")
+        key = self.subject.fetch("KEY", limit=1)[0]
+        key["experiment_id"] = 1000
+        key["experiment_date"] = "2018-10-30"
         self.experiment.insert1(key, allow_direct_insert=True)
 
+    def test_multi_processing(self):
+        assert self.subject, "root tables are empty"
+        assert not self.experiment, "table already filled?"
+        self.experiment.populate(processes=2)
+        assert (
+            len(self.experiment)
+            == len(self.subject) * self.experiment.fake_experiments_per_subject
+        )
+
+    def test_max_multi_processing(self):
+        assert self.subject, "root tables are empty"
+        assert not self.experiment, "table already filled?"
+        self.experiment.populate(processes=None)
+        assert (
+            len(self.experiment)
+            == len(self.subject) * self.experiment.fake_experiments_per_subject
+        )
+
     @raises(DataJointError)
     def test_allow_insert(self):
-        assert_true(self.subject, 'root tables are empty')
-        key = self.subject.fetch('KEY')[0]
-        key['experiment_id'] = 1001
-        key['experiment_date'] = '2018-10-30'
+        assert_true(self.subject, "root tables are empty")
+        key = self.subject.fetch("KEY")[0]
+        key["experiment_id"] = 1001
+        key["experiment_date"] = "2018-10-30"
         self.experiment.insert1(key)
 
     def test_load_dependencies(self):
-        schema = dj.Schema(f'{PREFIX}_load_dependencies_populate')
+        schema = dj.Schema(f"{PREFIX}_load_dependencies_populate")
 
         @schema
         class ImageSource(dj.Lookup):
diff --git a/tests/test_blob.py b/tests/test_blob.py
index 61e60a8d0..6e7943399 100644
--- a/tests/test_blob.py
+++ b/tests/test_blob.py
@@ -1,17 +1,38 @@
+import datajoint as dj
 import numpy as np
 import uuid
+from . import schema
 from decimal import Decimal
 from datetime import datetime
 from datajoint.blob import pack, unpack
 from numpy.testing import assert_array_equal
-from nose.tools import assert_equal, assert_true, assert_false, \
-    assert_list_equal, assert_set_equal, assert_tuple_equal, assert_dict_equal
+from nose.tools import (
+    assert_equal,
+    assert_true,
+    assert_false,
+    assert_list_equal,
+    assert_set_equal,
+    assert_tuple_equal,
+    assert_dict_equal,
+)
 
 
 def test_pack():
 
-    for x in (32, -3.7e-2, np.float64(3e31), -np.inf, np.int8(-3), np.uint8(-1),
-              np.int16(-33), np.uint16(-33), np.int32(-3), np.uint32(-1), np.int64(373), np.uint64(-3)):
+    for x in (
+        32,
+        -3.7e-2,
+        np.float64(3e31),
+        -np.inf,
+        np.int8(-3),
+        np.uint8(-1),
+        np.int16(-33),
+        np.uint16(-33),
+        np.int32(-3),
+        np.uint32(-1),
+        np.int64(373),
+        np.uint64(-3),
+    ):
         assert_equal(x, unpack(pack(x)), "Scalars don't match!")
 
     x = np.nan
@@ -37,19 +58,31 @@ def test_pack():
 
     x = -255
     y = unpack(pack(x))
-    assert_true(x == y and isinstance(y, int) and not isinstance(y, np.ndarray), "Scalar int did not match")
+    assert_true(
+        x == y and isinstance(y, int) and not isinstance(y, np.ndarray),
+        "Scalar int did not match",
+    )
 
     x = -25523987234234287910987234987098245697129798713407812347
     y = unpack(pack(x))
-    assert_true(x == y and isinstance(y, int) and not isinstance(y, np.ndarray), "Unbounded int did not match")
+    assert_true(
+        x == y and isinstance(y, int) and not isinstance(y, np.ndarray),
+        "Unbounded int did not match",
+    )
 
-    x = 7.
+    x = 7.0
     y = unpack(pack(x))
-    assert_true(x == y and isinstance(y, float) and not isinstance(y, np.ndarray), "Scalar float did not match")
+    assert_true(
+        x == y and isinstance(y, float) and not isinstance(y, np.ndarray),
+        "Scalar float did not match",
+    )
 
     x = 7j
     y = unpack(pack(x))
-    assert_true(x == y and isinstance(y, complex) and not isinstance(y, np.ndarray), "Complex scalar did not match")
+    assert_true(
+        x == y and isinstance(y, complex) and not isinstance(y, np.ndarray),
+        "Complex scalar did not match",
+    )
 
     x = True
     assert_true(unpack(pack(x)) is True, "Scalar bool did not match")
@@ -57,13 +90,21 @@ def test_pack():
     x = [None]
     assert_list_equal(x, unpack(pack(x)))
 
-    x = {'name': 'Anonymous', 'age': 15, 99: datetime.now(), 'range': [110, 190], (11, 12): None}
+    x = {
+        "name": "Anonymous",
+        "age": 15,
+        99: datetime.now(),
+        "range": [110, 190],
+        (11, 12): None,
+    }
     y = unpack(pack(x))
     assert_dict_equal(x, y, "Dict do not match!")
-    assert_false(isinstance(['range'][0], np.ndarray), "Scalar int was coerced into arrray.")
+    assert_false(
+        isinstance(["range"][0], np.ndarray), "Scalar int was coerced into arrray."
+    )
 
     x = uuid.uuid4()
-    assert_equal(x, unpack(pack(x)), 'UUID did not match')
+    assert_equal(x, unpack(pack(x)), "UUID did not match")
 
     x = Decimal("-112122121.000003000")
     assert_equal(x, unpack(pack(x)), "Decimal did not pack/unpack correctly")
@@ -74,18 +115,24 @@ def test_pack():
     x = (1, datetime.now(), {1: "one", "two": 2}, (uuid.uuid4(), 2))
     assert_tuple_equal(x, unpack(pack(x)), "Tuple did not pack/unpack correctly")
 
-    x = (1, {datetime.now().date(): "today", "now": datetime.now().date()}, {"yes!": [1, 2, np.array((3, 4))]})
+    x = (
+        1,
+        {datetime.now().date(): "today", "now": datetime.now().date()},
+        {"yes!": [1, 2, np.array((3, 4))]},
+    )
     y = unpack(pack(x))
     assert_dict_equal(x[1], y[1])
-    assert_array_equal(x[2]['yes!'][2], y[2]['yes!'][2])
+    assert_array_equal(x[2]["yes!"][2], y[2]["yes!"][2])
 
-    x = {'elephant'}
+    x = {"elephant"}
     assert_set_equal(x, unpack(pack(x)), "Set did not pack/unpack correctly")
 
     x = tuple(range(10))
-    assert_tuple_equal(x, unpack(pack(range(10))), "Iterator did not pack/unpack correctly")
+    assert_tuple_equal(
+        x, unpack(pack(range(10))), "Iterator did not pack/unpack correctly"
+    )
 
-    x = Decimal('1.24')
+    x = Decimal("1.24")
     assert_true(x == unpack(pack(x)), "Decimal object did not pack/unpack correctly")
 
     x = datetime.now()
@@ -94,21 +141,23 @@ def test_pack():
     x = np.bool_(True)
     assert_true(x == unpack(pack(x)), "Numpy bool object did not pack/unpack correctly")
 
-    x = 'test'
+    x = "test"
     assert_true(x == unpack(pack(x)), "String object did not pack/unpack correctly")
 
-    x = np.array(['yes'])
-    assert_true(x == unpack(pack(x)), "Numpy string array object did not pack/unpack correctly")
+    x = np.array(["yes"])
+    assert_true(
+        x == unpack(pack(x)), "Numpy string array object did not pack/unpack correctly"
+    )
 
 
 def test_recarrays():
-    x = np.array([(1.0, 2), (3.0, 4)], dtype=[('x', float), ('y', int)])
+    x = np.array([(1.0, 2), (3.0, 4)], dtype=[("x", float), ("y", int)])
     assert_array_equal(x, unpack(pack(x)))
 
     x = x.view(np.recarray)
     assert_array_equal(x, unpack(pack(x)))
 
-    x = np.array([(3, 4)], dtype=[('tmp0', float), ('tmp1', 'O')]).view(np.recarray)
+    x = np.array([(3, 4)], dtype=[("tmp0", float), ("tmp1", "O")]).view(np.recarray)
     assert_array_equal(x, unpack(pack(x)))
 
 
@@ -118,14 +167,58 @@ def test_object_arrays():
 
 
 def test_complex():
-    z = np.random.randn(8, 10) + 1j*np.random.randn(8,10)
+    z = np.random.randn(8, 10) + 1j * np.random.randn(8, 10)
     assert_array_equal(z, unpack(pack(z)), "Arrays do not match!")
 
-    z = np.random.randn(10) + 1j*np.random.randn(10)
+    z = np.random.randn(10) + 1j * np.random.randn(10)
     assert_array_equal(z, unpack(pack(z)), "Arrays do not match!")
 
-    x = np.float32(np.random.randn(3, 4, 5)) + 1j*np.float32(np.random.randn(3, 4, 5))
+    x = np.float32(np.random.randn(3, 4, 5)) + 1j * np.float32(np.random.randn(3, 4, 5))
     assert_array_equal(x, unpack(pack(x)), "Arrays do not match!")
 
-    x = np.int16(np.random.randn(1, 2, 3)) + 1j*np.int16(np.random.randn(1, 2, 3))
+    x = np.int16(np.random.randn(1, 2, 3)) + 1j * np.int16(np.random.randn(1, 2, 3))
     assert_array_equal(x, unpack(pack(x)), "Arrays do not match!")
+
+
+def test_insert_longblob():
+    insert_dj_blob = {"id": 1, "data": [1, 2, 3]}
+    schema.Longblob.insert1(insert_dj_blob)
+    assert (schema.Longblob & "id=1").fetch1() == insert_dj_blob
+    (schema.Longblob & "id=1").delete()
+
+    query_mym_blob = {"id": 1, "data": np.array([1, 2, 3])}
+    schema.Longblob.insert1(query_mym_blob)
+    assert (schema.Longblob & "id=1").fetch1()["data"].all() == query_mym_blob[
+        "data"
+    ].all()
+    (schema.Longblob & "id=1").delete()
+
+    query_32_blob = (
+        "INSERT INTO djtest_test1.longblob (id, data) VALUES (1, "
+        "X'6D596D00530200000001000000010000000400000068697473007369646573007461736B73007374"
+        "616765004D000000410200000001000000070000000600000000000000000000000000F8FF00000000"
+        "0000F03F000000000000F03F0000000000000000000000000000F03F00000000000000000000000000"
+        "00F8FF230000004102000000010000000700000004000000000000006C006C006C006C00720072006C"
+        "0023000000410200000001000000070000000400000000000000640064006400640064006400640025"
+        "00000041020000000100000008000000040000000000000053007400610067006500200031003000')"
+    )
+    dj.conn().query(query_32_blob).fetchall()
+    dj.blob.use_32bit_dims = True
+    assert (schema.Longblob & "id=1").fetch1() == {
+        "id": 1,
+        "data": np.rec.array(
+            [
+                [
+                    (
+                        np.array([[np.nan, 1.0, 1.0, 0.0, 1.0, 0.0, np.nan]]),
+                        np.array(["llllrrl"], dtype="<U7"),
+                        np.array(["ddddddd"], dtype="<U7"),
+                        np.array(["Stage 10"], dtype="<U8"),
+                    )
+                ]
+            ],
+            dtype=[("hits", "O"), ("sides", "O"), ("tasks", "O"), ("stage", "O")],
+        ),
+    }
+    (schema.Longblob & "id=1").delete()
+    dj.blob.use_32bit_dims = False
diff --git a/tests/test_blob_matlab.py b/tests/test_blob_matlab.py
index 0593fa9be..6104c9291 100644
--- a/tests/test_blob_matlab.py
+++ b/tests/test_blob_matlab.py
@@ -7,7 +7,7 @@
 
 from . import PREFIX, CONN_INFO
 
-schema = dj.Schema(PREFIX + '_test1', locals(), connection=dj.conn(**CONN_INFO))
+schema = dj.Schema(PREFIX + "_test1", locals(), connection=dj.conn(**CONN_INFO))
 
 
 @schema
@@ -38,7 +38,8 @@ def insert_blobs():
             mysqldump -u username -p --hex-blob test_schema blob_table > blob.sql
     """
 
-    schema.connection.query("""
+    schema.connection.query(
+        """
     INSERT INTO {table_name} VALUES
     (1,'simple string',0x6D596D00410200000000000000010000000000000010000000000000000400000000000000630068006100720061006300740065007200200073007400720069006E006700),
     (2,'1D vector',0x6D596D0041020000000000000001000000000000000C000000000000000600000000000000000000000000F03F00000000000030400000000000003F4000000000000047400000000000804E4000000000000053400000000000C056400000000000805A400000000000405E4000000000000061400000000000E062400000000000C06440),
@@ -48,14 +49,16 @@ def insert_blobs():
     (6,'3D uint8 array',0x6D596D0041030000000000000002000000000000000300000000000000040000000000000009000000000000000102030405060708090A0B0C0D0E0F101112131415161718),
     (7,'3D complex array',0x6D596D0041030000000000000002000000000000000300000000000000040000000000000006000000010000000000000000C0724000000000000028C000000000000038C0000000000000000000000000000038C0000000000000000000000000000052C00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000052C00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000052C00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000AA4C58E87AB62B400000000000000000AA4C58E87AB62BC0000000000000008000000000000052400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080000000000000008000000000000052C000000000000000800000000000000080000000000000008000000000000000800000000000000080
     );
-    """.format(table_name=Blob.full_table_name))
+    """.format(
+            table_name=Blob.full_table_name
+        )
+    )
 
 
 class TestFetch:
-
     @classmethod
     def setup_class(cls):
-        assert_false(dj.config['safemode'], 'safemode must be disabled')
+        assert_false(dj.config["safemode"], "safemode must be disabled")
         Blob().delete()
         insert_blobs()
 
@@ -64,10 +67,10 @@ def test_complex_matlab_blobs():
         """
         test correct de-serialization of various blob types
         """
-        blobs = Blob().fetch('blob', order_by='KEY')
+        blobs = Blob().fetch("blob", order_by="KEY")
 
         blob = blobs[0]  # 'simple string'    'character string'
-        assert_equal(blob[0], 'character string')
+        assert_equal(blob[0], "character string")
 
         blob = blobs[1]  # '1D vector'        1:15:180
         assert_array_equal(blob, np.r_[1:180:15][None, :])
@@ -75,14 +78,16 @@ def test_complex_matlab_blobs():
 
         blob = blobs[2]  # 'string array'     {'string1'  'string2'}
         assert_true(isinstance(blob, dj.MatCell))
-        assert_array_equal(blob, np.array([['string1', 'string2']]))
+        assert_array_equal(blob, np.array([["string1", "string2"]]))
         assert_array_equal(blob, unpack(pack(blob)))
 
-        blob = blobs[3]  # 'struct array'     struct('a', {1,2},  'b', {struct('c', magic(3)), struct('C', magic(5))})
+        blob = blobs[
+            3
+        ]  # 'struct array'     struct('a', {1,2},  'b', {struct('c', magic(3)), struct('C', magic(5))})
         assert_true(isinstance(blob, dj.MatStruct))
-        assert_tuple_equal(blob.dtype.names, ('a', 'b'))
-        assert_array_equal(blob.a[0, 0], np.array([[1.]]))
-        assert_array_equal(blob.a[0, 1], np.array([[2.]]))
+        assert_tuple_equal(blob.dtype.names, ("a", "b"))
+        assert_array_equal(blob.a[0, 0], np.array([[1.0]]))
+        assert_array_equal(blob.a[0, 1], np.array([[2.0]]))
         assert_true(isinstance(blob.b[0, 1], dj.MatStruct))
         assert_tuple_equal(blob.b[0, 1].C[0, 0].shape, (5, 5))
         b = unpack(pack(blob))
@@ -90,18 +95,18 @@ def test_complex_matlab_blobs():
         assert_array_equal(b[0, 1].b[0, 0].C, blob[0, 1].b[0, 0].C)
 
         blob = blobs[4]  # '3D double array'  reshape(1:24, [2,3,4])
-        assert_array_equal(blob, np.r_[1:25].reshape((2, 3, 4), order='F'))
-        assert_true(blob.dtype == 'float64')
+        assert_array_equal(blob, np.r_[1:25].reshape((2, 3, 4), order="F"))
+        assert_true(blob.dtype == "float64")
         assert_array_equal(blob, unpack(pack(blob)))
 
         blob = blobs[5]  # reshape(uint8(1:24), [2,3,4])
-        assert_true(np.array_equal(blob, np.r_[1:25].reshape((2, 3, 4), order='F')))
-        assert_true(blob.dtype == 'uint8')
+        assert_true(np.array_equal(blob, np.r_[1:25].reshape((2, 3, 4), order="F")))
+        assert_true(blob.dtype == "uint8")
         assert_array_equal(blob, unpack(pack(blob)))
 
         blob = blobs[6]  # fftn(reshape(1:24, [2,3,4]))
         assert_tuple_equal(blob.shape, (2, 3, 4))
-        assert_true(blob.dtype == 'complex128')
+        assert_true(blob.dtype == "complex128")
         assert_array_equal(blob, unpack(pack(blob)))
 
     @staticmethod
@@ -109,40 +114,62 @@ def test_complex_matlab_squeeze():
         """
         test correct de-serialization of various blob types
         """
-        blob = (Blob & 'id=1').fetch1('blob', squeeze=True)  # 'simple string'    'character string'
-        assert_equal(blob, 'character string')
-
-        blob = (Blob & 'id=2').fetch1('blob', squeeze=True)  # '1D vector'        1:15:180
+        blob = (Blob & "id=1").fetch1(
+            "blob", squeeze=True
+        )  # 'simple string'    'character string'
+        assert_equal(blob, "character string")
+
+        blob = (Blob & "id=2").fetch1(
+            "blob", squeeze=True
+        )  # '1D vector'        1:15:180
         assert_array_equal(blob, np.r_[1:180:15])
 
-        blob = (Blob & 'id=3').fetch1('blob', squeeze=True)  # 'string array'     {'string1'  'string2'}
+        blob = (Blob & "id=3").fetch1(
+            "blob", squeeze=True
+        )  # 'string array'     {'string1'  'string2'}
         assert_true(isinstance(blob, dj.MatCell))
-        assert_array_equal(blob, np.array(['string1', 'string2']))
+        assert_array_equal(blob, np.array(["string1", "string2"]))
 
-        blob = (Blob & 'id=4').fetch1('blob', squeeze=True)  # 'struct array' struct('a', {1,2},  'b', {struct('c', magic(3)), struct('C', magic(5))})
+        blob = (Blob & "id=4").fetch1(
+            "blob", squeeze=True
+        )  # 'struct array' struct('a', {1,2},  'b', {struct('c', magic(3)), struct('C', magic(5))})
         assert_true(isinstance(blob, dj.MatStruct))
-        assert_tuple_equal(blob.dtype.names, ('a', 'b'))
-        assert_array_equal(blob.a, np.array([1., 2,]))
+        assert_tuple_equal(blob.dtype.names, ("a", "b"))
+        assert_array_equal(
+            blob.a,
+            np.array(
+                [
+                    1.0,
+                    2,
+                ]
+            ),
+        )
         assert_true(isinstance(blob[1].b, dj.MatStruct))
         assert_tuple_equal(blob[1].b.C.item().shape, (5, 5))
 
-        blob = (Blob & 'id=5').fetch1('blob', squeeze=True)  # '3D double array'  reshape(1:24, [2,3,4])
-        assert_true(np.array_equal(blob, np.r_[1:25].reshape((2, 3, 4), order='F')))
-        assert_true(blob.dtype == 'float64')
-
-        blob = (Blob & 'id=6').fetch1('blob', squeeze=True)  # reshape(uint8(1:24), [2,3,4])
-        assert_true(np.array_equal(blob, np.r_[1:25].reshape((2, 3, 4), order='F')))
-        assert_true(blob.dtype == 'uint8')
-
-        blob = (Blob & 'id=7').fetch1('blob', squeeze=True)  # fftn(reshape(1:24, [2,3,4]))
+        blob = (Blob & "id=5").fetch1(
+            "blob", squeeze=True
+        )  # '3D double array'  reshape(1:24, [2,3,4])
+        assert_true(np.array_equal(blob, np.r_[1:25].reshape((2, 3, 4), order="F")))
+        assert_true(blob.dtype == "float64")
+
+        blob = (Blob & "id=6").fetch1(
+            "blob", squeeze=True
+        )  # reshape(uint8(1:24), [2,3,4])
+        assert_true(np.array_equal(blob, np.r_[1:25].reshape((2, 3, 4), order="F")))
+        assert_true(blob.dtype == "uint8")
+
+        blob = (Blob & "id=7").fetch1(
+            "blob", squeeze=True
+        )  # fftn(reshape(1:24, [2,3,4]))
         assert_tuple_equal(blob.shape, (2, 3, 4))
-        assert_true(blob.dtype == 'complex128')
+        assert_true(blob.dtype == "complex128")
 
     @staticmethod
     def test_iter():
         """
         test iterator over the entity set
         """
-        from_iter = {d['id']: d for d in Blob()}
+        from_iter = {d["id"]: d for d in Blob()}
         assert_equal(len(from_iter), len(Blob()))
-        assert_equal(from_iter[1]['blob'], 'character string')
+        assert_equal(from_iter[1]["blob"], "character string")
diff --git a/tests/test_blob_migrate.py b/tests/test_blob_migrate.py
index 54b0d75d0..fa806c557 100644
--- a/tests/test_blob_migrate.py
+++ b/tests/test_blob_migrate.py
@@ -9,54 +9,55 @@
 
 
 class TestBlobMigrate:
-
     @staticmethod
     def test_convert():
         # Configure stores
-        default_store = 'external'  # naming the unnamed external store
-        dj.config['stores'] = {
+        default_store = "external"  # naming the unnamed external store
+        dj.config["stores"] = {
             default_store: dict(
-                protocol='s3',
-                endpoint=S3_CONN_INFO['endpoint'],
+                protocol="s3",
+                endpoint=S3_CONN_INFO["endpoint"],
                 bucket=S3_MIGRATE_BUCKET,
-                location='store',
-                access_key=S3_CONN_INFO['access_key'],
-                secret_key=S3_CONN_INFO['secret_key']),
-            'shared': dict(
-                protocol='s3',
-                endpoint=S3_CONN_INFO['endpoint'],
+                location="store",
+                access_key=S3_CONN_INFO["access_key"],
+                secret_key=S3_CONN_INFO["secret_key"],
+            ),
+            "shared": dict(
+                protocol="s3",
+                endpoint=S3_CONN_INFO["endpoint"],
                 bucket=S3_MIGRATE_BUCKET,
-                location='maps',
-                access_key=S3_CONN_INFO['access_key'],
-                secret_key=S3_CONN_INFO['secret_key']),
-            'local': dict(
-                protocol='file',
-                location=str(Path(os.path.expanduser('~'),'temp',S3_MIGRATE_BUCKET)))
+                location="maps",
+                access_key=S3_CONN_INFO["access_key"],
+                secret_key=S3_CONN_INFO["secret_key"],
+            ),
+            "local": dict(
+                protocol="file",
+                location=str(Path(os.path.expanduser("~"), "temp", S3_MIGRATE_BUCKET)),
+            ),
         }
-        dj.config['cache'] = str(Path(os.path.expanduser('~'),'temp','dj-cache'))
+        dj.config["cache"] = str(Path(os.path.expanduser("~"), "temp", "dj-cache"))
 
-        dj.config['database.password'] = CONN_INFO['password']
-        dj.config['database.user'] = CONN_INFO['user']
-        dj.config['database.host'] = CONN_INFO['host']
-        schema = dj.Schema('djtest_blob_migrate')
+        dj.config["database.password"] = CONN_INFO["password"]
+        dj.config["database.user"] = CONN_INFO["user"]
+        dj.config["database.host"] = CONN_INFO["host"]
+        schema = dj.Schema("djtest_blob_migrate")
 
         # Test if migration throws unexpected exceptions
         _migrate_dj011_blob(schema, default_store)
 
         # Test Fetch
-        test_mod = dj.create_virtual_module('test_mod', 'djtest_blob_migrate')
-        r1 = test_mod.A.fetch('blob_share', order_by='id')
+        test_mod = dj.create_virtual_module("test_mod", "djtest_blob_migrate")
+        r1 = test_mod.A.fetch("blob_share", order_by="id")
         assert_equal(r1[1][1], 2)
 
         # Test Insert
-        test_mod.A.insert1({
-            'id': 3,
-            'blob_external': [9, 8, 7, 6],
-            'blob_share': {'number': 5}})
-        r2 = (test_mod.A & 'id=3').fetch1()
-        assert_equal(r2['blob_share']['number'], 5)
+        test_mod.A.insert1(
+            {"id": 3, "blob_external": [9, 8, 7, 6], "blob_share": {"number": 5}}
+        )
+        r2 = (test_mod.A & "id=3").fetch1()
+        assert_equal(r2["blob_share"]["number"], 5)
 
     @staticmethod
     @raises(ValueError)
     def test_type_check():
-        dj.migrate_dj011_external_blob_storage_to_dj012(10, 'store')
+        dj.migrate_dj011_external_blob_storage_to_dj012(10, "store")
diff --git a/tests/test_bypass_serialization.py b/tests/test_bypass_serialization.py
index 7f177f2a3..aa6e89ffc 100644
--- a/tests/test_bypass_serialization.py
+++ b/tests/test_bypass_serialization.py
@@ -6,12 +6,13 @@
 from nose.tools import assert_true
 
 
+schema_in = dj.Schema(
+    PREFIX + "_test_bypass_serialization_in", connection=dj.conn(**CONN_INFO)
+)
 
-schema_in = dj.Schema(PREFIX + '_test_bypass_serialization_in',
-                      connection=dj.conn(**CONN_INFO))
-
-schema_out = dj.Schema(PREFIX + '_test_blob_bypass_serialization_out',
-                       connection=dj.conn(**CONN_INFO))
+schema_out = dj.Schema(
+    PREFIX + "_test_blob_bypass_serialization_out", connection=dj.conn(**CONN_INFO)
+)
 
 
 test_blob = np.array([1, 2, 3])
@@ -39,8 +40,7 @@ class Output(dj.Manual):
 def test_bypass_serialization():
     dj.blob.bypass_serialization = True
     contents = Input.fetch(as_dict=True)
-    assert_true(isinstance(contents[0]['data'], bytes))
+    assert_true(isinstance(contents[0]["data"], bytes))
     Output.insert(contents)
     dj.blob.bypass_serialization = False
-    assert_array_equal(Input.fetch1('data'), Output.fetch1('data'))
-
+    assert_array_equal(Input.fetch1("data"), Output.fetch1("data"))
diff --git a/tests/test_cascading_delete.py b/tests/test_cascading_delete.py
index 6e730590a..ca55b0261 100644
--- a/tests/test_cascading_delete.py
+++ b/tests/test_cascading_delete.py
@@ -5,7 +5,6 @@
 
 
 class TestDelete:
-
     @staticmethod
     def setup():
         """
@@ -19,59 +18,71 @@ def setup():
 
     @staticmethod
     def test_delete_tree():
-        assert_false(dj.config['safemode'], 'safemode must be off for testing')
-        assert_true(L() and A() and B() and B.C() and D() and E() and E.F(),
-                    'schema is not populated')
+        assert_false(dj.config["safemode"], "safemode must be off for testing")
+        assert_true(
+            L() and A() and B() and B.C() and D() and E() and E.F(),
+            "schema is not populated",
+        )
         A().delete()
-        assert_false(A() or B() or B.C() or D() or E() or E.F(), 'incomplete delete')
+        assert_false(A() or B() or B.C() or D() or E() or E.F(), "incomplete delete")
 
     @staticmethod
     def test_stepwise_delete():
-        assert not dj.config['safemode'], 'safemode must be off for testing'
-        assert L() and A() and B() and B.C(), 'schema population failed'
+        assert not dj.config["safemode"], "safemode must be off for testing"
+        assert L() and A() and B() and B.C(), "schema population failed"
         B.C().delete(force=True)
-        assert not B.C(), 'failed to delete child tables'
+        assert not B.C(), "failed to delete child tables"
         B().delete()
-        assert not B(), \
-            'failed to delete from the parent table following child table deletion'
+        assert (
+            not B()
+        ), "failed to delete from the parent table following child table deletion"
 
     @staticmethod
     def test_delete_tree_restricted():
-        assert not dj.config['safemode'], 'safemode must be off for testing'
-        assert L() and A() and B() and B.C() and D() and E() and E.F(), \
-            'schema is not populated'
-        cond = 'cond_in_a'
+        assert not dj.config["safemode"], "safemode must be off for testing"
+        assert (
+            L() and A() and B() and B.C() and D() and E() and E.F()
+        ), "schema is not populated"
+        cond = "cond_in_a"
         rel = A() & cond
         rest = dict(
-            A=len(A())-len(rel),
-            B=len(B()-rel),
-            C=len(B.C()-rel),
-            D=len(D()-rel),
-            E=len(E()-rel),
-            F=len(E.F()-rel))
+            A=len(A()) - len(rel),
+            B=len(B() - rel),
+            C=len(B.C() - rel),
+            D=len(D() - rel),
+            E=len(E() - rel),
+            F=len(E.F() - rel),
+        )
         rel.delete()
-        assert not (rel or B() & rel or B.C() & rel or D() & rel or E() & rel
-                    or (E.F() & rel)), 'incomplete delete'
-        assert len(A()) == rest['A'], 'invalid delete restriction'
-        assert len(B()) == rest['B'], 'invalid delete restriction'
-        assert len(B.C()) == rest['C'], 'invalid delete restriction'
-        assert len(D()) == rest['D'], 'invalid delete restriction'
-        assert len(E()) == rest['E'], 'invalid delete restriction'
-        assert len(E.F()) == rest['F'], 'invalid delete restriction'
+        assert not (
+            rel or B() & rel or B.C() & rel or D() & rel or E() & rel or (E.F() & rel)
+        ), "incomplete delete"
+        assert len(A()) == rest["A"], "invalid delete restriction"
+        assert len(B()) == rest["B"], "invalid delete restriction"
+        assert len(B.C()) == rest["C"], "invalid delete restriction"
+        assert len(D()) == rest["D"], "invalid delete restriction"
+        assert len(E()) == rest["E"], "invalid delete restriction"
+        assert len(E.F()) == rest["F"], "invalid delete restriction"
 
     @staticmethod
     def test_delete_lookup():
-        assert_false(dj.config['safemode'], 'safemode must be off for testing')
-        assert_true(bool(L() and A() and B() and B.C() and D() and E() and E.F()), 'schema is not populated')
+        assert_false(dj.config["safemode"], "safemode must be off for testing")
+        assert_true(
+            bool(L() and A() and B() and B.C() and D() and E() and E.F()),
+            "schema is not populated",
+        )
         L().delete()
-        assert_false(bool(L() or D() or E() or E.F()), 'incomplete delete')
+        assert_false(bool(L() or D() or E() or E.F()), "incomplete delete")
         A().delete()  # delete all is necessary because delete L deletes from subtables.
 
     @staticmethod
     def test_delete_lookup_restricted():
-        assert_false(dj.config['safemode'], 'safemode must be off for testing')
-        assert_true(L() and A() and B() and B.C() and D() and E() and E.F(), 'schema is not populated')
-        rel = L() & 'cond_in_l'
+        assert_false(dj.config["safemode"], "safemode must be off for testing")
+        assert_true(
+            L() and A() and B() and B.C() and D() and E() and E.F(),
+            "schema is not populated",
+        )
+        rel = L() & "cond_in_l"
         original_count = len(L())
         deleted_count = len(rel)
         rel.delete()
@@ -81,18 +92,21 @@ def test_delete_lookup_restricted():
     def test_delete_complex_keys():
         # https://github.com/datajoint/datajoint-python/issues/883
         # https://github.com/datajoint/datajoint-python/issues/886
-        assert_false(dj.config['safemode'], 'safemode must be off for testing')
+        assert_false(dj.config["safemode"], "safemode must be off for testing")
         parent_key_count = 8
         child_key_count = 1
-        restriction = dict({'parent_id_{}'.format(i+1): i
-                            for i in range(parent_key_count)},
-                           **{'child_id_{}'.format(i+1): (i + parent_key_count)
-                              for i in range(child_key_count)})
-        assert len(ComplexParent & restriction) == 1, 'Parent record missing'
-        assert len(ComplexChild & restriction) == 1, 'Child record missing'
+        restriction = dict(
+            {"parent_id_{}".format(i + 1): i for i in range(parent_key_count)},
+            **{
+                "child_id_{}".format(i + 1): (i + parent_key_count)
+                for i in range(child_key_count)
+            }
+        )
+        assert len(ComplexParent & restriction) == 1, "Parent record missing"
+        assert len(ComplexChild & restriction) == 1, "Child record missing"
         (ComplexParent & restriction).delete()
-        assert len(ComplexParent & restriction) == 0, 'Parent record was not deleted'
-        assert len(ComplexChild & restriction) == 0, 'Child record was not deleted'
+        assert len(ComplexParent & restriction) == 0, "Parent record was not deleted"
+        assert len(ComplexChild & restriction) == 0, "Child record was not deleted"
 
     def test_delete_master(self):
         Profile().populate_random()
diff --git a/tests/test_connection.py b/tests/test_connection.py
index 05872018a..3d2861a95 100644
--- a/tests/test_connection.py
+++ b/tests/test_connection.py
@@ -43,7 +43,7 @@ def test_persistent_dj_conn():
 
 def test_repr():
     c1 = dj.conn(**CONN_INFO)
-    assert_true('disconnected' not in repr(c1) and 'connected' in repr(c1))
+    assert_true("disconnected" not in repr(c1) and "connected" in repr(c1))
 
 
 class TestTransactions:
@@ -51,7 +51,9 @@ class TestTransactions:
     test transaction management
     """
 
-    schema = dj.Schema(PREFIX + '_transactions', locals(), connection=dj.conn(**CONN_INFO))
+    schema = dj.Schema(
+        PREFIX + "_transactions", locals(), connection=dj.conn(**CONN_INFO)
+    )
 
     @schema
     class Subjects(dj.Manual):
@@ -77,10 +79,10 @@ def test_active(self):
 
     def test_transaction_rollback(self):
         """Test transaction cancellation using a with statement"""
-        tmp = np.array([
-            (1, 'Peter', 'mouse'),
-            (2, 'Klara', 'monkey')
-        ],  self.relation.heading.as_dtype)
+        tmp = np.array(
+            [(1, "Peter", "mouse"), (2, "Klara", "monkey")],
+            self.relation.heading.as_dtype,
+        )
 
         self.relation.delete()
         with self.conn.transaction:
@@ -91,23 +93,35 @@ def test_transaction_rollback(self):
                 raise DataJointError("Testing rollback")
         except DataJointError:
             pass
-        assert_equal(len(self.relation), 1,
-                     "Length is not 1. Expected because rollback should have happened.")
-        assert_equal(len(self.relation & 'subject_id = 2'), 0,
-                     "Length is not 0. Expected because rollback should have happened.")
+        assert_equal(
+            len(self.relation),
+            1,
+            "Length is not 1. Expected because rollback should have happened.",
+        )
+        assert_equal(
+            len(self.relation & "subject_id = 2"),
+            0,
+            "Length is not 0. Expected because rollback should have happened.",
+        )
 
     def test_cancel(self):
         """Tests cancelling a transaction explicitly"""
-        tmp = np.array([
-            (1, 'Peter', 'mouse'),
-            (2, 'Klara', 'monkey')
-        ],  self.relation.heading.as_dtype)
+        tmp = np.array(
+            [(1, "Peter", "mouse"), (2, "Klara", "monkey")],
+            self.relation.heading.as_dtype,
+        )
         self.relation.delete_quick()
         self.relation.insert1(tmp[0])
         self.conn.start_transaction()
         self.relation.insert1(tmp[1])
         self.conn.cancel_transaction()
-        assert_equal(len(self.relation), 1,
-                     "Length is not 1. Expected because rollback should have happened.")
-        assert_equal(len(self.relation & 'subject_id = 2'), 0,
-                     "Length is not 0. Expected because rollback should have happened.")
+        assert_equal(
+            len(self.relation),
+            1,
+            "Length is not 1. Expected because rollback should have happened.",
+        )
+        assert_equal(
+            len(self.relation & "subject_id = 2"),
+            0,
+            "Length is not 0. Expected because rollback should have happened.",
+        )
diff --git a/tests/test_declare.py b/tests/test_declare.py
index f0128e2b5..67f532449 100644
--- a/tests/test_declare.py
+++ b/tests/test_declare.py
@@ -1,4 +1,11 @@
-from nose.tools import assert_true, assert_false, assert_equal, assert_list_equal, raises, assert_set_equal
+from nose.tools import (
+    assert_true,
+    assert_false,
+    assert_equal,
+    assert_list_equal,
+    raises,
+    assert_set_equal,
+)
 from .schema import *
 import datajoint as dj
 import inspect
@@ -16,7 +23,6 @@
 
 
 class TestDeclare:
-
     @staticmethod
     def test_schema_decorator():
         assert_true(issubclass(Subject, dj.Lookup))
@@ -73,7 +79,7 @@ class Type(dj.Lookup):
             definition = """
             type :  varchar(255)
             """
-            contents = zip(('Type1', 'Type2', 'Type3'))
+            contents = zip(("Type1", "Type2", "Type3"))
 
         @local_schema
         class TypeMaster(dj.Manual):
@@ -90,39 +96,52 @@ class Type(dj.Part):
     @staticmethod
     def test_attributes():
         # test autoincrement declaration
-        assert_list_equal(auto.heading.names, ['id', 'name'])
-        assert_true(auto.heading.attributes['id'].autoincrement)
+        assert_list_equal(auto.heading.names, ["id", "name"])
+        assert_true(auto.heading.attributes["id"].autoincrement)
 
         # test attribute declarations
-        assert_list_equal(subject.heading.names,
-                          ['subject_id', 'real_id', 'species', 'date_of_birth', 'subject_notes'])
-        assert_list_equal(subject.primary_key,
-                          ['subject_id'])
-        assert_true(subject.heading.attributes['subject_id'].numeric)
-        assert_false(subject.heading.attributes['real_id'].numeric)
-
-        assert_list_equal(experiment.heading.names,
-                          ['subject_id', 'experiment_id', 'experiment_date',
-                           'username', 'data_path',
-                           'notes', 'entry_time'])
-        assert_list_equal(experiment.primary_key,
-                          ['subject_id', 'experiment_id'])
-
-        assert_list_equal(trial.heading.names,   # tests issue #516
-                          ['animal', 'experiment_id', 'trial_id', 'start_time'])
-        assert_list_equal(trial.primary_key,
-                          ['animal', 'experiment_id', 'trial_id'])
-
-        assert_list_equal(ephys.heading.names,
-                          ['animal', 'experiment_id', 'trial_id', 'sampling_frequency', 'duration'])
-        assert_list_equal(ephys.primary_key,
-                          ['animal', 'experiment_id', 'trial_id'])
-
-        assert_list_equal(channel.heading.names,
-                          ['animal', 'experiment_id', 'trial_id', 'channel', 'voltage', 'current'])
-        assert_list_equal(channel.primary_key,
-                          ['animal', 'experiment_id', 'trial_id', 'channel'])
-        assert_true(channel.heading.attributes['voltage'].is_blob)
+        assert_list_equal(
+            subject.heading.names,
+            ["subject_id", "real_id", "species", "date_of_birth", "subject_notes"],
+        )
+        assert_list_equal(subject.primary_key, ["subject_id"])
+        assert_true(subject.heading.attributes["subject_id"].numeric)
+        assert_false(subject.heading.attributes["real_id"].numeric)
+
+        assert_list_equal(
+            experiment.heading.names,
+            [
+                "subject_id",
+                "experiment_id",
+                "experiment_date",
+                "username",
+                "data_path",
+                "notes",
+                "entry_time",
+            ],
+        )
+        assert_list_equal(experiment.primary_key, ["subject_id", "experiment_id"])
+
+        assert_list_equal(
+            trial.heading.names,  # tests issue #516
+            ["animal", "experiment_id", "trial_id", "start_time"],
+        )
+        assert_list_equal(trial.primary_key, ["animal", "experiment_id", "trial_id"])
+
+        assert_list_equal(
+            ephys.heading.names,
+            ["animal", "experiment_id", "trial_id", "sampling_frequency", "duration"],
+        )
+        assert_list_equal(ephys.primary_key, ["animal", "experiment_id", "trial_id"])
+
+        assert_list_equal(
+            channel.heading.names,
+            ["animal", "experiment_id", "trial_id", "channel", "voltage", "current"],
+        )
+        assert_list_equal(
+            channel.primary_key, ["animal", "experiment_id", "trial_id", "channel"]
+        )
+        assert_true(channel.heading.attributes["voltage"].is_blob)
 
     @staticmethod
     def test_dependencies():
@@ -130,35 +149,67 @@ def test_dependencies():
         assert_equal(set(experiment.parents(primary=False)), {user.full_table_name})
         assert_true(experiment.full_table_name in user.children(primary=False))
         assert_set_equal(set(experiment.parents(primary=False)), {user.full_table_name})
-        assert_set_equal(set(s.full_table_name for s in experiment.parents(primary=False, as_objects=True)),
-                         {user.full_table_name})
+        assert_set_equal(
+            set(
+                s.full_table_name
+                for s in experiment.parents(primary=False, as_objects=True)
+            ),
+            {user.full_table_name},
+        )
 
         assert_true(experiment.full_table_name in subject.descendants())
-        assert_true(experiment.full_table_name in {s.full_table_name for s in subject.descendants(as_objects=True)})
+        assert_true(
+            experiment.full_table_name
+            in {s.full_table_name for s in subject.descendants(as_objects=True)}
+        )
         assert_true(subject.full_table_name in experiment.ancestors())
-        assert_true(subject.full_table_name in {s.full_table_name for s in experiment.ancestors(as_objects=True)})
+        assert_true(
+            subject.full_table_name
+            in {s.full_table_name for s in experiment.ancestors(as_objects=True)}
+        )
 
         assert_true(trial.full_table_name in experiment.descendants())
-        assert_true(trial.full_table_name in {s.full_table_name for s in experiment.descendants(as_objects=True)})
+        assert_true(
+            trial.full_table_name
+            in {s.full_table_name for s in experiment.descendants(as_objects=True)}
+        )
         assert_true(experiment.full_table_name in trial.ancestors())
-        assert_true(experiment.full_table_name in {s.full_table_name for s in trial.ancestors(as_objects=True)})
-
-        assert_set_equal(set(trial.children(primary=True)), {ephys.full_table_name, trial.Condition.full_table_name})
+        assert_true(
+            experiment.full_table_name
+            in {s.full_table_name for s in trial.ancestors(as_objects=True)}
+        )
+
+        assert_set_equal(
+            set(trial.children(primary=True)),
+            {ephys.full_table_name, trial.Condition.full_table_name},
+        )
         assert_set_equal(set(trial.parts()), {trial.Condition.full_table_name})
-        assert_set_equal(set(s.full_table_name for s in trial.parts(as_objects=True)),
-                         {trial.Condition.full_table_name})
-        assert_set_equal(set(ephys.parents(primary=True)),
-                         {trial.full_table_name})
-        assert_set_equal(set(s.full_table_name for s in ephys.parents(primary=True, as_objects=True)),
-                         {trial.full_table_name})
-        assert_set_equal(set(ephys.children(primary=True)),
-                         {channel.full_table_name})
-        assert_set_equal(set(s.full_table_name for s in ephys.children(primary=True, as_objects=True)),
-                         {channel.full_table_name})
-        assert_set_equal(set(channel.parents(primary=True)),
-                         {ephys.full_table_name})
-        assert_set_equal(set(s.full_table_name for s in channel.parents(primary=True, as_objects=True)),
-                         {ephys.full_table_name})
+        assert_set_equal(
+            set(s.full_table_name for s in trial.parts(as_objects=True)),
+            {trial.Condition.full_table_name},
+        )
+        assert_set_equal(set(ephys.parents(primary=True)), {trial.full_table_name})
+        assert_set_equal(
+            set(
+                s.full_table_name for s in ephys.parents(primary=True, as_objects=True)
+            ),
+            {trial.full_table_name},
+        )
+        assert_set_equal(set(ephys.children(primary=True)), {channel.full_table_name})
+        assert_set_equal(
+            set(
+                s.full_table_name for s in ephys.children(primary=True, as_objects=True)
+            ),
+            {channel.full_table_name},
+        )
+        assert_set_equal(set(channel.parents(primary=True)), {ephys.full_table_name})
+        assert_set_equal(
+            set(
+                s.full_table_name
+                for s in channel.parents(primary=True, as_objects=True)
+            ),
+            {ephys.full_table_name},
+        )
 
     @staticmethod
     def test_descendants_only_contain_part_table():
@@ -189,12 +240,15 @@ class Part(dj.Part):
                 -> B
                 """
 
-        assert A.descendants() == ['`djtest_test1`.`a`', '`djtest_test1`.`b`', '`djtest_test1`.`master__part`']
+        assert A.descendants() == [
+            "`djtest_test1`.`a`",
+            "`djtest_test1`.`b`",
+            "`djtest_test1`.`master__part`",
+        ]
 
     @staticmethod
     @raises(dj.DataJointError)
     def test_bad_attribute_name():
-
         @schema
         class BadName(dj.Manual):
             definition = """
@@ -241,7 +295,6 @@ class R(dj.Manual):
     @staticmethod
     @raises(dj.DataJointError)
     def test_unsupported_datatype():
-
         @schema
         class Q(dj.Manual):
             definition = """
@@ -252,7 +305,6 @@ class Q(dj.Manual):
 
     @staticmethod
     def test_int_datatype():
-
         @schema
         class Owner(dj.Manual):
             definition = """
@@ -264,7 +316,6 @@ class Owner(dj.Manual):
     @staticmethod
     @raises(dj.DataJointError)
     def test_unsupported_int_datatype():
-
         @schema
         class Driver(dj.Manual):
             definition = """
diff --git a/tests/test_dependencies.py b/tests/test_dependencies.py
index de9b13603..c359b602a 100644
--- a/tests/test_dependencies.py
+++ b/tests/test_dependencies.py
@@ -4,29 +4,54 @@
 
 
 def test_unite_master_parts():
-    assert_list_equal(unite_master_parts(
-        ['`s`.`a`', '`s`.`a__q`', '`s`.`b`', '`s`.`c`', '`s`.`c__q`', '`s`.`b__q`', '`s`.`d`', '`s`.`a__r`']),
-        ['`s`.`a`', '`s`.`a__q`', '`s`.`a__r`', '`s`.`b`', '`s`.`b__q`', '`s`.`c`', '`s`.`c__q`', '`s`.`d`'])
-    assert_list_equal(unite_master_parts(
+    assert_list_equal(
+        unite_master_parts(
+            [
+                "`s`.`a`",
+                "`s`.`a__q`",
+                "`s`.`b`",
+                "`s`.`c`",
+                "`s`.`c__q`",
+                "`s`.`b__q`",
+                "`s`.`d`",
+                "`s`.`a__r`",
+            ]
+        ),
         [
-            '`lab`.`#equipment`',
-            '`cells`.`cell_analysis_method`',
-            '`cells`.`cell_analysis_method_task_type`',
-            '`cells`.`cell_analysis_method_users`',
-            '`cells`.`favorite_selection`',
-            '`cells`.`cell_analysis_method__cell_selection_params`',
-            '`lab`.`#equipment__config`',
-            '`cells`.`cell_analysis_method__field_detect_params`']),
+            "`s`.`a`",
+            "`s`.`a__q`",
+            "`s`.`a__r`",
+            "`s`.`b`",
+            "`s`.`b__q`",
+            "`s`.`c`",
+            "`s`.`c__q`",
+            "`s`.`d`",
+        ],
+    )
+    assert_list_equal(
+        unite_master_parts(
+            [
+                "`lab`.`#equipment`",
+                "`cells`.`cell_analysis_method`",
+                "`cells`.`cell_analysis_method_task_type`",
+                "`cells`.`cell_analysis_method_users`",
+                "`cells`.`favorite_selection`",
+                "`cells`.`cell_analysis_method__cell_selection_params`",
+                "`lab`.`#equipment__config`",
+                "`cells`.`cell_analysis_method__field_detect_params`",
+            ]
+        ),
         [
-            '`lab`.`#equipment`',
-            '`lab`.`#equipment__config`',
-            '`cells`.`cell_analysis_method`',
-            '`cells`.`cell_analysis_method__cell_selection_params`',
-            '`cells`.`cell_analysis_method__field_detect_params`',
-            '`cells`.`cell_analysis_method_task_type`',
-            '`cells`.`cell_analysis_method_users`',
-            '`cells`.`favorite_selection`'
-        ])
+            "`lab`.`#equipment`",
+            "`lab`.`#equipment__config`",
+            "`cells`.`cell_analysis_method`",
+            "`cells`.`cell_analysis_method__cell_selection_params`",
+            "`cells`.`cell_analysis_method__field_detect_params`",
+            "`cells`.`cell_analysis_method_task_type`",
+            "`cells`.`cell_analysis_method_users`",
+            "`cells`.`favorite_selection`",
+        ],
+    )
 
 
 def test_nullable_dependency():
diff --git a/tests/test_erd.py b/tests/test_erd.py
index d1905e418..1a6293431 100644
--- a/tests/test_erd.py
+++ b/tests/test_erd.py
@@ -7,7 +7,6 @@
 
 
 class TestERD:
-
     @staticmethod
     def setup():
         """
@@ -27,18 +26,26 @@ def test_decorator():
     def test_dependencies():
         deps = schema.connection.dependencies
         deps.load()
-        assert_true(all(cls.full_table_name in deps for cls in (A, B, B.C, D, E, E.F, L)))
+        assert_true(
+            all(cls.full_table_name in deps for cls in (A, B, B.C, D, E, E.F, L))
+        )
         assert_true(set(A().children()) == set([B.full_table_name, D.full_table_name]))
         assert_true(set(D().parents(primary=True)) == set([A.full_table_name]))
         assert_true(set(D().parents(primary=False)) == set([L.full_table_name]))
-        assert_true(set(deps.descendants(L.full_table_name)).issubset(cls.full_table_name for cls in (L, D, E, E.F)))
+        assert_true(
+            set(deps.descendants(L.full_table_name)).issubset(
+                cls.full_table_name for cls in (L, D, E, E.F)
+            )
+        )
 
     @staticmethod
     def test_erd():
-        assert_true(dj.diagram.diagram_active, 'Failed to import networkx and pydot')
+        assert_true(dj.diagram.diagram_active, "Failed to import networkx and pydot")
         erd = dj.ERD(schema, context=namespace)
         graph = erd._make_graph()
-        assert_true(set(cls.__name__ for cls in (A, B, D, E, L)).issubset(graph.nodes()))
+        assert_true(
+            set(cls.__name__ for cls in (A, B, D, E, L)).issubset(graph.nodes())
+        )
 
     @staticmethod
     def test_erd_algebra():
@@ -48,16 +55,22 @@ def test_erd_algebra():
         erd3 = erd1 * erd2
         erd4 = (erd0 + E).add_parts() - B - E
         assert_true(erd0.nodes_to_show == set(cls.full_table_name for cls in [B]))
-        assert_true(erd1.nodes_to_show == set(cls.full_table_name for cls in (B, B.C, E, E.F)))
-        assert_true(erd2.nodes_to_show == set(cls.full_table_name for cls in (A, B, D, E, L)))
+        assert_true(
+            erd1.nodes_to_show == set(cls.full_table_name for cls in (B, B.C, E, E.F))
+        )
+        assert_true(
+            erd2.nodes_to_show == set(cls.full_table_name for cls in (A, B, D, E, L))
+        )
         assert_true(erd3.nodes_to_show == set(cls.full_table_name for cls in (B, E)))
-        assert_true(erd4.nodes_to_show == set(cls.full_table_name for cls in (B.C, E.F)))
+        assert_true(
+            erd4.nodes_to_show == set(cls.full_table_name for cls in (B.C, E.F))
+        )
 
     @staticmethod
     def test_repr_svg():
         erd = dj.ERD(schema_advanced, context=namespace)
         svg = erd._repr_svg_()
-        assert_true(svg.startswith('<svg') and svg.endswith('svg>'))
+        assert_true(svg.startswith("<svg") and svg.endswith("svg>"))
 
     @staticmethod
     def test_make_image():
@@ -70,5 +83,5 @@ def test_part_table_parsing():
         # https://github.com/datajoint/datajoint-python/issues/882
         erd = dj.Di(schema)
         graph = erd._make_graph()
-        assert 'OutfitLaunch' in graph.nodes()
-        assert 'OutfitLaunch.OutfitPiece' in graph.nodes()
+        assert "OutfitLaunch" in graph.nodes()
+        assert "OutfitLaunch.OutfitPiece" in graph.nodes()
diff --git a/tests/test_external.py b/tests/test_external.py
index fb85a2afc..fcbb21fce 100644
--- a/tests/test_external.py
+++ b/tests/test_external.py
@@ -7,24 +7,24 @@
 from .schema_external import stores_config, SimpleRemote, Simple, schema
 import os
 
-current_location_s3 = dj.config['stores']['share']['location']
-current_location_local = dj.config['stores']['local']['location']
+current_location_s3 = dj.config["stores"]["share"]["location"]
+current_location_local = dj.config["stores"]["local"]["location"]
 
 
 def setUp(self):
-    dj.config['stores'] = stores_config
+    dj.config["stores"] = stores_config
 
 
 def tearDown(self):
-    dj.config['stores']['share']['location'] = current_location_s3
-    dj.config['stores']['local']['location'] = current_location_local
+    dj.config["stores"]["share"]["location"] = current_location_s3
+    dj.config["stores"]["local"]["location"] = current_location_local
 
 
 def test_external_put():
     """
     external storage put and get and remove
     """
-    ext = ExternalTable(schema.connection, store='raw', database=schema.database)
+    ext = ExternalTable(schema.connection, store="raw", database=schema.database)
     initial_length = len(ext)
     input_ = np.random.randn(3, 7, 8)
     count = 7
@@ -34,7 +34,7 @@ def test_external_put():
     for i in range(extra):
         hash2 = ext.put(pack(np.random.randn(4, 3, 2)))
 
-    fetched_hashes = ext.fetch('hash')
+    fetched_hashes = ext.fetch("hash")
     assert_true(all(hash in fetched_hashes for hash in (hash1, hash2)))
     assert_equal(len(ext), initial_length + 1 + extra)
 
@@ -42,85 +42,94 @@ def test_external_put():
     assert_array_equal(input_, output_)
 
 
-def test_s3_leading_slash(index=100, store='share'):
+def test_s3_leading_slash(index=100, store="share"):
     """
     s3 external storage configured with leading slash
     """
 
-    oldConfig = dj.config['stores'][store]['location']
+    oldConfig = dj.config["stores"][store]["location"]
 
     value = np.array([1, 2, 3])
 
     id = index
-    dj.config['stores'][store]['location'] = 'leading/slash/test'
-    SimpleRemote.insert([{'simple': id, 'item': value}])
-    assert_true(np.array_equal(
-        value, (SimpleRemote & 'simple={}'.format(id)).fetch1('item')))
+    dj.config["stores"][store]["location"] = "leading/slash/test"
+    SimpleRemote.insert([{"simple": id, "item": value}])
+    assert_true(
+        np.array_equal(value, (SimpleRemote & "simple={}".format(id)).fetch1("item"))
+    )
 
     id = index + 1
-    dj.config['stores'][store]['location'] = '/leading/slash/test'
-    SimpleRemote.insert([{'simple': id, 'item': value}])
-    assert_true(np.array_equal(
-        value, (SimpleRemote & 'simple={}'.format(id)).fetch1('item')))
+    dj.config["stores"][store]["location"] = "/leading/slash/test"
+    SimpleRemote.insert([{"simple": id, "item": value}])
+    assert_true(
+        np.array_equal(value, (SimpleRemote & "simple={}".format(id)).fetch1("item"))
+    )
 
     id = index + 2
-    dj.config['stores'][store]['location'] = 'leading\\slash\\test'
-    SimpleRemote.insert([{'simple': id, 'item': value}])
-    assert_true(np.array_equal(
-        value, (SimpleRemote & 'simple={}'.format(id)).fetch1('item')))
+    dj.config["stores"][store]["location"] = "leading\\slash\\test"
+    SimpleRemote.insert([{"simple": id, "item": value}])
+    assert_true(
+        np.array_equal(value, (SimpleRemote & "simple={}".format(id)).fetch1("item"))
+    )
 
     id = index + 3
-    dj.config['stores'][store]['location'] = 'f:\\leading\\slash\\test'
-    SimpleRemote.insert([{'simple': id, 'item': value}])
-    assert_true(np.array_equal(
-        value, (SimpleRemote & 'simple={}'.format(id)).fetch1('item')))
+    dj.config["stores"][store]["location"] = "f:\\leading\\slash\\test"
+    SimpleRemote.insert([{"simple": id, "item": value}])
+    assert_true(
+        np.array_equal(value, (SimpleRemote & "simple={}".format(id)).fetch1("item"))
+    )
 
     id = index + 4
-    dj.config['stores'][store]['location'] = 'f:\\leading/slash\\test'
-    SimpleRemote.insert([{'simple': id, 'item': value}])
-    assert_true(np.array_equal(
-        value, (SimpleRemote & 'simple={}'.format(id)).fetch1('item')))
+    dj.config["stores"][store]["location"] = "f:\\leading/slash\\test"
+    SimpleRemote.insert([{"simple": id, "item": value}])
+    assert_true(
+        np.array_equal(value, (SimpleRemote & "simple={}".format(id)).fetch1("item"))
+    )
 
     id = index + 5
-    dj.config['stores'][store]['location'] = '/'
-    SimpleRemote.insert([{'simple': id, 'item': value}])
-    assert_true(np.array_equal(
-        value, (SimpleRemote & 'simple={}'.format(id)).fetch1('item')))
+    dj.config["stores"][store]["location"] = "/"
+    SimpleRemote.insert([{"simple": id, "item": value}])
+    assert_true(
+        np.array_equal(value, (SimpleRemote & "simple={}".format(id)).fetch1("item"))
+    )
 
     id = index + 6
-    dj.config['stores'][store]['location'] = 'C:\\'
-    SimpleRemote.insert([{'simple': id, 'item': value}])
-    assert_true(np.array_equal(
-        value, (SimpleRemote & 'simple={}'.format(id)).fetch1('item')))
+    dj.config["stores"][store]["location"] = "C:\\"
+    SimpleRemote.insert([{"simple": id, "item": value}])
+    assert_true(
+        np.array_equal(value, (SimpleRemote & "simple={}".format(id)).fetch1("item"))
+    )
 
     id = index + 7
-    dj.config['stores'][store]['location'] = ''
-    SimpleRemote.insert([{'simple': id, 'item': value}])
-    assert_true(np.array_equal(
-        value, (SimpleRemote & 'simple={}'.format(id)).fetch1('item')))
+    dj.config["stores"][store]["location"] = ""
+    SimpleRemote.insert([{"simple": id, "item": value}])
+    assert_true(
+        np.array_equal(value, (SimpleRemote & "simple={}".format(id)).fetch1("item"))
+    )
 
-    dj.config['stores'][store]['location'] = oldConfig
+    dj.config["stores"][store]["location"] = oldConfig
 
 
 def test_file_leading_slash():
     """
     file external storage configured with leading slash
     """
-    test_s3_leading_slash(index=200, store='local')
+    test_s3_leading_slash(index=200, store="local")
 
 
 def test_remove_fail():
     # https://github.com/datajoint/datajoint-python/issues/953
     data = dict(simple=2, item=[1, 2, 3])
     Simple.insert1(data)
-    path1 = dj.config['stores']['local']['location'] + '/djtest_extern/4/c/'
+    path1 = dj.config["stores"]["local"]["location"] + "/djtest_extern/4/c/"
     currentMode = int(oct(os.stat(path1).st_mode), 8)
     os.chmod(path1, 0o40555)
-    (Simple & 'simple=2').delete()
-    listOfErrors = schema.external['local'].delete(delete_external_files=True)
-    assert len(listOfErrors) == 1, 'unexpected number of errors'
-    assert len(schema.external['local'] & dict(hash=listOfErrors[0][0])) == 1, \
-        'unexpected number of rows in external table'
+    (Simple & "simple=2").delete()
+    listOfErrors = schema.external["local"].delete(delete_external_files=True)
+    assert len(listOfErrors) == 1, "unexpected number of errors"
+    assert (
+        len(schema.external["local"] & dict(hash=listOfErrors[0][0])) == 1
+    ), "unexpected number of rows in external table"
     # ---------------------CLEAN UP--------------------
     os.chmod(path1, currentMode)
-    listOfErrors = schema.external['local'].delete(delete_external_files=True)
\ No newline at end of file
+    listOfErrors = schema.external["local"].delete(delete_external_files=True)
diff --git a/tests/test_external_class.py b/tests/test_external_class.py
index e2018dd5b..63c1fb994 100644
--- a/tests/test_external_class.py
+++ b/tests/test_external_class.py
@@ -5,36 +5,36 @@
 
 
 def setUp(self):
-    dj.config['stores'] = modu.stores_config
+    dj.config["stores"] = modu.stores_config
 
 
 def test_heading():
     heading = modu.Simple().heading
-    assert_true('item' in heading)
-    assert_true(heading['item'].is_external)
+    assert_true("item" in heading)
+    assert_true(heading["item"].is_external)
 
 
 def test_insert_and_fetch():
     original_list = [1, 3, 8]
     modu.Simple().insert1(dict(simple=1, item=original_list))
     # test fetch
-    q = (modu.Simple() & {'simple': 1}).fetch('item')[0]
+    q = (modu.Simple() & {"simple": 1}).fetch("item")[0]
     assert_list_equal(list(q), original_list)
     # test fetch1 as a tuple
-    q = (modu.Simple() & {'simple': 1}).fetch1('item')
+    q = (modu.Simple() & {"simple": 1}).fetch1("item")
     assert_list_equal(list(q), original_list)
     # test fetch1 as a dict
-    q = (modu.Simple() & {'simple': 1}).fetch1()
-    assert_list_equal(list(q['item']), original_list)
+    q = (modu.Simple() & {"simple": 1}).fetch1()
+    assert_list_equal(list(q["item"]), original_list)
     # test without cache
-    previous_cache = dj.config['cache']
-    dj.config['cache'] = None
-    q = (modu.Simple() & {'simple': 1}).fetch1()
-    assert_list_equal(list(q['item']), original_list)
+    previous_cache = dj.config["cache"]
+    dj.config["cache"] = None
+    q = (modu.Simple() & {"simple": 1}).fetch1()
+    assert_list_equal(list(q["item"]), original_list)
     # test with cache
-    dj.config['cache'] = previous_cache
-    q = (modu.Simple() & {'simple': 1}).fetch1()
-    assert_list_equal(list(q['item']), original_list)
+    dj.config["cache"] = previous_cache
+    q = (modu.Simple() & {"simple": 1}).fetch1()
+    assert_list_equal(list(q["item"]), original_list)
 
 
 def test_populate():
@@ -42,7 +42,9 @@ def test_populate():
     image.populate()
     remaining, total = image.progress()
     assert_true(total == len(modu.Dimension() * modu.Seed()) and remaining == 0)
-    for img, neg, dimensions in zip(*(image * modu.Dimension()).fetch('img', 'neg', 'dimensions')):
+    for img, neg, dimensions in zip(
+        *(image * modu.Dimension()).fetch("img", "neg", "dimensions")
+    ):
         assert_list_equal(list(img.shape), list(dimensions))
         assert_almost_equal(img, -neg)
     image.delete()
@@ -50,6 +52,3 @@ def test_populate():
     for external_table in image.external.values():
         external_table.delete(display_progress=False, delete_external_files=True)
     dj.errors._switch_filepath_types(False)
-
-
-
diff --git a/tests/test_fetch.py b/tests/test_fetch.py
index e074ee7ca..e4d4da33b 100644
--- a/tests/test_fetch.py
+++ b/tests/test_fetch.py
@@ -1,4 +1,11 @@
-from nose.tools import assert_true, raises, assert_equal, assert_dict_equal, assert_list_equal, assert_set_equal
+from nose.tools import (
+    assert_true,
+    raises,
+    assert_equal,
+    assert_dict_equal,
+    assert_list_equal,
+    assert_set_equal,
+)
 from operator import itemgetter
 import itertools
 import numpy as np
@@ -12,7 +19,6 @@
 
 
 class TestFetch:
-
     @classmethod
     def setup_class(cls):
         cls.subject = schema.Subject()
@@ -20,78 +26,93 @@ def setup_class(cls):
 
     def test_getattribute(self):
         """Testing Fetch.__call__ with attributes"""
-        list1 = sorted(self.subject.proj().fetch(as_dict=True), key=itemgetter('subject_id'))
-        list2 = sorted(self.subject.fetch(dj.key), key=itemgetter('subject_id'))
+        list1 = sorted(
+            self.subject.proj().fetch(as_dict=True), key=itemgetter("subject_id")
+        )
+        list2 = sorted(self.subject.fetch(dj.key), key=itemgetter("subject_id"))
         for l1, l2 in zip(list1, list2):
-            assert_dict_equal(l1, l2, 'Primary key is not returned correctly')
+            assert_dict_equal(l1, l2, "Primary key is not returned correctly")
 
-        tmp = self.subject.fetch(order_by='subject_id')
+        tmp = self.subject.fetch(order_by="subject_id")
 
-        subject_notes, key, real_id = self.subject.fetch('subject_notes', dj.key, 'real_id')
+        subject_notes, key, real_id = self.subject.fetch(
+            "subject_notes", dj.key, "real_id"
+        )
 
-        np.testing.assert_array_equal(sorted(subject_notes), sorted(tmp['subject_notes']))
-        np.testing.assert_array_equal(sorted(real_id), sorted(tmp['real_id']))
-        list1 = sorted(key, key=itemgetter('subject_id'))
+        np.testing.assert_array_equal(
+            sorted(subject_notes), sorted(tmp["subject_notes"])
+        )
+        np.testing.assert_array_equal(sorted(real_id), sorted(tmp["real_id"]))
+        list1 = sorted(key, key=itemgetter("subject_id"))
         for l1, l2 in zip(list1, list2):
-            assert_dict_equal(l1, l2, 'Primary key is not returned correctly')
+            assert_dict_equal(l1, l2, "Primary key is not returned correctly")
 
     def test_getattribute_for_fetch1(self):
         """Testing Fetch1.__call__ with attributes"""
-        assert_true((self.subject & "subject_id=10").fetch1('subject_id') == 10)
-        assert_equal((self.subject & "subject_id=10").fetch1('subject_id', 'species'),
-                     (10, 'monkey'))
+        assert_true((self.subject & "subject_id=10").fetch1("subject_id") == 10)
+        assert_equal(
+            (self.subject & "subject_id=10").fetch1("subject_id", "species"),
+            (10, "monkey"),
+        )
 
     def test_order_by(self):
         """Tests order_by sorting order"""
         languages = schema.Language.contents
 
-        for ord_name, ord_lang in itertools.product(*2 * [['ASC', 'DESC']]):
-            cur = self.lang.fetch(order_by=('name ' + ord_name, 'language ' + ord_lang))
-            languages.sort(key=itemgetter(1), reverse=ord_lang == 'DESC')
-            languages.sort(key=itemgetter(0), reverse=ord_name == 'DESC')
+        for ord_name, ord_lang in itertools.product(*2 * [["ASC", "DESC"]]):
+            cur = self.lang.fetch(order_by=("name " + ord_name, "language " + ord_lang))
+            languages.sort(key=itemgetter(1), reverse=ord_lang == "DESC")
+            languages.sort(key=itemgetter(0), reverse=ord_name == "DESC")
             for c, l in zip(cur, languages):
-                assert_true(np.all(cc == ll for cc, ll in zip(c, l)), 'Sorting order is different')
+                assert_true(
+                    np.all(cc == ll for cc, ll in zip(c, l)),
+                    "Sorting order is different",
+                )
 
     def test_order_by_default(self):
         """Tests order_by sorting order with defaults"""
         languages = schema.Language.contents
-        cur = self.lang.fetch(order_by=('language', 'name DESC'))
+        cur = self.lang.fetch(order_by=("language", "name DESC"))
         languages.sort(key=itemgetter(0), reverse=True)
         languages.sort(key=itemgetter(1), reverse=False)
         for c, l in zip(cur, languages):
-            assert_true(np.all([cc == ll for cc, ll in zip(c, l)]), 'Sorting order is different')
+            assert_true(
+                np.all([cc == ll for cc, ll in zip(c, l)]), "Sorting order is different"
+            )
 
     def test_limit(self):
         """Test the limit kwarg"""
         limit = 4
         cur = self.lang.fetch(limit=limit)
-        assert_equal(len(cur), limit, 'Length is not correct')
+        assert_equal(len(cur), limit, "Length is not correct")
 
     def test_order_by_limit(self):
         """Test the combination of order by and limit kwargs"""
         languages = schema.Language.contents
 
-        cur = self.lang.fetch(limit=4, order_by=['language', 'name DESC'])
+        cur = self.lang.fetch(limit=4, order_by=["language", "name DESC"])
         languages.sort(key=itemgetter(0), reverse=True)
         languages.sort(key=itemgetter(1), reverse=False)
-        assert_equal(len(cur), 4, 'Length is not correct')
+        assert_equal(len(cur), 4, "Length is not correct")
         for c, l in list(zip(cur, languages))[:4]:
-            assert_true(np.all([cc == ll for cc, ll in zip(c, l)]), 'Sorting order is different')
+            assert_true(
+                np.all([cc == ll for cc, ll in zip(c, l)]), "Sorting order is different"
+            )
 
     @staticmethod
     def test_head_tail():
         query = schema.User * schema.Language
         n = 5
-        frame = query.head(n, format='frame')
+        frame = query.head(n, format="frame")
         assert_true(isinstance(frame, pandas.DataFrame))
-        array = query.head(n, format='array')
+        array = query.head(n, format="array")
         assert_equal(array.size, n)
         assert_equal(len(frame), n)
         assert_list_equal(query.primary_key, frame.index.names)
 
         n = 4
-        frame = query.tail(n, format='frame')
-        array = query.tail(n, format='array')
+        frame = query.tail(n, format="frame")
+        array = query.tail(n, format="array")
         assert_equal(array.size, n)
         assert_equal(len(frame), n)
         assert_list_equal(query.primary_key, frame.index.names)
@@ -100,25 +121,30 @@ def test_limit_offset(self):
         """Test the limit and offset kwargs together"""
         languages = schema.Language.contents
 
-        cur = self.lang.fetch(offset=2, limit=4, order_by=['language', 'name DESC'])
+        cur = self.lang.fetch(offset=2, limit=4, order_by=["language", "name DESC"])
         languages.sort(key=itemgetter(0), reverse=True)
         languages.sort(key=itemgetter(1), reverse=False)
-        assert_equal(len(cur), 4, 'Length is not correct')
+        assert_equal(len(cur), 4, "Length is not correct")
         for c, l in list(zip(cur, languages[2:6])):
-            assert_true(np.all([cc == ll for cc, ll in zip(c, l)]), 'Sorting order is different')
+            assert_true(
+                np.all([cc == ll for cc, ll in zip(c, l)]), "Sorting order is different"
+            )
 
     def test_iter(self):
         """Test iterator"""
         languages = schema.Language.contents
-        cur = self.lang.fetch(order_by=['language', 'name DESC'])
+        cur = self.lang.fetch(order_by=["language", "name DESC"])
         languages.sort(key=itemgetter(0), reverse=True)
         languages.sort(key=itemgetter(1), reverse=False)
         for (name, lang), (tname, tlang) in list(zip(cur, languages)):
-            assert_true(name == tname and lang == tlang, 'Values are not the same')
+            assert_true(name == tname and lang == tlang, "Values are not the same")
         # now as dict
-        cur = self.lang.fetch(as_dict=True, order_by=('language', 'name DESC'))
+        cur = self.lang.fetch(as_dict=True, order_by=("language", "name DESC"))
         for row, (tname, tlang) in list(zip(cur, languages)):
-            assert_true(row['name'] == tname and row['language'] == tlang, 'Values are not the same')
+            assert_true(
+                row["name"] == tname and row["language"] == tlang,
+                "Values are not the same",
+            )
 
     def test_keys(self):
         """test key fetch"""
@@ -126,25 +152,26 @@ def test_keys(self):
         languages.sort(key=itemgetter(0), reverse=True)
         languages.sort(key=itemgetter(1), reverse=False)
 
-        cur = self.lang.fetch('name', 'language', order_by=('language', 'name DESC'))
-        cur2 = list(self.lang.fetch("KEY", order_by=['language', 'name DESC']))
+        cur = self.lang.fetch("name", "language", order_by=("language", "name DESC"))
+        cur2 = list(self.lang.fetch("KEY", order_by=["language", "name DESC"]))
 
         for c, c2 in zip(zip(*cur), cur2):
-            assert_true(c == tuple(c2.values()), 'Values are not the same')
+            assert_true(c == tuple(c2.values()), "Values are not the same")
 
-    def test_attributes_as_dict(self):   # issue #595
-        attrs = ('species', 'date_of_birth')
+    def test_attributes_as_dict(self):  # issue #595
+        attrs = ("species", "date_of_birth")
         result = self.subject.fetch(*attrs, as_dict=True)
         assert_true(bool(result) and len(result) == len(self.subject))
         assert_set_equal(set(result[0]), set(attrs))
 
     def test_fetch1_step1(self):
-        key = {'name': 'Edgar', 'language': 'Japanese'}
+        key = {"name": "Edgar", "language": "Japanese"}
         true = schema.Language.contents[-1]
         dat = (self.lang & key).fetch1()
         for k, (ke, c) in zip(true, dat.items()):
-            assert_true(k == c == (self.lang & key).fetch1(ke),
-                        'Values are not the same')
+            assert_true(
+                k == c == (self.lang & key).fetch1(ke), "Values are not the same"
+            )
 
     @raises(dj.DataJointError)
     def test_misspelled_attribute(self):
@@ -153,8 +180,8 @@ def test_misspelled_attribute(self):
     def test_repr(self):
         """Test string representation of fetch, returning table preview"""
         repr = self.subject.fetch.__repr__()
-        n = len(repr.strip().split('\n'))
-        limit = dj.config['display.limit']
+        n = len(repr.strip().split("\n"))
+        limit = dj.config["display.limit"]
         # 3 lines are used for headers (2) and summary statement (1)
         assert_true(n - 3 <= limit)
 
@@ -172,14 +199,16 @@ def test_asdict(self):
     def test_offset(self):
         """Tests offset"""
         with warnings.catch_warnings(record=True) as w:
-            cur = self.lang.fetch(limit=4, offset=1, order_by=['language', 'name DESC'])
+            cur = self.lang.fetch(limit=4, offset=1, order_by=["language", "name DESC"])
 
         languages = self.lang.contents
         languages.sort(key=itemgetter(0), reverse=True)
         languages.sort(key=itemgetter(1), reverse=False)
-        assert_equal(len(cur), 4, 'Length is not correct')
+        assert_equal(len(cur), 4, "Length is not correct")
         for c, l in list(zip(cur, languages[1:]))[:4]:
-            assert_true(np.all([cc == ll for cc, ll in zip(c, l)]), 'Sorting order is different')
+            assert_true(
+                np.all([cc == ll for cc, ll in zip(c, l)]), "Sorting order is different"
+            )
 
     def test_limit_warning(self):
         """Tests whether warning is raised if offset is used without limit."""
@@ -189,7 +218,9 @@ def test_limit_warning(self):
 
     def test_len(self):
         """Tests __len__"""
-        assert_equal(len(self.lang.fetch()), len(self.lang), '__len__ is not behaving properly')
+        assert_equal(
+            len(self.lang.fetch()), len(self.lang), "__len__ is not behaving properly"
+        )
 
     @raises(dj.DataJointError)
     def test_fetch1_step2(self):
@@ -199,84 +230,100 @@ def test_fetch1_step2(self):
     @raises(dj.DataJointError)
     def test_fetch1_step3(self):
         """Tests whether fetch1 raises error"""
-        self.lang.fetch1('name')
+        self.lang.fetch1("name")
 
     def test_decimal(self):
-        """ Tests that decimal fields are correctly fetched and used in restrictions, see issue #334"""
+        """Tests that decimal fields are correctly fetched and used in restrictions, see issue #334"""
         rel = schema.DecimalPrimaryKey()
-        rel.insert1([decimal.Decimal('3.1415926')])
+        rel.insert1([decimal.Decimal("3.1415926")])
         keys = rel.fetch()
         assert_true(len(rel & keys[0]) == 1)
         keys = rel.fetch(dj.key)
         assert_true(len(rel & keys[1]) == 1)
 
     def test_nullable_numbers(self):
-        """ test mixture of values and nulls in numeric attributes """
+        """test mixture of values and nulls in numeric attributes"""
         table = schema.NullableNumbers()
-        table.insert((
-            (k, np.random.randn(), np.random.randint(-1000, 1000), np.random.randn())
-            for k in range(10)))
+        table.insert(
+            (
+                (
+                    k,
+                    np.random.randn(),
+                    np.random.randint(-1000, 1000),
+                    np.random.randn(),
+                )
+                for k in range(10)
+            )
+        )
         table.insert1((100, None, None, None))
-        f, d, i = table.fetch('fvalue', 'dvalue', 'ivalue')
+        f, d, i = table.fetch("fvalue", "dvalue", "ivalue")
         assert_true(None in i)
         assert_true(any(np.isnan(d)))
         assert_true(any(np.isnan(f)))
 
     def test_fetch_format(self):
         """test fetch_format='frame'"""
-        with dj.config(fetch_format='frame'):
+        with dj.config(fetch_format="frame"):
             # test if lists are both dicts
-            list1 = sorted(self.subject.proj().fetch(as_dict=True), key=itemgetter('subject_id'))
-            list2 = sorted(self.subject.fetch(dj.key), key=itemgetter('subject_id'))
+            list1 = sorted(
+                self.subject.proj().fetch(as_dict=True), key=itemgetter("subject_id")
+            )
+            list2 = sorted(self.subject.fetch(dj.key), key=itemgetter("subject_id"))
             for l1, l2 in zip(list1, list2):
-                assert_dict_equal(l1, l2, 'Primary key is not returned correctly')
+                assert_dict_equal(l1, l2, "Primary key is not returned correctly")
 
             # tests if pandas dataframe
-            tmp = self.subject.fetch(order_by='subject_id')
+            tmp = self.subject.fetch(order_by="subject_id")
             assert_true(isinstance(tmp, pandas.DataFrame))
             tmp = tmp.to_records()
 
-            subject_notes, key, real_id = self.subject.fetch('subject_notes', dj.key, 'real_id')
+            subject_notes, key, real_id = self.subject.fetch(
+                "subject_notes", dj.key, "real_id"
+            )
 
-            np.testing.assert_array_equal(sorted(subject_notes), sorted(tmp['subject_notes']))
-            np.testing.assert_array_equal(sorted(real_id), sorted(tmp['real_id']))
-            list1 = sorted(key, key=itemgetter('subject_id'))
+            np.testing.assert_array_equal(
+                sorted(subject_notes), sorted(tmp["subject_notes"])
+            )
+            np.testing.assert_array_equal(sorted(real_id), sorted(tmp["real_id"]))
+            list1 = sorted(key, key=itemgetter("subject_id"))
             for l1, l2 in zip(list1, list2):
-                assert_dict_equal(l1, l2, 'Primary key is not returned correctly')
+                assert_dict_equal(l1, l2, "Primary key is not returned correctly")
 
     def test_key_fetch1(self):
         """test KEY fetch1 - issue #976"""
         with dj.config(fetch_format="array"):
-            k1 = (self.subject & 'subject_id=10').fetch1('KEY')
+            k1 = (self.subject & "subject_id=10").fetch1("KEY")
         with dj.config(fetch_format="frame"):
-            k2 = (self.subject & 'subject_id=10').fetch1('KEY')
+            k2 = (self.subject & "subject_id=10").fetch1("KEY")
         assert_equal(k1, k2)
 
     def test_same_secondary_attribute(self):
-        children = (schema.Child * schema.Parent().proj()).fetch()['name']
+        children = (schema.Child * schema.Parent().proj()).fetch()["name"]
         assert len(children) == 1
-        assert children[0] == 'Dan'
+        assert children[0] == "Dan"
 
     def test_query_caching(self):
         # initialize cache directory
-        os.mkdir(os.path.expanduser('~/dj_query_cache'))
+        os.mkdir(os.path.expanduser("~/dj_query_cache"))
 
-        with dj.config(query_cache=os.path.expanduser('~/dj_query_cache')):
+        with dj.config(query_cache=os.path.expanduser("~/dj_query_cache")):
             conn = schema.TTest3.connection
             # insert sample data and load cache
-            schema.TTest3.insert([dict(key=100+i, value=200+i) for i in range(2)])
-            conn.set_query_cache(query_cache='main')
+            schema.TTest3.insert([dict(key=100 + i, value=200 + i) for i in range(2)])
+            conn.set_query_cache(query_cache="main")
             cached_res = schema.TTest3().fetch()
             # attempt to insert while caching enabled
             try:
-                schema.TTest3.insert([dict(key=200+i, value=400+i) for i in range(2)])
-                assert False, 'Insert allowed while query caching enabled'
+                schema.TTest3.insert(
+                    [dict(key=200 + i, value=400 + i) for i in range(2)]
+                )
+                assert False, "Insert allowed while query caching enabled"
             except dj.DataJointError:
                 conn.set_query_cache()
             # insert new data
-            schema.TTest3.insert([dict(key=600+i, value=800+i) for i in range(2)])
+            schema.TTest3.insert([dict(key=600 + i, value=800 + i) for i in range(2)])
             # re-enable cache to access old results
-            conn.set_query_cache(query_cache='main')
+            conn.set_query_cache(query_cache="main")
             previous_cache = schema.TTest3().fetch()
             # verify properly cached and how to refresh results
             assert all([c == p for c, p in zip(cached_res, previous_cache)])
@@ -287,35 +334,32 @@ def test_query_caching(self):
             conn.purge_query_cache()
 
         # reset cache directory state (will fail if purge was unsuccessful)
-        os.rmdir(os.path.expanduser('~/dj_query_cache'))
+        os.rmdir(os.path.expanduser("~/dj_query_cache"))
 
     def test_fetch_group_by(self):
         # https://github.com/datajoint/datajoint-python/issues/914
 
-        assert Parent().fetch('KEY', order_by='name') == [{'parent_id': 1}]
+        assert Parent().fetch("KEY", order_by="name") == [{"parent_id": 1}]
 
     def test_dj_u_distinct(self):
         # Test developed to see if removing DISTINCT from the select statement
         # generation breakes the dj.U universal set imlementation
 
         # Contents to be inserted
-        contents = [
-            (1,2,3),
-            (2,2,3),
-            (3,3,2),
-            (4,5,5)
-        ]
+        contents = [(1, 2, 3), (2, 2, 3), (3, 3, 2), (4, 5, 5)]
         Stimulus.insert(contents)
 
         # Query the whole table
         test_query = Stimulus()
 
         # Use dj.U to create a list of unique contrast and brightness combinations
-        result = dj.U('contrast', 'brightness') & test_query
-        expected_result = [{'contrast': 2, 'brightness': 3},
-                           {'contrast': 3, 'brightness': 2},
-                           {'contrast': 5, 'brightness': 5}]
+        result = dj.U("contrast", "brightness") & test_query
+        expected_result = [
+            {"contrast": 2, "brightness": 3},
+            {"contrast": 3, "brightness": 2},
+            {"contrast": 5, "brightness": 5},
+        ]
 
-        fetched_result = result.fetch(as_dict=True, order_by=('contrast', 'brightness'))
+        fetched_result = result.fetch(as_dict=True, order_by=("contrast", "brightness"))
         Stimulus.delete_quick()
         assert fetched_result == expected_result
diff --git a/tests/test_fetch_same.py b/tests/test_fetch_same.py
index 976095b84..1cd7e461b 100644
--- a/tests/test_fetch_same.py
+++ b/tests/test_fetch_same.py
@@ -3,7 +3,7 @@
 import numpy as np
 import datajoint as dj
 
-schema = dj.Schema(PREFIX + '_fetch_same', connection=dj.conn(**CONN_INFO))
+schema = dj.Schema(PREFIX + "_fetch_same", connection=dj.conn(**CONN_INFO))
 
 
 @schema
@@ -18,36 +18,47 @@ class ProjData(dj.Manual):
     """
 
 
-ProjData().insert([
-    {'id': 0, 'resp': 20.33, 'sim': 45.324, 'big': 3, 'blah': 'yes'},
-    {'id': 1, 'resp': 94.3, 'sim': 34.23,
-        'big': {'key1': np.random.randn(20, 10)}, 'blah': 'si'},
-    {'id': 2, 'resp': 1.90, 'sim': 10.23,
-        'big': np.random.randn(4, 2), 'blah': 'sim'}
-])
+ProjData().insert(
+    [
+        {"id": 0, "resp": 20.33, "sim": 45.324, "big": 3, "blah": "yes"},
+        {
+            "id": 1,
+            "resp": 94.3,
+            "sim": 34.23,
+            "big": {"key1": np.random.randn(20, 10)},
+            "blah": "si",
+        },
+        {
+            "id": 2,
+            "resp": 1.90,
+            "sim": 10.23,
+            "big": np.random.randn(4, 2),
+            "blah": "sim",
+        },
+    ]
+)
 
 
 class TestFetchSame:
-
     @staticmethod
     def test_object_conversion_one():
-        new = ProjData.proj(sub='resp').fetch('sub')
+        new = ProjData.proj(sub="resp").fetch("sub")
         assert_equal(new.dtype, np.float64)
 
     @staticmethod
     def test_object_conversion_two():
-        [sub, add] = ProjData.proj(sub='resp', add='sim').fetch('sub', 'add')
+        [sub, add] = ProjData.proj(sub="resp", add="sim").fetch("sub", "add")
         assert_equal(sub.dtype, np.float64)
         assert_equal(add.dtype, np.float64)
 
     @staticmethod
     def test_object_conversion_all():
-        new = ProjData.proj(sub='resp', add='sim').fetch()
-        assert_equal(new['sub'].dtype, np.float64)
-        assert_equal(new['add'].dtype, np.float64)
+        new = ProjData.proj(sub="resp", add="sim").fetch()
+        assert_equal(new["sub"].dtype, np.float64)
+        assert_equal(new["add"].dtype, np.float64)
 
     @staticmethod
     def test_object_no_convert():
         new = ProjData.fetch()
-        assert_equal(new['big'].dtype, 'object')
-        assert_equal(new['blah'].dtype, 'object')
+        assert_equal(new["big"].dtype, "object")
+        assert_equal(new["blah"].dtype, "object")
diff --git a/tests/test_filepath.py b/tests/test_filepath.py
index e01004827..d0834b071 100644
--- a/tests/test_filepath.py
+++ b/tests/test_filepath.py
@@ -8,20 +8,20 @@
 
 
 def setUp(self):
-    dj.config['stores'] = stores_config
+    dj.config["stores"] = stores_config
 
 
 def test_path_match(store="repo"):
-    """ test file path matches and empty file"""
+    """test file path matches and empty file"""
     dj.errors._switch_filepath_types(True)
     ext = schema.external[store]
-    stage_path = dj.config['stores'][store]['stage']
+    stage_path = dj.config["stores"][store]["stage"]
 
     # create a mock file
-    relpath = 'path/to/films'
-    managed_file = Path(stage_path, relpath, 'vid.mov')
+    relpath = "path/to/films"
+    managed_file = Path(stage_path, relpath, "vid.mov")
     managed_file.parent.mkdir(parents=True, exist_ok=True)
-    open(str(managed_file), 'a').close()
+    open(str(managed_file), "a").close()
 
     # put the file
     uuid = ext.upload_filepath(str(managed_file))
@@ -32,9 +32,10 @@ def test_path_match(store="repo"):
 
     # check filepath
     assert_equal(
-        (ext & {'hash': uuid}).fetch1('filepath'),
-        str(managed_file.relative_to(stage_path).as_posix()))
-    
+        (ext & {"hash": uuid}).fetch1("filepath"),
+        str(managed_file.relative_to(stage_path).as_posix()),
+    )
+
     # # Download the file and check its contents.
     restored_path, checksum = ext.download_filepath(uuid)
     assert_equal(restored_path, str(managed_file))
@@ -46,19 +47,19 @@ def test_path_match(store="repo"):
 
 
 def test_filepath(store="repo"):
-    """ test file management """
+    """test file management"""
     dj.errors._switch_filepath_types(True)
 
     ext = schema.external[store]
-    stage_path = dj.config['stores'][store]['stage']
-    filename = 'picture.dat'
+    stage_path = dj.config["stores"][store]["stage"]
+    filename = "picture.dat"
 
     # create a mock file
-    relpath = 'one/two/three'
+    relpath = "one/two/three"
     managed_file = Path(stage_path, relpath, filename)
     managed_file.parent.mkdir(parents=True, exist_ok=True)
     data = os.urandom(3000)
-    with managed_file.open('wb') as f:
+    with managed_file.open("wb") as f:
         f.write(data)
 
     # put the same file twice to ensure storing once
@@ -78,7 +79,7 @@ def test_filepath(store="repo"):
         assert_equal(checksum, dj.hash.uuid_from_file(str(managed_file)))
 
     # verify same data
-    with managed_file.open('rb') as f:
+    with managed_file.open("rb") as f:
         synced_data = f.read()
     assert_equal(data, synced_data)
 
@@ -90,58 +91,58 @@ def test_filepath(store="repo"):
 
 
 def test_filepath_s3():
-    """ test file management with s3 """
-    test_filepath(store="repo_s3")
+    """test file management with s3"""
+    test_filepath(store="repo-s3")
 
 
 def test_duplicate_upload(store="repo"):
     ext = schema.external[store]
-    stage_path = dj.config['stores'][store]['stage']
-    relpath = 'one/two/three'
-    managed_file = Path(stage_path, relpath, 'plot.dat')
+    stage_path = dj.config["stores"][store]["stage"]
+    relpath = "one/two/three"
+    managed_file = Path(stage_path, relpath, "plot.dat")
     managed_file.parent.mkdir(parents=True, exist_ok=True)
-    with managed_file.open('wb') as f:
+    with managed_file.open("wb") as f:
         f.write(os.urandom(300))
     ext.upload_filepath(str(managed_file))
     ext.upload_filepath(str(managed_file))  # this is fine because the file is the same
 
 
 def test_duplicate_upload_s3():
-    test_duplicate_upload(store="repo_s3")
+    test_duplicate_upload(store="repo-s3")
 
 
 @raises(dj.DataJointError)
 def test_duplicate_error(store="repo"):
-    """ syncing duplicate non-matching file should fail """
+    """syncing duplicate non-matching file should fail"""
     ext = schema.external[store]
-    stage_path = dj.config['stores'][store]['stage']
-    relpath = 'one/two/three'
-    managed_file = Path(stage_path, relpath, 'thesis.dat')
+    stage_path = dj.config["stores"][store]["stage"]
+    relpath = "one/two/three"
+    managed_file = Path(stage_path, relpath, "thesis.dat")
     managed_file.parent.mkdir(parents=True, exist_ok=True)
-    with managed_file.open('wb') as f:
+    with managed_file.open("wb") as f:
         f.write(os.urandom(300))
     ext.upload_filepath(str(managed_file))
-    with managed_file.open('wb') as f:
+    with managed_file.open("wb") as f:
         f.write(os.urandom(300))
     # this should raise exception because the file has changed
     ext.upload_filepath(str(managed_file))
 
 
 def test_duplicate_error_s3():
-    test_duplicate_error(store="repo_s3")
+    test_duplicate_error(store="repo-s3")
 
 
 def test_filepath_class(table=Filepath(), store="repo"):
     dj.errors._switch_filepath_types(True)
-    stage_path = dj.config['stores'][store]['stage']
+    stage_path = dj.config["stores"][store]["stage"]
     # create a mock file
-    relative_path = 'one/two/three'
-    managed_file = Path(stage_path, relative_path, 'attachment.dat')
+    relative_path = "one/two/three"
+    managed_file = Path(stage_path, relative_path, "attachment.dat")
     managed_file.parent.mkdir(parents=True, exist_ok=True)
     data = os.urandom(3000)
-    with managed_file.open('wb') as f:
+    with managed_file.open("wb") as f:
         f.write(data)
-    with managed_file.open('rb') as f:
+    with managed_file.open("rb") as f:
         contents = f.read()
     assert_equal(data, contents)
 
@@ -153,11 +154,11 @@ def test_filepath_class(table=Filepath(), store="repo"):
     assert_false(managed_file.is_file())
 
     # fetch file from remote
-    filepath = (table & {'fnum': 1}).fetch1('img')
+    filepath = (table & {"fnum": 1}).fetch1("img")
     assert_equal(filepath, str(managed_file))
 
     # verify original contents
-    with managed_file.open('rb') as f:
+    with managed_file.open("rb") as f:
         contents = f.read()
     assert_equal(data, contents)
 
@@ -176,27 +177,27 @@ def test_filepath_class_again():
 
 
 def test_filepath_class_s3():
-    test_filepath_class(FilepathS3(), "repo_s3")
+    test_filepath_class(FilepathS3(), "repo-s3")
 
 
 def test_filepath_class_s3_again():
     """test_filepath_class_s3 again to deal with existing remote files"""
-    test_filepath_class(FilepathS3(), "repo_s3")
+    test_filepath_class(FilepathS3(), "repo-s3")
 
 
 def test_filepath_cleanup(table=Filepath(), store="repo"):
-    """test deletion of filepath entries from external table """
+    """test deletion of filepath entries from external table"""
 
     dj.errors._switch_filepath_types(True)
 
-    stage_path = dj.config['stores'][store]['stage']
+    stage_path = dj.config["stores"][store]["stage"]
     n = 20
     contents = os.urandom(345)
     for i in range(n):
-        relative_path = Path(*random.sample(('one', 'two', 'three', 'four'), k=3))
-        managed_file = Path(stage_path, relative_path, 'file.dat')
+        relative_path = Path(*random.sample(("one", "two", "three", "four"), k=3))
+        managed_file = Path(stage_path, relative_path, "file.dat")
         managed_file.parent.mkdir(parents=True, exist_ok=True)
-        with managed_file.open('wb') as f:
+        with managed_file.open("wb") as f:
             f.write(contents)  # same in all files
         table.insert1((i, str(managed_file)))
     assert_equal(len(table), n)
@@ -206,7 +207,7 @@ def test_filepath_cleanup(table=Filepath(), store="repo"):
     assert_equal(len(table), n)
     assert_true(0 < len(ext) < n)
 
-    (table & 'fnum in (1, 2, 3, 4, 5, 6)').delete()
+    (table & "fnum in (1, 2, 3, 4, 5, 6)").delete()
     m = n - len(table)  # number deleted
     assert_true(m == 6)
 
@@ -217,8 +218,8 @@ def test_filepath_cleanup(table=Filepath(), store="repo"):
 
 
 def test_filepath_cleanup_s3():
-    """test deletion of filepath entries from external table """
-    store = "repo_s3"
+    """test deletion of filepath entries from external table"""
+    store = "repo-s3"
     test_filepath_cleanup(FilepathS3(), store)
 
 
@@ -231,17 +232,17 @@ def test_delete_without_files(store="repo"):
 
 
 def test_return_string(table=Filepath(), store="repo"):
-    """ test returning string on fetch """
+    """test returning string on fetch"""
     dj.errors._switch_filepath_types(True)
-    stage_path = dj.config['stores'][store]['stage']
+    stage_path = dj.config["stores"][store]["stage"]
     # create a mock file
-    relative_path = 'this/is/a/test'
-    managed_file = Path(stage_path, relative_path, 'string.dat')
+    relative_path = "this/is/a/test"
+    managed_file = Path(stage_path, relative_path, "string.dat")
     managed_file.parent.mkdir(parents=True, exist_ok=True)
     data = os.urandom(3000)
-    with managed_file.open('wb') as f:
+    with managed_file.open("wb") as f:
         f.write(data)
-    with managed_file.open('rb') as f:
+    with managed_file.open("rb") as f:
         contents = f.read()
     assert_equal(data, contents)
 
@@ -253,6 +254,6 @@ def test_return_string(table=Filepath(), store="repo"):
     assert_false(managed_file.is_file())
 
     # fetch file from remote
-    filepath = (table & {'fnum': 138}).fetch1('img')
+    filepath = (table & {"fnum": 138}).fetch1("img")
     assert_true(isinstance(filepath, str))
     dj.errors._switch_filepath_types(False)
diff --git a/tests/test_foreign_keys.py b/tests/test_foreign_keys.py
index 2c38c1036..d082960e4 100644
--- a/tests/test_foreign_keys.py
+++ b/tests/test_foreign_keys.py
@@ -14,10 +14,12 @@ def test_aliased_fk():
     parent.fill()
     assert_true(person)
     assert_true(parent)
-    link = person.proj(parent_name='full_name', parent='person_id')
+    link = person.proj(parent_name="full_name", parent="person_id")
     parents = person * parent * link
     parents &= dict(full_name="May K. Hall")
-    assert_equal(set(parents.fetch('parent_name')), {'Hanna R. Walters', 'Russel S. James'})
+    assert_equal(
+        set(parents.fetch("parent_name")), {"Hanna R. Walters", "Russel S. James"}
+    )
     delete_count = person.delete()
     assert delete_count == 16
 
@@ -26,8 +28,10 @@ def test_describe():
     """real_definition should match original definition"""
     for rel in (schema_advanced.LocalSynapse, schema_advanced.GlobalSynapse):
         describe = rel.describe()
-        s1 = declare(rel.full_table_name, rel.definition, schema_advanced.schema.context)[0].split('\n')
-        s2 = declare(rel.full_table_name, describe, globals())[0].split('\n')
+        s1 = declare(
+            rel.full_table_name, rel.definition, schema_advanced.schema.context
+        )[0].split("\n")
+        s2 = declare(rel.full_table_name, describe, globals())[0].split("\n")
         for c1, c2 in zip(s1, s2):
             assert_equal(c1, c2)
 
@@ -42,6 +46,6 @@ def test_delete():
     parent.fill()
     assert_true(parent)
     original_len = len(parent)
-    to_delete = len(parent & '11 in (person_id, parent)')
-    (person & 'person_id=11').delete()
+    to_delete = len(parent & "11 in (person_id, parent)")
+    (person & "person_id=11").delete()
     assert_true(to_delete and len(parent) == original_len - to_delete)
diff --git a/tests/test_groupby.py b/tests/test_groupby.py
index cdf8d7b53..3d3be530e 100644
--- a/tests/test_groupby.py
+++ b/tests/test_groupby.py
@@ -3,9 +3,9 @@
 
 def test_aggr_with_proj():
     # issue #944 - only breaks with MariaDB
-    # MariaDB implements the SQL:1992 standard that prohibits fields in the select statement that are 
+    # MariaDB implements the SQL:1992 standard that prohibits fields in the select statement that are
     # not also in the GROUP BY statement.
-    # An improved specification in SQL:2003 allows fields that are functionally dependent on the group by 
-    # attributes to be allowed in the select. This behavior is implemented by MySQL correctly but not MariaDB yet. 
+    # An improved specification in SQL:2003 allows fields that are functionally dependent on the group by
+    # attributes to be allowed in the select. This behavior is implemented by MySQL correctly but not MariaDB yet.
     # See MariaDB issue: https://jira.mariadb.org/browse/MDEV-11588
-    A.aggr(D.proj(m='id_l'), ..., n='max(m) - min(m)')
+    A.aggr(D.proj(m="id_l"), ..., n="max(m) - min(m)")
diff --git a/tests/test_hash.py b/tests/test_hash.py
index a9a8ece95..dc88290eb 100644
--- a/tests/test_hash.py
+++ b/tests/test_hash.py
@@ -3,5 +3,5 @@
 
 
 def test_hash():
-    assert_equal(hash.uuid_from_buffer(b'abc').hex, '900150983cd24fb0d6963f7d28e17f72')
-    assert_equal(hash.uuid_from_buffer(b'').hex, 'd41d8cd98f00b204e9800998ecf8427e')
+    assert_equal(hash.uuid_from_buffer(b"abc").hex, "900150983cd24fb0d6963f7d28e17f72")
+    assert_equal(hash.uuid_from_buffer(b"").hex, "d41d8cd98f00b204e9800998ecf8427e")
diff --git a/tests/test_jobs.py b/tests/test_jobs.py
index 65864879e..371b21a2f 100644
--- a/tests/test_jobs.py
+++ b/tests/test_jobs.py
@@ -12,50 +12,58 @@ def test_reserve_job():
 
     schema.schema.jobs.delete()
     assert_true(subjects)
-    table_name = 'fake_table'
+    table_name = "fake_table"
 
     # reserve jobs
-    for key in subjects.fetch('KEY'):
-        assert_true(schema.schema.jobs.reserve(table_name, key), 'failed to reserve a job')
+    for key in subjects.fetch("KEY"):
+        assert_true(
+            schema.schema.jobs.reserve(table_name, key), "failed to reserve a job"
+        )
 
     # refuse jobs
-    for key in subjects.fetch('KEY'):
-        assert_false(schema.schema.jobs.reserve(table_name, key), 'failed to respect reservation')
+    for key in subjects.fetch("KEY"):
+        assert_false(
+            schema.schema.jobs.reserve(table_name, key), "failed to respect reservation"
+        )
 
     # complete jobs
-    for key in subjects.fetch('KEY'):
+    for key in subjects.fetch("KEY"):
         schema.schema.jobs.complete(table_name, key)
-    assert_false(schema.schema.jobs, 'failed to free jobs')
+    assert_false(schema.schema.jobs, "failed to free jobs")
 
     # reserve jobs again
-    for key in subjects.fetch('KEY'):
-        assert_true(schema.schema.jobs.reserve(table_name, key), 'failed to reserve new jobs')
+    for key in subjects.fetch("KEY"):
+        assert_true(
+            schema.schema.jobs.reserve(table_name, key), "failed to reserve new jobs"
+        )
 
     # finish with error
-    for key in subjects.fetch('KEY'):
+    for key in subjects.fetch("KEY"):
         schema.schema.jobs.error(table_name, key, "error message")
 
     # refuse jobs with errors
-    for key in subjects.fetch('KEY'):
-        assert_false(schema.schema.jobs.reserve(table_name, key), 'failed to ignore error jobs')
+    for key in subjects.fetch("KEY"):
+        assert_false(
+            schema.schema.jobs.reserve(table_name, key), "failed to ignore error jobs"
+        )
 
     # clear error jobs
     (schema.schema.jobs & dict(status="error")).delete()
-    assert_false(schema.schema.jobs, 'failed to clear error jobs')
+    assert_false(schema.schema.jobs, "failed to clear error jobs")
 
 
 def test_restrictions():
     jobs = schema.schema.jobs
     jobs.delete()
-    jobs.reserve('a', {'key': 'a1'})
-    jobs.reserve('a', {'key': 'a2'})
-    jobs.reserve('b', {'key': 'b1'})
-    jobs.error('a', {'key': 'a2'}, 'error')
-    jobs.error('b', {'key': 'b1'}, 'error')
-
-    assert_true(len(jobs & {'table_name': "a"}) == 2)
-    assert_true(len(jobs & {'status': "error"}) == 2)
-    assert_true(len(jobs & {'table_name': "a", 'status': "error"}) == 1)
+    jobs.reserve("a", {"key": "a1"})
+    jobs.reserve("a", {"key": "a2"})
+    jobs.reserve("b", {"key": "b1"})
+    jobs.error("a", {"key": "a2"}, "error")
+    jobs.error("b", {"key": "b1"}, "error")
+
+    assert_true(len(jobs & {"table_name": "a"}) == 2)
+    assert_true(len(jobs & {"status": "error"}) == 2)
+    assert_true(len(jobs & {"table_name": "a", "status": "error"}) == 1)
     jobs.delete()
 
 
@@ -67,9 +75,9 @@ def test_sigint():
     except KeyboardInterrupt:
         pass
 
-    status, error_message = schema.schema.jobs.fetch1('status', 'error_message')
-    assert_equals(status, 'error')
-    assert_equals(error_message, 'KeyboardInterrupt')
+    status, error_message = schema.schema.jobs.fetch1("status", "error_message")
+    assert_equals(status, "error")
+    assert_equals(error_message, "KeyboardInterrupt")
     schema.schema.jobs.delete()
 
 
@@ -81,14 +89,14 @@ def test_sigterm():
     except SystemExit:
         pass
 
-    status, error_message = schema.schema.jobs.fetch1('status', 'error_message')
-    assert_equals(status, 'error')
-    assert_equals(error_message, 'SystemExit: SIGTERM received')
+    status, error_message = schema.schema.jobs.fetch1("status", "error_message")
+    assert_equals(status, "error")
+    assert_equals(error_message, "SystemExit: SIGTERM received")
     schema.schema.jobs.delete()
 
 
 def test_suppress_dj_errors():
-    """ test_suppress_dj_errors: dj errors suppressible w/o native py blobs """
+    """test_suppress_dj_errors: dj errors suppressible w/o native py blobs"""
     schema.schema.jobs.delete()
     with dj.config(enable_python_native_blobs=False):
         schema.ErrorClass.populate(reserve_jobs=True, suppress_errors=True)
@@ -100,27 +108,40 @@ def test_long_error_message():
     schema.schema.jobs.delete()
 
     # create long error message
-    long_error_message = ''.join(random.choice(string.ascii_letters) for _ in range(ERROR_MESSAGE_LENGTH + 100))
-    short_error_message = ''.join(random.choice(string.ascii_letters) for _ in range(ERROR_MESSAGE_LENGTH // 2))
+    long_error_message = "".join(
+        random.choice(string.ascii_letters) for _ in range(ERROR_MESSAGE_LENGTH + 100)
+    )
+    short_error_message = "".join(
+        random.choice(string.ascii_letters) for _ in range(ERROR_MESSAGE_LENGTH // 2)
+    )
     assert_true(subjects)
-    table_name = 'fake_table'
+    table_name = "fake_table"
 
-    key = subjects.fetch('KEY')[0]
+    key = subjects.fetch("KEY")[0]
 
     # test long error message
     schema.schema.jobs.reserve(table_name, key)
     schema.schema.jobs.error(table_name, key, long_error_message)
-    error_message = schema.schema.jobs.fetch1('error_message')
-    assert_true(len(error_message) == ERROR_MESSAGE_LENGTH, 'error message is longer than max allowed')
-    assert_true(error_message.endswith(TRUNCATION_APPENDIX), 'appropriate ending missing for truncated error message')
+    error_message = schema.schema.jobs.fetch1("error_message")
+    assert_true(
+        len(error_message) == ERROR_MESSAGE_LENGTH,
+        "error message is longer than max allowed",
+    )
+    assert_true(
+        error_message.endswith(TRUNCATION_APPENDIX),
+        "appropriate ending missing for truncated error message",
+    )
     schema.schema.jobs.delete()
 
     # test long error message
     schema.schema.jobs.reserve(table_name, key)
     schema.schema.jobs.error(table_name, key, short_error_message)
-    error_message = schema.schema.jobs.fetch1('error_message')
-    assert_true(error_message == short_error_message, 'error messages do not agree')
-    assert_false(error_message.endswith(TRUNCATION_APPENDIX), 'error message should not be truncated')
+    error_message = schema.schema.jobs.fetch1("error_message")
+    assert_true(error_message == short_error_message, "error messages do not agree")
+    assert_false(
+        error_message.endswith(TRUNCATION_APPENDIX),
+        "error message should not be truncated",
+    )
     schema.schema.jobs.delete()
 
 
@@ -129,16 +150,20 @@ def test_long_error_stack():
     schema.schema.jobs.delete()
 
     # create long error stack
-    STACK_SIZE = 89942  # Does not fit into small blob (should be 64k, but found to be higher)
-    long_error_stack = ''.join(random.choice(string.ascii_letters) for _ in range(STACK_SIZE))
+    STACK_SIZE = (
+        89942  # Does not fit into small blob (should be 64k, but found to be higher)
+    )
+    long_error_stack = "".join(
+        random.choice(string.ascii_letters) for _ in range(STACK_SIZE)
+    )
     assert subjects
-    table_name = 'fake_table'
+    table_name = "fake_table"
 
-    key = subjects.fetch('KEY')[0]
+    key = subjects.fetch("KEY")[0]
 
     # test long error stack
     schema.schema.jobs.reserve(table_name, key)
-    schema.schema.jobs.error(table_name, key, 'error message', long_error_stack)
-    error_stack = schema.schema.jobs.fetch1('error_stack')
-    assert error_stack == long_error_stack, 'error stacks do not agree'
+    schema.schema.jobs.error(table_name, key, "error message", long_error_stack)
+    error_stack = schema.schema.jobs.fetch1("error_stack")
+    assert error_stack == long_error_stack, "error stacks do not agree"
     schema.schema.jobs.delete()
diff --git a/tests/test_log.py b/tests/test_log.py
index f13edeeb8..86a48bc37 100644
--- a/tests/test_log.py
+++ b/tests/test_log.py
@@ -3,5 +3,7 @@
 
 
 def test_log():
-    ts, events = (schema.schema.log & 'event like "Declared%%"').fetch('timestamp', 'event')
+    ts, events = (schema.schema.log & 'event like "Declared%%"').fetch(
+        "timestamp", "event"
+    )
     assert_true(len(ts) >= 2)
diff --git a/tests/test_nan.py b/tests/test_nan.py
index cc3e5376e..b06848fdf 100644
--- a/tests/test_nan.py
+++ b/tests/test_nan.py
@@ -3,7 +3,7 @@
 import datajoint as dj
 from . import PREFIX, CONN_INFO
 
-schema = dj.Schema(PREFIX + '_nantest', locals(), connection=dj.conn(**CONN_INFO))
+schema = dj.Schema(PREFIX + "_nantest", locals(), connection=dj.conn(**CONN_INFO))
 
 
 @schema
@@ -21,18 +21,25 @@ def setup_class(cls):
         cls.rel = NanTest()
         with dj.config(safemode=False):
             cls.rel.delete()
-        a = np.array([0, 1/3, np.nan, np.pi, np.nan])
+        a = np.array([0, 1 / 3, np.nan, np.pi, np.nan])
         cls.rel.insert(((i, value) for i, value in enumerate(a)))
         cls.a = a
 
     def test_insert_nan(self):
         """Test fetching of null values"""
-        b = self.rel.fetch('value', order_by='id')
-        assert_true((np.isnan(self.a) == np.isnan(b)).all(),
-                    'incorrect handling of Nans')
-        assert_true(np.allclose(self.a[np.logical_not(np.isnan(self.a))], b[np.logical_not(np.isnan(b))]),
-                    'incorrect storage of floats')
+        b = self.rel.fetch("value", order_by="id")
+        assert_true(
+            (np.isnan(self.a) == np.isnan(b)).all(), "incorrect handling of Nans"
+        )
+        assert_true(
+            np.allclose(
+                self.a[np.logical_not(np.isnan(self.a))], b[np.logical_not(np.isnan(b))]
+            ),
+            "incorrect storage of floats",
+        )
 
     def test_nulls_do_not_affect_primary_keys(self):
         """Test against a case that previously caused a bug when skipping existing entries."""
-        self.rel.insert(((i, value) for i, value in enumerate(self.a)), skip_duplicates=True)
+        self.rel.insert(
+            ((i, value) for i, value in enumerate(self.a)), skip_duplicates=True
+        )
diff --git a/tests/test_plugin.py b/tests/test_plugin.py
index 5fb7d46bc..f70f4c2ef 100644
--- a/tests/test_plugin.py
+++ b/tests/test_plugin.py
@@ -5,47 +5,54 @@
 
 
 def test_check_pubkey():
-    base_name = 'datajoint'
+    base_name = "datajoint"
     base_meta = pkg_resources.get_distribution(base_name)
-    pubkey_meta = base_meta.get_metadata('{}.pub'.format(base_name))
+    pubkey_meta = base_meta.get_metadata("{}.pub".format(base_name))
 
-    with open(path.join(path.abspath(
-            path.dirname(__file__)), '..', 'datajoint.pub'), "r") as f:
-        assert(f.read() == pubkey_meta)
+    with open(
+        path.join(path.abspath(path.dirname(__file__)), "..", "datajoint.pub"), "r"
+    ) as f:
+        assert f.read() == pubkey_meta
 
 
 def test_normal_djerror():
     try:
         raise djerr.DataJointError
     except djerr.DataJointError as e:
-        assert(e.__cause__ is None)
+        assert e.__cause__ is None
 
 
-def test_verified_djerror(category='connection'):
+def test_verified_djerror(category="connection"):
     try:
-        curr_plugins = getattr(p, '{}_plugins'.format(category))
-        setattr(p, '{}_plugins'.format(category), dict(test_plugin_id=dict(
-                                                        verified=True, object='example')))
+        curr_plugins = getattr(p, "{}_plugins".format(category))
+        setattr(
+            p,
+            "{}_plugins".format(category),
+            dict(test_plugin_id=dict(verified=True, object="example")),
+        )
         raise djerr.DataJointError
     except djerr.DataJointError as e:
-        setattr(p, '{}_plugins'.format(category), curr_plugins)
-        assert(e.__cause__ is None)
+        setattr(p, "{}_plugins".format(category), curr_plugins)
+        assert e.__cause__ is None
 
 
 def test_verified_djerror_type():
-    test_verified_djerror(category='type')
+    test_verified_djerror(category="type")
 
 
-def test_unverified_djerror(category='connection'):
+def test_unverified_djerror(category="connection"):
     try:
-        curr_plugins = getattr(p, '{}_plugins'.format(category))
-        setattr(p, '{}_plugins'.format(category), dict(test_plugin_id=dict(
-                                                        verified=False, object='example')))
+        curr_plugins = getattr(p, "{}_plugins".format(category))
+        setattr(
+            p,
+            "{}_plugins".format(category),
+            dict(test_plugin_id=dict(verified=False, object="example")),
+        )
         raise djerr.DataJointError("hello")
     except djerr.DataJointError as e:
-        setattr(p, '{}_plugins'.format(category), curr_plugins)
-        assert(isinstance(e.__cause__, djerr.PluginWarning))
+        setattr(p, "{}_plugins".format(category), curr_plugins)
+        assert isinstance(e.__cause__, djerr.PluginWarning)
 
 
 def test_unverified_djerror_type():
-    test_unverified_djerror(category='type')
+    test_unverified_djerror(category="type")
diff --git a/tests/test_privileges.py b/tests/test_privileges.py
index 2e483aefa..9332a56b7 100644
--- a/tests/test_privileges.py
+++ b/tests/test_privileges.py
@@ -6,29 +6,36 @@
 
 
 class TestUnprivileged:
-
     @classmethod
     def setup_class(cls):
         """A connection with only SELECT privilege to djtest schemas"""
-        cls.connection = dj.conn(host=CONN_INFO['host'], user='djview', password='djview',
-                                 reset=True)
+        cls.connection = dj.conn(
+            host=CONN_INFO["host"], user="djview", password="djview", reset=True
+        )
 
     @raises(dj.DataJointError)
     def test_fail_create_schema(self):
         """creating a schema with no CREATE privilege"""
-        return dj.Schema('forbidden_schema', namespace, connection=self.connection)
+        return dj.Schema("forbidden_schema", namespace, connection=self.connection)
 
     @raises(dj.DataJointError)
     def test_insert_failure(self):
-        unprivileged = dj.Schema(schema.schema.database, namespace, connection=self.connection)
+        unprivileged = dj.Schema(
+            schema.schema.database, namespace, connection=self.connection
+        )
         unprivileged.spawn_missing_classes()
-        assert_true(issubclass(Language, dj.Lookup) and len(Language()) == len(schema.Language()),
-                    'failed to spawn missing classes')
-        Language().insert1(('Socrates', 'Greek'))
+        assert_true(
+            issubclass(Language, dj.Lookup)
+            and len(Language()) == len(schema.Language()),
+            "failed to spawn missing classes",
+        )
+        Language().insert1(("Socrates", "Greek"))
 
     @raises(dj.DataJointError)
     def test_failure_to_create_table(self):
-        unprivileged = dj.Schema(schema.schema.database, namespace, connection=self.connection)
+        unprivileged = dj.Schema(
+            schema.schema.database, namespace, connection=self.connection
+        )
 
         @unprivileged
         class Try(dj.Manual):
diff --git a/tests/test_reconnection.py b/tests/test_reconnection.py
index 4ddef39f7..b275766ae 100644
--- a/tests/test_reconnection.py
+++ b/tests/test_reconnection.py
@@ -24,7 +24,7 @@ def test_close(self):
     def test_reconnect(self):
         assert_true(self.conn.is_connected, "Connection should be alive")
         self.conn.close()
-        self.conn.query('SHOW DATABASES;', reconnect=True).fetchall()
+        self.conn.query("SHOW DATABASES;", reconnect=True).fetchall()
         assert_true(self.conn.is_connected, "Connection should be alive")
 
     @raises(DataJointError)
@@ -32,4 +32,4 @@ def test_reconnect_throws_error_in_transaction(self):
         assert_true(self.conn.is_connected, "Connection should be alive")
         with self.conn.transaction:
             self.conn.close()
-            self.conn.query('SHOW DATABASES;', reconnect=True).fetchall()
+            self.conn.query("SHOW DATABASES;", reconnect=True).fetchall()
diff --git a/tests/test_relation.py b/tests/test_relation.py
index 37adab7f5..53dbf957b 100644
--- a/tests/test_relation.py
+++ b/tests/test_relation.py
@@ -2,7 +2,13 @@
 import re
 import pandas
 import numpy as np
-from nose.tools import assert_equal, assert_not_equal, assert_true, assert_list_equal, raises
+from nose.tools import (
+    assert_equal,
+    assert_not_equal,
+    assert_true,
+    assert_list_equal,
+    raises,
+)
 import datajoint as dj
 from datajoint.table import Table
 from unittest.mock import patch
@@ -43,14 +49,18 @@ def test_contents(self):
         # test contents
         assert_true(self.user)
         assert_true(len(self.user) == len(self.user.contents))
-        u = self.user.fetch(order_by=['username'])
-        assert_list_equal(list(u['username']), sorted([s[0] for s in self.user.contents]))
+        u = self.user.fetch(order_by=["username"])
+        assert_list_equal(
+            list(u["username"]), sorted([s[0] for s in self.user.contents])
+        )
 
         # test prepare
         assert_true(self.subject)
         assert_true(len(self.subject) == len(self.subject.contents))
-        u = self.subject.fetch(order_by=['subject_id'])
-        assert_list_equal(list(u['subject_id']), sorted([s[0] for s in self.subject.contents]))
+        u = self.subject.fetch(order_by=["subject_id"])
+        assert_list_equal(
+            list(u["subject_id"]), sorted([s[0] for s in self.subject.contents])
+        )
 
     @raises(dj.DataJointError)
     def test_misnamed_attribute1(self):
@@ -65,7 +75,9 @@ def test_extra_attribute1(self):
         self.user.insert1(dict(username="Robert", spouse="Alice"))
 
     def test_extra_attribute2(self):
-        self.user.insert1(dict(username="Robert", spouse="Alice"), ignore_extra_fields=True)
+        self.user.insert1(
+            dict(username="Robert", spouse="Alice"), ignore_extra_fields=True
+        )
 
     @raises(NotImplementedError)
     def test_missing_definition(self):
@@ -87,7 +99,7 @@ def test_empty_insert(self):
 
     @raises(dj.DataJointError)
     def test_wrong_arguments_insert(self):
-        self.user.insert1(('First', 'Second'))
+        self.user.insert1(("First", "Second"))
 
     @raises(dj.DataJointError)
     def test_wrong_insert_type(self):
@@ -99,17 +111,23 @@ def test_insert_select(self):
         assert_equal(len(schema.TTest2()), len(schema.TTest()))
 
         original_length = len(self.subject)
-        elements = self.subject.proj(..., s='subject_id')
-        elements = elements.proj('real_id', 'date_of_birth', 'subject_notes', subject_id='s+1000', species='"human"')
+        elements = self.subject.proj(..., s="subject_id")
+        elements = elements.proj(
+            "real_id",
+            "date_of_birth",
+            "subject_notes",
+            subject_id="s+1000",
+            species='"human"',
+        )
         self.subject.insert(elements, ignore_extra_fields=True)
-        assert_equal(len(self.subject), 2*original_length)
+        assert_equal(len(self.subject), 2 * original_length)
 
     def test_insert_pandas_roundtrip(self):
-        """ ensure fetched frames can be inserted """
+        """ensure fetched frames can be inserted"""
         schema.TTest2.delete()
         n = len(schema.TTest())
         assert_true(n > 0)
-        df = schema.TTest.fetch(format='frame')
+        df = schema.TTest.fetch(format="frame")
         assert_true(isinstance(df, pandas.DataFrame))
         assert_equal(len(df), n)
         schema.TTest2.insert(df)
@@ -131,35 +149,40 @@ def test_insert_pandas_userframe(self):
 
     @raises(dj.DataJointError)
     def test_insert_select_ignore_extra_fields0(self):
-        """ need ignore extra fields for insert select """
-        self.test_extra.insert1((self.test.fetch('key').max() + 1, 0, 0))
+        """need ignore extra fields for insert select"""
+        self.test_extra.insert1((self.test.fetch("key").max() + 1, 0, 0))
         self.test.insert(self.test_extra)
 
     def test_insert_select_ignore_extra_fields1(self):
-        """ make sure extra fields works in insert select """
+        """make sure extra fields works in insert select"""
         self.test_extra.delete()
-        keyno = self.test.fetch('key').max() + 1
+        keyno = self.test.fetch("key").max() + 1
         self.test_extra.insert1((keyno, 0, 0))
         self.test.insert(self.test_extra, ignore_extra_fields=True)
-        assert(keyno in self.test.fetch('key'))
+        assert keyno in self.test.fetch("key")
 
     def test_insert_select_ignore_extra_fields2(self):
-        """ make sure insert select still works when ignoring extra fields when there are none """
+        """make sure insert select still works when ignoring extra fields when there are none"""
         self.test_no_extra.delete()
         self.test_no_extra.insert(self.test, ignore_extra_fields=True)
 
     def test_insert_select_ignore_extra_fields3(self):
-        """ make sure insert select works for from query result """
+        """make sure insert select works for from query result"""
         self.test_no_extra.delete()
-        keystr = str(self.test_extra.fetch('key').max())
-        self.test_no_extra.insert((self.test_extra & '`key`=' + keystr),
-                                  ignore_extra_fields=True)
+        keystr = str(self.test_extra.fetch("key").max())
+        self.test_no_extra.insert(
+            (self.test_extra & "`key`=" + keystr), ignore_extra_fields=True
+        )
 
     def test_skip_duplicates(self):
-        """ test that skip_dublicates works when inserting from another relation  """
+        """test that skip_dublicates works when inserting from another relation"""
         self.test_no_extra.delete()
-        self.test_no_extra.insert(self.test, ignore_extra_fields=True, skip_duplicates=True)
-        self.test_no_extra.insert(self.test, ignore_extra_fields=True, skip_duplicates=True)
+        self.test_no_extra.insert(
+            self.test, ignore_extra_fields=True, skip_duplicates=True
+        )
+        self.test_no_extra.insert(
+            self.test, ignore_extra_fields=True, skip_duplicates=True
+        )
 
     def test_replace(self):
         """
@@ -167,51 +190,74 @@ def test_replace(self):
         """
         key = dict(subject_id=7)
         date = "2015-01-01"
-        self.subject.insert1(
-            dict(key, real_id=7, date_of_birth=date, subject_notes=""))
-        assert_equal(date, str((self.subject & key).fetch1('date_of_birth')), 'incorrect insert')
+        self.subject.insert1(dict(key, real_id=7, date_of_birth=date, subject_notes=""))
+        assert_equal(
+            date, str((self.subject & key).fetch1("date_of_birth")), "incorrect insert"
+        )
         date = "2015-01-02"
         self.subject.insert1(
-            dict(key, real_id=7, date_of_birth=date, subject_notes=""), skip_duplicates=True)
-        assert_not_equal(date, str((self.subject & key).fetch1('date_of_birth')),
-                         'inappropriate replace')
+            dict(key, real_id=7, date_of_birth=date, subject_notes=""),
+            skip_duplicates=True,
+        )
+        assert_not_equal(
+            date,
+            str((self.subject & key).fetch1("date_of_birth")),
+            "inappropriate replace",
+        )
         self.subject.insert1(
-            dict(key, real_id=7, date_of_birth=date, subject_notes=""), replace=True)
-        assert_equal(date, str((self.subject & key).fetch1('date_of_birth')), "replace failed")
+            dict(key, real_id=7, date_of_birth=date, subject_notes=""), replace=True
+        )
+        assert_equal(
+            date, str((self.subject & key).fetch1("date_of_birth")), "replace failed"
+        )
 
     def test_delete_quick(self):
         """Tests quick deletion"""
-        tmp = np.array([
-            (2, 'Klara', 'monkey', '2010-01-01', ''),
-            (1, 'Peter', 'mouse', '2015-01-01', '')],
-            dtype=self.subject.heading.as_dtype)
+        tmp = np.array(
+            [
+                (2, "Klara", "monkey", "2010-01-01", ""),
+                (1, "Peter", "mouse", "2015-01-01", ""),
+            ],
+            dtype=self.subject.heading.as_dtype,
+        )
         self.subject.insert(tmp)
-        s = self.subject & ('subject_id in (%s)' % ','.join(str(r) for r in tmp['subject_id']))
-        assert_true(len(s) == 2, 'insert did not work.')
+        s = self.subject & (
+            "subject_id in (%s)" % ",".join(str(r) for r in tmp["subject_id"])
+        )
+        assert_true(len(s) == 2, "insert did not work.")
         s.delete_quick()
-        assert_true(len(s) == 0, 'delete did not work.')
+        assert_true(len(s) == 0, "delete did not work.")
 
     def test_skip_duplicate(self):
         """Tests if duplicates are properly skipped."""
-        tmp = np.array([
-            (2, 'Klara', 'monkey', '2010-01-01', ''),
-            (1, 'Peter', 'mouse', '2015-01-01', '')],
-            dtype=self.subject.heading.as_dtype)
+        tmp = np.array(
+            [
+                (2, "Klara", "monkey", "2010-01-01", ""),
+                (1, "Peter", "mouse", "2015-01-01", ""),
+            ],
+            dtype=self.subject.heading.as_dtype,
+        )
         self.subject.insert(tmp)
-        tmp = np.array([
-            (2, 'Klara', 'monkey', '2010-01-01', ''),
-            (1, 'Peter', 'mouse', '2015-01-01', '')],
-            dtype=self.subject.heading.as_dtype)
+        tmp = np.array(
+            [
+                (2, "Klara", "monkey", "2010-01-01", ""),
+                (1, "Peter", "mouse", "2015-01-01", ""),
+            ],
+            dtype=self.subject.heading.as_dtype,
+        )
         self.subject.insert(tmp, skip_duplicates=True)
 
     @raises(dj.errors.DuplicateError)
     def test_not_skip_duplicate(self):
         """Tests if duplicates are not skipped."""
-        tmp = np.array([
-            (2, 'Klara', 'monkey', '2010-01-01', ''),
-            (2, 'Klara', 'monkey', '2010-01-01', ''),
-            (1, 'Peter', 'mouse', '2015-01-01', '')],
-            dtype=self.subject.heading.as_dtype)
+        tmp = np.array(
+            [
+                (2, "Klara", "monkey", "2010-01-01", ""),
+                (2, "Klara", "monkey", "2010-01-01", ""),
+                (1, "Peter", "mouse", "2015-01-01", ""),
+            ],
+            dtype=self.subject.heading.as_dtype,
+        )
         self.subject.insert(tmp, skip_duplicates=False)
 
     @raises(dj.errors.MissingAttributeError)
@@ -223,31 +269,38 @@ def test_blob_insert(self):
         """Tests inserting and retrieving blobs."""
         X = np.random.randn(20, 10)
         self.img.insert1((1, X))
-        Y = self.img.fetch()[0]['img']
-        assert_true(np.all(X == Y), 'Inserted and retrieved image are not identical')
+        Y = self.img.fetch()[0]["img"]
+        assert_true(np.all(X == Y), "Inserted and retrieved image are not identical")
 
     @raises(dj.DataJointError)
     def test_drop(self):
         """Tests dropping tables"""
-        dj.config['safemode'] = True
+        dj.config["safemode"] = True
         try:
-            with patch.object(dj.utils, "input", create=True, return_value='yes'):
+            with patch.object(dj.utils, "input", create=True, return_value="yes"):
                 self.trash.drop()
         except:
             pass
         finally:
-            dj.config['safemode'] = False
+            dj.config["safemode"] = False
         self.trash.fetch()
 
     def test_table_regexp(self):
         """Test whether table names are matched by regular expressions"""
         tiers = [dj.Imported, dj.Manual, dj.Lookup, dj.Computed]
         for name, rel in getmembers(schema, relation_selector):
-            assert_true(re.match(rel.tier_regexp, rel.table_name),
-                        'Regular expression does not match for {name}'.format(name=name))
+            assert_true(
+                re.match(rel.tier_regexp, rel.table_name),
+                "Regular expression does not match for {name}".format(name=name),
+            )
             for tier in tiers:
-                assert_true(issubclass(rel, tier) or not re.match(tier.tier_regexp, rel.table_name),
-                            'Regular expression matches for {name} but should not'.format(name=name))
+                assert_true(
+                    issubclass(rel, tier)
+                    or not re.match(tier.tier_regexp, rel.table_name),
+                    "Regular expression matches for {name} but should not".format(
+                        name=name
+                    ),
+                )
 
     def test_table_size(self):
         """test getting the size of the table and its indices in bytes"""
diff --git a/tests/test_relation_u.py b/tests/test_relation_u.py
index 281b67d38..ff30711b3 100644
--- a/tests/test_relation_u.py
+++ b/tests/test_relation_u.py
@@ -22,65 +22,67 @@ def setup_class(cls):
 
     def test_restriction(self):
         language_set = {s[1] for s in self.language.contents}
-        rel = dj.U('language') & self.language
-        assert_list_equal(rel.heading.names, ['language'])
+        rel = dj.U("language") & self.language
+        assert_list_equal(rel.heading.names, ["language"])
         assert_true(len(rel) == len(language_set))
-        assert_true(set(rel.fetch('language')) == language_set)
+        assert_true(set(rel.fetch("language")) == language_set)
         # Test for issue #342
-        rel = self.trial*dj.U('start_time')
-        assert_list_equal(rel.primary_key, self.trial.primary_key + ['start_time'])
-        assert_list_equal(rel.primary_key, (rel & 'trial_id>3').primary_key)
-        assert_list_equal((dj.U('start_time') & self.trial).primary_key, ['start_time'])
+        rel = self.trial * dj.U("start_time")
+        assert_list_equal(rel.primary_key, self.trial.primary_key + ["start_time"])
+        assert_list_equal(rel.primary_key, (rel & "trial_id>3").primary_key)
+        assert_list_equal((dj.U("start_time") & self.trial).primary_key, ["start_time"])
 
     @staticmethod
     @raises(dj.DataJointError)
     def test_invalid_restriction():
-        result = dj.U('color') & dict(color="red")
+        result = dj.U("color") & dict(color="red")
 
     def test_ineffective_restriction(self):
-        rel = self.language & dj.U('language')
+        rel = self.language & dj.U("language")
         assert_true(rel.make_sql() == self.language.make_sql())
 
     def test_join(self):
-        rel = self.experiment*dj.U('experiment_date')
-        assert_equal(self.experiment.primary_key, ['subject_id', 'experiment_id'])
-        assert_equal(rel.primary_key, self.experiment.primary_key + ['experiment_date'])
+        rel = self.experiment * dj.U("experiment_date")
+        assert_equal(self.experiment.primary_key, ["subject_id", "experiment_id"])
+        assert_equal(rel.primary_key, self.experiment.primary_key + ["experiment_date"])
 
-        rel = dj.U('experiment_date')*self.experiment
-        assert_equal(self.experiment.primary_key, ['subject_id', 'experiment_id'])
-        assert_equal(rel.primary_key, self.experiment.primary_key + ['experiment_date'])
+        rel = dj.U("experiment_date") * self.experiment
+        assert_equal(self.experiment.primary_key, ["subject_id", "experiment_id"])
+        assert_equal(rel.primary_key, self.experiment.primary_key + ["experiment_date"])
 
     @staticmethod
     @raises(dj.DataJointError)
     def test_invalid_join():
-        rel = dj.U('language') * dict(language="English")
+        rel = dj.U("language") * dict(language="English")
 
     def test_repr_without_attrs(self):
         """test dj.U() display"""
-        query = dj.U().aggr(schema.Language, n='count(*)')
+        query = dj.U().aggr(schema.Language, n="count(*)")
         repr(query)
 
     def test_aggregations(self):
         lang = schema.Language()
         # test total aggregation on expression object
-        n1 = dj.U().aggr(lang, n='count(*)').fetch1('n')
+        n1 = dj.U().aggr(lang, n="count(*)").fetch1("n")
         assert_equal(n1, len(lang.fetch()))
         # test total aggregation on expression class
-        n2 = dj.U().aggr(schema.Language, n='count(*)').fetch1('n')
+        n2 = dj.U().aggr(schema.Language, n="count(*)").fetch1("n")
         assert_equal(n1, n2)
-        rel = dj.U('language').aggr(schema.Language, number_of_speakers='count(*)')
+        rel = dj.U("language").aggr(schema.Language, number_of_speakers="count(*)")
         assert_equal(len(rel), len(set(l[1] for l in schema.Language.contents)))
-        assert_equal((rel & 'language="English"').fetch1('number_of_speakers'), 3)
+        assert_equal((rel & 'language="English"').fetch1("number_of_speakers"), 3)
 
     def test_argmax(self):
         rel = schema.TTest()
         # get the tuples corresponding to maximum value
-        mx = (rel * dj.U().aggr(rel, mx='max(value)')) & 'mx=value'
-        assert_equal(mx.fetch('value')[0], max(rel.fetch('value')))
+        mx = (rel * dj.U().aggr(rel, mx="max(value)")) & "mx=value"
+        assert_equal(mx.fetch("value")[0], max(rel.fetch("value")))
 
     def test_aggr(self):
         rel = schema_simple.ArgmaxTest()
-        amax1 = (dj.U('val') * rel) & dj.U('secondary_key').aggr(rel, val='min(val)')
-        amax2 = (dj.U('val') * rel) * dj.U('secondary_key').aggr(rel, val='min(val)')
-        assert_true(len(amax1) == len(amax2) == rel.n,
-                    'Aggregated argmax with join and restriction does not yield same length.')
+        amax1 = (dj.U("val") * rel) & dj.U("secondary_key").aggr(rel, val="min(val)")
+        amax2 = (dj.U("val") * rel) * dj.U("secondary_key").aggr(rel, val="min(val)")
+        assert_true(
+            len(amax1) == len(amax2) == rel.n,
+            "Aggregated argmax with join and restriction does not yield same length.",
+        )
diff --git a/tests/test_relational_operand.py b/tests/test_relational_operand.py
index e558650a2..48be3bdf6 100644
--- a/tests/test_relational_operand.py
+++ b/tests/test_relational_operand.py
@@ -4,14 +4,43 @@
 import datetime
 
 import numpy as np
-from nose.tools import (assert_equal, assert_false, assert_true, raises, assert_set_equal,
-                        assert_list_equal)
+from nose.tools import (
+    assert_equal,
+    assert_false,
+    assert_true,
+    raises,
+    assert_set_equal,
+    assert_list_equal,
+)
 
 import datajoint as dj
-from .schema_simple import (A, B, D, E, F, L, DataA, DataB, TTestUpdate, IJ, JI,
-                            ReservedWord, OutfitLaunch)
-from .schema import (Experiment, TTest3, Trial, Ephys, Child, Parent, SubjectA, SessionA,
-                     SessionStatusA, SessionDateA)
+from .schema_simple import (
+    A,
+    B,
+    D,
+    E,
+    F,
+    L,
+    DataA,
+    DataB,
+    TTestUpdate,
+    IJ,
+    JI,
+    ReservedWord,
+    OutfitLaunch,
+)
+from .schema import (
+    Experiment,
+    TTest3,
+    Trial,
+    Ephys,
+    Child,
+    Parent,
+    SubjectA,
+    SessionA,
+    SessionStatusA,
+    SessionDateA,
+)
 
 from . import PREFIX, CONN_INFO
 
@@ -31,40 +60,52 @@ def setup():
 class TestRelational:
     @staticmethod
     def test_populate():
-        assert_false(B().progress(display=False)[0], 'B incompletely populated')
-        assert_false(D().progress(display=False)[0], 'D incompletely populated')
-        assert_false(E().progress(display=False)[0], 'E incompletely populated')
+        assert_false(B().progress(display=False)[0], "B incompletely populated")
+        assert_false(D().progress(display=False)[0], "D incompletely populated")
+        assert_false(E().progress(display=False)[0], "E incompletely populated")
 
-        assert_true(len(B()) == 40, 'B populated incorrectly')
-        assert_true(len(B.C()) > 0, 'C populated incorrectly')
-        assert_true(len(D()) == 40, 'D populated incorrectly')
-        assert_true(len(E()) == len(B()) * len(D()) / len(A()), 'E populated incorrectly')
-        assert_true(len(E.F()) > 0, 'F populated incorrectly')
+        assert_true(len(B()) == 40, "B populated incorrectly")
+        assert_true(len(B.C()) > 0, "C populated incorrectly")
+        assert_true(len(D()) == 40, "D populated incorrectly")
+        assert_true(
+            len(E()) == len(B()) * len(D()) / len(A()), "E populated incorrectly"
+        )
+        assert_true(len(E.F()) > 0, "F populated incorrectly")
 
     @staticmethod
     def test_free_relation():
         b = B()
         free = dj.FreeTable(b.connection, b.full_table_name)
-        assert_true(repr(free).startswith('FreeTable') and b.full_table_name in repr(free))
-        r = 'n>5'
+        assert_true(
+            repr(free).startswith("FreeTable") and b.full_table_name in repr(free)
+        )
+        r = "n>5"
         assert_equal((B() & r).make_sql(), (free & r).make_sql())
 
     @staticmethod
     def test_rename():
         # test renaming
-        x = B().proj(i='id_a') & 'i in (1,2,3,4)'
+        x = B().proj(i="id_a") & "i in (1,2,3,4)"
         lenx = len(x)
-        assert_equal(len(x), len(B() & 'id_a in (1,2,3,4)'),
-                     'incorrect restriction of renamed attributes')
-        assert_equal(len(x & 'id_b in (1,2)'), len(B() & 'id_b in (1,2) and id_a in (1,2,3,4)'),
-                     'incorrect restriction of renamed restriction')
-        assert_equal(len(x), lenx, 'restriction modified original')
-        y = x.proj(j='i')
-        assert_equal(len(y), len(B() & 'id_a in (1,2,3,4)'),
-                     'incorrect projection of restriction')
-        z = y & 'j in (3, 4, 5, 6)'
-        assert_equal(len(z), len(B() & 'id_a in (3,4)'),
-                     'incorrect nested subqueries')
+        assert_equal(
+            len(x),
+            len(B() & "id_a in (1,2,3,4)"),
+            "incorrect restriction of renamed attributes",
+        )
+        assert_equal(
+            len(x & "id_b in (1,2)"),
+            len(B() & "id_b in (1,2) and id_a in (1,2,3,4)"),
+            "incorrect restriction of renamed restriction",
+        )
+        assert_equal(len(x), lenx, "restriction modified original")
+        y = x.proj(j="i")
+        assert_equal(
+            len(y),
+            len(B() & "id_a in (1,2,3,4)"),
+            "incorrect projection of restriction",
+        )
+        z = y & "j in (3, 4, 5, 6)"
+        assert_equal(len(z), len(B() & "id_a in (3,4)"), "incorrect nested subqueries")
 
     @staticmethod
     def test_rename_order():
@@ -73,8 +114,8 @@ def test_rename_order():
         See issues #483 and #516.
         """
         pk1 = D.primary_key
-        pk2 = D.proj(a='id_a').primary_key
-        assert_list_equal(['a' if i == 'id_a' else i for i in pk1], pk2)
+        pk2 = D.proj(a="id_a").primary_key
+        assert_list_equal(["a" if i == "id_a" else i for i in pk1], pk2)
 
     @staticmethod
     def test_join():
@@ -82,80 +123,105 @@ def test_join():
         x = A()
         y = L()
         rel = x * y
-        assert_equal(len(rel), len(x) * len(y),
-                     'incorrect join')
-        assert_equal(set(x.heading.names).union(y.heading.names), set(rel.heading.names),
-                     'incorrect join heading')
-        assert_equal(set(x.primary_key).union(y.primary_key), set(rel.primary_key),
-                     'incorrect join primary_key')
+        assert_equal(len(rel), len(x) * len(y), "incorrect join")
+        assert_equal(
+            set(x.heading.names).union(y.heading.names),
+            set(rel.heading.names),
+            "incorrect join heading",
+        )
+        assert_equal(
+            set(x.primary_key).union(y.primary_key),
+            set(rel.primary_key),
+            "incorrect join primary_key",
+        )
 
         # Test cartesian product of restricted relations
-        x = A() & 'cond_in_a=1'
-        y = L() & 'cond_in_l=1'
+        x = A() & "cond_in_a=1"
+        y = L() & "cond_in_l=1"
         rel = x * y
-        assert_equal(len(rel), len(x) * len(y),
-                     'incorrect join')
-        assert_equal(set(x.heading.names).union(y.heading.names), set(rel.heading.names),
-                     'incorrect join heading')
-        assert_equal(set(x.primary_key).union(y.primary_key), set(rel.primary_key),
-                     'incorrect join primary_key')
+        assert_equal(len(rel), len(x) * len(y), "incorrect join")
+        assert_equal(
+            set(x.heading.names).union(y.heading.names),
+            set(rel.heading.names),
+            "incorrect join heading",
+        )
+        assert_equal(
+            set(x.primary_key).union(y.primary_key),
+            set(rel.primary_key),
+            "incorrect join primary_key",
+        )
 
         # Test join with common attributes
-        cond = A() & 'cond_in_a=1'
+        cond = A() & "cond_in_a=1"
         x = B() & cond
         y = D()
         rel = x * y
-        assert_true(len(rel) >= len(x) and len(rel) >= len(y), 'incorrect join')
-        assert_false(rel - cond, 'incorrect join, restriction, or antijoin')
-        assert_equal(set(x.heading.names).union(y.heading.names), set(rel.heading.names),
-                     'incorrect join heading')
-        assert_equal(set(x.primary_key).union(y.primary_key), set(rel.primary_key),
-                     'incorrect join primary_key')
+        assert_true(len(rel) >= len(x) and len(rel) >= len(y), "incorrect join")
+        assert_false(rel - cond, "incorrect join, restriction, or antijoin")
+        assert_equal(
+            set(x.heading.names).union(y.heading.names),
+            set(rel.heading.names),
+            "incorrect join heading",
+        )
+        assert_equal(
+            set(x.primary_key).union(y.primary_key),
+            set(rel.primary_key),
+            "incorrect join primary_key",
+        )
 
         # test renamed join
-        x = B().proj(i='id_a')  # rename the common attribute to achieve full cartesian product
+        x = B().proj(
+            i="id_a"
+        )  # rename the common attribute to achieve full cartesian product
         y = D()
         rel = x * y
-        assert_equal(len(rel), len(x) * len(y), 'incorrect join')
-        assert_equal(set(x.heading.names).union(y.heading.names), set(rel.heading.names),
-                     'incorrect join heading')
-        assert_equal(set(x.primary_key).union(y.primary_key), set(rel.primary_key),
-                     'incorrect join primary_key')
-        x = B().proj(a='id_a')
+        assert_equal(len(rel), len(x) * len(y), "incorrect join")
+        assert_equal(
+            set(x.heading.names).union(y.heading.names),
+            set(rel.heading.names),
+            "incorrect join heading",
+        )
+        assert_equal(
+            set(x.primary_key).union(y.primary_key),
+            set(rel.primary_key),
+            "incorrect join primary_key",
+        )
+        x = B().proj(a="id_a")
         y = D()
         rel = x * y
-        assert_equal(len(rel), len(x) * len(y), 'incorrect join')
-        assert_equal(set(x.heading.names).union(y.heading.names), set(rel.heading.names),
-                     'incorrect join heading')
-        assert_equal(set(x.primary_key).union(y.primary_key), set(rel.primary_key),
-                     'incorrect join primary_key')
+        assert_equal(len(rel), len(x) * len(y), "incorrect join")
+        assert_equal(
+            set(x.heading.names).union(y.heading.names),
+            set(rel.heading.names),
+            "incorrect join heading",
+        )
+        assert_equal(
+            set(x.primary_key).union(y.primary_key),
+            set(rel.primary_key),
+            "incorrect join primary_key",
+        )
 
         # test pairing
         # Approach 1: join then restrict
-        x = A.proj(a1='id_a', c1='cond_in_a')
-        y = A.proj(a2='id_a', c2='cond_in_a')
-        rel = x * y & 'c1=0' & 'c2=1'
-        lenx = len(x & 'c1=0')
-        leny = len(y & 'c2=1')
-        assert_equal(lenx + leny, len(A()),
-                     'incorrect restriction')
-        assert_equal(len(rel), len(x & 'c1=0') * len(y & 'c2=1'),
-                     'incorrect pairing')
+        x = A.proj(a1="id_a", c1="cond_in_a")
+        y = A.proj(a2="id_a", c2="cond_in_a")
+        rel = x * y & "c1=0" & "c2=1"
+        lenx = len(x & "c1=0")
+        leny = len(y & "c2=1")
+        assert_equal(lenx + leny, len(A()), "incorrect restriction")
+        assert_equal(len(rel), len(x & "c1=0") * len(y & "c2=1"), "incorrect pairing")
         # Approach 2: restrict then join
-        x = (A & 'cond_in_a=0').proj(a1='id_a')
-        y = (A & 'cond_in_a=1').proj(a2='id_a')
+        x = (A & "cond_in_a=0").proj(a1="id_a")
+        y = (A & "cond_in_a=1").proj(a2="id_a")
         assert_equal(len(rel), len(x * y))
 
     @staticmethod
     def test_issue_376():
         tab = TTest3()
         tab.delete_quick()
-        tab.insert((
-            (1, '%%%'),
-            (2, 'one%'),
-            (3, 'one')))
+        tab.insert(((1, "%%%"), (2, "one%"), (3, "one")))
         assert_equal(len(tab & 'value="%%%"'), 1)
-        assert_equal(len(tab & {'value': "%%%"}), 1)
+        assert_equal(len(tab & {"value": "%%%"}), 1)
         assert_equal(len(tab & 'value like "o%"'), 2)
         assert_equal(len(tab & 'value like "o%%"'), 2)
 
@@ -165,35 +231,44 @@ def test_issue_463():
 
     @staticmethod
     def test_project():
-        x = A().proj(a='id_a')  # rename
-        assert_equal(x.heading.names, ['a'],
-                     'renaming does not work')
-        x = A().proj(a='(id_a)')  # extend
-        assert_equal(set(x.heading.names), set(('id_a', 'a')),
-                     'extend does not work')
+        x = A().proj(a="id_a")  # rename
+        assert_equal(x.heading.names, ["a"], "renaming does not work")
+        x = A().proj(a="(id_a)")  # extend
+        assert_equal(set(x.heading.names), set(("id_a", "a")), "extend does not work")
 
         # projection after restriction
-        cond = L() & 'cond_in_l'
-        assert_equal(len(D() & cond) + len(D() - cond), len(D()),
-                     'failed semijoin or antijoin')
-        assert_equal(len((D() & cond).proj()), len((D() & cond)),
-                     'projection failed: altered its argument''s cardinality')
+        cond = L() & "cond_in_l"
+        assert_equal(
+            len(D() & cond) + len(D() - cond), len(D()), "failed semijoin or antijoin"
+        )
+        assert_equal(
+            len((D() & cond).proj()),
+            len((D() & cond)),
+            "projection failed: altered its argument" "s cardinality",
+        )
 
     @staticmethod
     def test_rename_non_dj_attribute():
-        schema = PREFIX + '_test1'
+        schema = PREFIX + "_test1"
         connection = dj.conn(**CONN_INFO)
-        connection.query(f'CREATE TABLE {schema}.test_table (oldID int PRIMARY KEY)').fetchall()
+        connection.query(
+            f"CREATE TABLE {schema}.test_table (oldID int PRIMARY KEY)"
+        ).fetchall()
         mySchema = dj.VirtualModule(schema, schema)
-        assert 'oldID' not in  mySchema.TestTable.proj(new_name='oldID').heading.attributes.keys(), 'Failed to rename attribute correctly'
-        connection.query(f'DROP TABLE {schema}.test_table')
+        assert (
+            "oldID"
+            not in mySchema.TestTable.proj(new_name="oldID").heading.attributes.keys()
+        ), "Failed to rename attribute correctly"
+        connection.query(f"DROP TABLE {schema}.test_table")
 
     @staticmethod
     def test_union():
-        x = set(zip(*IJ.fetch('i', 'j')))
-        y = set(zip(*JI.fetch('i', 'j')))
-        assert_true(len(x) > 0 and len(y) > 0 and len(IJ() * JI()) < len(x))  # ensure the IJ and JI are non-trivial
-        z = set(zip(*(IJ + JI).fetch('i', 'j')))   # union
+        x = set(zip(*IJ.fetch("i", "j")))
+        y = set(zip(*JI.fetch("i", "j")))
+        assert_true(
+            len(x) > 0 and len(y) > 0 and len(IJ() * JI()) < len(x)
+        )  # ensure the IJ and JI are non-trivial
+        z = set(zip(*(IJ + JI).fetch("i", "j")))  # union
         assert_set_equal(x.union(y), z)
         assert_equal(len(IJ + JI), len(z))
 
@@ -208,22 +283,32 @@ def test_outer_union_fail():
         """Union of two tables with different primary keys raises an error."""
         t = Trial + Ephys
         t.fetch()
-        assert_set_equal(set(t.heading.names), set(Trial.heading.names) | set(Ephys.heading.names))
+        assert_set_equal(
+            set(t.heading.names), set(Trial.heading.names) | set(Ephys.heading.names)
+        )
         len(t)
 
     @staticmethod
     def test_preview():
         with dj.config(display__limit=7):
-            x = A().proj(a='id_a')
+            x = A().proj(a="id_a")
             s = x.preview()
-            assert_equal(len(s.split('\n')), len(x) + 2)
+            assert_equal(len(s.split("\n")), len(x) + 2)
 
     @staticmethod
     def test_heading_repr():
         x = A * D
         s = repr(x.heading)
-        assert_equal(len(list(1 for g in s.split('\n') if g.strip() and not g.strip().startswith(('-', '#')))),
-                     len(x.heading.attributes))
+        assert_equal(
+            len(
+                list(
+                    1
+                    for g in s.split("\n")
+                    if g.strip() and not g.strip().startswith(("-", "#"))
+                )
+            ),
+            len(x.heading.attributes),
+        )
 
     @staticmethod
     def test_aggregate():
@@ -233,23 +318,39 @@ def test_aggregate():
         x = B().aggregate(B.C(), keep_all_rows=True)
         assert_equal(len(x), len(B()))  # test LEFT join
 
-        assert_equal(len((x & 'id_b=0').fetch()), len(B() & 'id_b=0'))  # test restricted aggregation
-
-        x = B().aggregate(B.C(), 'n', count='count(id_c)', mean='avg(value)', max='max(value)', keep_all_rows=True)
+        assert_equal(
+            len((x & "id_b=0").fetch()), len(B() & "id_b=0")
+        )  # test restricted aggregation
+
+        x = B().aggregate(
+            B.C(),
+            "n",
+            count="count(id_c)",
+            mean="avg(value)",
+            max="max(value)",
+            keep_all_rows=True,
+        )
         assert_equal(len(x), len(B()))
-        y = x & 'mean>0'  # restricted aggregation
+        y = x & "mean>0"  # restricted aggregation
         assert_true(len(y) > 0)
-        assert_true(all(y.fetch('mean') > 0))
-        for n, count, mean, max_, key in zip(*x.fetch('n', 'count', 'mean', 'max', dj.key)):
-            assert_equal(n, count, 'aggregation failed (count)')
-            values = (B.C() & key).fetch('value')
-            assert_true(bool(len(values)) == bool(n),
-                        'aggregation failed (restriction)')
+        assert_true(all(y.fetch("mean") > 0))
+        for n, count, mean, max_, key in zip(
+            *x.fetch("n", "count", "mean", "max", dj.key)
+        ):
+            assert_equal(n, count, "aggregation failed (count)")
+            values = (B.C() & key).fetch("value")
+            assert_true(
+                bool(len(values)) == bool(n), "aggregation failed (restriction)"
+            )
             if n:
-                assert_true(np.isclose(mean, values.mean(), rtol=1e-4, atol=1e-5),
-                            "aggregation failed (mean)")
-                assert_true(np.isclose(max_, values.max(), rtol=1e-4, atol=1e-5),
-                            "aggregation failed (max)")
+                assert_true(
+                    np.isclose(mean, values.mean(), rtol=1e-4, atol=1e-5),
+                    "aggregation failed (mean)",
+                )
+                assert_true(
+                    np.isclose(max_, values.max(), rtol=1e-4, atol=1e-5),
+                    "aggregation failed (max)",
+                )
 
     @staticmethod
     def test_aggr():
@@ -261,23 +362,39 @@ def test_aggr():
         x = B().aggr(B.C(), keep_all_rows=True)
         assert_equal(len(x), len(B()))  # test LEFT join
 
-        assert_equal(len((x & 'id_b=0').fetch()), len(B() & 'id_b=0'))  # test restricted aggregation
-
-        x = B().aggr(B.C(), 'n', count='count(id_c)', mean='avg(value)', max='max(value)', keep_all_rows=True)
+        assert_equal(
+            len((x & "id_b=0").fetch()), len(B() & "id_b=0")
+        )  # test restricted aggregation
+
+        x = B().aggr(
+            B.C(),
+            "n",
+            count="count(id_c)",
+            mean="avg(value)",
+            max="max(value)",
+            keep_all_rows=True,
+        )
         assert_equal(len(x), len(B()))
-        y = x & 'mean>0'  # restricted aggregation
+        y = x & "mean>0"  # restricted aggregation
         assert_true(len(y) > 0)
-        assert_true(all(y.fetch('mean') > 0))
-        for n, count, mean, max_, key in zip(*x.fetch('n', 'count', 'mean', 'max', dj.key)):
-            assert_equal(n, count, 'aggregation failed (count)')
-            values = (B.C() & key).fetch('value')
-            assert_true(bool(len(values)) == bool(n),
-                        'aggregation failed (restriction)')
+        assert_true(all(y.fetch("mean") > 0))
+        for n, count, mean, max_, key in zip(
+            *x.fetch("n", "count", "mean", "max", dj.key)
+        ):
+            assert_equal(n, count, "aggregation failed (count)")
+            values = (B.C() & key).fetch("value")
+            assert_true(
+                bool(len(values)) == bool(n), "aggregation failed (restriction)"
+            )
             if n:
-                assert_true(np.isclose(mean, values.mean(), rtol=1e-4, atol=1e-5),
-                            "aggregation failed (mean)")
-                assert_true(np.isclose(max_, values.max(), rtol=1e-4, atol=1e-5),
-                            "aggregation failed (max)")
+                assert_true(
+                    np.isclose(mean, values.mean(), rtol=1e-4, atol=1e-5),
+                    "aggregation failed (mean)",
+                )
+                assert_true(
+                    np.isclose(max_, values.max(), rtol=1e-4, atol=1e-5),
+                    "aggregation failed (max)",
+                )
 
     @staticmethod
     def test_semijoin():
@@ -299,170 +416,192 @@ def test_semijoin():
 
     @staticmethod
     def test_pandas_fetch_and_restriction():
-        q = (L & 'cond_in_l = 0')
-        df = q.fetch(format='frame')   # pandas dataframe
+        q = L & "cond_in_l = 0"
+        df = q.fetch(format="frame")  # pandas dataframe
         assert_true(isinstance(df, pandas.DataFrame))
         assert_equal(len(E & q), len(E & df))
 
     @staticmethod
     def test_restriction_by_null():
-        assert_true(len(Experiment & 'username is null') > 0)
-        assert_true(len(Experiment & 'username is not null') > 0)
+        assert_true(len(Experiment & "username is null") > 0)
+        assert_true(len(Experiment & "username is not null") > 0)
 
     @staticmethod
-    def test_restriction_between():   # see issue
-        assert_true(len(Experiment & 'username between "S" and "Z"') < len(Experiment()))
+    def test_restriction_between():  # see issue
+        assert_true(
+            len(Experiment & 'username between "S" and "Z"') < len(Experiment())
+        )
 
     @staticmethod
     def test_restrictions_by_lists():
         x = D()
-        y = L() & 'cond_in_l'
+        y = L() & "cond_in_l"
 
         lenx = len(x)
-        assert_true(lenx > 0 and len(y) > 0 and len(x & y) < len(x), 'incorrect test setup')
+        assert_true(
+            lenx > 0 and len(y) > 0 and len(x & y) < len(x), "incorrect test setup"
+        )
 
         assert_equal(len(D()), len(D & dj.AndList([])))
         assert_true(len(D & []) == 0)
         assert_true(len(D & [[]]) == 0)  # an OR-list of OR-list
 
         lenx = len(x)
-        assert_true(lenx > 0 and len(y) > 0 and len(x & y) < len(x), 'incorrect test setup')
-        assert_equal(len(x & y), len(D * L & 'cond_in_l'),
-                     'incorrect semijoin')
-        assert_equal(len(x - y), len(x) - len(x & y),
-                     'incorrect antijoin')
-        assert_equal(len(y - x), len(y) - len(y & x),
-                     'incorrect antijoin')
-        assert_true(len(x & []) == 0,
-                    'incorrect restriction by an empty list')
-        assert_true(len(x & ()) == 0,
-                    'incorrect restriction by an empty tuple')
-        assert_true(len(x & set()) == 0,
-                    'incorrect restriction by an empty set')
-        assert_equal(len(x - []), lenx,
-                     'incorrect restriction by an empty list')
-        assert_equal(len(x - ()), lenx,
-                     'incorrect restriction by an empty tuple')
-        assert_equal(len(x - set()), lenx,
-                     'incorrect restriction by an empty set')
-        assert_equal(len(x & {}), lenx,
-                     'incorrect restriction by a tuple with no attributes')
-        assert_true(len(x - {}) == 0,
-                    'incorrect restriction by a tuple with no attributes')
-        assert_equal(len(x & {'foo': 0}), lenx,
-                     'incorrect restriction by a tuple with no matching attributes')
-        assert_true(len(x - {'foo': 0}) == 0,
-                    'incorrect restriction by a tuple with no matching attributes')
-        assert_equal(len(x & y), len(x & y.fetch()),
-                     'incorrect restriction by a list')
-        assert_equal(len(x - y), len(x - y.fetch()),
-                     'incorrect restriction by a list')
+        assert_true(
+            lenx > 0 and len(y) > 0 and len(x & y) < len(x), "incorrect test setup"
+        )
+        assert_equal(len(x & y), len(D * L & "cond_in_l"), "incorrect semijoin")
+        assert_equal(len(x - y), len(x) - len(x & y), "incorrect antijoin")
+        assert_equal(len(y - x), len(y) - len(y & x), "incorrect antijoin")
+        assert_true(len(x & []) == 0, "incorrect restriction by an empty list")
+        assert_true(len(x & ()) == 0, "incorrect restriction by an empty tuple")
+        assert_true(len(x & set()) == 0, "incorrect restriction by an empty set")
+        assert_equal(len(x - []), lenx, "incorrect restriction by an empty list")
+        assert_equal(len(x - ()), lenx, "incorrect restriction by an empty tuple")
+        assert_equal(len(x - set()), lenx, "incorrect restriction by an empty set")
+        assert_equal(
+            len(x & {}), lenx, "incorrect restriction by a tuple with no attributes"
+        )
+        assert_true(
+            len(x - {}) == 0, "incorrect restriction by a tuple with no attributes"
+        )
+        assert_equal(
+            len(x & {"foo": 0}),
+            lenx,
+            "incorrect restriction by a tuple with no matching attributes",
+        )
+        assert_true(
+            len(x - {"foo": 0}) == 0,
+            "incorrect restriction by a tuple with no matching attributes",
+        )
+        assert_equal(len(x & y), len(x & y.fetch()), "incorrect restriction by a list")
+        assert_equal(len(x - y), len(x - y.fetch()), "incorrect restriction by a list")
         w = A()
-        assert_true(len(w) > 0, 'incorrect test setup: w is empty')
-        assert_false(bool(set(w.heading.names) & set(y.heading.names)),
-                     'incorrect test setup: w and y should have no common attributes')
-        assert_equal(len(w), len(w & y),
-                     'incorrect restriction without common attributes')
-        assert_true(len(w - y) == 0,
-                    'incorrect restriction without common attributes')
+        assert_true(len(w) > 0, "incorrect test setup: w is empty")
+        assert_false(
+            bool(set(w.heading.names) & set(y.heading.names)),
+            "incorrect test setup: w and y should have no common attributes",
+        )
+        assert_equal(
+            len(w), len(w & y), "incorrect restriction without common attributes"
+        )
+        assert_true(len(w - y) == 0, "incorrect restriction without common attributes")
 
     @staticmethod
     def test_datetime():
         """Test date retrieval"""
-        date = Experiment().fetch('experiment_date')[0]
+        date = Experiment().fetch("experiment_date")[0]
         e1 = Experiment() & dict(experiment_date=str(date))
         e2 = Experiment() & dict(experiment_date=date)
-        assert_true(len(e1) == len(e2) > 0, 'Two date restriction do not yield the same result')
+        assert_true(
+            len(e1) == len(e2) > 0, "Two date restriction do not yield the same result"
+        )
 
     @staticmethod
     def test_date():
         """Test date update"""
         # https://github.com/datajoint/datajoint-python/issues/664
-        F.insert1((2, '2019-09-25'))
+        F.insert1((2, "2019-09-25"))
 
         new_value = None
-        (F & 'id=2')._update('date', new_value)
-        assert_equal((F & 'id=2').fetch1('date'), new_value)
+        (F & "id=2")._update("date", new_value)
+        assert_equal((F & "id=2").fetch1("date"), new_value)
 
         new_value = datetime.date(2019, 10, 25)
-        (F & 'id=2')._update('date', new_value)
-        assert_equal((F & 'id=2').fetch1('date'), new_value)
+        (F & "id=2")._update("date", new_value)
+        assert_equal((F & "id=2").fetch1("date"), new_value)
 
-        (F & 'id=2')._update('date')
-        assert_equal((F & 'id=2').fetch1('date'), None)
+        (F & "id=2")._update("date")
+        assert_equal((F & "id=2").fetch1("date"), None)
 
     @staticmethod
     def test_join_project():
         """Test join of projected relations with matching non-primary key"""
         q = DataA.proj() * DataB.proj()
-        assert_true(len(q) == len(DataA()) == len(DataB()),
-                    "Join of projected relations does not work")
+        assert_true(
+            len(q) == len(DataA()) == len(DataB()),
+            "Join of projected relations does not work",
+        )
 
     @staticmethod
     def test_ellipsis():
-        r = Experiment.proj(..., '- data_path').head(1, as_dict=True)
-        assert_set_equal(set(Experiment.heading).difference(r[0]), {'data_path'})
+        r = Experiment.proj(..., "- data_path").head(1, as_dict=True)
+        assert_set_equal(set(Experiment.heading).difference(r[0]), {"data_path"})
 
     @staticmethod
     @raises(dj.DataJointError)
     def test_update_single_key():
         """Test that only one row can be updated"""
-        TTestUpdate()._update('string_attr', 'my new string')
+        TTestUpdate()._update("string_attr", "my new string")
 
     @staticmethod
     @raises(dj.DataJointError)
     def test_update_no_primary():
         """Test that no primary key can be updated"""
-        TTestUpdate()._update('primary_key', 2)
+        TTestUpdate()._update("primary_key", 2)
 
     @staticmethod
     @raises(dj.DataJointError)
     def test_update_missing_attribute():
         """Test that attribute is in table"""
-        TTestUpdate()._update('not_existing', 2)
+        TTestUpdate()._update("not_existing", 2)
 
     @staticmethod
     def test_update_string_attribute():
         """Test replacing a string value"""
-        rel = (TTestUpdate() & dict(primary_key=0))
-        s = ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(10))
-        rel._update('string_attr', s)
-        assert_equal(s, rel.fetch1('string_attr'), "Updated string does not match")
+        rel = TTestUpdate() & dict(primary_key=0)
+        s = "".join(
+            random.choice(string.ascii_uppercase + string.digits) for _ in range(10)
+        )
+        rel._update("string_attr", s)
+        assert_equal(s, rel.fetch1("string_attr"), "Updated string does not match")
 
     @staticmethod
     def test_update_numeric_attribute():
         """Test replacing a string value"""
-        rel = (TTestUpdate() & dict(primary_key=0))
+        rel = TTestUpdate() & dict(primary_key=0)
         s = random.randint(0, 10)
-        rel._update('num_attr', s)
-        assert_equal(s, rel.fetch1('num_attr'), "Updated integer does not match")
-        rel._update('num_attr', None)
-        assert_true(np.isnan(rel.fetch1('num_attr')), "Numeric value is not NaN")
+        rel._update("num_attr", s)
+        assert_equal(s, rel.fetch1("num_attr"), "Updated integer does not match")
+        rel._update("num_attr", None)
+        assert_true(np.isnan(rel.fetch1("num_attr")), "Numeric value is not NaN")
 
     @staticmethod
     def test_update_blob_attribute():
         """Test replacing a string value"""
-        rel = (TTestUpdate() & dict(primary_key=0))
-        s = rel.fetch1('blob_attr')
-        rel._update('blob_attr', s.T)
-        assert_equal(s.T.shape, rel.fetch1('blob_attr').shape, "Array dimensions do not match")
+        rel = TTestUpdate() & dict(primary_key=0)
+        s = rel.fetch1("blob_attr")
+        rel._update("blob_attr", s.T)
+        assert_equal(
+            s.T.shape, rel.fetch1("blob_attr").shape, "Array dimensions do not match"
+        )
 
     @staticmethod
     def test_reserved_words():
         """Test the user of SQL reserved words as attributes"""
         rel = ReservedWord()
-        rel.insert1({'key': 1, 'in': 'ouch', 'from': 'bummer', 'int': 3, 'select': 'major pain'})
-        assert_true((rel & {'key': 1, 'in': 'ouch', 'from': 'bummer'}).fetch1('int') == 3)
-        assert_true((rel.proj('int', double='from') & {'double': 'bummer'}).fetch1('int') == 3)
-        (rel & {'key': 1}).delete()
+        rel.insert1(
+            {"key": 1, "in": "ouch", "from": "bummer", "int": 3, "select": "major pain"}
+        )
+        assert_true(
+            (rel & {"key": 1, "in": "ouch", "from": "bummer"}).fetch1("int") == 3
+        )
+        assert_true(
+            (rel.proj("int", double="from") & {"double": "bummer"}).fetch1("int") == 3
+        )
+        (rel & {"key": 1}).delete()
 
     @staticmethod
     @raises(dj.DataJointError)
     def test_reserved_words2():
         """Test the user of SQL reserved words as attributes"""
         rel = ReservedWord()
-        rel.insert1({'key': 1, 'in': 'ouch', 'from': 'bummer', 'int': 3, 'select': 'major pain'})
-        (rel & 'key=1').fetch('in')  # error because reserved word `key` is not in backquotes. See issue #249
+        rel.insert1(
+            {"key": 1, "in": "ouch", "from": "bummer", "int": 3, "select": "major pain"}
+        )
+        (rel & "key=1").fetch(
+            "in"
+        )  # error because reserved word `key` is not in backquotes. See issue #249
 
     @staticmethod
     def test_permissive_join_basic():
@@ -478,15 +617,15 @@ def test_permissive_restriction_basic():
     def test_complex_date_restriction():
         # https://github.com/datajoint/datajoint-python/issues/892
         """Test a complex date restriction"""
-        q = OutfitLaunch & 'day between curdate() - interval 30 day and curdate()'
+        q = OutfitLaunch & "day between curdate() - interval 30 day and curdate()"
         assert len(q) == 1
-        q = OutfitLaunch & 'day between curdate() - interval 4 week and curdate()'
+        q = OutfitLaunch & "day between curdate() - interval 4 week and curdate()"
         assert len(q) == 1
-        q = OutfitLaunch & 'day between curdate() - interval 1 month and curdate()'
+        q = OutfitLaunch & "day between curdate() - interval 1 month and curdate()"
         assert len(q) == 1
-        q = OutfitLaunch & 'day between curdate() - interval 1 year and curdate()'
+        q = OutfitLaunch & "day between curdate() - interval 1 year and curdate()"
         assert len(q) == 1
-        q = OutfitLaunch & '`day` between curdate() - interval 30 day and curdate()'
+        q = OutfitLaunch & "`day` between curdate() - interval 30 day and curdate()"
         assert len(q) == 1
         q.delete()
 
@@ -495,7 +634,7 @@ def test_null_dict_restriction():
         # https://github.com/datajoint/datajoint-python/issues/824
         """Test a restriction for null using dict"""
         F.insert([dict(id=5)])
-        q = F & dj.AndList([dict(id=5), 'date is NULL'])
+        q = F & dj.AndList([dict(id=5), "date is NULL"])
         assert len(q) == 1
         q = F & dict(id=5, date=None)
         assert len(q) == 1
@@ -506,15 +645,18 @@ def test_joins_with_aggregation():
         # https://github.com/datajoint/datajoint-python/issues/899
         subjects = SubjectA.aggr(
             SessionStatusA & 'status="trained_1a" or status="trained_1b"',
-            date_trained='min(date(session_start_time))')
+            date_trained="min(date(session_start_time))",
+        )
         assert len(SessionDateA * subjects) == 4
         assert len(subjects * SessionDateA) == 4
 
         subj_query = SubjectA.aggr(
             SessionA * SessionStatusA & 'status="trained_1a" or status="trained_1b"',
-            date_trained='min(date(session_start_time))')
-        session_dates = ((SessionDateA * (subj_query & 'date_trained<"2020-12-21"')) &
-                         'session_date<date_trained')
+            date_trained="min(date(session_start_time))",
+        )
+        session_dates = (
+            SessionDateA * (subj_query & 'date_trained<"2020-12-21"')
+        ) & "session_date<date_trained"
         assert len(session_dates) == 1
 
     @staticmethod
@@ -522,7 +664,7 @@ def test_union_multiple():
         # https://github.com/datajoint/datajoint-python/issues/926
         q1 = IJ & dict(j=2)
         q2 = (IJ & dict(j=2, i=0)) + (IJ & dict(j=2, i=1)) + (IJ & dict(j=2, i=2))
-        x = set(zip(*q1.fetch('i', 'j')))
-        y = set(zip(*q2.fetch('i', 'j')))
+        x = set(zip(*q1.fetch("i", "j")))
+        y = set(zip(*q2.fetch("i", "j")))
         assert x == y
         assert q1.fetch(as_dict=True) == q2.fetch(as_dict=True)
diff --git a/tests/test_s3.py b/tests/test_s3.py
index 288a05f05..90449f64f 100644
--- a/tests/test_s3.py
+++ b/tests/test_s3.py
@@ -10,50 +10,53 @@
 
 
 class TestS3:
-
     @staticmethod
     def test_connection():
 
         # Initialize httpClient with relevant timeout.
         http_client = urllib3.PoolManager(
-                                          timeout=30, cert_reqs='CERT_REQUIRED',
-                                          ca_certs=certifi.where(),
-                                          retries=urllib3.Retry(total=3, backoff_factor=0.2,
-                                                                status_forcelist=[
-                                                                                  500, 502,
-                                                                                  503, 504]))
+            timeout=30,
+            cert_reqs="CERT_REQUIRED",
+            ca_certs=certifi.where(),
+            retries=urllib3.Retry(
+                total=3, backoff_factor=0.2, status_forcelist=[500, 502, 503, 504]
+            ),
+        )
 
         # Initialize minioClient with an endpoint and access/secret keys.
         minio_client = Minio(
-            S3_CONN_INFO['endpoint'],
-            access_key=S3_CONN_INFO['access_key'],
-            secret_key=S3_CONN_INFO['secret_key'],
+            S3_CONN_INFO["endpoint"],
+            access_key=S3_CONN_INFO["access_key"],
+            secret_key=S3_CONN_INFO["secret_key"],
             secure=False,
-            http_client=http_client)
+            http_client=http_client,
+        )
 
-        assert_true(minio_client.bucket_exists(S3_CONN_INFO['bucket']))
+        assert_true(minio_client.bucket_exists(S3_CONN_INFO["bucket"]))
 
     @staticmethod
     def test_connection_secure():
 
         # Initialize httpClient with relevant timeout.
         http_client = urllib3.PoolManager(
-                                          timeout=30, cert_reqs='CERT_REQUIRED',
-                                          ca_certs=certifi.where(),
-                                          retries=urllib3.Retry(total=3, backoff_factor=0.2,
-                                                                status_forcelist=[
-                                                                                  500, 502,
-                                                                                  503, 504]))
+            timeout=30,
+            cert_reqs="CERT_REQUIRED",
+            ca_certs=certifi.where(),
+            retries=urllib3.Retry(
+                total=3, backoff_factor=0.2, status_forcelist=[500, 502, 503, 504]
+            ),
+        )
 
         # Initialize minioClient with an endpoint and access/secret keys.
         minio_client = Minio(
-            S3_CONN_INFO['endpoint'],
-            access_key=S3_CONN_INFO['access_key'],
-            secret_key=S3_CONN_INFO['secret_key'],
+            S3_CONN_INFO["endpoint"],
+            access_key=S3_CONN_INFO["access_key"],
+            secret_key=S3_CONN_INFO["secret_key"],
             secure=True,
-            http_client=http_client)
+            http_client=http_client,
+        )
 
-        assert_true(minio_client.bucket_exists(S3_CONN_INFO['bucket']))
+        assert_true(minio_client.bucket_exists(S3_CONN_INFO["bucket"]))
 
     @staticmethod
     @raises(DataJointError)
@@ -66,22 +69,24 @@ def test_remove_object_exception():
         SimpleRemote.delete()
 
         # Save the old external table minio client
-        old_client = schema.external['share'].s3.client
+        old_client = schema.external["share"].s3.client
 
         # Apply our new minio client which has a user that does not exist
-        schema.external['share'].s3.client = Minio(
-            S3_CONN_INFO['endpoint'],
-            access_key='jeffjeff',
-            secret_key='jeffjeff',
-            secure=False)
+        schema.external["share"].s3.client = Minio(
+            S3_CONN_INFO["endpoint"],
+            access_key="jeffjeff",
+            secret_key="jeffjeff",
+            secure=False,
+        )
 
         # This method returns a list of errors
-        error_list = schema.external['share'].delete(delete_external_files=True,
-                                                     errors_as_string=False)
+        error_list = schema.external["share"].delete(
+            delete_external_files=True, errors_as_string=False
+        )
 
         # Teardown
-        schema.external['share'].s3.client = old_client
-        schema.external['share'].delete(delete_external_files=True)
+        schema.external["share"].s3.client = old_client
+        schema.external["share"].delete(delete_external_files=True)
 
         # Raise the error we want if the error matches the expected uuid
         if str(error_list[0][0]) == str(uuid_from_buffer(pack(test[1]))):
diff --git a/tests/test_schema.py b/tests/test_schema.py
index 1ff41efdf..ff4c0ebec 100644
--- a/tests/test_schema.py
+++ b/tests/test_schema.py
@@ -33,20 +33,27 @@ def test_schema_list():
 
 @raises(dj.errors.AccessError)
 def test_drop_unauthorized():
-    info_schema = dj.schema('information_schema')
+    info_schema = dj.schema("information_schema")
     info_schema.drop()
 
 
 def test_namespace_population():
     for name, rel in getmembers(schema, relation_selector):
-        assert_true(hasattr(schema_empty, name), '{name} not found in schema_empty'.format(name=name))
-        assert_true(rel.__base__ is getattr(schema_empty, name).__base__,
-                    'Wrong tier for {name}'.format(name=name))
+        assert_true(
+            hasattr(schema_empty, name),
+            "{name} not found in schema_empty".format(name=name),
+        )
+        assert_true(
+            rel.__base__ is getattr(schema_empty, name).__base__,
+            "Wrong tier for {name}".format(name=name),
+        )
 
         for name_part in dir(rel):
             if name_part[0].isupper() and part_selector(getattr(rel, name_part)):
-                assert_true(getattr(rel, name_part).__base__ is dj.Part,
-                            'Wrong tier for {name}'.format(name=name_part))
+                assert_true(
+                    getattr(rel, name_part).__base__ is dj.Part,
+                    "Wrong tier for {name}".format(name=name_part),
+                )
 
 
 @raises(dj.DataJointError)
@@ -82,11 +89,13 @@ def test_unauthorized_database():
     """
     an attempt to create a database to which user has no privileges should raise an informative exception.
     """
-    dj.Schema('unauthorized_schema', connection=dj.conn(reset=True, **CONN_INFO))
+    dj.Schema("unauthorized_schema", connection=dj.conn(reset=True, **CONN_INFO))
 
 
 def test_drop_database():
-    schema = dj.Schema(PREFIX + '_drop_test', connection=dj.conn(reset=True, **CONN_INFO))
+    schema = dj.Schema(
+        PREFIX + "_drop_test", connection=dj.conn(reset=True, **CONN_INFO)
+    )
     assert_true(schema.exists)
     schema.drop()
     assert_false(schema.exists)
@@ -94,7 +103,9 @@ def test_drop_database():
 
 
 def test_overlapping_name():
-    test_schema = dj.Schema(PREFIX + '_overlapping_schema', connection=dj.conn(**CONN_INFO))
+    test_schema = dj.Schema(
+        PREFIX + "_overlapping_schema", connection=dj.conn(**CONN_INFO)
+    )
 
     @test_schema
     class Unit(dj.Manual):
@@ -122,11 +133,30 @@ class Unit(dj.Part):
 
 def test_list_tables():
     # https://github.com/datajoint/datajoint-python/issues/838
-    assert(set(['reserved_word', '#l', '#a', '__d', '__b', '__b__c', '__e', '__e__f',
-                '#outfit_launch', '#outfit_launch__outfit_piece', '#i_j', '#j_i',
-                '#t_test_update', '#data_a', '#data_b', 'f', '#argmax_test',
-                '#website', 'profile', 'profile__website'
-                ]) == set(schema_simple.list_tables()))
+    assert set(
+        [
+            "reserved_word",
+            "#l",
+            "#a",
+            "__d",
+            "__b",
+            "__b__c",
+            "__e",
+            "__e__f",
+            "#outfit_launch",
+            "#outfit_launch__outfit_piece",
+            "#i_j",
+            "#j_i",
+            "#t_test_update",
+            "#data_a",
+            "#data_b",
+            "f",
+            "#argmax_test",
+            "#website",
+            "profile",
+            "profile__website",
+        ]
+    ) == set(schema_simple.list_tables())
 
 
 def test_schema_save():
@@ -137,7 +167,7 @@ def test_schema_save():
 def test_uppercase_schema():
     # https://github.com/datajoint/datajoint-python/issues/564
     dj.conn(**CONN_INFO_ROOT, reset=True)
-    schema1 = dj.Schema('Schema_A')
+    schema1 = dj.Schema("Schema_A")
 
     @schema1
     class Subject(dj.Manual):
@@ -145,9 +175,9 @@ class Subject(dj.Manual):
         name: varchar(32)
         """
 
-    Schema_A = dj.VirtualModule('Schema_A', 'Schema_A')
+    Schema_A = dj.VirtualModule("Schema_A", "Schema_A")
 
-    schema2 = dj.Schema('schema_b')
+    schema2 = dj.Schema("schema_b")
 
     @schema2
     class Recording(dj.Manual):
diff --git a/tests/test_schema_keywords.py b/tests/test_schema_keywords.py
index 50cb25558..49f380f57 100644
--- a/tests/test_schema_keywords.py
+++ b/tests/test_schema_keywords.py
@@ -3,7 +3,7 @@
 from nose.tools import assert_true
 
 
-schema = dj.Schema(PREFIX + '_keywords', connection=dj.conn(**CONN_INFO))
+schema = dj.Schema(PREFIX + "_keywords", connection=dj.conn(**CONN_INFO))
 
 
 @schema
@@ -39,8 +39,8 @@ class D(B):
 
 
 def test_inherited_part_table():
-    assert_true('a_id' in D().heading.attributes)
-    assert_true('b_id' in D().heading.attributes)
-    assert_true('a_id' in D.C().heading.attributes)
-    assert_true('b_id' in D.C().heading.attributes)
-    assert_true('name' in D.C().heading.attributes)
+    assert_true("a_id" in D().heading.attributes)
+    assert_true("b_id" in D().heading.attributes)
+    assert_true("a_id" in D.C().heading.attributes)
+    assert_true("b_id" in D.C().heading.attributes)
+    assert_true("name" in D.C().heading.attributes)
diff --git a/tests/test_settings.py b/tests/test_settings.py
index 48ca560a9..63c3dad36 100644
--- a/tests/test_settings.py
+++ b/tests/test_settings.py
@@ -6,49 +6,49 @@
 import datajoint as dj
 import os
 
-__author__ = 'Fabian Sinz'
+__author__ = "Fabian Sinz"
 
 
 def test_load_save():
     """Testing load and save"""
-    dj.config.save('tmp.json')
+    dj.config.save("tmp.json")
     conf = settings.Config()
-    conf.load('tmp.json')
-    assert_true(conf == dj.config, 'Two config files do not match.')
-    os.remove('tmp.json')
+    conf.load("tmp.json")
+    assert_true(conf == dj.config, "Two config files do not match.")
+    os.remove("tmp.json")
 
 
 def test_singleton():
     """Testing singleton property"""
-    dj.config.save('tmp.json')
+    dj.config.save("tmp.json")
     conf = settings.Config()
-    conf.load('tmp.json')
-    conf['dummy.val'] = 2
+    conf.load("tmp.json")
+    conf["dummy.val"] = 2
 
-    assert_true(conf == dj.config, 'Config does not behave like a singleton.')
-    os.remove('tmp.json')
+    assert_true(conf == dj.config, "Config does not behave like a singleton.")
+    os.remove("tmp.json")
 
 
 def test_singleton2():
     """Testing singleton property"""
     conf = settings.Config()
-    conf['dummy.val'] = 2
-    _ = settings.Config() # a new instance should not delete dummy.val
-    assert_true(conf['dummy.val'] == 2, 'Config does not behave like a singleton.')
+    conf["dummy.val"] = 2
+    _ = settings.Config()  # a new instance should not delete dummy.val
+    assert_true(conf["dummy.val"] == 2, "Config does not behave like a singleton.")
 
 
 @raises(dj.DataJointError)
 def test_validator():
     """Testing validator"""
-    dj.config['database.port'] = 'harbor'
+    dj.config["database.port"] = "harbor"
 
 
 def test_del():
     """Testing del"""
-    dj.config['peter'] = 2
-    assert_true('peter' in dj.config)
-    del dj.config['peter']
-    assert_true('peter' not in dj.config)
+    dj.config["peter"] = 2
+    assert_true("peter" in dj.config)
+    del dj.config["peter"]
+    assert_true("peter" not in dj.config)
 
 
 def test_len():
@@ -68,7 +68,9 @@ def test_repr():
 
 def test_save():
     """Testing save of config"""
-    tmpfile = ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(20))
+    tmpfile = "".join(
+        random.choice(string.ascii_uppercase + string.digits) for _ in range(20)
+    )
     moved = False
     if os.path.isfile(settings.LOCALCONFIG):
         os.rename(settings.LOCALCONFIG, tmpfile)
@@ -82,7 +84,12 @@ def test_save():
 def test_load_save():
     """Testing load and save of config"""
     filename_old = dj.settings.LOCALCONFIG
-    filename = ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(50)) + '.json'
+    filename = (
+        "".join(
+            random.choice(string.ascii_uppercase + string.digits) for _ in range(50)
+        )
+        + ".json"
+    )
     dj.settings.LOCALCONFIG = filename
     dj.config.save_local()
     dj.config.load(filename=filename)
@@ -92,7 +99,7 @@ def test_load_save():
 
 def test_contextmanager():
     """Testing context manager"""
-    dj.config['arbitrary.stuff'] = 7
+    dj.config["arbitrary.stuff"] = 7
     with dj.config(arbitrary__stuff=10):
-        assert_true(dj.config['arbitrary.stuff'] == 10)
-    assert_true(dj.config['arbitrary.stuff'] == 7)
+        assert_true(dj.config["arbitrary.stuff"] == 10)
+    assert_true(dj.config["arbitrary.stuff"] == 7)
diff --git a/tests/test_tls.py b/tests/test_tls.py
index c39402fcd..1bac17e7e 100644
--- a/tests/test_tls.py
+++ b/tests/test_tls.py
@@ -1,28 +1,37 @@
-from nose.tools import assert_true, assert_false, assert_equal, \
-                        assert_list_equal, raises
+from nose.tools import (
+    assert_true,
+    assert_false,
+    assert_equal,
+    assert_list_equal,
+    raises,
+)
 import datajoint as dj
 from . import CONN_INFO
 from pymysql.err import OperationalError
 
 
 class TestTLS:
-
     @staticmethod
     def test_secure_connection():
-        result = dj.conn(reset=True, **CONN_INFO).query(
-                "SHOW STATUS LIKE 'Ssl_cipher';").fetchone()[1]
+        result = (
+            dj.conn(reset=True, **CONN_INFO)
+            .query("SHOW STATUS LIKE 'Ssl_cipher';")
+            .fetchone()[1]
+        )
         assert_true(len(result) > 0)
 
     @staticmethod
     def test_insecure_connection():
-        result = dj.conn(use_tls=False, reset=True, **CONN_INFO).query(
-                "SHOW STATUS LIKE 'Ssl_cipher';").fetchone()[1]
-        assert_equal(result, '')
+        result = (
+            dj.conn(use_tls=False, reset=True, **CONN_INFO)
+            .query("SHOW STATUS LIKE 'Ssl_cipher';")
+            .fetchone()[1]
+        )
+        assert_equal(result, "")
 
     @staticmethod
     @raises(OperationalError)
     def test_reject_insecure():
         dj.conn(
-            CONN_INFO['host'], user='djssl', password='djssl',
-            use_tls=False, reset=True
+            CONN_INFO["host"], user="djssl", password="djssl", use_tls=False, reset=True
         ).query("SHOW STATUS LIKE 'Ssl_cipher';").fetchone()[1]
diff --git a/tests/test_university.py b/tests/test_university.py
index 63321d6ef..f0575e41e 100644
--- a/tests/test_university.py
+++ b/tests/test_university.py
@@ -14,27 +14,40 @@ def _hash4(table):
 
 @raises(DataJointError)
 def test_activate_unauthorized():
-    schema.activate('unauthorized', connection=dj.conn(**CONN_INFO))
+    schema.activate("unauthorized", connection=dj.conn(**CONN_INFO))
 
 
 def test_activate():
-    schema.activate(PREFIX + '_university',  connection=dj.conn(**CONN_INFO))  # deferred activation
+    schema.activate(
+        PREFIX + "_university", connection=dj.conn(**CONN_INFO)
+    )  # deferred activation
     # ---------------  Fill University -------------------
-    for table in Student, Department, StudentMajor, Course, Term, CurrentTerm, Section, Enroll, Grade:
+    for table in (
+        Student,
+        Department,
+        StudentMajor,
+        Course,
+        Term,
+        CurrentTerm,
+        Section,
+        Enroll,
+        Grade,
+    ):
         import csv
-        with open('./data/' + table.__name__ + '.csv') as f:
+
+        with open("./data/" + table.__name__ + ".csv") as f:
             reader = csv.DictReader(f)
             table().insert(reader)
 
 
 def test_fill():
-    """ check that the randomized tables are consistently defined """
+    """check that the randomized tables are consistently defined"""
     # check randomized tables
-    assert_true(len(Student()) == 300 and _hash4(Student) == '1e1a')
-    assert_true(len(StudentMajor()) == 226 and _hash4(StudentMajor) == '3129')
-    assert_true(len(Section()) == 756 and _hash4(Section) == 'dc7e')
-    assert_true(len(Enroll()) == 3364 and _hash4(Enroll) == '177d')
-    assert_true(len(Grade()) == 3027 and _hash4(Grade) == '4a9d')
+    assert_true(len(Student()) == 300 and _hash4(Student) == "1e1a")
+    assert_true(len(StudentMajor()) == 226 and _hash4(StudentMajor) == "3129")
+    assert_true(len(Section()) == 756 and _hash4(Section) == "dc7e")
+    assert_true(len(Enroll()) == 3364 and _hash4(Enroll) == "177d")
+    assert_true(len(Grade()) == 3027 and _hash4(Grade) == "4a9d")
 
 
 def test_restrict():
@@ -42,22 +55,27 @@ def test_restrict():
     test diverse restrictions from the university database.
     This test relies on a specific instantiation of the database.
     """
-    utahns1 = Student & {'home_state': 'UT'}
+    utahns1 = Student & {"home_state": "UT"}
     utahns2 = Student & 'home_state="UT"'
-    assert_true(len(utahns1) == len(utahns2.fetch('KEY')) == 7)
+    assert_true(len(utahns1) == len(utahns2.fetch("KEY")) == 7)
 
     # male nonutahns
-    sex1, state1 = ((Student & 'sex="M"') - {'home_state': 'UT'}).fetch(
-        'sex', 'home_state', order_by='student_id')
-    sex2, state2 = ((Student & 'sex="M"') - {'home_state': 'UT'}).fetch(
-        'sex', 'home_state', order_by='student_id')
+    sex1, state1 = ((Student & 'sex="M"') - {"home_state": "UT"}).fetch(
+        "sex", "home_state", order_by="student_id"
+    )
+    sex2, state2 = ((Student & 'sex="M"') - {"home_state": "UT"}).fetch(
+        "sex", "home_state", order_by="student_id"
+    )
     assert_true(len(set(state1)) == len(set(state2)) == 44)
     assert_true(set(sex1).pop() == set(sex2).pop() == "M")
 
     # students from OK, NM, TX
-    s1 = (Student & [{'home_state': s} for s in ('OK', 'NM', 'TX')]).fetch(
-        "KEY", order_by="student_id")
-    s2 = (Student & 'home_state in ("OK", "NM", "TX")').fetch('KEY', order_by="student_id")
+    s1 = (Student & [{"home_state": s} for s in ("OK", "NM", "TX")]).fetch(
+        "KEY", order_by="student_id"
+    )
+    s2 = (Student & 'home_state in ("OK", "NM", "TX")').fetch(
+        "KEY", order_by="student_id"
+    )
     assert_true(len(s1) == 11)
     assert_list_equal(s1, s2)
 
@@ -70,7 +88,7 @@ def test_restrict():
     assert_true(len(inactive_students) == 204)
 
     # Females who are active or major in non-math
-    special = Student & [Enroll, StudentMajor - {'dept': "MATH"}] & {'sex': "F"}
+    special = Student & [Enroll, StudentMajor - {"dept": "MATH"}] & {"sex": "F"}
     assert_true(len(special) == 158)
 
 
@@ -81,42 +99,49 @@ def test_advanced_join():
     assert_true(len(ungraded) == 34)
 
     # add major
-    major = StudentMajor.proj(..., major='dept')
+    major = StudentMajor.proj(..., major="dept")
     assert_true(len(ungraded.join(major, left=True)) == len(ungraded) == 34)
     assert_true(len(ungraded.join(major)) == len(ungraded & major) == 31)
 
 
 def test_union():
     # effective left join Enroll with Major
-    q1 = (Enroll & 'student_id=101') + (Enroll & 'student_id=102')
-    q2 = (Enroll & 'student_id in (101, 102)')
+    q1 = (Enroll & "student_id=101") + (Enroll & "student_id=102")
+    q2 = Enroll & "student_id in (101, 102)"
     assert_true(len(q1) == len(q2) == 41)
 
 
 def test_aggr():
-    avg_grade_per_course = Course.aggr(Grade*LetterGrade, avg_grade='round(avg(points), 2)')
+    avg_grade_per_course = Course.aggr(
+        Grade * LetterGrade, avg_grade="round(avg(points), 2)"
+    )
     assert_true(len(avg_grade_per_course) == 45)
 
     # GPA
     student_gpa = Student.aggr(
-        Course * Grade * LetterGrade,
-        gpa='round(sum(points*credits)/sum(credits), 2)')
-    gpa = student_gpa.fetch('gpa')
+        Course * Grade * LetterGrade, gpa="round(sum(points*credits)/sum(credits), 2)"
+    )
+    gpa = student_gpa.fetch("gpa")
     assert_true(len(gpa) == 261)
     assert_true(2 < gpa.mean() < 3)
 
     # Sections in biology department with zero students in them
     section = (Section & {"dept": "BIOL"}).aggr(
-        Enroll, n='count(student_id)', keep_all_rows=True) & 'n=0'
-    assert_true(len(set(section.fetch('dept'))) == 1)
+        Enroll, n="count(student_id)", keep_all_rows=True
+    ) & "n=0"
+    assert_true(len(set(section.fetch("dept"))) == 1)
     assert_true(len(section) == 17)
     assert_true(bool(section))
 
     # Test correct use of ellipses in a similar query
     section = (Section & {"dept": "BIOL"}).aggr(
-        Grade, ..., n='count(student_id)', keep_all_rows=True) & 'n>1'
+        Grade, ..., n="count(student_id)", keep_all_rows=True
+    ) & "n>1"
     assert_false(
-        any(name in section.heading.names for name in Grade.heading.secondary_attributes))
-    assert_true(len(set(section.fetch('dept'))) == 1)
+        any(
+            name in section.heading.names for name in Grade.heading.secondary_attributes
+        )
+    )
+    assert_true(len(set(section.fetch("dept"))) == 1)
     assert_true(len(section) == 168)
     assert_true(bool(section))
diff --git a/tests/test_update1.py b/tests/test_update1.py
index e52ff1a01..e0a17f5e6 100644
--- a/tests/test_update1.py
+++ b/tests/test_update1.py
@@ -7,16 +7,13 @@
 from . import PREFIX, CONN_INFO
 from datajoint import DataJointError
 
-schema = dj.Schema(PREFIX + '_update1', connection=dj.conn(**CONN_INFO))
+schema = dj.Schema(PREFIX + "_update1", connection=dj.conn(**CONN_INFO))
 
-dj.config['stores']['update_store'] = dict(
-    protocol='file',
-    location=tempfile.mkdtemp())
+dj.config["stores"]["update_store"] = dict(protocol="file", location=tempfile.mkdtemp())
 
-dj.config['stores']['update_repo'] = dict(
-    stage=tempfile.mkdtemp(),
-    protocol='file',
-    location=tempfile.mkdtemp())
+dj.config["stores"]["update_repo"] = dict(
+    stage=tempfile.mkdtemp(), protocol="file", location=tempfile.mkdtemp()
+)
 
 
 scratch_folder = tempfile.mkdtemp()
@@ -51,7 +48,7 @@ def test_update1():
     # numbers and datetimes
     Thing.update1(dict(key, number=3, frac=30, timestamp="2020-01-01 10:00:00"))
     # attachment
-    attach_file = Path(scratch_folder, 'attach1.dat')
+    attach_file = Path(scratch_folder, "attach1.dat")
     buffer1 = os.urandom(100)
     attach_file.write_bytes(buffer1)
     Thing.update1(dict(key, picture=attach_file))
@@ -59,48 +56,62 @@ def test_update1():
     assert_false(attach_file.is_file())
 
     # filepath
-    stage_path = dj.config['stores']['update_repo']['stage']
-    relpath, filename = 'one/two/three', 'picture.dat'
+    stage_path = dj.config["stores"]["update_repo"]["stage"]
+    relpath, filename = "one/two/three", "picture.dat"
     managed_file = Path(stage_path, relpath, filename)
     managed_file.parent.mkdir(parents=True, exist_ok=True)
     original_file_data = os.urandom(3000)
-    with managed_file.open('wb') as f:
+    with managed_file.open("wb") as f:
         f.write(original_file_data)
     Thing.update1(dict(key, img_file=managed_file))
     managed_file.unlink()
     assert_false(managed_file.is_file())
 
     check2 = Thing.fetch1(download_path=scratch_folder)
-    buffer2 = Path(check2['picture']).read_bytes()   # read attachment
-    final_file_data = managed_file.read_bytes()    # read filepath
+    buffer2 = Path(check2["picture"]).read_bytes()  # read attachment
+    final_file_data = managed_file.read_bytes()  # read filepath
 
     # CHECK 3 -- reset to default values using None
-    Thing.update1(dict(key, number=None, timestamp=None, picture=None, img_file=None, params=np.random.randn(3, 3)))
+    Thing.update1(
+        dict(
+            key,
+            number=None,
+            timestamp=None,
+            picture=None,
+            img_file=None,
+            params=np.random.randn(3, 3),
+        )
+    )
     check3 = Thing.fetch1()
 
-    assert_true(check1['number'] == 0 and
-                check1['picture'] is None and
-                check1['params'] is None)
-
-    assert_true(check2['number'] == 3 and
-                check2['frac'] == 30.0 and
-                check2['picture'] is not None and
-                check2['params'] is None and buffer1==buffer2)
-
-    assert_true(check3['number'] == 0 and
-                check3['frac'] == 30.0 and
-                check3['picture'] is None and
-                check3['img_file'] is None and
-                isinstance(check3['params'], np.ndarray))
-
-    assert_true(check3['timestamp'] > check2['timestamp'])
+    assert_true(
+        check1["number"] == 0 and check1["picture"] is None and check1["params"] is None
+    )
+
+    assert_true(
+        check2["number"] == 3
+        and check2["frac"] == 30.0
+        and check2["picture"] is not None
+        and check2["params"] is None
+        and buffer1 == buffer2
+    )
+
+    assert_true(
+        check3["number"] == 0
+        and check3["frac"] == 30.0
+        and check3["picture"] is None
+        and check3["img_file"] is None
+        and isinstance(check3["params"], np.ndarray)
+    )
+
+    assert_true(check3["timestamp"] > check2["timestamp"])
     assert_equal(buffer1, buffer2)
     assert_equal(original_file_data, final_file_data)
 
 
 @raises(DataJointError)
 def test_update1_nonexistent():
-    Thing.update1(dict(thing=100, frac=0.5))   # updating a non-existent entry
+    Thing.update1(dict(thing=100, frac=0.5))  # updating a non-existent entry
 
 
 @raises(DataJointError)
@@ -112,4 +123,4 @@ def test_update1_noprimary():
 def test_update1_misspelled_attribute():
     key = dict(thing=17)
     Thing.insert1(dict(key, frac=1.5))
-    Thing.update1(dict(key, numer=3))    # misspelled attribute
+    Thing.update1(dict(key, numer=3))  # misspelled attribute
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 2ec35486f..b5ed96af3 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -15,21 +15,19 @@ def teardown():
 
 
 def test_from_camel_case():
-    assert_equal(from_camel_case('AllGroups'), 'all_groups')
+    assert_equal(from_camel_case("AllGroups"), "all_groups")
     with assert_raises(DataJointError):
-        from_camel_case('repNames')
+        from_camel_case("repNames")
     with assert_raises(DataJointError):
-        from_camel_case('10_all')
+        from_camel_case("10_all")
     with assert_raises(DataJointError):
-        from_camel_case('hello world')
+        from_camel_case("hello world")
     with assert_raises(DataJointError):
-        from_camel_case('#baisc_names')
+        from_camel_case("#baisc_names")
 
 
 def test_to_camel_case():
-    assert_equal(to_camel_case('all_groups'), 'AllGroups')
-    assert_equal(to_camel_case('hello'), 'Hello')
-    assert_equal(to_camel_case('this_is_a_sample_case'), 'ThisIsASampleCase')
-    assert_equal(to_camel_case('This_is_Mixed'), 'ThisIsMixed')
-
-
+    assert_equal(to_camel_case("all_groups"), "AllGroups")
+    assert_equal(to_camel_case("hello"), "Hello")
+    assert_equal(to_camel_case("this_is_a_sample_case"), "ThisIsASampleCase")
+    assert_equal(to_camel_case("This_is_Mixed"), "ThisIsMixed")
diff --git a/tests/test_uuid.py b/tests/test_uuid.py
index aa1dc4047..1d5aa0818 100644
--- a/tests/test_uuid.py
+++ b/tests/test_uuid.py
@@ -10,19 +10,19 @@ def test_uuid():
     u, n = uuid.uuid4(), -1
     Basic().insert1(dict(item=u, number=n))
     Basic().insert(zip(map(uuid.uuid1, range(20)), count()))
-    number = (Basic() & {'item': u}).fetch1('number')
+    number = (Basic() & {"item": u}).fetch1("number")
     assert_equal(number, n)
-    item = (Basic & {'number': n}).fetch1('item')
+    item = (Basic & {"number": n}).fetch1("item")
     assert_equal(u, item)
 
 
 def test_string_uuid():
     """test that only UUID objects are accepted when inserting UUID fields"""
-    u, n = '00000000-0000-0000-0000-000000000000', 24601
+    u, n = "00000000-0000-0000-0000-000000000000", 24601
     Basic().insert1(dict(item=u, number=n))
-    k, m = (Basic & {'item': u}).fetch1('KEY', 'number')
+    k, m = (Basic & {"item": u}).fetch1("KEY", "number")
     assert_equal(m, n)
-    assert_true(isinstance(k['item'], uuid.UUID))
+    assert_true(isinstance(k["item"], uuid.UUID))
 
 
 @raises(DataJointError)
@@ -35,7 +35,7 @@ def test_invalid_uuid_insert1():
 @raises(DataJointError)
 def test_invalid_uuid_insert2():
     """test that only UUID objects are accepted when inserting UUID fields"""
-    u, n = 'abc', 24601
+    u, n = "abc", 24601
     Basic().insert1(dict(item=u, number=n))
 
 
@@ -43,19 +43,27 @@ def test_invalid_uuid_insert2():
 def test_invalid_uuid_restrict1():
     """test that only UUID objects are accepted when inserting UUID fields"""
     u = 0
-    k, m = (Basic & {'item': u}).fetch1('KEY', 'number')
+    k, m = (Basic & {"item": u}).fetch1("KEY", "number")
 
 
 @raises(DataJointError)
 def test_invalid_uuid_restrict1():
     """test that only UUID objects are accepted when inserting UUID fields"""
-    u = 'abc'
-    k, m = (Basic & {'item': u}).fetch1('KEY', 'number')
+    u = "abc"
+    k, m = (Basic & {"item": u}).fetch1("KEY", "number")
 
 
 def test_uuid_dependencies():
-    """ test the use of UUID in foreign keys """
-    for word in ('Neuroscience', 'Knowledge', 'Curiosity', 'Inspiration', 'Science', 'Philosophy', 'Conscience'):
+    """test the use of UUID in foreign keys"""
+    for word in (
+        "Neuroscience",
+        "Knowledge",
+        "Curiosity",
+        "Inspiration",
+        "Science",
+        "Philosophy",
+        "Conscience",
+    ):
         Topic().add(word)
     Item.populate()
     assert_equal(Item().progress(), (0, len(Topic())))
diff --git a/tests/test_virtual_module.py b/tests/test_virtual_module.py
index 3a99af5e0..58180916f 100644
--- a/tests/test_virtual_module.py
+++ b/tests/test_virtual_module.py
@@ -6,5 +6,7 @@
 
 
 def test_virtual_module():
-    module = dj.VirtualModule('module', schema.schema.database, connection=dj.conn(**CONN_INFO))
-    assert_true(issubclass(module.Experiment, UserTable))
\ No newline at end of file
+    module = dj.VirtualModule(
+        "module", schema.schema.database, connection=dj.conn(**CONN_INFO)
+    )
+    assert_true(issubclass(module.Experiment, UserTable))