Repository: potassco/clorm (language: python)

Issue:
Add typing
I would ask if is possible to add typing to at least the 'public'-Attributes (including methods)? In this way if you use an IDE like PyCharm or VS-Code you would get completion suggestions and also information about the 'allowed' type of an argument immediately instead of reading the doc-string.
Take the `add_facts`-Method for example
```
def add_facts(self, facts):
    '''Add facts to the control object. Note: facts must be added before grounding.

       This function can take an arbitrary collection containing a mixture
       of ``clorm.Predicate`` and ``clingo.Symbol`` objects. A
       ``clorm.FactBase`` is also a valid collection but it can only contain
       ``clorm.Predicate`` instances.

    Args:
      facts: a collection of ``clorm.Predicate`` or ``clingo.Symbol`` objects

    '''
    control_add_facts(self._wrapped, facts)
```
So instead of describing the allowed types for `facts` i would propose to add the type-information like
```
def add_facts(self, facts: Sequence[Union[Predicate, oclingo.Symbol]]):
    '''Add facts to the control object. Note: facts must be added before grounding.

    Args
    ----------
    facts: a sequence of facts

    '''
    control_add_facts(self._wrapped, facts)
```

What do you think?


Discussion / hints:
Yes. I think this sounds reasonable and would make clorm more usable for a wider audience.  It might also be a good opportunity to clean up a few API calls that over-use variable positional arguments. 

I think there will be some API calls where the return type hint may not be very useful. In particular, `Query.all()` would have to have as a return type hint a generator over `Any`; since the value that is iterated over depends on how the query is constructed. 

However, overall I agree that adding type hints is a good idea.
Glad to hear, that you like the idea :-)

Yeah, I agree with you that inferring the correct return type of a query is probably not so easy especially if you select multiple predicates, joining them etc... Perhaps it's possible when selecting just a single predicate.

If you want some help with the typing-stuff just let me know


Fix patch:
diff --git a/clorm/clingo.py b/clorm/clingo.py
index b65c351..d2d5a9b 100644
--- a/clorm/clingo.py
+++ b/clorm/clingo.py
@@ -10,7 +10,7 @@
 
 import functools
 import itertools
-from typing import TYPE_CHECKING, Any, ClassVar, Iterable, List, Optional, Sequence, Tuple, Union, cast, overload
+from typing import TYPE_CHECKING, Any, Iterable, List, Optional, Sequence, Tuple, Type, Union, cast, overload
 from .orm import *
 from .util.wrapper import init_wrapper, make_class_wrapper
 
@@ -36,15 +36,16 @@
 from clingo import *
 
 __all__ = list([k for k in oclingo.__dict__.keys() if k[0] != '_'])
+
 __version__ = oclingo.__version__
 
 # ------------------------------------------------------------------------------
 # Helper function to smartly build a unifier if only a list of predicates have
 # been provided.
 # ------------------------------------------------------------------------------
+_Unifier = Union[List[Type[Predicate]], SymbolPredicateUnifier]
 
-
-def _build_unifier(unifier: Optional[Union[List[Predicate], SymbolPredicateUnifier]]) -> Optional[SymbolPredicateUnifier]:
+def _build_unifier(unifier: Optional[_Unifier]) -> Optional[SymbolPredicateUnifier]:
     if unifier is None:
         return None
     if isinstance(unifier, SymbolPredicateUnifier):
@@ -80,7 +81,7 @@ class ModelOverride(object):
     if TYPE_CHECKING:
         _wrapped: OModel  # will be set through init_wrapper
 
-    def __init__(self, model: OModel, unifier: Optional[Union[List[Predicate], SymbolPredicateUnifier]] = None) -> None:
+    def __init__(self, model: OModel, unifier: Optional[_Unifier] = None) -> None:
         self._unifier = _build_unifier(unifier)
         _check_is_func(model, "symbols")
         init_wrapper(self, wrapped_=model)
@@ -156,12 +157,11 @@ def contains(self, fact: Union[Predicate, oclingo.Symbol]) -> bool:
         return self.model_.contains(atom)
 
 
-__clorm_Model = make_class_wrapper(OModel, ModelOverride)
 if TYPE_CHECKING:
-    class Model(__clorm_Model, OModel):  # type: ignore
+    class Model(ModelOverride, OModel):  # type: ignore
         pass
 else:
-    Model = __clorm_Model
+    Model = make_class_wrapper(OModel, ModelOverride)
 
 # ------------------------------------------------------------------------------
 # Wrap clingo.SolveHandle and override some functions
@@ -182,7 +182,7 @@ class SolveHandleOverride(object):
     if TYPE_CHECKING:
         _wrapped: OSolveHandle  # will be set through init_wrapper
 
-    def __init__(self, handle: OSolveHandle, unifier: Optional[Union[List[Predicate], SymbolPredicateUnifier]] = None) -> None:
+    def __init__(self, handle: OSolveHandle, unifier: Optional[_Unifier] = None) -> None:
         init_wrapper(self, wrapped_=handle)
         self._unifier = _build_unifier(unifier)
 
@@ -200,23 +200,22 @@ def solvehandle_(self) -> OSolveHandle:
 
     def __iter__(self):
         for model in self.solvehandle_:
-            yield Model(model, unifier=self._unifier)  # type: ignore
+            yield Model(model, unifier=self._unifier)
 
     def __enter__(self):
-        self.solvehandle_.__enter__()  # type: ignore
+        self.solvehandle_.__enter__()
         return self
 
     def __exit__(self, exception_type, exception_value, traceback):
-        self.solvehandle_.__exit__(exception_type, exception_value, traceback)  # type: ignore
+        self.solvehandle_.__exit__(exception_type, exception_value, traceback)
         return None
 
 
-__clorm_SolveHandle = make_class_wrapper(OSolveHandle, SolveHandleOverride)
 if TYPE_CHECKING:
-    class SolveHandle(__clorm_SolveHandle, OSolveHandle):  # type: ignore
+    class SolveHandle(SolveHandleOverride, OSolveHandle):  # type: ignore
         pass
 else:
-    SolveHandle = __clorm_SolveHandle
+    SolveHandle = make_class_wrapper(OSolveHandle, SolveHandleOverride)
 
 
 # ------------------------------------------------------------------------------
@@ -287,18 +286,12 @@ class ControlOverride(object):
 
     @overload
     def __init__(self, arguments: Sequence[str] = [],
-                 logger: Optional[Logger] = None, message_limit: int = 20) -> None: ...
+                 logger: Optional[oclingo.Logger] = None, message_limit: int = 20,
+                 unifier: Optional[Union[List[Predicate], SymbolPredicateUnifier]] = None) -> None: ...
 
     @overload
     def __init__(self, control_: OControl) -> None: ...
 
-    @overload
-    def __init__(
-        self,
-        *args: Any,
-        unifier: Union[List[Predicate], SymbolPredicateUnifier],
-        **kwargs: Any
-    ) -> None: ...
 
     @overload
     def __init__(self, *args: Any, **kwargs: Any) -> None: ...
@@ -344,7 +337,7 @@ def unifier(self) -> Optional[SymbolPredicateUnifier]:
         return self._unifier
 
     @unifier.setter
-    def unifier(self, unifier: Union[List[Predicate], SymbolPredicateUnifier]) -> None:
+    def unifier(self, unifier: _Unifier) -> None:
         self._unifier = _build_unifier(unifier)
 
     # ------------------------------------------------------------------------------
@@ -422,7 +415,7 @@ def _release_fact(fact: Union[Predicate, oclingo.Symbol, int]) -> None:
     # function parameters. At some point will drop support for older clingo and
     # can simplify this function.
     # ---------------------------------------------------------------------------
