diff --git a/DataQuality/DQDefects/python/db.py b/DataQuality/DQDefects/python/db.py
index be88bac2b335f7115d8b31acf5f70f034655b895..813b7d5db6cd34fb2bf11874b0af7898dc61efa8 100644
--- a/DataQuality/DQDefects/python/db.py
+++ b/DataQuality/DQDefects/python/db.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2002-2020 CERN for the benefit of the ATLAS collaboration
+# Copyright (C) 2002-2022 CERN for the benefit of the ATLAS collaboration
 
 """
 Authors: Peter Waller <peter.waller@cern.ch> and "Peter Onyisi" <peter.onyisi@cern.ch>
@@ -19,23 +19,21 @@ from logging import getLogger; log = getLogger("DQDefects.db")
 from contextlib import contextmanager
 
 from DQUtils import fetch_iovs, IOVSet
+from DQUtils.sugar.iovtype import IOVType
 from DQUtils.channel_mapping import list_to_channelselection
+from DQUtils.sugar import RunLumi
 
 from DQDefects import DEFAULT_CONNECTION_STRING
 
 from .exceptions import DefectExistsError, DefectUnknownError
 from .folders import DefectsDBFoldersMixin
 from .ids import DefectsDBIDsNamesMixin, choose_new_defect_id
-from .tags import DefectsDBTagsMixin
+from .tags import DefectsDBTagsMixin, tagtype
 from .virtual_mixin import DefectsDBVirtualDefectsMixin
 from .virtual_calculator import calculate_virtual_defects
+from typing import Union, Tuple, Optional, Iterable, Collection
 
 import six
-if six.PY2:
-    def _encode (s, enc): return s.encode(enc)
-else:
-    def _encode (s, enc): return s
-
 
 class DefectsDB(DefectsDBVirtualDefectsMixin, 
                 DefectsDBTagsMixin,
@@ -60,8 +58,8 @@ class DefectsDB(DefectsDBVirtualDefectsMixin,
     Public interface is nominally defined in this class (DefectsDB).
     """
 
-    def __init__(self, connection_string=DEFAULT_CONNECTION_STRING,
-                 read_only=True, create=False, tag="HEAD"):
+    def __init__(self, connection_string: str = DEFAULT_CONNECTION_STRING,
+                 read_only: bool = True, create: bool = False, tag: Union[str, Tuple] = "HEAD") -> None:
         """
         Create a new DefectsDB instance.
         
@@ -85,8 +83,6 @@ class DefectsDB(DefectsDBVirtualDefectsMixin,
         self.connection_string = connection_string
         self._read_only = read_only
         self._create = create
-        import collections
-        tagtype = collections.namedtuple('tagtype', ['defects', 'logic'])
         if isinstance(tag, six.string_types):
             self._tag = tagtype(tag, tag) if tag else tagtype("HEAD", "HEAD")
         else:
@@ -97,8 +93,8 @@ class DefectsDB(DefectsDBVirtualDefectsMixin,
             if len(tag) != 2:
                 raise TypeError('tag argument must be a 2-element sequence')
             self._tag = tag
-        self._tag = tagtype(_encode(self._tag[0],'ascii'),
-                            _encode(self._tag[1],'ascii'))
+        self._tag = tagtype(self._tag[0],
+                            self._tag[1])
 
         # COOL has no way of emptying a storage buffer. Creating a new storage
         # buffer flushes the old one. Therefore, if an exception happens 
@@ -114,7 +110,7 @@ class DefectsDB(DefectsDBVirtualDefectsMixin,
             self.defects_folder
             self.defect_logic_folder
     
-    def __del__(self):
+    def __del__(self) -> None:
         """
         Ideally we would use inheritance to call destructors, but this isn't 
         possible in the general case with the way we (ab)use mixins, so we just 