-    def solve(self, *args: Any, **kwargs: Any) -> Union[SolveHandle, SolveResult]:
+    def solve(self, *args: Any, **kwargs: Any) -> Union[SolveHandle, oclingo.SolveResult]:
         '''Run the clingo solver.
 
         This function extends ``clingo.Control.solve()`` in two ways:
@@ -472,7 +465,7 @@ def solve(self, *args: Any, **kwargs: Any) -> Union[SolveHandle, SolveResult]:
 
             @functools.wraps(on_model)
             def on_model_wrapper(model):
-                return on_model(Model(model, self.unifier))  # type: ignore
+                return on_model(Model(model, self.unifier))
             nkwargs["on_model"] = on_model_wrapper
 
         # Call the wrapped solve function and handle the return value
@@ -482,18 +475,17 @@ def on_model_wrapper(model):
            (async_keyword in nkwargs and nkwargs[async_keyword]):
             return SolveHandle(cast(OSolveHandle, result), unifier=self._unifier)  # type: ignore
         else:
-            return cast(SolveResult, result)
+            return cast(oclingo.SolveResult, result)
 
     def __getattr__(self, attr):
         return getattr(self.control_, attr)
 
 
-__clorm_control = make_class_wrapper(OControl, ControlOverride)
 if TYPE_CHECKING:
-    class Control(__clorm_control, OControl):  # type: ignore
+    class Control(ControlOverride, OControl):  # type: ignore
         pass
 else:
-    Control = __clorm_control
+    Control = make_class_wrapper(OControl, ControlOverride)
 
 
 # ------------------------------------------------------------------------------
diff --git a/clorm/orm/atsyntax.py b/clorm/orm/atsyntax.py
index e425e14..89feb3f 100644
--- a/clorm/orm/atsyntax.py
+++ b/clorm/orm/atsyntax.py
@@ -5,17 +5,10 @@
 
 #import logging
 #import os
-import io
-import contextlib
 import inspect
-import operator
 import collections.abc as cabc
-import bisect
 import functools
-import itertools
-import clingo
-from typing import Any, Callable, List, Type
-import re
+from typing import Any, Callable, List, Sequence, Tuple, Type
 
 from .core import *
 from .core import get_field_definition, infer_field_definition
@@ -30,6 +23,7 @@
 #------------------------------------------------------------------------------
 # Global
 #------------------------------------------------------------------------------
+_AnyCallable = Callable[..., Any]
 
 #------------------------------------------------------------------------------
 #------------------------------------------------------------------------------
@@ -98,7 +92,7 @@ def _is_output_field(o):
                 return True
             return inspect.isclass(o) and issubclass(o, BaseField)
 
-        if isinstance(se, cabc.Iterable):
+        if isinstance(se, Sequence):
             if len(se) != 1: return False
             return _is_output_field(se[0])
         return _is_output_field(se)
@@ -109,14 +103,14 @@ def _validate_basic_sig(sig):
             raise TypeError(("TypeCastSignature element {} must be a BaseField "
                              "subclass".format(sig)))
 
-        self._insigs: List[Type[BaseField]] = []
+        insigs: List[Type[BaseField]] = []
         for s in sigs[:-1]:
             field = None
             try:
                 field = infer_field_definition(s, "")
             except Exception:
                 pass
-            self._insigs.append(field if field else type(get_field_definition(s)))
+            insigs.append(field if field else type(get_field_definition(s)))
         try:
             self._outsig = infer_field_definition(sigs[-1], "") or sigs[-1]
         except Exception:
@@ -125,21 +119,21 @@ def _validate_basic_sig(sig):
         # A tuple is a special case that we want to convert into a complex field
         if isinstance(self._outsig, tuple):
             self._outsig = type(get_field_definition(self._outsig))
-        elif isinstance(self._outsig, cabc.Iterable):
+        elif isinstance(self._outsig, list):
             if len(self._outsig) != 1:
                 raise TypeError("Return value list signature not a singleton")
             if isinstance(self._outsig[0], tuple):
                 self._outsig[0] = type(get_field_definition(self._outsig[0]))
 
         # Validate the signature
-        for s in self._insigs: _validate_basic_sig(s)
-        if isinstance(self._outsig, cabc.Iterable):
+        for s in insigs: _validate_basic_sig(s)
+        if isinstance(self._outsig, Sequence):
             _validate_basic_sig(self._outsig[0])
         else:
             _validate_basic_sig(self._outsig)
 
         # Turn the signature into a tuple
-        self._insigs = tuple(self._insigs)
+        self._insigs = tuple(insigs)
 
     def _input(self, sig, arg):
         return sig.cltopy(arg)
@@ -150,7 +144,7 @@ def _output(self, sig, arg):
             return sig.pytocl(arg)
 
         # Deal with a list
-        if isinstance(sig, cabc.Iterable) and isinstance(arg, cabc.Iterable):
+        if isinstance(sig, Sequence) and isinstance(arg, cabc.Iterable):
             return [ self._output(sig[0], v) for v in arg ]
         raise ValueError("Value {} does not match signature {}".format(arg, sig))
 
@@ -234,7 +228,7 @@ def __repr__(self):
 # annotations. ignore_first is useful when dealing with member functions.
 # ------------------------------------------------------------------------------
 
-def _get_annotations(fn: Callable[..., Any], ignore_first: bool = False) -> List[Any]:
+def _get_annotations(fn: Callable[..., Any], ignore_first: bool = False) -> Tuple[Any, ...]:
     fsig = inspect.signature(fn)
     qname = fn.__qualname__
     fsigparam = fsig.parameters
@@ -256,13 +250,13 @@ def _get_annotations(fn: Callable[..., Any], ignore_first: bool = False) -> List
     if inspect.Signature.empty in annotations:
         raise TypeError(("Missing type cast annotations in function "
                          "arguments: {} ").format(qname))
-    return annotations
+    return tuple(annotations)
 
 
 #------------------------------------------------------------------------------
 #------------------------------------------------------------------------------
 
-def make_function_asp_callable(*args: Any):
+def make_function_asp_callable(*args: Any) -> _AnyCallable:
     r"""A decorator for making a function callable from within an ASP program.
 
     Can be called in a number of ways. Can be called as a decorator with or
@@ -330,7 +324,7 @@ def _sig_decorate(func):
 #------------------------------------------------------------------------------
 #------------------------------------------------------------------------------
 
-def make_method_asp_callable(*args):
+def make_method_asp_callable(*args: Any) -> _AnyCallable:
     """A decorator for making a member function callable from within an ASP program.
 
     See ``make_function_asp_callable`` for details. The only difference is that
diff --git a/clorm/orm/core.py b/clorm/orm/core.py
index 40e0f3e..f535092 100644
--- a/clorm/orm/core.py
+++ b/clorm/orm/core.py
@@ -11,17 +11,13 @@
 #import logging
 #import os
 import datetime
-import io
 import abc
-import contextlib
 import inspect
 import operator
 import collections
 import collections.abc as cabc
-import bisect
 import enum
 import functools
-import itertools
 import sys
 import typing
 import re
@@ -37,7 +33,7 @@
                          NO_DEFAULTS_TEMPLATE, ASSIGN_DEFAULT_TEMPLATE,
                          ASSIGN_COMPLEX_TEMPLATE, PREDICATE_UNIFY_DOCSTRING)
 
-from typing import ( Any, Callable, Dict, Iterator, List, Optional, Sequence, Tuple,
+from typing import ( TYPE_CHECKING, Any, Callable, Dict, Iterable, Iterator, List, Optional, Sequence, Tuple,
                      Type, TypeVar, Union, overload, cast )
 
 
@@ -127,18 +123,19 @@ class _MISSING_TYPE:
 
 _T = TypeVar('_T')
 _P = TypeVar('_P', bound='Predicate')
+_BF = TypeVar('_BF', bound='BaseField')
 
 AnySymbol = Union[Symbol, NoSymbol]
 
 #------------------------------------------------------------------------------
-# A _classproperty decorator. (see
-# https://stackoverflow.com/questions/3203286/how-to-create-a-read-only-class-property-in-python)
+# A _classproperty decorator used in combination with @classmethod. (see
+# https://stackoverflow.com/a/2544313)
 #------------------------------------------------------------------------------
-class _classproperty(object):
-    def __init__(self, getter):
-        self.getter= getter
-    def __get__(self, instance, owner):
-        return self.getter(owner)
+class _classproperty(property):
+    def __get__(self, obj, type_):
+        return self.fget.__get__(None, type_)()
+    def __set__(self, obj, value):
+        return self.fset.__get__(None, type(obj))(value)
 
 # ------------------------------------------------------------------------------
 # PEP681 https://www.python.org/dev/peps/pep-0681/
@@ -492,7 +489,7 @@ def notin_(path, seq):
 #
 # ------------------------------------------------------------------------------
 
-def _define_predicate_path_subclass(predicate_class: 'Predicate') -> Type['PredicatePath']:
+def _define_predicate_path_subclass(predicate_class: 'Type[Predicate]') -> Type['PredicatePath']:
     class_name = predicate_class.__name__ + "_PredicatePath"
     return type(class_name, (PredicatePath,), { "_predicate_class" : predicate_class })
 
@@ -570,6 +567,10 @@ class PredicatePath(object, metaclass=_PredicatePathMeta):
     this property.
 
     '''
+    if TYPE_CHECKING:
+        _predicate_class: Type['Predicate']
+        _complexterm_classes: Dict[str, Type['Predicate']]
+
 
     #--------------------------------------------------------------------------
     # An inner class that provides a hashable variant of a path. Because
@@ -673,7 +674,7 @@ def is_sign(self):
         @property
         def root(self):
             if len(self._parent._pathseq) == 1: return self._parent
-            pi = self._parent._pathseq[0]
+            pi = cast(PathIdentity, self._parent._pathseq[0])
             if pi.predicate.__name__ == pi.name: return self.predicate.meta.path
             return pi.predicate.meta.path_class([pi])
 
@@ -682,14 +683,14 @@ def root(self):
         # --------------------------------------------------------------------------
         @property
         def predicate(self):
-            return self._parent._pathseq[0].predicate
+            return cast(PathIdentity, self._parent._pathseq[0]).predicate
 
         # --------------------------------------------------------------------------
         # Return a dealiased version of this path
         # --------------------------------------------------------------------------
         @property
         def dealiased(self):
-            pi = self._parent._pathseq[0]
+            pi = cast(PathIdentity, self._parent._pathseq[0])
             if pi.predicate.__name__ == pi.name: return self._parent
             dealised = pi.predicate.meta.path
             for key in self._parent._pathseq[1:]:
@@ -710,7 +711,7 @@ def field(self) -> 'BaseField':
         # All the subpaths of this path
         #--------------------------------------------------------------------------
         @property
-        def subpaths(self) -> Tuple['PredicatePath']:
+        def subpaths(self) -> Tuple['PredicatePath', ...]:
             return self._parent._allsubpaths
 
         #--------------------------------------------------------------------------
@@ -722,7 +723,7 @@ def subpaths(self) -> Tuple['PredicatePath']:
         # --------------------------------------------------------------------------
         def resolve(self, fact):
             pseq = self._parent._pathseq
-            if type(fact) != pseq[0].predicate:
+            if type(fact) != cast(PathIdentity, pseq[0]).predicate:
                 raise TypeError("{} is not of type {}".format(fact, pseq[0]))
             return self._parent._attrgetter(fact)
 
@@ -743,12 +744,12 @@ def __init__(self, pathseq: List[Union['PathIdentity',str]]):
         self._meta = PredicatePath.Meta(self)
         self._pathseq = tuple(pathseq)
         self._subpath = {}
-        self._allsubpaths = tuple([])
+        self._allsubpaths: Tuple[Any, ...] = tuple([])
         self._field = self._get_field()
         self._hashable = PredicatePath.Hashable(self)
         tmp = pathseq[1:]
         if not tmp: self._attrgetter = lambda x: x
-        else: self._attrgetter = operator.attrgetter(".".join(tmp))
+        else: self._attrgetter = operator.attrgetter(".".join(tmp)) # type: ignore
 
 
         if not pathseq or not isinstance(pathseq[0], PathIdentity) or \
@@ -953,8 +954,13 @@ def hashable_path(arg,exception=True):
 #------------------------------------------------------------------------------
 # API function to return the an alias path for a predicate
 # ------------------------------------------------------------------------------
+@overload
+def alias(predicate: Type[_P], name: str = "") -> Type[_P]: ...
+
+@overload
+def alias(predicate: Union[PredicatePath, PredicatePath.Hashable], name: str = "") -> Type['Predicate']: ...
 
-def alias(predicate, name=None):
+def alias(predicate: Any, name: str = "") -> Type['Predicate']:
     '''Return an alias :class:`PredicatePath` instance for a :class:`Predicate` sub-class.
 
     A predicate alias can be used to support self joins in queries. The alias
@@ -994,7 +1000,7 @@ class F(Predicate):
     errormsg = ("predicate argument must refer to a Predicate sub-class "
                 "or the root path corresponding to a Predicate sub-class")
     arg = predicate
-    if isinstance(arg,PredicatePath.Hashable): arg = predicate.path
+    if isinstance(arg,PredicatePath.Hashable): arg = arg.path
     if isinstance(arg, PredicatePath):
         if not arg.meta.is_root:
             raise ValueError("Invalid argument {}: {}".format(arg,errormsg))
@@ -1103,9 +1109,9 @@ def _raise_pytocl_nie(v):
         # For complex-terms provide an interface to the underlying complex term
         # object
         if "complex" in dct:
-            dct["complex"] = _classproperty(dct["complex"])
+            dct["complex"] = _classproperty(classmethod(dct["complex"]))
         else:
-            dct["complex"] = _classproperty(lambda cls: None)
+            dct["complex"] = _classproperty(classmethod(lambda cls: None))
 
         return super(_BaseFieldMeta, meta).__new__(meta, name, bases, dct)
 
@@ -1225,7 +1231,8 @@ def pytocl(v):
 
     # Internal property - not part of official API
     @_classproperty
-    def complex(cls) -> 'Predicate':
+    @classmethod
+    def complex(cls) -> Optional['Predicate']:
         return None
 
     @property
@@ -1271,7 +1278,7 @@ def __repr__(self):
 # https://github.com/python/cpython/blob/0b58bac3e7877d722bdbd3c38913dba2cb212f13/Lib/dataclasses.py#L346
 # https://github.com/python/typeshed/blob/e434b23741a5e3f2ea899ccfb0ef2a15f168ebf1/stubs/dataclasses/dataclasses.pyi#L45
 
-_FieldDefinition = Union[Type[BaseField], Tuple['_FieldDefinition']]
+_FieldDefinition = Union[Type[BaseField], Tuple['_FieldDefinition', ...]]  # type: ignore
 
 @overload
 def field(basefield: _FieldDefinition) -> Any: ...
@@ -1279,13 +1286,16 @@ def field(basefield: _FieldDefinition) -> Any: ...
 @overload
 def field(basefield: _FieldDefinition,*, default: _T) -> _T: ...
 
+@overload
+def field(basefield: _FieldDefinition,*, default: _T, kw_only: bool) -> _T: ...
+
 @overload
 def field(basefield: _FieldDefinition,*, default_factory: Callable[[], _T]) -> _T: ...
 
 def field(basefield: _FieldDefinition,*, default: Any=MISSING, default_factory: Any=MISSING, kw_only: bool=False) -> Any:
     if default is not MISSING and default_factory is not MISSING:
         raise ValueError('can not specify both default and default_factory')
-    if isinstance(basefield, Tuple):
+    if isinstance(basefield, tuple):
         try:
             module = sys._getframe(1).f_globals.get('__name__', '__main__')
         except (AttributeError, ValueError):
@@ -1313,7 +1323,7 @@ def field(basefield: _FieldDefinition,*, default: Any=MISSING, default_factory:
 
 class Raw(object):
     __slots__ = ("_symbol", )
-    def __init__(self, symbol: AnySymbol):
+    def __init__(self, symbol: AnySymbol) -> None:
         self._symbol = symbol
 
     def __str__(self):
@@ -1555,7 +1565,9 @@ def _test_value(v):
                 { "pytocl": _test_value,
                   "cltopy": _test_value})
 
-def refine_field(field_class,values,*,name=None):
+def refine_field(field_class: Type[_BF],
+                 values: Union[Iterable[_T], Callable[[_T], bool]], *,
+                 name: Optional[str]=None) -> Type[_BF]:
     """Factory function that returns a field sub-class with restricted values.
 
     A helper factory function to define a sub-class of a BaseField (or sub-class)
@@ -1624,7 +1636,7 @@ class WorksOn(Predicate):
 # helper function.
 # ------------------------------------------------------------------------------
 
-def combine_fields(fields,*,name=None):
+def combine_fields(fields: Sequence[Type[BaseField]], *, name: str="") -> Type[BaseField]:
     """Factory function that returns a field sub-class that combines other fields
 
     A helper factory function to define a sub-class of BaseField that combines
@@ -1685,7 +1697,7 @@ def _cltopy(r):
 # deals with a list/tuple encoded and an asp basic tuple.
 # ------------------------------------------------------------------------------
 
-def define_flat_list_field(element_field,*,name=None):
+def define_flat_list_field(element_field: Type[BaseField], *, name: str="") -> Type[BaseField]:
     """Factory function that returns a BaseField sub-class for flat lists
 
     This function is a helper factory function to define a sub-class of
@@ -1747,7 +1759,10 @@ def _cltopy(sym):
 # deals with nested list encoded asp.
 # ------------------------------------------------------------------------------
 
-def define_nested_list_field(element_field,*,headlist=True,reverse=False,name=None):
+def define_nested_list_field(element_field: Type[BaseField], *,
+                             headlist: bool=True,
+                             reverse: bool=False,
+                             name: str="") -> Type[BaseField]:
     """Factory function that returns a BaseField sub-class for nested lists
 
     This function is a helper factory function to define a sub-class of
@@ -1925,7 +1940,9 @@ def _listtail_cltopy_reverse(raw):
 # deals with nested list encoded asp.
 # ------------------------------------------------------------------------------
 
-def define_enum_field(parent_field,enum_class,*,name=None):
+def define_enum_field(parent_field: Type[BaseField],
+                      enum_class: Type[enum.Enum], *,
+                      name: str="") -> Type[BaseField]:
     """Factory function that returns a BaseField sub-class for an Enum
 
     Enums are part of the standard library since Python 3.4. This method
@@ -2066,7 +2083,14 @@ def __set__(self, instance, value):
 # ClormAnonTuple).
 # ------------------------------------------------------------------------------
 
-def get_field_definition(defn, module=None) -> BaseField:
+@overload
+def get_field_definition(defn: Type[_BF]) -> _BF: ...
+@overload
+def get_field_definition(defn: _BF) -> _BF: ...
+@overload
+def get_field_definition(defn: Tuple[Any, ...], module: str = "") -> BaseField: ...
+
+def get_field_definition(defn: Any, module: str="") -> BaseField:
     errmsg = ("Unrecognised field definition object '{}'. Expecting: "
               "1) BaseField (sub-)class, 2) BaseField (sub-)class instance, "
               "3) a tuple containing a field definition")
@@ -2084,7 +2108,7 @@ def get_field_definition(defn, module=None) -> BaseField:
 
     return _create_complex_term(defn, module=module)
 
-def _create_complex_term(defn, default_value=MISSING, module=None) -> BaseField:
+def _create_complex_term(defn: Any, default_value: Any = MISSING, module: str = "") -> BaseField:
     # NOTE: I was using a dict rather than OrderedDict which just happened to
     # work. Apparently, in Python 3.6 this was an implmentation detail and
     # Python 3.7 it is a language specification (see:
@@ -2092,8 +2116,8 @@ def _create_complex_term(defn, default_value=MISSING, module=None) -> BaseField:
     # However, since Clorm is meant to be Python 3.5 compatible change this to
     # use an OrderedDict.
     # proto = { "arg{}".format(i+1) : get_field_definition(d) for i,d in enumerate(defn) }
-    proto = collections.OrderedDict([(f"arg{i+1}", get_field_definition(d, module))
-                                     for i,d in enumerate(defn)])
+    proto: Dict[str, Any] = collections.OrderedDict([(f"arg{i+1}", get_field_definition(d, module))
+                                                      for i,d in enumerate(defn)])
     class_name = f'ClormAnonTuple({",".join(f"{arg[0]}={repr(arg[1])}" for arg in proto.items())})'
 
     if default_value is not MISSING:
@@ -2110,7 +2134,7 @@ def _create_complex_term(defn, default_value=MISSING, module=None) -> BaseField:
     # where the named tuple is created.
     if module is not None:
         proto['__module__'] = module
-    ct = type(class_name, (Predicate,), proto)
+    ct: Type[Predicate] = type(class_name, (Predicate,), proto)
     # For pickling to work, the frame must know the class
     if module is not None:
         try:
@@ -2134,7 +2158,7 @@ def _create_complex_term(defn, default_value=MISSING, module=None) -> BaseField:
 # Return the list of field_paths associated with a predicate (ignoring the base
 # predicate path itself).
 # ------------------------------------------------------------------------------
-def _get_paths(predicate: 'Predicate'):
+def _get_paths(predicate: Type['Predicate']) -> List[PredicatePath]:
     def get_subpaths(path: 'PredicatePath') -> List[PredicatePath]:
         paths=[]
         for subpath in path.meta.subpaths:
@@ -2147,12 +2171,12 @@ def get_subpaths(path: 'PredicatePath') -> List[PredicatePath]:
 #------------------------------------------------------------------------------
 # Return the list of field_paths that are specified as indexed
 #------------------------------------------------------------------------------
-def _get_paths_for_default_indexed_fields(predicate: 'Predicate') -> Iterator[PredicatePath]:
-    def is_indexed(path: PredicatePath):
+def _get_paths_for_default_indexed_fields(cls: Type['Predicate']) -> Iterator[PredicatePath]:
+    def is_indexed(path: PredicatePath) -> bool:
         field = path.meta.field
-        if field and field.index: return True
-        return False
-    return filter(is_indexed, _get_paths(predicate))
+        return field and field.index
+
+    return filter(is_indexed, _get_paths(cls))
 
 # ------------------------------------------------------------------------------
 # Determine if an attribute name has the pattern of an official attribute
@@ -2192,7 +2216,7 @@ class PredicateDefn(object):
 
     """
 
-    def __init__(self, name, field_accessors: List[FieldAccessor], anon=False,sign=None):
+    def __init__(self, name: str, field_accessors: List[FieldAccessor], anon: bool=False,sign: Optional[bool]=None) -> None:
         self._name = name
         self._byidx = tuple(field_accessors)
         self._byname = { f.name : f for f in field_accessors }
@@ -2200,7 +2224,7 @@ def __init__(self, name, field_accessors: List[FieldAccessor], anon=False,sign=N
         self._anon = anon
         self._key2canon = { f.index : f.name for f in field_accessors }
         self._key2canon.update({f.name : f.name for f in field_accessors })
-        self._parent_cls = None
+        self._parent_cls: Type[Predicate] = None  # type: ignore
         self._indexed_fields = ()
         self._sign = sign
 
@@ -2265,7 +2289,7 @@ def parent(self):
 
     # Internal property
     @parent.setter
-    def parent(self, pc: 'Predicate'):
+    def parent(self, pc: Type['Predicate']) -> None:
         if self._parent_cls:
             raise RuntimeError(("Trying to reset the parent for a "
                                 "PredicateDefn doesn't make sense"))
@@ -2319,7 +2343,7 @@ def __iter__(self):
 # tuple. Otherwise simply returns the value.
 # ------------------------------------------------------------------------------
 
-def _generate_dynamic_predicate_functions(class_name: str, namespace: Dict):
+def _generate_dynamic_predicate_functions(class_name: str, namespace: Dict) -> None:
     pdefn = namespace["_meta"]
 
     gdict = {"Predicate": Predicate,
@@ -2328,7 +2352,7 @@ def _generate_dynamic_predicate_functions(class_name: str, namespace: Dict):
              "AnySymbol": AnySymbol,
              "Type": Type,
              "Optional" : Optional,
-             "List": List,
+             "Sequence": Sequence,
              "_P": _P}
 
     for f in pdefn:
@@ -2369,16 +2393,16 @@ def _generate_dynamic_predicate_functions(class_name: str, namespace: Dict):
 
     # Create the check for dealing with complex assignment
     tmp = []
-    tmp2 = []
+    tmp3 = []
     for f in pdefn:
         cmplx = f.defn.complex
         if cmplx and cmplx.meta.is_tuple:
             tmp.append(ASSIGN_COMPLEX_TEMPLATE.format(arg=f.name))
-            tmp2.append(f"{f.name}.symbol, ")
+            tmp3.append(f"{f.name}.symbol, ")
         else:
-            tmp2.append(f"{f.name}_pytocl({f.name}), ")
+            tmp3.append(f"{f.name}_pytocl({f.name}), ")
     check_complex = "".join(tmp)
-    args_raw = "".join(tmp2)
+    args_raw = "".join(tmp3)
 
     tmp = []
     for idx, f in enumerate(pdefn):
@@ -2399,7 +2423,7 @@ def _generate_dynamic_predicate_functions(class_name: str, namespace: Dict):
     predicate_functions = expand_template(template, **expansions)
     # print(f"INIT:\n\n{predicate_functions}\n\n")
 
-    ldict = {}
+    ldict: Dict[str, Any] = {}
     exec(predicate_functions, gdict, ldict)
 
     init_doc_args = f"{args_signature}*, sign=True, raw=None"
@@ -2464,18 +2488,29 @@ def infer_field_definition(type_: Type[Any], module: str) -> Optional[Type[BaseF
     args = get_args(type_)
 
     if origin is HeadList:
-        return define_nested_list_field(infer_field_definition(args[0], module))
+        field = infer_field_definition(args[0], module)
+        return define_nested_list_field(field) if field else None
     if origin is HeadListReversed:
-        return define_nested_list_field(infer_field_definition(args[0], module),reverse=True)
+        field = infer_field_definition(args[0], module)
+        return define_nested_list_field(field, reverse=True) if field else None
     if origin is TailList:
-        return define_nested_list_field(infer_field_definition(args[0], module),headlist=False)
+        field = infer_field_definition(args[0], module)
+        return define_nested_list_field(field, headlist=False) if field else None
     if origin is TailListReversed:
-        return define_nested_list_field(infer_field_definition(args[0], module),headlist=False, reverse=True)
+        field = infer_field_definition(args[0], module)
+        return define_nested_list_field(field, headlist=False, reverse=True) if field else None
     if origin is Union:
-        return combine_fields([infer_field_definition(arg, module) for arg in args])
-    if len(args) > 1 and issubclass(origin, Tuple):
+        fields: List[Type[BaseField]] = []
+        for arg in args:
+            field = infer_field_definition(arg, module)
+            if field is None:
+                return None
+            fields.append(field)
+        return combine_fields(fields)
+    if len(args) > 1 and origin and issubclass(origin, tuple):
         if len(args) == 2 and args[1] is Ellipsis: # e.g. Tuple[int, ...]
-            return define_flat_list_field(infer_field_definition(args[0], module))
+            field = infer_field_definition(args[0], module)
+            return define_flat_list_field(field) if field else None
 
         # not just return a tuple of Fields, but create a Field-Instance and take the type so
         # Tuple[...] can be used within Union
@@ -2485,7 +2520,7 @@ def infer_field_definition(type_: Type[Any], module: str) -> Optional[Type[BaseF
     if issubclass(type_, enum.Enum):
         # if type_ just inherits from Enum is IntegerField, otherwise find appropriate Field
         field = IntegerField if len(type_.__bases__) == 1 else infer_field_definition(type_.__bases__[0], module)
-        return define_enum_field(field, type_)
+        return define_enum_field(field, type_) if field else None
     if issubclass(type_, bool):
         from clorm.lib.boolean import BooleanField
         return BooleanField
@@ -2626,12 +2661,13 @@ def _make_predicatedefn(class_name: str, namespace: Dict[str, Any], meta_dct: Di
 # instances and clingo symbol objects.
 # ------------------------------------------------------------------------------
 
-def _define_field_for_predicate(cls) -> Type[BaseField]:
-    if not issubclass(cls, Predicate):
+def _define_field_for_predicate(cls_meta: '_PredicateMeta') -> Type[BaseField]:
+    if not isinstance(cls_meta, _PredicateMeta):
         raise TypeError(("Class {} is not a Predicate/ComplexTerm "
-                         "sub-class").format(cls))
+                         "sub-class").format(cls_meta))
 
-    field_name = "{}Field".format(cls.__name__)
+    field_name = "{}Field".format(cls_meta.__name__)
+    cls = cast(Type['Predicate'], cls_meta)
     def _pytocl(v):
         if isinstance(v,cls): return v.raw
         if isinstance(v,tuple):
@@ -2665,6 +2701,8 @@ def _cltopy(v):
 #------------------------------------------------------------------------------
 @__dataclass_transform__(field_descriptors=(field,))
 class _PredicateMeta(type):
+    if TYPE_CHECKING:
+        meta: PredicateDefn
 
     #--------------------------------------------------------------------------
     # Allocate the new metaclass
@@ -2720,7 +2758,7 @@ def __init__(cls, cls_name, bases, dct, **kwargs):
         md.parent = cls
         for field in md:
             dct[field.name].parent = cls
-        md.indexes=_get_paths_for_default_indexed_fields(cls)
+        md.indexes=_get_paths_for_default_indexed_fields(cast(Type[Predicate], cls))
 
         # Assign the parent for the SignAccessor
         dct["sign"].parent = cls
@@ -2733,7 +2771,7 @@ def __init__(cls, cls_name, bases, dct, **kwargs):
     def __getitem__(self, idx):
         return self.meta.path[idx]
 
-    def __iter__(self):
+    def __iter__(self) -> Iterator[PredicatePath]:
         return iter([self[k] for k in self.meta.keys()])
 
 #------------------------------------------------------------------------------
@@ -2808,7 +2846,7 @@ def __new__(cls, *args, **kwargs):
         @classmethod
         def _unify(cls: Type[_P],
                    raw: AnySymbol,
-                   raw_args: Optional[List[AnySymbol]]=None,
+                   raw_args: Optional[Sequence[AnySymbol]]=None,
                    raw_name: Optional[str]=None) -> Optional[_P]:
             pass
 
@@ -2832,12 +2870,13 @@ def raw(self) -> Symbol:
         return self._raw if isinstance(self._raw, Symbol) else noclingo_to_clingo(self._raw)
 
     @_classproperty
+    @classmethod
     def Field(cls) -> BaseField:
         """A BaseField sub-class corresponding to a Field for this class."""
         return cls._field
 
     # Clone the object with some differences
-    def clone(self, **kwargs):
+    def clone(self: _P, **kwargs: Any) -> _P:
         """Clone the object with some differences.
 
         For any field name that is not one of the parameter keywords the clone
@@ -2872,6 +2911,7 @@ def clone(self, **kwargs):
 
     # Get the metadata for the Predicate definition
     @_classproperty
+    @classmethod
     def meta(cls) -> PredicateDefn:
         """The meta data (definitional information) for the Predicate/Complex-term"""
         return cls._meta
@@ -2880,12 +2920,12 @@ def meta(cls) -> PredicateDefn:
     # Overloaded index operator to access the values and len operator
     #--------------------------------------------------------------------------
 
-    def __iter__(self) -> Iterator[PredicatePath]:
+    def __iter__(self) -> Iterator[Any]:
         # The number of parameters in a predicate are always small so convenient
         # to generate a list of values rather than have a specialised iterator.
         return iter([self[idx] for idx in range(0,len(self))])
 
-    def __getitem__(self, idx):
+    def __getitem__(self, idx: int) -> Any:
         """Allows for index based access to field elements."""
         return self.meta[idx].__get__(self)
 
@@ -3006,7 +3046,7 @@ def __setstate__(self, newstate):
 # for easy display/printing.
 # ------------------------------------------------------------------------------
 
-def simple_predicate(predicate_name,arity,*,name=None,module=None):
+def simple_predicate(predicate_name: str, arity: int, *,name: str="", module: str="") -> Type[Predicate]:
     """Factory function to define a predicate with only RawField arguments.
 
     A helper factory function that takes a name and an arity and returns a
@@ -3036,8 +3076,8 @@ def simple_predicate(predicate_name,arity,*,name=None,module=None):
     subclass_name = name if name else "AnonSimplePredicate"
 
     # Use an OrderedDict to ensure the correct order of the field arguments
-    proto = collections.OrderedDict([("arg{}".format(i+1), RawField())
-                                     for i in range(0,arity)])
+    proto: Dict[str, Any] = collections.OrderedDict([("arg{}".format(i+1), RawField())
+                                                     for i in range(0,arity)])
     proto['Meta'] = type("Meta", (object,),
                          {"name" : predicate_name,
                           "is_tuple" : False, "_anon" : True})
diff --git a/clorm/orm/factbase.py b/clorm/orm/factbase.py
index 0d354f1..48aa461 100644
--- a/clorm/orm/factbase.py
+++ b/clorm/orm/factbase.py
@@ -5,27 +5,22 @@
 
 import sys
 import io
-import operator
-import bisect
-import inspect
 import abc
-import functools
 import itertools
-from typing import Iterable, Union
+from typing import Callable, Iterable, Iterator, List, Optional, TextIO, Tuple, Type, Union, cast
 
 from .core import *
-from .core import get_field_definition, PredicatePath, kwargs_check_keys, \
-    validate_root_paths
+from .core import PredicatePath, validate_root_paths
 from .core import PredicateDefn
 
 from .query import *
 
-from .query import Placeholder, OrderBy, desc, asc
+from .query import Placeholder, desc, asc
 
 from .query import process_where, process_join, process_orderby, \
     make_query_plan, QuerySpec, QueryExecutor
 
-from .factcontainers import FactSet, FactIndex, FactMap, factset_equality
+from .factcontainers import FactMap, factset_equality
 
 __all__ = [
     'FactBase',
@@ -36,6 +31,7 @@
 #------------------------------------------------------------------------------
 # Global
 #------------------------------------------------------------------------------
+_Facts = Union[Iterable[Predicate], Callable[[], Iterable[Predicate]]]
 
 #------------------------------------------------------------------------------
 # Support function for printing ASP facts: Note: _trim_docstring() is taken from
@@ -97,7 +93,7 @@ def _format_docstring(docstring,output):
 def _maxwidth(lines):
     return max([len(l) for l in lines])
 
-def _format_commented(fm: FactMap, out):
+def _format_commented(fm: FactMap, out: TextIO) -> None:
     pm: PredicateDefn = fm.predicate.meta
     docstring = _trim_docstring(fm.predicate.__doc__) \
         if fm.predicate.__doc__ else ""
@@ -105,11 +101,11 @@ def _format_commented(fm: FactMap, out):
     if pm.arity == 0:
         lines = [ "Unary predicate signature:", indent + pm.name ]
     else:
-        def build_signature(p: Predicate) -> str:
+        def build_signature(p: Type[Predicate]) -> str:
             args = []
             for pp in p:
                 complex = pp.meta.field.complex
-                args.append(pp._pathseq[1] if not complex else build_signature(complex))
+                args.append(cast(str, pp._pathseq[1]) if not complex else build_signature(complex))
             return f"{p.meta.name}({','.join(args)})"
 
         lines = [ "Predicate signature:",
@@ -169,7 +165,7 @@ class FactBase(object):
     def _init(self, facts=None, indexes=None):
 
         # flag that initialisation has taken place
-        self._delayed_init = None
+        self._delayed_init: Optional[Callable[[], None]] = None
 
         # If it is delayed initialisation then get the facts
         if facts and callable(facts):
@@ -209,12 +205,12 @@ def _add(self, arg: Union[Predicate, Iterable[Predicate]]) -> None:
             raise TypeError(f"'{arg}' is not a Predicate instance")
 
         sorted_facts = sorted(arg, key=lambda x: x.__class__.__name__)
-        for ptype, grouped_facts in itertools.groupby(sorted_facts, lambda x: x.__class__):
-            if not issubclass(ptype, Predicate):
+        for type_, grouped_facts in itertools.groupby(sorted_facts, lambda x: x.__class__):
+            if not issubclass(type_, Predicate):
                 raise TypeError(f"{list(grouped_facts)} are not Predicate instances")
-            if not ptype in self._factmaps:
-                self._factmaps[ptype] = FactMap(ptype)
-            self._factmaps[ptype].add_facts(grouped_facts)
+            if not type_ in self._factmaps:
+                self._factmaps[type_] = FactMap(type_)
+            self._factmaps[type_].add_facts(grouped_facts)
         return
 
     def _remove(self, fact, raise_on_missing):
@@ -227,7 +223,7 @@ def _remove(self, fact, raise_on_missing):
     #--------------------------------------------------------------------------
     # Initiliser
     #--------------------------------------------------------------------------
-    def __init__(self, facts=None, indexes=None):
+    def __init__(self, facts: Optional[_Facts] = None, indexes: Optional[Iterable[PredicatePath]] = None) -> None:
         self._delayed_init=None
         if callable(facts):
             def delayed_init():
@@ -263,17 +259,17 @@ def add(self, arg: Union[Predicate, Iterable[Predicate]]) -> None:
         self._check_init()  # Check for delayed init
         return self._add(arg)
 
-    def remove(self, arg):
+    def remove(self, arg: Predicate) -> None:
         """Remove a fact from the fact base (raises an exception if no fact). """
         self._check_init()  # Check for delayed init
         return self._remove(arg, raise_on_missing=True)
 
-    def discard(self, arg):
+    def discard(self, arg: Predicate) -> None:
         """Remove a fact from the fact base. """
         self._check_init()  # Check for delayed init
         return self._remove(arg, raise_on_missing=False)
 
-    def pop(self):
+    def pop(self) -> Predicate:
         """Pop an element from the FactBase. """
         self._check_init()  # Check for delayed init
         for pt, fm in self._factmaps.items():
@@ -350,25 +346,25 @@ def query(self, *roots):
         return QueryImpl(self._factmaps, qspec)
 
     @property
-    def predicates(self):
+    def predicates(self) -> Tuple[Type[Predicate], ...]:
         """Return the list of predicate types that this fact base contains."""
 
         self._check_init()  # Check for delayed init
         return tuple([pt for pt, fm in self._factmaps.items() if fm])
 
     @property
-    def indexes(self):
+    def indexes(self) -> Tuple[PredicatePath, ...]:
         self._check_init()  # Check for delayed init
         return self._indexes
 
-    def facts(self):
+    def facts(self) -> List[Predicate]:
         """Return all facts."""
 
         self._check_init()  # Check for delayed init
         tmp = [ fm.factset for fm in self._factmaps.values() if fm]
         return list(itertools.chain(*tmp))
 
-    def asp_str(self,*,width=0,commented=False,sorted=False):
+    def asp_str(self, *, width: int = 0, commented: bool = False, sorted: bool = False) -> str:
         """Return a ASP string representation of the fact base.
 
         The generated ASP string representation is syntactically correct ASP
@@ -418,9 +414,7 @@ def asp_str(self,*,width=0,commented=False,sorted=False):
         out.close()
         return data
 
-
-
-    def __str__(self):
+    def __str__(self) -> str:
         self._check_init()  # Check for delayed init
         tmp = ", ".join([str(f) for f in self])
         return '{' + tmp + '}'
@@ -455,7 +449,7 @@ def __len__(self):
         self._check_init() # Check for delayed init
         return sum([len(fm.factset) for fm in self._factmaps.values()])
 
-    def __iter__(self):
+    def __iter__(self) -> Iterator[Predicate]:
         self._check_init() # Check for delayed init
 
         for fm in self._factmaps.values():
@@ -569,60 +563,60 @@ def __getstate__(self):
     #--------------------------------------------------------------------------
     # Set functions
     #--------------------------------------------------------------------------
-    def union(self,*others):
+    def union(self, *others: _Facts) -> 'FactBase':
         """Implements the set union() function"""
-        others=[o if isinstance(o, self.__class__) else FactBase(o) for o in others]
+        factbases = [o if isinstance(o, self.__class__) else FactBase(o) for o in others]
         self._check_init() # Check for delayed init
-        for o in others: o._check_init()
+        for fb in factbases: fb._check_init()
 
-        fb=FactBase()
+        fb = FactBase()
         predicates = set(self._factmaps.keys())
-        for o in others: predicates.update(o._factmaps.keys())
+        for o in factbases: predicates.update(o._factmaps.keys())
 
         for p in predicates:
-            pothers=[ o._factmaps[p] for o in others if p in o._factmaps]
+            pothers = [o._factmaps[p] for o in factbases if p in o._factmaps]
             if p in self._factmaps:
                 fb._factmaps[p] = self._factmaps[p].union(*pothers)
             else:
                 fb._factmaps[p] = FactMap(p).union(*pothers)
         return fb
 
-    def intersection(self,*others):
+    def intersection(self, *others: _Facts) -> 'FactBase':
         """Implements the set intersection() function"""
-        others=[o if isinstance(o, self.__class__) else FactBase(o) for o in others]
+        factbases = [o if isinstance(o, self.__class__) else FactBase(o) for o in others]
         self._check_init() # Check for delayed init
-        for o in others: o._check_init()
+        for fb in factbases: fb._check_init()
 
-        fb=FactBase()
+        fb = FactBase()
         predicates = set(self._factmaps.keys())
-        for o in others: predicates.intersection_update(o._factmaps.keys())
+        for fb_ in factbases: predicates.intersection_update(fb_._factmaps.keys())
 
         for p in predicates:
-            pothers=[ o._factmaps[p] for o in others if p in o._factmaps]
+            pothers = [o._factmaps[p] for o in factbases if p in o._factmaps]
             fb._factmaps[p] = self._factmaps[p].intersection(*pothers)
         return fb
 
-    def difference(self,*others):
+    def difference(self, *others: _Facts) -> 'FactBase':
         """Implements the set difference() function"""
-        others=[o if isinstance(o, self.__class__) else FactBase(o) for o in others]
+        factbases = [o if isinstance(o, self.__class__) else FactBase(o) for o in others]
         self._check_init() # Check for delayed init
-        for o in others: o._check_init()
+        for fb in factbases: fb._check_init()
 
-        fb=FactBase()
+        fb = FactBase()
         predicates = set(self._factmaps.keys())
 
         for p in predicates:
-            pothers=[ o._factmaps[p] for o in others if p in o._factmaps]
+            pothers = [o._factmaps[p] for o in factbases if p in o._factmaps]
             fb._factmaps[p] = self._factmaps[p].difference(*pothers)
         return fb
 
-    def symmetric_difference(self,other):
+    def symmetric_difference(self, other: _Facts) -> 'FactBase':
         """Implements the set symmetric_difference() function"""
         if not isinstance(other, self.__class__): other=FactBase(other)
         self._check_init() # Check for delayed init
         other._check_init()
 
-        fb=FactBase()
+        fb = FactBase()
         predicates = set(self._factmaps.keys())
         predicates.update(other._factmaps.keys())
 
@@ -637,44 +631,43 @@ def symmetric_difference(self,other):
 
         return fb
 
-    def update(self,*others):
+    def update(self, *others: _Facts) -> None:
         """Implements the set update() function"""
-        others=[o if isinstance(o, self.__class__) else FactBase(o) for o in others]
+        factbases = [o if isinstance(o, self.__class__) else FactBase(o) for o in others]
         self._check_init() # Check for delayed init
-        for o in others: o._check_init()
+        for fb in factbases: fb._check_init()
 
-        for o in others:
-            for p,fm in o._factmaps.items():
+        for fb in factbases:
+            for p,fm in fb._factmaps.items():
                 if p in self._factmaps: self._factmaps[p].update(fm)
                 else: self._factmaps[p] = fm.copy()
 
-    def intersection_update(self,*others):
+    def intersection_update(self, *others: _Facts) -> None:
         """Implements the set intersection_update() function"""
-        others=[o if isinstance(o, self.__class__) else FactBase(o) for o in others]
+        factbases = [o if isinstance(o, self.__class__) else FactBase(o) for o in others]
         self._check_init() # Check for delayed init
-        for o in others: o._check_init()
-        num = len(others)
+        for fb in factbases: fb._check_init()
 
         predicates = set(self._factmaps.keys())
-        for o in others: predicates.intersection_update(o._factmaps.keys())
+        for fb in factbases: predicates.intersection_update(fb._factmaps.keys())
         pred_to_delete = set(self._factmaps.keys()) - predicates
 
         for p in pred_to_delete: self._factmaps[p].clear()
         for p in predicates:
-            pothers=[ o._factmaps[p] for o in others if p in o._factmaps]
+            pothers = [o._factmaps[p] for o in factbases if p in o._factmaps]
             self._factmaps[p].intersection_update(*pothers)
 
-    def difference_update(self,*others):
+    def difference_update(self, *others: _Facts) -> None:
         """Implements the set difference_update() function"""
-        others=[o if isinstance(o, self.__class__) else FactBase(o) for o in others]
+        factbases = [o if isinstance(o, self.__class__) else FactBase(o) for o in others]
         self._check_init() # Check for delayed init
-        for o in others: o._check_init()
+        for fb in factbases: fb._check_init()
 
         for p in self._factmaps.keys():
-            pothers=[ o._factmaps[p] for o in others if p in o._factmaps ]
+            pothers = [o._factmaps[p] for o in factbases if p in o._factmaps]
             self._factmaps[p].difference_update(*pothers)
 
-    def symmetric_difference_update(self,other):
+    def symmetric_difference_update(self, other: _Facts) -> None:
         """Implements the set symmetric_difference_update() function"""
         if not isinstance(other, self.__class__): other=FactBase(other)
         self._check_init() # Check for delayed init
@@ -689,12 +682,11 @@ def symmetric_difference_update(self,other):
             else:
                 if p in other._factmaps: self._factmaps[p] = other._factmaps[p].copy()
 
-
-    def copy(self):
+    def copy(self) -> 'FactBase':
         """Implements the set copy() function"""
         self._check_init() # Check for delayed init
-        fb=FactBase()
-        for p,fm in self._factmaps.items():
+        fb = FactBase()
+        for p, _ in self._factmaps.items():
             fb._factmaps[p] = self._factmaps[p].copy()
         return fb
 
diff --git a/clorm/orm/factcontainers.py b/clorm/orm/factcontainers.py
index 39d3941..0008e85 100644
--- a/clorm/orm/factcontainers.py
+++ b/clorm/orm/factcontainers.py
@@ -3,18 +3,14 @@
 # FactMap.
 # ------------------------------------------------------------------------------
 
-import io
 import operator
 import collections
 import bisect
-import inspect
-import abc
-import functools
 import itertools
+from typing import Any, Iterable, List, Type
 
 from .core import *
-from .core import notcontains, PredicatePath, \
-    get_field_definition, kwargs_check_keys
+from .core import notcontains, PredicatePath
 
 # ------------------------------------------------------------------------------
 # In order to implement FactBase I originally used the built in 'set'
@@ -264,7 +260,7 @@ def _fm_iterable(other):
     else: return other
 
 class FactMap(object):
-    def __init__(self, ptype: Predicate, indexes=[]):
+    def __init__(self, ptype: Type[Predicate], indexes: Iterable[Any]=[]) -> None:
         def clean_path(p):
             p = path(p)
             if hashable_path(p) != hashable_path(p.meta.dealiased):
@@ -281,11 +277,10 @@ def clean_path(p):
         self._ptype = ptype
         self._factset = FactSet()
         self._path2factindex = collections.OrderedDict()
-        self._factindexes = []
 
         # Validate the paths to be indexed
         allindexes = set([clean_path(p) for p in indexes])
-
+        factindexes: List[FactIndex] = []
         for pth in allindexes:
             tmppath=path(pth)
             if hashable_path(tmppath.meta.dealiased) != hashable_path(tmppath):
@@ -296,8 +291,8 @@ def clean_path(p):
                                   "'{}'").format(tmppath, path(ptype)))
             tmpfi = FactIndex(tmppath)
             self._path2factindex[hashable_path(tmppath)] = tmpfi
-            self._factindexes.append(tmpfi)
-        self._factindexes = tuple(self._factindexes)
+            factindexes.append(tmpfi)
+        self._factindexes = tuple(factindexes)
 
     def add_facts(self, facts):
         for f in facts:
@@ -328,7 +323,7 @@ def clear(self):
         for fi in self._factindexes: fi.clear()
 
     @property
-    def predicate(self) -> Predicate:
+    def predicate(self) -> Type[Predicate]:
         return self._ptype
 
     @property
diff --git a/clorm/orm/noclingo.py b/clorm/orm/noclingo.py
index 3ea8c32..412af7e 100644
--- a/clorm/orm/noclingo.py
+++ b/clorm/orm/noclingo.py
@@ -140,7 +140,8 @@ def __init__(self, stype: SymbolType, value: Any = None, args: Sequence['NoSymbo
             self._value = int(value)
             self._hash = hash(self._value)
         elif stype == SymbolType.String:
-            _ = value.encode()
+            if not isinstance(value, str):
+                raise TypeError("{value} is not a str")
             self._value = value
             self._hash = hash(self._value)
         elif stype == SymbolType.Function:
@@ -241,7 +242,7 @@ def __gt__(self, other: object) -> bool:
             return True
         if self.positive and other.negative:
             return False
-        return self.arguments > tuple(other.arguments)
+        return self.arguments > tuple(clingo_to_noclingo(arg) for arg in other.arguments)
 
     def __le__(self, other: object) -> bool:
         """Overloaded boolean operator."""
@@ -268,7 +269,7 @@ def __lt__(self, other: object) -> bool:
             return False
         if self.positive and other.negative:
             return True
-        return self.arguments < tuple(other.arguments)
+        return self.arguments < tuple(clingo_to_noclingo(arg) for arg in other.arguments)
 
     def __ge__(self, other: object) -> bool:
         """Overloaded boolean operator."""
@@ -335,7 +336,7 @@ def NoTuple_(arguments: Sequence[NoSymbol]) -> NoSymbol:
 # Functions to convert between clingo.Symbol and noclingo.Symbol
 # --------------------------------------------------------------------------------
 
-def clingo_to_noclingo(clsym: Symbol) -> NoSymbol:
+def clingo_to_noclingo(clsym: AnySymbol) -> NoSymbol:
     if isinstance(clsym, NoSymbol):
         return clsym
     if clsym.type == clingo.SymbolType.Infimum:
@@ -355,7 +356,7 @@ def clingo_to_noclingo(clsym: Symbol) -> NoSymbol:
                       clsym.positive)
 
 
-def noclingo_to_clingo(nclsym: NoSymbol) -> Symbol:
+def noclingo_to_clingo(nclsym: AnySymbol) -> Symbol:
     if isinstance(nclsym, clingo.Symbol):
         return nclsym
     if nclsym.type == SymbolType.Infimum:
diff --git a/clorm/orm/symbols_facts.py b/clorm/orm/symbols_facts.py
index aa34ace..154edab 100644
--- a/clorm/orm/symbols_facts.py
+++ b/clorm/orm/symbols_facts.py
@@ -10,18 +10,21 @@
 # ------------------------------------------------------------------------------
 
 import itertools
-from typing import Iterable, Iterator, Optional, Union
-from collections import abc, defaultdict
+import sys
+from typing import Dict, Iterable, Iterator, List, Optional, Sequence, Set, Tuple, Type, TypeVar, Union, cast, overload
+from collections import defaultdict
 
 import clingo
 import clingo.ast as clast
 from .core import *
-from .core import AnySymbol
-from .noclingo import SymbolMode, SymbolType, Function, Number, String
+from .noclingo import SymbolMode, Function, Number, String
 from .factbase import *
-from .core import get_field_definition, PredicatePath, kwargs_check_keys, \
-    get_symbol_mode
+from .core import AnySymbol, PredicatePath, get_symbol_mode
 
+if sys.version_info < (3, 8):
+    from typing_extensions import Literal
+else:
+    from typing import Literal
 
 __all__ = [
     'SymbolPredicateUnifier',
@@ -38,6 +41,8 @@
 #------------------------------------------------------------------------------
 # Global
 #------------------------------------------------------------------------------
+_PredicateGroups = Dict[Tuple[int, str], List[Type[Predicate]]]
+_P = TypeVar("_P", bound=Predicate)
 
 #------------------------------------------------------------------------------
 # Clorm exception subclasses
@@ -66,16 +71,16 @@ def __init__(self,message: str, line: int, column: int):
 # ------------------------------------------------------------------------------
 
 class Unifier(object):
-    def __init__(self,predicates=[]):
+    def __init__(self,predicates: Iterable[Type[Predicate]]) -> None:
         self._predicates=tuple(predicates)
-        self._pgroups = defaultdict(list)
+        self._pgroups: _PredicateGroups = defaultdict(list)
         self._add_predicates(predicates)
 
-    def _add_predicates(self,predicates=[]):
+    def _add_predicates(self,predicates: Iterable[Type[Predicate]]) -> None:
         for p in predicates:
             self._pgroups[(p.meta.arity,p.meta.name)].append(p)
 
-    def add_predicate(self,predicate):
+    def add_predicate(self,predicate: Type[Predicate]) -> None:
         self._add_predicates([predicate])
 
     def iter_unify(self, symbols: Iterable[AnySymbol], raise_nomatch: bool) -> Iterator[Predicate]:
@@ -108,7 +113,7 @@ def unify(self, symbols: Iterable[AnySymbol], *, factbase: Optional[FactBase]=No
 # matters) and a set of raw clingo symbols against this list.
 # ------------------------------------------------------------------------------
 
-def _unify(predicates, symbols):
+def _unify(predicates: Iterable[Type[Predicate]], symbols: Iterable[AnySymbol]) -> Iterator[Predicate]:
     return Unifier(predicates).iter_unify(symbols, raise_nomatch=False) 
 
 
@@ -123,14 +128,15 @@ class SymbolPredicateUnifier(object):
     decorator.
     """
 
-    def __init__(self, predicates=[], indexes=[], suppress_auto_index=False):
-        self._predicates = ()
-        self._indexes = ()
+    def __init__(self,
+                 predicates: Iterable[Type[Predicate]] = [],
+                 indexes: Iterable[PredicatePath] = [],
+                 suppress_auto_index: bool = False) -> None:
         self._suppress_auto_index = suppress_auto_index
-        tmppreds = []
-        tmpinds = []
-        tmppredset = set()
-        tmpindset = set()
+        tmppreds: List[Type[Predicate]] = []
+        tmpinds: List[PredicatePath] = []
+        tmppredset: Set[Type[Predicate]] = set()
+        tmpindset: Set[PredicatePath.Hashable] = set()
         for pred in predicates:
                 self._register_predicate(pred,tmppreds,tmpinds,tmppredset,tmpindset)
         for fld in indexes:
@@ -138,7 +144,12 @@ def __init__(self, predicates=[], indexes=[], suppress_auto_index=False):
         self._predicates = tuple(tmppreds)
         self._indexes = tuple(tmpinds)
 
-    def _register_predicate(self, cls, predicates, indexes, predicateset, indexset):
+    def _register_predicate(self,
+                            cls: Type[Predicate],
+                            predicates: List[Type[Predicate]],
+                            indexes: List[PredicatePath],
+                            predicateset: Set[Type[Predicate]],
+                            indexset: Set[PredicatePath.Hashable]) -> None:
         if not issubclass(cls, Predicate):
             raise TypeError("{} is not a Predicate sub-class".format(cls))
         if cls in predicateset: return
@@ -150,7 +161,12 @@ def _register_predicate(self, cls, predicates, indexes, predicateset, indexset):
         for fp in cls.meta.indexes:
             self._register_index(fp,predicates,indexes,predicateset,indexset)
 
-    def _register_index(self, path, predicates, indexes, predicateset, indexset):
+    def _register_index(self,
+                        path: PredicatePath,
+                        predicates: Iterable[Type[Predicate]],
+                        indexes: List[PredicatePath],
+                        predicateset: Set[Type[Predicate]],
+                        indexset: Set[PredicatePath.Hashable]) -> None:
         if path.meta.hashable in indexset: return
         if isinstance(path, PredicatePath) and path.meta.predicate in predicateset:
             indexset.add(path.meta.hashable)
@@ -159,7 +175,7 @@ def _register_index(self, path, predicates, indexes, predicateset, indexset):
             raise TypeError("{} is not a predicate field for one of {}".format(
                 path, [ p.__name__ for p in predicates ]))
 
-    def register(self, cls):
+    def register(self, cls: Type[_P])-> Type[_P]:
         if cls in self._predicates: return cls
         predicates = list(self._predicates)
         indexes = list(self._indexes)
@@ -170,7 +186,10 @@ def register(self, cls):
         self._indexes = tuple(indexes)
         return cls
 
-    def unify(self, symbols, delayed_init=False, raise_on_empty=False):
+    def unify(self,
+              symbols: Iterable[AnySymbol],
+              delayed_init: bool=False,
+              raise_on_empty: bool=False) -> FactBase:
         def _populate():
             facts=list(_unify(self.predicates, symbols))
             if not facts and raise_on_empty:
@@ -193,7 +212,20 @@ def indexes(self): return self._indexes
 # the symbol object contained in `symbols`.
 # ------------------------------------------------------------------------------
 
-def unify(unifier,symbols,ordered=False):
+@overload
+def unify(unifier: Union[Iterable[Type[Predicate]], SymbolPredicateUnifier],
+          symbols: Iterable[AnySymbol],
+          ordered: Literal[True]) -> List[Predicate]: ...
+
+
+@overload
+def unify(unifier: Union[Iterable[Type[Predicate]], SymbolPredicateUnifier],
+          symbols: Iterable[AnySymbol]) -> FactBase: ...
+
+
+def unify(unifier: Union[Iterable[Type[Predicate]], SymbolPredicateUnifier],
+          symbols: Iterable[AnySymbol],
+          ordered: bool=False) -> Union[FactBase, List[Predicate]]:
     '''Unify raw symbols against a list of predicates or a SymbolPredicateUnifier.
 
     Symbols are tested against each predicate unifier until a match is