@@ -122,7 +118,7 @@ class DefectsDB(DefectsDBVirtualDefectsMixin,
         """
         self._clear_connections()
     
-    def create_defect(self, name, description):
+    def create_defect(self, name: str, description: str) -> None:
         """
         Create a new type of defect; tries to figure out system ID from the
         defect name. See also: `create_defect_with_id`, `new_system_defect`.
@@ -135,9 +131,9 @@ class DefectsDB(DefectsDBVirtualDefectsMixin,
         """        
         sysid = choose_new_defect_id(self.defect_id_map, name)
         log.info("Creating new defect %s: system ID %08x", name, sysid)
-        return self._create_defect_with_id(sysid, name, description)
+        self._create_defect_with_id(sysid, name, description)
 
-    def _create_defect_with_id(self, did, name, description):
+    def _create_defect_with_id(self, did: int, name: str, description: str) -> None:
         """
         Create a new type of defect, specifying the defect ID.
         See also: `create_defect`, `new_system_defect`.
@@ -151,21 +147,21 @@ class DefectsDB(DefectsDBVirtualDefectsMixin,
         if did in self.defect_ids: raise DefectExistsError(did)
         try:
             oldname = self.normalize_defect_names(name)
-            already_exists = True
+            raise DefectExistsError(f'Defect {oldname} already exists')
         except DefectUnknownError:
-            already_exists = False
-        if already_exists:
-            raise DefectExistsError('Defect %s already exists' % oldname)
+            pass
         
         self.defects_folder.createChannel(did,
-                                          _encode(name,'ascii'),
-                                          _encode(description,'utf-8'))
+                                          name.encode('ascii'),
+                                          description.encode('utf-8'))
         self._new_defect(did, name)
     
-    def retrieve(self, since=None, until=None, channels=None, nonpresent=False,
-                 primary_only=False, ignore=None,
-                 with_primary_dependencies=False, intersect=False,
-                 with_time=False, evaluate_full=True):
+    def retrieve(self, since: Optional[Union[int, Tuple[int,int], RunLumi]] = None,
+                 until: Optional[Union[int, Tuple[int,int], RunLumi]] = None, 
+                 channels: Optional[Iterable[Union[str,int]]] = None, nonpresent: bool = False,
+                 primary_only: bool = False, ignore: Collection[str] = None,
+                 with_primary_dependencies: bool = False, intersect: bool = False,
+                 with_time: bool = False, evaluate_full: bool = True) -> IOVSet:
         """
         Retrieve defects from the database.
         
@@ -203,7 +199,7 @@ class DefectsDB(DefectsDBVirtualDefectsMixin,
                 virtual_channels = None
             else:
                 virtual_channels = self.virtual_defect_ids
-            primary_channels = sorted(self.defect_ids)
+            primary_channels = set(self.defect_ids)
             query_channels = None # (all)
         
         primary_output_names = [self.defect_id_map[pid] for pid in primary_channels]
@@ -230,6 +226,8 @@ class DefectsDB(DefectsDBVirtualDefectsMixin,
         
             for logic in ordered_logics:
                 logic.set_evaluation(evaluate_full)
+        else:
+            ordered_logics = []
         
         # Figure out if the set of channels will produce too many ranges for COOL
         if query_channels is not None:
@@ -301,8 +299,8 @@ class DefectsDB(DefectsDBVirtualDefectsMixin,
                 self.defects_folder.flushStorageBuffer()
         return thunk()
     
-    def insert(self, defect_id, since, until, comment, added_by, 
-               present=True, recoverable=False):
+    def insert(self, defect_id: Union[str, int], since: int, until: int, comment: str, added_by: str, 
+               present: bool = True, recoverable: bool = False) -> None:
         """
         Insert a new defect into the database.
         
@@ -319,7 +317,7 @@ class DefectsDB(DefectsDBVirtualDefectsMixin,
         return self._insert(defect_id, since, until, comment, added_by,
                             present, recoverable, self.defects_tag)
 
-    def _insert_iov(self, iov, tag):
+    def _insert_iov(self, iov: IOVType, tag: str) -> None:
         """
         Helper function for inserting IOV objects, since record order doesn't
         match function argument order
@@ -327,8 +325,9 @@ class DefectsDB(DefectsDBVirtualDefectsMixin,
         return self._insert(iov.channel, iov.since, iov.until, iov.comment,
                             iov.user, iov.present, iov.recoverable, tag)
 
-    def _insert(self, defect_id, since, until, comment, added_by,
-                present=True, recoverable=False, tag='HEAD'):
+    def _insert(self, defect_id: Union[str, int], since: Union[int, Tuple[int, int], RunLumi], 
+                until: Union[int, Tuple[int, int], RunLumi], comment: str, added_by: str,
+                present: bool = True, recoverable: bool = False, tag: str = 'HEAD') -> None:
         """
         Implementation of insert, allows tag specification for internal
         functions
@@ -342,10 +341,10 @@ class DefectsDB(DefectsDBVirtualDefectsMixin,
         
         p["present"] = present
         p["recoverable"] = recoverable
-        p["user"] = _encode(added_by, 'utf-8')
-        p["comment"] = _encode(comment, 'utf-8')
+        p["user"] = added_by.encode('utf-8')
+        p["comment"] = comment.encode('utf-8')
 
         defect_id = self.defect_chan_as_id(defect_id, True)
         
-        store(since, until, p, defect_id, tag,
+        store(since, until, p, defect_id, tag.encode('ascii'),
               (True if tag != 'HEAD' else False))
diff --git a/DataQuality/DQDefects/python/folders.py b/DataQuality/DQDefects/python/folders.py
index 5e8e9b56aaca94d66bf9a8ed401e01ac19462e5b..cc61f1629f71b143bfb673d0203405f2ae483512 100644
--- a/DataQuality/DQDefects/python/folders.py
+++ b/DataQuality/DQDefects/python/folders.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2002-2019 CERN for the benefit of the ATLAS collaboration
+# Copyright (C) 2002-2022 CERN for the benefit of the ATLAS collaboration
 
 from logging import getLogger; log = getLogger("DQDefects.folders")
 
@@ -8,6 +8,9 @@ from DQDefects import (DEFECTS_FOLDER, DEFECT_FOLDER_DESC, DEFECT_LOGIC_FOLDER,
                        DEFECT_LOGIC_FOLDER_DESC, PARENT_FOLDERSET)
 
 from PyCool import cool
+
+from typing import Tuple
+
 ST = cool.StorageType
 FV = cool.FolderVersioning
 
@@ -16,21 +19,21 @@ class DefectsDBFoldersMixin(object):
     This mixin contains code for managing the defect folders
     """
     
-    def __init__(self):
+    def __init__(self) -> None:
         self._defects_folder = None
         self._defect_logic_folder = None
         self._parent_folderset = None
         self._connections = []
         super(DefectsDBFoldersMixin, self).__init__()
         
-    def _clear_connections(self):
+    def _clear_connections(self) -> None:
         log.debug("DefectsDB connections cleared")
         for connection in self._connections:
             connection.closeDatabase()
         self._connections = []
     
     @property
-    def parent_folderset(self):
+    def parent_folderset(self) -> cool.IFolderSet:
         """
         Lazy-loads parent folderset (useful for heirarchical tag manipulation)
         """
@@ -40,7 +43,7 @@ class DefectsDBFoldersMixin(object):
             self._parent_folderset = db.getFolderSet(PARENT_FOLDERSET)
         return self._parent_folderset
     
-    def _load_folder(self, folder, create_function):
+    def _load_folder(self, folder, create_function) -> Tuple[cool.IFolder, cool.Record]:
         """
         Internal function used to load a COOL folder
         """
@@ -61,14 +64,14 @@ class DefectsDBFoldersMixin(object):
         return folder, payload
         
     @property
-    def defects_folder(self):
+    def defects_folder(self) -> cool.IFolder:
         """
         Returns the folder containing the defects, loading it if necessary
         """
         if self._defects_folder is None: self._load_defects_folder()
         return self._defects_folder
     
-    def _load_defects_folder(self, read_only=True, create=False):
+    def _load_defects_folder(self) -> None:
         """
         Internal function for populating the self._defects_folder variable
         """
@@ -76,7 +79,7 @@ class DefectsDBFoldersMixin(object):
         res = self._load_folder(DEFECTS_FOLDER, self._create_defects_folder)
         self._defects_folder, self._defect_payload = res
     
-    def _create_defects_folder(self, db):
+    def _create_defects_folder(self, db: cool.IDatabase) -> cool.IFolder:
         """
         Creates the COOL database/folder for defects, if they don't exist.
         Internal - use create=True in the constructor to create the COOL folder.
@@ -94,14 +97,14 @@ class DefectsDBFoldersMixin(object):
                                 DEFECT_FOLDER_DESC, True)
                                 
     @property
-    def defect_logic_folder(self):
+    def defect_logic_folder(self) -> cool.IFolder:
         """
         Returns the folder containing the virtual defect logic, loading it if necessary
         """
         if self._defect_logic_folder is None: self._load_defect_logic_folder()
         return self._defect_logic_folder
     
-    def _load_defect_logic_folder(self):
+    def _load_defect_logic_folder(self) -> None:
         """
         Internal function for populating the self._defect_logic_folder variable
         """        
@@ -109,7 +112,7 @@ class DefectsDBFoldersMixin(object):
                                 self._create_defect_logic_folder)
         self._defect_logic_folder, self._defect_logic_payload = res
         
-    def _create_defect_logic_folder(self, db):
+    def _create_defect_logic_folder(self, db: cool.IDatabase) -> cool.IFolder:
         """
         Creates the COOL database/folder for virtual defect logic, if they don't exist.
         Internal - use create=True in the constructor to create the COOL folder.
diff --git a/DataQuality/DQDefects/python/ids.py b/DataQuality/DQDefects/python/ids.py
index 6befeb8ddecc93be0a2381e0fe6ad2a63de2c53f..c24417e44c0abcbb03058228936f20e9e3326032 100644
--- a/DataQuality/DQDefects/python/ids.py
+++ b/DataQuality/DQDefects/python/ids.py
@@ -1,8 +1,8 @@
-# Copyright (C) 2002-2020 CERN for the benefit of the ATLAS collaboration
+# Copyright (C) 2002-2022 CERN for the benefit of the ATLAS collaboration
 
 from logging import getLogger; log = getLogger("DQDefects.defect_ids")
 
-from ctypes import Union, Structure, c_uint
+from ctypes import Structure, c_uint, Union as cUnion
 
 from DQUtils.channel_mapping import get_channel_ids_names
 
@@ -10,10 +10,8 @@ from .exceptions import (DefectUnknownError,
                          InvalidLogicTagError)
 
 import six
-if six.PY2:
-    def _decode (s): return s.decode('utf-8')
-else:
-    def _decode (s): return s
+from typing import Set, Iterable, Tuple, Union, List
+from collections.abc import Mapping, MutableMapping
 
 
 class DefectIDBitfield(Structure):
@@ -26,7 +24,7 @@ class DefectIDBitfield(Structure):
         return "(is_virtual=%s, defect=%i)" % args
 
 
-class DefectID(Union):
+class DefectID(cUnion):
     _fields_ = [("as_int", c_uint),
                 ("as_bitfield", DefectIDBitfield)]
     
@@ -47,7 +45,8 @@ class DefectID(Union):
         args = self.as_int, self.as_bitfield
         return '<DefectID value=0x%08x fields=%r>' % args
 
-def choose_new_defect_id(existing_map, defect_name, virtual=False):
+def choose_new_defect_id(existing_map: Mapping[Union[str, int], Union[str,int]], 
+                         defect_name: str, virtual: bool = False) -> int:
     """
     Function to create a new defect ID.
 
@@ -77,14 +76,18 @@ class DefectsDBIDsNamesMixin(object):
     Contains the logic for storing knowledge of which defects exist, and their
     channel names and IDs
     """
-    def __init__(self):
-        self._defect_id_map = self._defect_ids = self._defect_names = None
-        self._virtual_defect_id_map = None
-        self._virtual_defect_ids = None
-        self._virtual_defect_names = None
+    def __init__(self) -> None:
+        self._initialized = False
+        self._virtual_initialized = False
+        self._defect_id_map: MutableMapping[Union[str,int], Union[str,int]] = {}
+        self._defect_ids: Set[int] = set()
+        self._defect_names: Set[str] = set()
+        self._virtual_defect_id_map: MutableMapping[Union[str,int], Union[str,int]] = {}
+        self._virtual_defect_ids: Set[int] = set()
+        self._virtual_defect_names: Set[str] = set()
         super(DefectsDBIDsNamesMixin, self).__init__()
     
-    def _populate_defect_ids(self):
+    def _populate_defect_ids(self) -> None:
         """
         Called the first time any of defect_{ids,names,id_map,etc} is called,
         and populates internal variables to store the channel ids/names for the
@@ -95,13 +98,13 @@ class DefectsDBIDsNamesMixin(object):
         self._defect_names = set(names)
         self._defect_id_map = mapping
         
-    def _populate_virtual_defect_ids(self):
+    def _populate_virtual_defect_ids(self) -> None:
         """
         Called the first time any of virtual_defect_{ids,names,id_map,etc} is called,
         and populates internal variables to store the channel ids/names for the
         life of the DefectsDB instance.
         """
-        ids, names, mapping = get_channel_ids_names(self.defect_logic_folder)
+        _, _, mapping = get_channel_ids_names(self.defect_logic_folder)
         try:
             self._virtual_defect_names = set(self.virtual_defect_logics.keys())
             self._virtual_defect_ids = set(mapping[name] for name in self._virtual_defect_names)
@@ -115,7 +118,7 @@ class DefectsDBIDsNamesMixin(object):
             self._virtual_defect_ids = set()
             self._virtual_defect_map = {}
     
-    def _new_defect(self, did, dname):
+    def _new_defect(self, did: int, dname: str) -> None:
         """
         Internal function used to keep defect IDs/names uptodate.
         """
@@ -124,7 +127,7 @@ class DefectsDBIDsNamesMixin(object):
         self.defect_id_map[did] = dname
         self.defect_id_map[dname] = did
         
-    def _new_virtual_defect(self, did, dname):
+    def _new_virtual_defect(self, did: int, dname: str) -> None:
         """
         Internal function used to keep defect IDs/names uptodate.
         """
@@ -136,55 +139,68 @@ class DefectsDBIDsNamesMixin(object):
         self._virtual_defect_logics = None
     
     @property
-    def defect_ids(self):
+    def defect_ids(self) -> Set[int]:
         """
         Gives the set of defect IDs that exist in COOL
         """
-        if self._defect_ids is None: self._populate_defect_ids()
+        if not self._initialized: 
+            self._populate_defect_ids()
+            self._initialized = True
         return self._defect_ids
     
     @property
-    def defect_names(self):
+    def defect_names(self) -> Set[str]:
         """
         Gives the set of defect names that exist in COOL
         """
-        if self._defect_names is None: self._populate_defect_ids()
+        if not self._initialized: 
+            self._populate_defect_ids()
+            self._initialized = True
+        assert self._defect_names is not None, self._initialized
         return self._defect_names
     
     @property
-    def defect_id_map(self):
+    def defect_id_map(self) -> MutableMapping[Union[str,int], Union[str,int]]:
         """
         Gives the dictionary relating COOL channel ids to defect names and vice
         versa, retrieving them from the database if necessary.
         """
-        if self._defect_id_map is None: self._populate_defect_ids()
+        if not self._initialized: 
+            self._populate_defect_ids()
+            self._initialized = True
         return self._defect_id_map
         
     @property
-    def virtual_defect_ids(self):
+    def virtual_defect_ids(self) -> Set[int]:
         """
         Returns the set of existing virtual defect IDs
         """
-        if self._virtual_defect_ids is None: self._populate_virtual_defect_ids()
+        if not self._virtual_initialized: 
+            self._populate_virtual_defect_ids()
+            self._virtual_initialized = True
         return self._virtual_defect_ids
     
     @property
-    def virtual_defect_names(self):
+    def virtual_defect_names(self) -> Set[str]:
         """
         Returns the set of existing virtual defect names
         """
-        if self._virtual_defect_names is None: self._populate_virtual_defect_ids()
+        if not self._virtual_initialized: 
+            self._populate_virtual_defect_ids()
+            self._virtual_initialized = True
         return self._virtual_defect_names
     
     @property
-    def virtual_defect_id_map(self):
+    def virtual_defect_id_map(self) -> MutableMapping[Union[str,int], Union[str,int]]:
         """
         Returns a dict() mapping virtual defect names to IDs and vice-versa.
         """
-        if self._virtual_defect_id_map is None: self._populate_virtual_defect_ids()
+        if not self._virtual_initialized: 
+            self._populate_virtual_defect_ids()
+            self._virtual_initialized = True
         return self._virtual_defect_id_map
         
-    def defect_chan_as_id(self, channel, primary_only=False):
+    def defect_chan_as_id(self, channel: Union[str, int], primary_only: bool = False) -> int:
         """
         Returns the defect ID for a virtual defect.
         Accepts a `channel` as an integer/string and returns it as an integer.
@@ -209,44 +225,44 @@ class DefectsDBIDsNamesMixin(object):
         raise RuntimeError("Invalid `channel` type, got %s, expected integer"
                               " or string" % type(channel))
     
-    def defect_names_as_ids(self, channels):
+    def defect_names_as_ids(self, channels: Iterable[Union[str, int]]) -> List[int]:
         """
         Returns a list of channels as IDs
         """
         return [self.defect_chan_as_id(chan) for chan in channels]
     
-    def get_channels(self):
+    def get_channels(self) -> Tuple[Set[int], Set[str], Mapping[Union[str, int], Union[str, int]]]:
         """
         Return channel IDs, names, and dict relating the two
         """
         return self.defect_ids, self.defect_names, self.defect_id_map
     
-    def get_virtual_channels(self):
+    def get_virtual_channels(self) -> Tuple[Set[int], Set[str], Mapping[Union[str, int], Union[str, int]]]:
         """
         Return channel IDs, names, and dict relating the two
         """
         return self.virtual_defect_ids, self.virtual_defect_names, self.virtual_defect_id_map
     
-    def get_channel_descriptions(self, channels):
+    def get_channel_descriptions(self, channels: Iterable[Union[str, int]]) -> MutableMapping[Union[str, int], str]:
         """
         For the list of channel IDs "channels", return dict mapping ID to
         description
         """
         get_desc = self.defects_folder.channelDescription
-        return dict((channel, _decode (get_desc(self.defect_chan_as_id(channel))))
-                     for channel in channels)
+        return {channel: get_desc(self.defect_chan_as_id(channel))
+                     for channel in channels}
                      
-    def get_virtual_channel_descriptions(self, channels):
+    def get_virtual_channel_descriptions(self, channels: Iterable[Union[str, int]]) -> MutableMapping[Union[str, int], str]:
         """
         For the list of channel IDs "channels", return dict mapping ID to
         descriptiondefect_id
         """
         get_desc = self.defect_logic_folder.channelDescription
-        return dict((channel, _decode(get_desc(self.defect_chan_as_id(channel))))
-                    for channel in channels)
+        return {channel: get_desc(self.defect_chan_as_id(channel))
+                    for channel in channels}
 
     @property
-    def all_defect_descriptions(self):
+    def all_defect_descriptions(self) -> Mapping[Union[str, int], str]:
         """
         A dictionary of all (virtual and primary) defect descriptions
         """
@@ -255,7 +271,7 @@ class DefectsDBIDsNamesMixin(object):
         result.update(gvcd(self.virtual_defect_names))
         return result
     
-    def set_channel_description(self, channel, description):
+    def set_channel_description(self, channel: Union[str, int], description: str) -> None:
         """
         Set a defect description
         """
@@ -264,9 +280,9 @@ class DefectsDBIDsNamesMixin(object):
             folder = self.defect_logic_folder
         else:
             folder = self.defects_folder
-        folder.setChannelDescription(chan_id, description)
+        folder.setChannelDescription(chan_id, description.encode('utf-8'))
     
-    def defect_is_virtual(self, defect_id):
+    def defect_is_virtual(self, defect_id: Union[str, int]) -> bool:
         """
         Returns True if the `defect_id` represents a virtual defect, False if it 
         is not and raises if it doesn't exist
@@ -289,7 +305,7 @@ class DefectsDBIDsNamesMixin(object):
         
         raise DefectUnknownError(defect_id)
 
-    def normalize_defect_names(self, defect_id):
+    def normalize_defect_names(self, defect_id: Union[str, Iterable[str]]) -> Union[str, List[str]]:
         """
         Returns correct name(s) of defects, given name(s) that possibly differ
         from the correct ones by case.  Raises if an input name doesn't map to
diff --git a/DataQuality/DQDefects/python/tags.py b/DataQuality/DQDefects/python/tags.py
index 1cb562cf0f0da0fae57d13285e15a57f1e7e31a6..89519567414131d5e43b0ff73c91477da2363d73 100644
--- a/DataQuality/DQDefects/python/tags.py
+++ b/DataQuality/DQDefects/python/tags.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2002-2020 CERN for the benefit of the ATLAS collaboration
+# Copyright (C) 2002-2022 CERN for the benefit of the ATLAS collaboration
 
 from logging import getLogger; log = getLogger("DQDefects.tags")
 
@@ -12,34 +12,33 @@ from .exceptions import InvalidTagError, InvalidDefectTagError, InvalidLogicTagE
 
 from .virtual_mixin import NONHEAD_MODIFICATION_MSG
 
-import six
-if six.PY2:
-    def _encode (s, enc): return s.encode(enc)
-else:
-    def _encode (s, enc): return s
+from typing import List, Optional, Iterable
+
+import collections
+tagtype = collections.namedtuple('tagtype', ['defects', 'logic'])
 
 class DefectsDBTagsMixin(object):
-    def __init__(self):
+    def __init__(self) -> None:
         super(DefectsDBTagsMixin, self).__init__()
         
         self.check_tag_validity()
 
     @property
-    def logics_tags(self):
+    def logics_tags(self) -> List[str]:
         """
         Returns a list of existing logic tags
         """
-        return [t for t in self.defect_logic_folder.listTags()]
+        return [str(t) for t in self.defect_logic_folder.listTags()]
 
     @property
-    def defects_tags(self):
+    def defects_tags(self) -> List[str]:
         """
         Returns a list of existing defect tags
         """
-        return [t for t in self.defects_folder.listTags()]
+        return [str(t) for t in self.defects_folder.listTags()]
     
     @property
-    def next_logics_tag(self):
+    def next_logics_tag(self) -> str:
         """
         Gives the next available DEFECTLOGICS tag
         """
@@ -50,17 +49,16 @@ class DefectsDBTagsMixin(object):
             new_id = 0
         return DEFECT_LOGIC_TAG_FORMAT % new_id
     
-    def _tag_head_and_lock(self, folder, name, description):
+    def _tag_head_and_lock(self, folder: cool.IFolder, name: str, description: str) -> None:
         """
         Give the current HEAD of `folder` a new tag and lock it.
         """
         LOCKED = cool.HvsTagLock.LOCKED
-        name = _encode(name,'ascii')
-        description = _encode(description,'utf-8')
-        folder.cloneTagAsUserTag('HEAD', name, description)
-        folder.setTagLockStatus(name, LOCKED)
+        aname = name.encode('ascii')
+        folder.cloneTagAsUserTag('HEAD', aname, description.encode('utf-8'))
+        folder.setTagLockStatus(aname, LOCKED)
     
-    def new_hierarchical_tag(self, defects_tag, logics_tag):
+    def new_hierarchical_tag(self, defects_tag: str, logics_tag: str) -> str:
         """
         Create a new heirarchical tag relating the defects and logics
         
@@ -69,28 +67,30 @@ class DefectsDBTagsMixin(object):
         and has description
         "(v%i) blah"
         """
-        defects_tag = _encode(defects_tag,'ascii')
-        logics_tag = _encode(logics_tag,'ascii')
         logic_revision = int(logics_tag.split("-")[-1])
         defect_part = "-".join(defects_tag.split("-")[1:])
         hierarchical_tag = "DetStatus-v%02i-%s" % (logic_revision, defect_part)
+        logicspart = "(%i) " % logic_revision
         
-        defect_descr = self.defects_folder.tagDescription(defects_tag)
+        adefects_tag = defects_tag.encode('ascii')
+        alogics_tag = logics_tag.encode('ascii')
+        ahierarchical_tag = hierarchical_tag.encode('ascii')
+        logicspart = logicspart.encode('ascii')
+        defect_descr = self.defects_folder.tagDescription(adefects_tag).encode('utf-8')
         
-        logicspart = "(%i) " % logic_revision
         # Protection here against making descriptions too long
         description = logicspart + defect_descr[:255 - len(logicspart)]
         
         parent_folder = self.parent_folderset
-        self.defects_folder.createTagRelation(hierarchical_tag, defects_tag)
-        self.defect_logic_folder.createTagRelation(hierarchical_tag, logics_tag)
-        parent_folder.setTagDescription(hierarchical_tag, description)
+        self.defects_folder.createTagRelation(ahierarchical_tag, adefects_tag)
+        self.defect_logic_folder.createTagRelation(ahierarchical_tag, alogics_tag)
+        parent_folder.setTagDescription(ahierarchical_tag, description)
         
         log.info("New hierarchical tag %s", hierarchical_tag)
         return hierarchical_tag
     
     @property
-    def defects_tag_valid(self):
+    def defects_tag_valid(self) -> bool:
         try:
             self.defects_tag
         except InvalidTagError:
@@ -99,7 +99,7 @@ class DefectsDBTagsMixin(object):
             return True
             
     @property
-    def logics_tag_valid(self):
+    def logics_tag_valid(self) -> bool:
         try:
             self.logics_tag
         except InvalidTagError:
@@ -107,7 +107,7 @@ class DefectsDBTagsMixin(object):
         else:
             return True
     
-    def check_tag_validity(self):
+    def check_tag_validity(self) -> None:
         """
         Ensure that the tags that this DefectsDB instance was constructed with
         are functional
@@ -116,7 +116,7 @@ class DefectsDBTagsMixin(object):
             raise InvalidTagError("Tag doesn't resolve: {0}".format(self._tag))
     
     @property
-    def defects_tag(self):
+    def defects_tag(self) -> str:
         """
         Return the tag used for retrieving defects on this DefectsDB instance
         """
@@ -130,7 +130,7 @@ class DefectsDBTagsMixin(object):
             return self._tag.defects
         
     @property
-    def logics_tag(self):
+    def logics_tag(self) -> str:
         """
         Return the tag used for retrieving virtual defect clauses
         """
@@ -144,27 +144,26 @@ class DefectsDBTagsMixin(object):
             return self._tag.logic
     
     @property
-    def tag(self):
+    def tag(self) -> tagtype:
         """
         Readonly property stating the tag that the database was instantiated on
         """
         return self._tag
     
     @property
-    def tags(self):
+    def tags(self) -> List[str]:
         """
         The list of tags which are on the database
         """
-        return [t.decode() for t in self.parent_folderset.listTags()]
+        return [str(t) for t in self.parent_folderset.listTags()]
     
-    def new_logics_tag(self, description=""):
+    def new_logics_tag(self, description: str = "") -> str:
         """
         Create a new tag for the logic folder and lock it.
         
         Parameters:
             `description` : What changed in this tag? (optional, default "")
         """
-        description = _encode(description,'utf-8')
         assert self.logics_tag == "HEAD", NONHEAD_MODIFICATION_MSG
         
         new_tag_name = self.next_logics_tag
@@ -173,7 +172,7 @@ class DefectsDBTagsMixin(object):
         log.info("Tagged DEFECTLOGICS HEAD with %s", new_tag_name)
         return new_tag_name
     
-    def new_defects_tag(self, name, description, iovranges=None):
+    def new_defects_tag(self, name: str, description: str, iovranges: Optional[Iterable] = None) -> str:
         """
         Clones the current DEFECTS tag (specified in the constructor) to a new one
         If iovranges != None, does the slower thing of copying IOVs one by one
@@ -182,17 +181,17 @@ class DefectsDBTagsMixin(object):
             `name` : Name of the new tag
             `description` : Description of the contents of this tag
         """
-        name = _encode(name,'ascii')
-        description = _encode(description,'utf-8')
         if name.startswith("DetStatus"):
             raise RuntimeError("Only specify the last part of the defect tag")
+        adescription = description.encode('utf-8')
         
         log.info("Creating new tag %s", name)
         
-        name = "DetStatusDEFECTS-%s" % name
+        name = f"DetStatusDEFECTS-{name}"
+        aname = name.encode('ascii')
         
         if iovranges is None:
-            self.defects_folder.cloneTagAsUserTag(self.defects_tag, name, description)
+            self.defects_folder.cloneTagAsUserTag(self.defects_tag.encode('ascii'), aname, adescription)
             return name
         
         # Fetch all primary defects relevant to `iovranges`
@@ -208,6 +207,6 @@ class DefectsDBTagsMixin(object):
         
         # If there are no IOVs to copy, there is no new tag to describe
         if to_copy:
-            self.defects_folder.setTagDescription(name, description)
+            self.defects_folder.setTagDescription(aname, adescription)
             
         return name
diff --git a/DataQuality/DQDefects/python/tests/__init__.py b/DataQuality/DQDefects/python/tests/__init__.py
index 533078dfa0d44abe3212f427eb4695f1015bcfa9..0e1d9513fea7b84b8b01698837076699c9b55b4b 100644
--- a/DataQuality/DQDefects/python/tests/__init__.py
+++ b/DataQuality/DQDefects/python/tests/__init__.py
@@ -1,10 +1,11 @@
-# Copyright (C) 2002-2020 CERN for the benefit of the ATLAS collaboration
+# Copyright (C) 2002-2022 CERN for the benefit of the ATLAS collaboration
 # -*- coding: utf-8 -*-
 # flake8: noqa
 #  unicode characters embedded below confuse flake8
 from os import unlink, environ
 from os.path import exists
 from nose import with_setup
+from nose.tools import raises
 
 from logging import getLogger; log = getLogger("DQDefects.tests")
 
@@ -188,6 +189,22 @@ def test_defect_insertion_retrieval_unicode():
     assert iov.comment == TEST_COMMENT
     assert ddb.all_defect_descriptions[TEST_DEFECT_NAME] == TEST_DEFECT_DESCRIPTION
     
+@raises(UnicodeEncodeError)
+@with_setup(create_database, teardown_database)
+def test_defect_failure_nonascii_name():
+    """
+    Check that we raise an error if the defect name is not ASCII
+    """
+    if six.PY3:
+        import ROOT
+        if ROOT.gROOT.GetVersionInt() < 62000:
+            # Passing str objects using multibyte encodings is broken
+            # with pyroot up to 6.18.  Should be fixed in 6.20?
+            return
+    ddb = DefectsDB(TEST_DATABASE, read_only=False)
+    
+    ddb.create_defect(u"DQD_TÉST_DÉFÉCT_0", "Test")
+
 @with_setup(create_database, teardown_database)
 def test_defect_empty_retrieval():
     ddb = DefectsDB(TEST_DATABASE, read_only=False)
@@ -227,6 +244,28 @@ def test_virtual_defect_creation():
     assert len(ddb.virtual_defect_ids) == 1
     assert "DQD_TEST_VIRTUAL_DEFECT" in ddb.virtual_defect_names
 
+@with_setup(create_database, teardown_database)
+def test_defect_list_update_on_creation():
+    """
+    Check that the defect name list is updated when a defect is created
+    """
+    ddb = DefectsDB(TEST_DATABASE, read_only=False)
+    name = create_defect_type(ddb, 0)
+        
+    assert ddb.defect_names == set([name])
+
+@with_setup(create_database, teardown_database)
+def test_virtual_defect_list_update_on_creation():
+    """
+    Check that the virtual defect name list is updated when a defect is created
+    """
+    ddb = DefectsDB(TEST_DATABASE, read_only=False)
+    name = create_defect_type(ddb, 0)
+    ddb.new_virtual_defect("DQD_TEST_VIRTUAL_DEFECT", 
+                           "Comment", name)
+       
+    assert ddb.virtual_defect_names == set(["DQD_TEST_VIRTUAL_DEFECT"])
+
 @with_setup(create_database, teardown_database)
 def test_virtual_defect_creation_unicode():
     
@@ -249,6 +288,24 @@ def test_virtual_defect_creation_unicode():
     assert len(ddb.virtual_defect_ids) == 1
     assert "DQD_TEST_VIRTUAL_DEFECT" in ddb.virtual_defect_names
 
+@raises(UnicodeEncodeError)
+@with_setup(create_database, teardown_database)
+def test_virtual_defect_failure_nonascii_name():
+    """
+    Check that we raise an error if the virtual defect name is not ASCII
+    """
+    if six.PY3:
+        import ROOT
+        if ROOT.gROOT.GetVersionInt() < 62000:
+            # Passing str objects using multibyte encodings is broken
+            # with pyroot up to 6.18.  Should be fixed in 6.20?
+            return
+    ddb = DefectsDB(TEST_DATABASE, read_only=False)
+    
+    ddb.create_defect(u"DQD_TEST_DEFECT_0", "Test")
+    ddb.new_virtual_defect(u"DQD_TÉST_VIRTUAL_DÉFÉCT", 
+                           u"Comment", u"DQD_TEST_DEFECT_0")
+
 @with_setup(create_database, teardown_database)
 def test_nonpresent():
     ddb = DefectsDB(TEST_DATABASE, read_only=False)
@@ -630,6 +687,8 @@ def test_independent_tags():
     
     ltag2 = ddb.new_logics_tag()
     
+    assert ltag1 != ltag2, f'{ltag1} and {ltag2} should be different'
+
     iovs = DefectsDB(TEST_DATABASE, tag=("HEAD", ltag1)).retrieve(channels=["DQD_TEST_VIRTUAL_DEFECT"])
     assert iov_ranges(iovs) == [(100, 200)], str(iov_ranges(iovs))
     
@@ -687,7 +746,7 @@ def test_virtual_defect_consistency():
     """
     Checking that virtual flags don't depend on non-existent flags
     """
-    DefectsDB('oracle://ATLAS_COOLPROD;schema=ATLAS_COOLOFL_GLOBAL;dbname=CONDBR2')._virtual_defect_consistency_check()
+    DefectsDB()._virtual_defect_consistency_check()
     
 @with_setup(create_database, teardown_database)
 def test_iov_tag_defects():
@@ -706,7 +765,7 @@ def test_iov_tag_defects():
     ddb.new_defects_tag("dqd-test", "New iov tag",
                         iovranges=[(0, 51),
                                    ((0,210), (0,306))])
-    ddb2 = DefectsDB(TEST_DATABASE, tag='DetStatusDEFECTS-dqd-test')
+    ddb2 = DefectsDB(TEST_DATABASE, tag=('DetStatusDEFECTS-dqd-test','HEAD'))
     iovs = ddb2.retrieve(nonpresent=True)
     assert len(iovs) == 2
     assert ((iovs[0].channel, iovs[0].since, iovs[0].until,
@@ -788,7 +847,45 @@ def test_reject_duplicate_names():
     except DefectExistsError:
         wasthrown = True
     assert wasthrown, 'Creation of duplicate defects should throw, even with different case'
-    
+
+    wasthrown = False
+    try:
+        ddb.create_defect('A_test_defect_2', 'A test defect 2')
+        ddb.rename_defect('A_test_defect_2', 'A_TEST_DEFECT')
+    except DefectExistsError:
+        wasthrown = True
+    assert wasthrown, 'Rename of defect to existing defect name should throw'
+
+    ddb.new_virtual_defect('TEST_VIRTUAL_DEFECT', 'Comment', 'A_TEST_DEFECT')
+    wasthrown = False
+    try:
+        ddb.new_virtual_defect('Test_Virtual_Defect', 'Comment', 'A_TEST_DEFECT')
+    except DefectExistsError:
+        wasthrown = True
+    assert wasthrown, 'Creation of duplicate virtual defects should throw'
+
+    ddb.new_virtual_defect('Test_Virtual_Defect_2', 'Comment', 'A_TEST_DEFECT')
+    wasthrown = False
+    try:
+        ddb.rename_defect('Test_Virtual_Defect_2', 'TEST_VIRTUAL_DEFECT')
+    except DefectExistsError:
+        wasthrown = True
+    assert wasthrown, 'Rename of virtual defect to existing virtual defect name should throw'
+
+    wasthrown = False
+    try:
+        ddb.rename_defect('Test_Virtual_Defect_2', 'A_TEST_DEFECT')
+    except DefectExistsError:
+        wasthrown = True
+    assert wasthrown, 'Rename of virtual defect to existing defect name should throw'
+
+    wasthrown = False
+    try:
+        ddb.rename_defect('A_TEST_DEFECT', 'TEST_VIRTUAL_DEFECT')
+    except DefectExistsError:
+        wasthrown = True
+    assert wasthrown, 'Rename of defect to existing virtual defect name should throw'
+   
 @with_setup(create_database, teardown_database)
 def test_get_intolerable_defects():
     '''
@@ -845,3 +942,57 @@ def test_noncontiguous_defectid_creation():
     assert m == {0: 'DQD_TEST_DEFECT_0', 1: 'DQD_TEST_DEFECT_1', 2: 'TEST_DEFECT_DQD_2', 3: 'DQD_TEST_DEFECT_3', 'DQD_TEST_DEFECT_3': 3, 'DQD_TEST_DEFECT_0': 0, 'DQD_TEST_DEFECT_1': 1, 'TEST_DEFECT_DQD_2': 2}, 'Primary defect problem'
     ids, names, m = ddb.get_virtual_channels()
     assert m == {2147483648: 'DQD_TEST_VIRTUAL_DEFECT', 2147483649: 'DQD_TEST_VIRTUAL_DEFECT_2', 'DQD_TEST_VIRTUAL_DEFECT': 2147483648, 'DQD_TEST_VIRTUAL_DEFECT_2': 2147483649}, 'Virtual defect problem'
+
+@with_setup(create_database, teardown_database)
+def test_return_types():
+    """
+    Test that return types are appropriate
+    """
+    ddb = DefectsDB(TEST_DATABASE, read_only=False)
+    
+    # Create a defect
+    create_defect_type(ddb, 0)
+    did = ddb.new_virtual_defect("DQD_TEST_VIRTUAL_DEFECT",
+                                 "Comment", "DQD_TEST_DEFECT_0")
+    assert type(did) == int
+    
+    ddb.insert("DQD_TEST_DEFECT_0",   0, 100, "", "")
+    
+    dtag = ddb.new_defects_tag("dqd-test", "New iov tag")
+    assert type(dtag) == str, f'{type(dtag)} instead of str'
+    ltag = ddb.new_logics_tag()
+    assert type(ltag) == str
+    htag = ddb.new_hierarchical_tag(dtag, ltag)
+    assert type(htag) == str
+
+    assert type(ddb.defects_tags) == list, f'{type(ddb.defects_tags)} instead of list'
+    assert type(ddb.defects_tags[0]) == str, f'{type(ddb.defects_tags[0])} instead of str'
+    assert type(ddb.logics_tags) == list, f'{type(ddb.logics_tags)} instead of list'
+    assert type(ddb.logics_tags[0]) == str, f'{type(ddb.logics_tags[0])} instead of str'
+    assert type(ddb.tags) == list, f'{type(ddb.tags)} instead of list'
+    assert type(ddb.tags[0]) == str, f'{type(ddb.tags[0])} instead of str'
+
+    ids, names, _ = ddb.get_channels()
+    ids = set(ids); names = set(names)  # Duplicate so we don't stomp on the underlying data
+    assert type(ids.pop()) == int
+    assert type(names.pop()) == str
+    ids, names, _ = ddb.get_virtual_channels()
+    ids = set(ids); names = set(names)
+    assert type(ids.pop()) == int
+    assert type(names.pop()) == str
+    
+    iov = ddb.retrieve()
+    assert type(iov[0].channel) == str
+    assert type(iov[0].comment) == str
+    assert type(iov[0].user) == str
+    assert type(iov[0].present) == bool
+    assert type(iov[0].recoverable) == bool
+
+    assert type(ddb.all_defect_descriptions["DQD_TEST_DEFECT_0"]) == str, str(ddb.all_defect_descriptions)
+    assert type(ddb.all_defect_descriptions["DQD_TEST_VIRTUAL_DEFECT"]) == str, str(ddb.all_defect_descriptions)
+
+    ddb2 = DefectsDB(TEST_DATABASE, read_only=False, tag=htag)
+    assert type(ddb2.defects_tag) == str
+    assert type(ddb2.logics_tag) == str
+    assert type(ddb2.tag.defects) == str
+    assert type(ddb2.tag.logic) == str
\ No newline at end of file
diff --git a/DataQuality/DQDefects/python/virtual_calculator.py b/DataQuality/DQDefects/python/virtual_calculator.py
index 8e451f87f93a1161e2e2024e237d31e14bfc7be3..25cb345429682b3e2d89efbce7ff2642ceb86f18 100644
--- a/DataQuality/DQDefects/python/virtual_calculator.py
+++ b/DataQuality/DQDefects/python/virtual_calculator.py
@@ -1,6 +1,8 @@
 # Copyright (C) 2002-2020 CERN for the benefit of the ATLAS collaboration
 
-from logging import getLogger; log = getLogger("DQDefects.virtual_defect_calculator")
+from logging import getLogger
+
+from .virtual_logic import DefectLogic; log = getLogger("DQDefects.virtual_defect_calculator")
 
 from collections import defaultdict
 
@@ -11,8 +13,15 @@ from DQDefects import DEFECT_IOV
 
 import six
 
+from collections.abc import Container
+from typing import Tuple, Union, Optional, Iterable, Generator, Mapping
+
 
-def generate_virtual_defects(by_channel, logics, since_cr, until_cr, ignore):
+def generate_virtual_defects(by_channel: Mapping[str, IOVSet], logics: Iterable[DefectLogic], 
+                             since_cr: Union[int, Tuple[int, int], RunLumi], 
+                             until_cr: Union[int, Tuple[int, int], RunLumi], 
+                             ignore: Optional[Container[str]]
+                            ) -> Generator[Tuple[RunLumi, RunLumi, Mapping[str, DEFECT_IOV]], None, None]:
     """
     An iterator which emits (since, until, {channel name : iov})
     
@@ -27,7 +36,7 @@ def generate_virtual_defects(by_channel, logics, since_cr, until_cr, ignore):
     for since, until, current_iovs, changes in process_iovs_changed(*iovsets):
         # Update things that changed since last iteration in the states dict
         for change in changes:
-            if ignore and channels[change] in ignore:
+            if ignore is not None and channels[change] in ignore:
                 continue
             states[channels[change]] = current_iovs[change]
         
@@ -43,7 +52,7 @@ def generate_virtual_defects(by_channel, logics, since_cr, until_cr, ignore):
         
         yield since, until, states
         
-def bad_iov(since, until):
+def bad_iov(since: RunLumi, until: RunLumi) -> bool:
     """
     Skip some commonly emitted nonsensical IoVs.
     """
@@ -54,9 +63,11 @@ def bad_iov(since, until):
         (since.lumi == 0 and until.lumi == 1 and since.run == until.run)
     )
 
-def calculate_virtual_defects(primary_iovs, evaluation_order,
-                              virtual_output_channels, primary_output_channels, 
-                              since, until, ignore):
+def calculate_virtual_defects(primary_iovs: IOVSet, evaluation_order: Iterable[DefectLogic],
+                              virtual_output_channels: Iterable[str], primary_output_channels: Iterable[str], 
+                              since: Optional[Union[int, Tuple[int, int], RunLumi]], 
+                              until: Optional[Union[int, Tuple[int, int], RunLumi]], 
+                              ignore: Optional[Container[str]]) -> IOVSet:
     """
     Returns a list of IoVs for a given query in the normal COOL order
     (sorted by channelID, then by since)
@@ -73,7 +84,7 @@ def calculate_virtual_defects(primary_iovs, evaluation_order,
     # Copy desired primary channels to the result
     for primary_channel, primary_iovs in six.iteritems(primary_by_channel):
         if primary_channel in primary_output_channels:
-            if ignore and primary_channel in ignore:
+            if ignore is not None and primary_channel in ignore:
                 continue
             result[primary_channel] = primary_iovs
     
@@ -96,7 +107,7 @@ def calculate_virtual_defects(primary_iovs, evaluation_order,
     # Sort them by traditional COOL sort ordering (by channelId first, 
     # then by since. `iovs` are already ordered by since.)
     result_list = IOVSet()
-    for channel, iovs in sorted(six.iteritems(result)):
+    for _, iovs in sorted(six.iteritems(result)):
         result_list.extend(iovs.solidify(DEFECT_IOV))
         
     return result_list
diff --git a/DataQuality/DQDefects/python/virtual_logic.py b/DataQuality/DQDefects/python/virtual_logic.py
index 01770d6930158f1f86c06839c8b89e2a7bea060a..dca4d13431987ff0c4979740c5831382af65961a 100644
--- a/DataQuality/DQDefects/python/virtual_logic.py
+++ b/DataQuality/DQDefects/python/virtual_logic.py
@@ -1,29 +1,32 @@
-# Copyright (C) 2002-2020 CERN for the benefit of the ATLAS collaboration
+# Copyright (C) 2002-2022 CERN for the benefit of the ATLAS collaboration
+from __future__ import annotations
 
 from DQDefects import DEFECT_IOV
 from functools import reduce
 
+from typing import Mapping
+
 class DefectLogic(object):
-    def __init__(self, defect_iov):
+    def __init__(self, defect_iov) -> None:
         self.name = defect_iov.channel
         self.clauses = defect_iov.clauses.split()
         self.operations = set()
         self.inverting = set()
         self._operations_parse()
         self.realclauses = set(_[1] for _ in self.operations)
-        self.primary_defects = None
+        self.primary_defects = set()
         self.evaluate = self.bad_evaluator
         
-    def bad_evaluator(self):
+    def bad_evaluator(self, states) -> DEFECT_IOV:
         assert False, "bad_evaluation should never be called"
     
-    def set_evaluation(self, full):
+    def set_evaluation(self, full: bool) -> None:
         if full:
             self.evaluate = self.evaluate_full
         else:
             self.evaluate = self.evaluate_partial
 
-    def _operations_parse(self):
+    def _operations_parse(self) -> None:
         from operator import truth, not_
         for name in self.clauses:
             if name[0] == '!':
@@ -32,7 +35,7 @@ class DefectLogic(object):
             else:
                 self.operations.add((truth, name))
     
-    def _populate(self, defect_logics):
+    def _populate(self, defect_logics: Mapping[str, DefectLogic]) -> None:
         """
         Populate the dependencies with logic objects, and the primary defects.
         """
@@ -43,7 +46,7 @@ class DefectLogic(object):
         self.primary_defects = set(c for _, c in self.operations
                                    if c not in defect_logics)
                                    
-    def evaluate_full(self, states):
+    def evaluate_full(self, states: Mapping[str, DEFECT_IOV]) -> DEFECT_IOV:
         """
         Evaluate this defect logic for a point in time.
         
@@ -65,7 +68,7 @@ class DefectLogic(object):
                              +['!'+_ for _ in uninverted]))
         )
         
-    def evaluate_partial(self, states):
+    def evaluate_partial(self, states: Mapping[str, DEFECT_IOV]) -> DEFECT_IOV:
         """
         Evaluate this defect logic for a point in time.
         
diff --git a/DataQuality/DQDefects/python/virtual_mixin.py b/DataQuality/DQDefects/python/virtual_mixin.py
index 4270c90afa82029412c9263a328d4d0cf93c8874..cb63c78f67b2411170f459f7dbab522f2331294d 100644
--- a/DataQuality/DQDefects/python/virtual_mixin.py
+++ b/DataQuality/DQDefects/python/virtual_mixin.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2002-2020 CERN for the benefit of the ATLAS collaboration
+# Copyright (C) 2002-2022 CERN for the benefit of the ATLAS collaboration
 
 from logging import getLogger; log = getLogger("DQDefects.virtual_defect_mixin")
 from DQUtils import fetch_iovs
@@ -7,10 +7,9 @@ from .exceptions import DefectUnknownError, DefectExistsError
 from .ids import choose_new_defect_id
 from .virtual_logic import DefectLogic
 import six
-if six.PY2:
-    def _encode (s, enc): return s.encode(enc)
-else:
-    def _encode (s, enc): return s
+
+from typing import Optional, Iterable, Mapping, Set, Sequence, List
+
 
 NONHEAD_MODIFICATION_MSG = ("Operations which modify virtual defects can only "
     "be done on the HEAD tag.")
@@ -43,84 +42,85 @@ class DefectsDBVirtualDefectsMixin(object):
     A DefectsDB mixin for managing virtual defects 
     """
     
-    def __init__(self):
+    def __init__(self) -> None:
         self._virtual_defect_logics = None
         super(DefectsDBVirtualDefectsMixin, self).__init__()
 
-    def validate_clauses(self, clauses):
+    def validate_clauses(self, clauses: str) -> None:
         all_defects = self.defect_names | self.virtual_defect_names
         for clause in clauses.split():
             if clause[0] in ('!', '-'):
                 clause = clause[1:]
             assert clause in all_defects, (clause + " is not a known defect")
     
-    def update_virtual_defect(self, defect_name, clauses, comment=None):
+    def update_virtual_defect(self, defect_name: str, clauses: str, comment: Optional[str] = None) -> None:
         # HEAD protection is here, to allow other functions to
         # change other tags in a known manner with _update_virtual_defect
         assert self.logics_tag == "HEAD", NONHEAD_MODIFICATION_MSG
         assert not self._read_only, "Insertion on read-only database"
-        clauses = _encode(clauses,'ascii')
-        comment = _encode(comment,'utf-8') if comment is not None else comment
         self._update_virtual_defect(defect_name, clauses, comment)
     
-    def _update_virtual_defect(self, defect_name, clauses, comment=None, tag=None):
-        if not tag: tag = 'HEAD'
+    def _update_virtual_defect(self, defect_name: str, clauses: str, 
+                               comment: Optional[str] = None, tag: Optional[str] = None) -> None:
         defect_id = self.defect_chan_as_id(defect_name)
         
         assert self.defect_is_virtual(defect_id), ("Tried to update nonvirtual"
             " defect with update_virtual_defect()")
 
         self.validate_clauses(clauses)
-        
-        self._defect_logic_payload["clauses"] = clauses
+
+        tag = tag if tag is not None else 'HEAD'
+        ucomment = comment.encode('utf-8') if comment is not None else None
+
+        self._defect_logic_payload["clauses"] = clauses.encode('ascii')
         
         store = self.defect_logic_folder.storeObject
-        store(0, 2**63-1, self._defect_logic_payload, defect_id, tag,
+        store(0, 2**63-1, self._defect_logic_payload, defect_id, tag.encode('ascii'),
               (True if tag != 'HEAD' else False))
         
-        if comment:
-            self.defect_logic_folder.setChannelDescription(defect_id, comment)
+        if comment is not None:
+            self.defect_logic_folder.setChannelDescription(defect_id, ucomment)
 
-    def rename_defect(self, defect_name, new_defect_name):
+    def rename_defect(self, defect_name: str, new_defect_name: str) -> None:
         """
         Rename a defect (primary or virtual).  Will keep data and channel ID.
         Will fix up all virtual defect dependencies in all tags.
         """
         assert not self._read_only, "Channel rename on read-only database"
-        defect_name = _encode(defect_name,'ascii')
-        new_defect_name = _encode(new_defect_name,'ascii')
         
         try:
             oldname = self.normalize_defect_names(new_defect_name)
-            already_exists = True
-        except DefectUnknownError:
-            already_exists = False
-        if already_exists:
             raise DefectExistsError('Defect %s already exists' % oldname)
+        except DefectUnknownError:
+            pass
         
         defect_id = self.defect_chan_as_id(defect_name)
         
+        anew_defect_name = new_defect_name.encode('ascii')
         if self.defect_is_virtual(defect_id):
-            self.defect_logic_folder.setChannelName(defect_id, new_defect_name)
-            self._virtual_defect_names = None
-            self._virtual_defect_id_map = None
+            self.defect_logic_folder.setChannelName(defect_id, anew_defect_name)
+            self._virtual_defect_names = set()
+            self._virtual_defect_id_map = {}
             self._virtual_defect_logics = None
+            self._virtual_initialized = False
         else:
-            self.defects_folder.setChannelName(defect_id, new_defect_name)
-            self._defect_names = None
-            self._defect_id_map = None
+            self.defects_folder.setChannelName(defect_id, anew_defect_name)
+            self._defect_names = set()
+            self._defect_id_map = {}
+            self._initialized = False
         
         import contextlib
         @contextlib.contextmanager
         def logics_unlocking(tag):
+            atag = tag.encode('ascii')
             log.info('Unlocking tag %s', tag)
             folder = self.defect_logic_folder
-            orig_status = folder.tagLockStatus(tag)
+            orig_status = folder.tagLockStatus(atag)
             if orig_status != 0:
-                folder.setTagLockStatus(tag, 0)
+                folder.setTagLockStatus(atag, 0)
             yield
             if orig_status != 0:
-                folder.setTagLockStatus(tag, orig_status)
+                folder.setTagLockStatus(atag, orig_status)
             log.info('Done with tag %s', tag)
 
         def logic_substitute(clause, oldname, newname):
@@ -146,16 +146,13 @@ class DefectsDBVirtualDefectsMixin(object):
                         self._update_virtual_defect(vd, newclauses, tag=tag)
         self._virtual_defect_logics = None
     
-    def new_virtual_defect(self, defect_name, comment, clauses):
+    def new_virtual_defect(self, defect_name: str, comment: Optional[str], clauses: str) -> int:
         """
         Create a new virtual defect
         """
         assert self.logics_tag == "HEAD", NONHEAD_MODIFICATION_MSG
         assert not self._read_only, "Insertion on read-only database"
         from DQUtils.channel_mapping import get_channel_ids_names
-        clauses = _encode(clauses,'ascii')
-        defect_name = _encode(defect_name,'ascii')
-        comment = _encode(comment,'utf-8') if comment is not None else comment
         
         # Force load of defects_folder to populate _defect_payload
         store = self.defect_logic_folder.storeObject
@@ -163,30 +160,28 @@ class DefectsDBVirtualDefectsMixin(object):
         
         self.validate_clauses(clauses)
 
-        p["clauses"] = clauses
+        p["clauses"] = clauses.encode('ascii')
 
         # need to get true mapping of folder, not just what is visible
         # from this tag
-        ids, names, mapping = get_channel_ids_names(self.defect_logic_folder)
+        ids, _, mapping = get_channel_ids_names(self.defect_logic_folder)
         defect_id = choose_new_defect_id(mapping, defect_name, True)
         log.debug("Creating new defect %s: 0x%08x", defect_name, defect_id)
         
         try:
             oldname = self.normalize_defect_names(defect_name)
-            already_exists = True
-        except DefectUnknownError:
-            already_exists = False
-        if already_exists:
             raise DefectExistsError('Defect %s already exists' % oldname)
+        except DefectUnknownError:
+            pass
 
         if (defect_id in ids):
             raise DefectExistsError(defect_name)
-        self.defect_logic_folder.createChannel(defect_id, defect_name, comment)
+        self.defect_logic_folder.createChannel(defect_id, defect_name.encode('ascii'), comment.encode('utf-8'))
         store(0, 2**63-1, p, defect_id)
         self._new_virtual_defect(defect_id, defect_name)
         return defect_id
     
-    def _resolve_evaluation_order(self, defects=None):
+    def _resolve_evaluation_order(self, defects: Optional[Iterable[str]] = None) -> List[DefectLogic]:
         """
         Returns a list of DefectLogic objects that need to be evaluated, 
         in the correct order for them to be consistent.
@@ -211,7 +206,7 @@ class DefectsDBVirtualDefectsMixin(object):
         resolved.remove(MasterNode)
         return resolved
     
-    def resolve_primary_defects(self, defect_logics):
+    def resolve_primary_defects(self, defect_logics: Iterable[DefectLogic]) -> Set[str]:
         """
         Determine which primary flags are used for 
         a given list of input `virtual_defect_names`.
@@ -223,7 +218,7 @@ class DefectsDBVirtualDefectsMixin(object):
         return primary_defects
     
     @property
-    def virtual_defect_logics(self):
+    def virtual_defect_logics(self) -> Mapping[str, DefectLogic]:
         """
         Returns all virtual defects in the form {"defect_name" : DefectLogic()}
         for the tag DefectDB was constructed on.
@@ -235,23 +230,23 @@ class DefectsDBVirtualDefectsMixin(object):
         
         return self._virtual_defect_logics
 
-    def _get_virtual_defect_logics(self, tag):
-        if tag != "HEAD" and not self.defect_logic_folder.existsUserTag(tag):
+    def _get_virtual_defect_logics(self, tag: str) -> Mapping[str, DefectLogic]:
+        if tag != "HEAD" and not self.defect_logic_folder.existsUserTag(tag.encode('ascii')):
             # The tag doesn't exist, so there is nothing to fetch.
             return {}
         
-        logics = fetch_iovs(self.defect_logic_folder, tag=tag, 
+        logics = fetch_iovs(self.defect_logic_folder, tag=tag.encode('ascii'), 
                             named_channels=True)
-                            
-        logics = dict((l.channel, DefectLogic(l)) for l in logics)
+
+        logics = {l.channel: DefectLogic(l) for l in logics}
         
-        for defect_name, defect_logic in six.iteritems (logics):
+        for _, defect_logic in six.iteritems (logics):
             defect_logic._populate(logics)
             
         return logics
 
-    def get_intolerable_defects(self, old_primary_only=True,
-                                exclude=['TIGHT', 'IDTIGHT', 'PHYS_.*']):
+    def get_intolerable_defects(self, old_primary_only: bool = True,
+                                exclude: Sequence[str] = ['TIGHT', 'IDTIGHT', 'PHYS_.*']):
         """
         Returns primary defects that are depended on by a virtual defect
         if old_primary_only == True, only return those depended on by a
@@ -268,7 +263,7 @@ class DefectsDBVirtualDefectsMixin(object):
             vdl = dict(l for l in vdl.items() if not rex.match(l[0]))
         return self.resolve_primary_defects(vdl.values())
         
-    def _virtual_defect_consistency_check(self):
+    def _virtual_defect_consistency_check(self) -> bool:
         """
         When called, uses an assertion to check that there are no missing 
         defects. This is a database consistency check which should never be
diff --git a/DataQuality/DQDefects/share/DQDefects.ref b/DataQuality/DQDefects/share/DQDefects.ref
index 0a6d4f2f829be72554f797afbf29a6f8c27b79b3..2b56d25d11c5069db9c9c15114a372e269053fd7 100644
--- a/DataQuality/DQDefects/share/DQDefects.ref
+++ b/DataQuality/DQDefects/share/DQDefects.ref
@@ -5,9 +5,13 @@ DQDefects.tests.test_defect_creation ... ok
 Testing virtual defects involving inversion ... ok
 DQDefects.tests.test_defect_insertion_retrieval ... ok
 DQDefects.tests.test_defect_insertion_retrieval_unicode ... ok
+Check that we raise an error if the defect name is not ASCII ... ok
 DQDefects.tests.test_defect_empty_retrieval ... ok
 DQDefects.tests.test_virtual_defect_creation ... ok
+Check that the defect name list is updated when a defect is created ... ok
+Check that the virtual defect name list is updated when a defect is created ... ok
 DQDefects.tests.test_virtual_defect_creation_unicode ... ok
+Check that we raise an error if the virtual defect name is not ASCII ... ok
 DQDefects.tests.test_nonpresent ... ok
 Testing many virtual channels with sparsely placed IoVs ... ok
 Testing virtual defect basics ... ok
@@ -29,8 +33,9 @@ DQDefects.tests.test_reject_duplicate_names ... ok
 Check that the intolerable defect listing function works ... ok
 Checking that listing a tag works even if the tag is empty ... ok
 Test that defects are inserted correctly into non-contiguous ranges ... ok
+Test that return types are appropriate ... ok
 
 ----------------------------------------------------------------------
-Ran 31 tests in 26.579s
+Ran 36 tests in 26.579s
 
 OK