@@ -215,9 +247,8 @@ def unify(unifier,symbols,ordered=False):
         raise ValueError(("The unifier must be a list of predicates "
                           "or a SymbolPredicateUnifier"))
     if ordered:
-        if isinstance(unifier, SymbolPredicateUnifier):
-            unifier=unifier.predicates
-        return list(_unify(unifier,symbols))
+        predicates = unifier.predicates if isinstance(unifier, SymbolPredicateUnifier) else unifier
+        return list(_unify(predicates, symbols))
     else:
         if not isinstance(unifier, SymbolPredicateUnifier):
             unifier=SymbolPredicateUnifier(predicates=unifier)
@@ -239,15 +270,15 @@ def control_add_facts(ctrl: clingo.Control, facts: Iterable[Union[Predicate, cli
     import clingo.ast as ast
 
     def control_add_facts(ctrl: clingo.Control, facts: Iterable[Union[Predicate, clingo.Symbol]]) -> None:
-        with ctrl.builder() as bldr:
+        with ctrl.builder() as bldr:  # type: ignore[attr-defined]
             line=1
             for f in facts:
                 raw=f.raw if isinstance(f,Predicate) else f
                 floc = { "filename" : "<input>", "line" : line , "column" : 1 }
                 location = { "begin" : floc, "end" : floc }
-                r = ast.Rule(location,
-                             ast.Literal(location, ast.Sign.NoSign,
-                                         ast.SymbolicAtom(ast.Symbol(location,raw))),
+                r = ast.Rule(location,  # type: ignore[arg-type]
+                             ast.Literal(location, ast.Sign.NoSign,  # type: ignore[arg-type]
+                                         ast.SymbolicAtom(ast.Symbol(location,raw))),  # type: ignore
                              [])
                 bldr.add(r)
                 line += 1
@@ -267,8 +298,10 @@ def control_add_facts(ctrl: clingo.Control, facts: Iterable[Union[Predicate, cli
 # Function to take a SymbolicAtoms object and extract facts from it
 # ------------------------------------------------------------------------------
 
-def symbolic_atoms_to_facts(symbolic_atoms, unifier, *,
-                            facts_only=False, factbase=None):
+def symbolic_atoms_to_facts(symbolic_atoms: clingo.SymbolicAtoms,
+                            unifier: Iterable[Type[Predicate]], *,
+                            facts_only: bool = False,
+                            factbase: Optional[FactBase] = None) -> FactBase:
     '''Extract `clorm.FactBase` from `clingo.SymbolicAtoms`
 
     A `clingo.SymbolicAtoms` object is returned from the
@@ -378,18 +411,18 @@ class NonFactVisitor:
         ASTType.Defined,
         ASTType.TheoryDefinition})
 
-    def __call__(self, stmt: AST):
+    def __call__(self, stmt: AST) -> None:
         self._stmt = stmt
         self._visit(stmt)
 
-    def _visit(self, ast: AST):
+    def _visit(self, ast: AST) -> None:
         '''
         Dispatch to a visit method.
         '''
         if (ast.ast_type in NonFactVisitor.ERROR_AST or
             (ast.ast_type == ASTType.Function and ast.external)):
-            line = ast.location.begin.line
-            column = ast.location.begin.column
+            line = cast(clast.Location, ast.location).begin.line
+            column = cast(clast.Location, ast.location).begin.column
             exc = FactParserError(message=f"Non-fact '{self._stmt}'",
                                   line=line, column=column)
             raise ClingoParserWrapperError(exc)
@@ -402,8 +435,8 @@ def _visit(self, ast: AST):
             if isinstance(subast, AST):
                 self._visit(subast)
 
-def parse_fact_string(aspstr,unifier,*,factbase=None,
-                      raise_nomatch=False,raise_nonfact=False):
+def parse_fact_string(aspstr: str, unifier: Iterable[Type[Predicate]], *, factbase: Optional[FactBase] = None,
+                      raise_nomatch: bool = False, raise_nonfact: bool = False) -> FactBase:
     '''Parse a string of ASP facts into a FactBase
 
     Facts must be of a simple form that can correspond to clorm predicate
@@ -436,7 +469,7 @@ def parse_fact_string(aspstr,unifier,*,factbase=None,
         if raise_nonfact:
             with clast.ProgramBuilder(ctrl) as bld:
                 nfv = NonFactVisitor()
-                def on_rule(ast: AST):
+                def on_rule(ast: AST) -> None:
                     nonlocal nfv, bld
                     if nfv: nfv(ast)
                     bld.add(ast)
@@ -455,8 +488,8 @@ def on_rule(ast: AST):
 
 
 
-def parse_fact_files(files,unifier,*,factbase=None,
-                     raise_nomatch=False,raise_nonfact=False):
+def parse_fact_files(files: Sequence[str], unifier: Iterable[Type[Predicate]], *, factbase: Optional[FactBase] = None,
+                     raise_nomatch: bool = False, raise_nonfact: bool = False) -> FactBase:
     '''Parse the facts from a list of files into a FactBase
 
     Facts must be of a simple form that can correspond to clorm predicate
@@ -488,7 +521,7 @@ def parse_fact_files(files,unifier,*,factbase=None,
         if raise_nonfact:
             with clast.ProgramBuilder(ctrl) as bld:
                 nfv = NonFactVisitor()
-                def on_rule(ast: AST):
+                def on_rule(ast: AST) -> None:
                     nonlocal nfv, bld
                     if nfv: nfv(ast)
                     bld.add(ast)
@@ -508,7 +541,7 @@ def on_rule(ast: AST):
 # A pure-python fact parser that uses Lark
 #------------------------------------------------------------------------------
 
-from .lark_fact_parser import Lark_StandAlone, Transformer, VisitError, LarkError, UnexpectedInput
+from .lark_fact_parser import Lark_StandAlone, Transformer, LarkError, UnexpectedInput
 
 class _END:
     pass
@@ -547,21 +580,21 @@ def tuple(self, v):
     def start(self, v):
         return v
 
-def lark_parse_fact_string(aspstr,unifier, *,
-                           factbase=None, raise_nomatch=False):
+def lark_parse_fact_string(aspstr: str, unifier: Iterable[Type[Predicate]], *,
+                           factbase: Optional[FactBase] = None, raise_nomatch: bool = False) -> FactBase:
     try:
         fact_parser = Lark_StandAlone(transformer=LarkFactTransformer())
-        symbols = fact_parser.parse(aspstr)
+        symbols =cast(List[clingo.Symbol], fact_parser.parse(aspstr))
         un = Unifier(unifier)
         return un.unify(symbols, factbase=factbase, raise_nomatch=raise_nomatch)
     except UnexpectedInput as e:
         raise FactParserError(str(e), line=e.line, column=e.column)
     except LarkError as e:
-        raise FactParserError(str(e))
+        raise FactParserError(str(e), line=0, column=0)
 
-def lark_parse_fact_files(files, unifier, *,
-                          factbase=None, raise_nomatch=False):
-    fb=FactBase() if factbase is None else factbase
+def lark_parse_fact_files(files: Iterable[str], unifier: Iterable[Type[Predicate]], *,
+                          factbase: Optional[FactBase] = None, raise_nomatch: bool = False) -> FactBase:
+    fb = FactBase() if factbase is None else factbase
     for fn in files:
         with open(fn, 'r') as file:
             aspstr = file.read()
diff --git a/clorm/orm/templating.py b/clorm/orm/templating.py
index bbe3a52..50daae5 100644
--- a/clorm/orm/templating.py
+++ b/clorm/orm/templating.py
@@ -5,14 +5,13 @@
 to help apply these templates.
 
 """
-from typing import Dict
 
 # ------------------------------------------------------------------------------
 # Helper functions for PredicateMeta class to create a Predicate
 # class constructor.
 # ------------------------------------------------------------------------------
 
-def expand_template(template: str, **kwargs: Dict[str, str]):
+def expand_template(template: str, **kwargs: str) -> str:
     """Expand the template by substituting the arguments.
 
     If the argument contains multiple lines then extra spaces are added to each
@@ -72,7 +71,7 @@ def __init__(self,
                          self._sign)
 
 @classmethod
-def _unify(cls: Type[_P], raw: AnySymbol, raw_args: Optional[List[AnySymbol]]=None, raw_name: Optional[str]=None) -> Optional[_P]:
+def _unify(cls: Type[_P], raw: AnySymbol, raw_args: Optional[Sequence[AnySymbol]]=None, raw_name: Optional[str]=None) -> Optional[_P]:
     try:
         raw_args = raw_args if raw_args else raw.arguments
         raw_name = raw_name if raw_name else raw.name
diff --git a/clorm/util/wrapper.py b/clorm/util/wrapper.py
index 7e8faf7..3a1a149 100644
--- a/clorm/util/wrapper.py
+++ b/clorm/util/wrapper.py
@@ -26,7 +26,7 @@
 import inspect
 from typing import Any, Optional, Type, TypeVar, Union, overload
 
-_T = TypeVar("_T", bound=object)
+_T = TypeVar("_T", bound=type)
 
 # ------------------------------------------------------------------------------
 # Make proxy member functions and properties
@@ -114,10 +114,10 @@ def make_class_wrapper(inputclass: Type[object]) -> Type[object]: ...
 
 
 @overload
-def make_class_wrapper(inputclass: Type[object], override: Type[_T]) -> Type[_T]: ...
+def make_class_wrapper(inputclass: Type[object], override: _T) -> _T: ...
 
 
-def make_class_wrapper(inputclass: Type[object], override: Optional[Type[_T]] = None) -> Type[Union[object, _T]]:
+def make_class_wrapper(inputclass: Type[object], override: Optional[_T] = None) -> Union[Type[object], _T]:
     def getattrdoc(cls, key):
         if not cls: return None
         try:


Test patch:
diff --git a/tests/test_orm_atsyntax.py b/tests/test_orm_atsyntax.py
index 0aca240..072f6a7 100644
--- a/tests/test_orm_atsyntax.py
+++ b/tests/test_orm_atsyntax.py
@@ -45,7 +45,7 @@
 class TypeCastSignatureTestCase(unittest.TestCase):
     def setUp(self):
         class DateField(StringField):
-            pytocl = lambda dt: dt.strftime("%Y%m%d")
+            pytocl = lambda dt: datetime.datetime.strftime(dt,"%Y%m%d")
             cltopy = lambda s: datetime.datetime.strptime(s,"%Y%m%d").date()
 
         class DowField(ConstantField):
diff --git a/tests/test_orm_factbase.py b/tests/test_orm_factbase.py
index 07ee9f1..a7f1dd8 100644
--- a/tests/test_orm_factbase.py
+++ b/tests/test_orm_factbase.py
@@ -1545,7 +1545,7 @@ def test_api_complex_query_join_ordered(self):
     #--------------------------------------------------------------------------
     #   Improve error message for bad join statement
     #--------------------------------------------------------------------------
-    def test_api_complex_query_join_ordered(self):
+    def test_api_bad_join_statement(self):
         F = self.F
         G = self.G
         fb = self.factbase
diff --git a/tests/test_orm_query.py b/tests/test_orm_query.py
index a0c902a..d8e3e34 100644
--- a/tests/test_orm_query.py
+++ b/tests/test_orm_query.py
@@ -1803,12 +1803,12 @@ def test_nonapi_oppref_join_order(self):
         joins = pj([F.anum == G.anum, F.anum < GA.anum, cross(G,FA)],[F,G,FA,GA])
         qspec = QuerySpec(roots=[F,G,FA,GA],join=joins,where=[],order_by=[])
         qorder = oppref_join_order([], qspec)
-        self.assertEqual(qorder,[FA,GA,G,F])
+        self.assertEqual(qorder,[F,G,GA,FA])
 
     # ------------------------------------------------------------------------------
     # Test the basic heuristic for generating the join order
     # ------------------------------------------------------------------------------
-    def test_nonapi_oppref_join_order(self):
+    def test_nonapi_basic_join_order(self):
         F = path(self.F)
         G = path(self.G)
         FA = alias(F)