Repository: eventbrite/conformity

Issue:
Error: unorderable types: str() < int() in introspect
Whet  `fields.Constant` has values that are not all the same type and `field.introspect()` is called, you get errors like this:

```
unorderable types: str() < int() in introspect
```

Discussion:

Patch:
diff --git a/conformity/fields/basic.py b/conformity/fields/basic.py
index 95173b6..6d9a014 100644
--- a/conformity/fields/basic.py
+++ b/conformity/fields/basic.py
@@ -3,6 +3,7 @@ from __future__ import (
     unicode_literals,
 )
 
+import datetime
 import decimal
 from typing import (  # noqa: F401 TODO Python 3
     Any as AnyType,
@@ -32,6 +33,17 @@ from conformity.utils import (
 )
 
 
+# Blah ... until recursive types are supported, this is not strict enough: https://github.com/python/mypy/issues/731
+Introspection = Dict[
+    six.text_type,
+    Union[
+        int, float, bool, six.text_type, decimal.Decimal, datetime.datetime, datetime.date, datetime.time, None,
+        ListType[AnyType],
+        Dict[AnyType, AnyType],
+    ],
+]
+
+
 @attr.s
 class Base(object):
     """
@@ -44,7 +56,7 @@ class Base(object):
         """
         return [Error('Validation not implemented on base type')]
 
-    def introspect(self):  # type: () -> Dict[six.text_type, AnyType]
+    def introspect(self):  # type: () -> Introspection
         """
         Returns a JSON-serializable dictionary containing introspection data that can be used to document the schema.
         """
@@ -81,15 +93,18 @@ class Constant(Base):
         else:
             self._error_message = 'Value is not one of: {}'.format(', '.join(sorted(_repr(v) for v in self.values)))
 
-    def errors(self, value):
+    def errors(self, value):  # type: (AnyType) -> ListType[Error]
         if value not in self.values:
             return [Error(self._error_message, code=ERROR_CODE_UNKNOWN)]
         return []
 
-    def introspect(self):
+    def introspect(self):  # type: () -> Introspection
         return strip_none({
             'type': self.introspect_type,
-            'values': sorted(self.values),
+            'values': [
+                s if isinstance(s, (six.text_type, bool, int, float, type(None))) else six.text_type(s)
+                for s in sorted(self.values, key=six.text_type)
+            ],
             'description': self.description,
         })
 
@@ -104,10 +119,10 @@ class Anything(Base):
 
     description = attr.ib(default=None, validator=attr_is_optional(attr_is_string()))  # type: Optional[six.text_type]
 
-    def errors(self, value):
+    def errors(self, value):  # type: (AnyType) -> ListType[Error]
         return []
 
-    def introspect(self):
+    def introspect(self):  # type: () -> Introspection
         return strip_none({
             'type': self.introspect_type,
             'description': self.description,
@@ -122,7 +137,7 @@ class Hashable(Anything):
 
     introspect_type = 'hashable'
 
-    def errors(self, value):
+    def errors(self, value):  # type: (AnyType) -> ListType[Error]
         try:
             hash(value)
         except TypeError:
@@ -131,7 +146,7 @@ class Hashable(Anything):
             ]
         return []
 
-    def introspect(self):
+    def introspect(self):  # type: () -> Introspection
         return strip_none({
             'type': self.introspect_type,
             'description': self.description,
@@ -148,14 +163,14 @@ class Boolean(Base):
 
     description = attr.ib(default=None, validator=attr_is_optional(attr_is_string()))  # type: Optional[six.text_type]
 
-    def errors(self, value):
+    def errors(self, value):  # type: (AnyType) -> ListType[Error]
         if not isinstance(value, bool):
             return [
                 Error('Not a boolean'),
             ]
         return []
 
-    def introspect(self):
+    def introspect(self):  # type: () -> Introspection
         return strip_none({
             'type': self.introspect_type,
             'description': self.description,
@@ -190,7 +205,7 @@ class Integer(Base):
     )  # type: Optional[Union[int, float, decimal.Decimal]]
     description = attr.ib(default=None, validator=attr_is_optional(attr_is_string()))  # type: Optional[six.text_type]
 
-    def errors(self, value):
+    def errors(self, value):  # type: (AnyType) -> ListType[Error]
         if not isinstance(value, self.valid_type) or isinstance(value, bool):
             return [Error('Not {}'.format(self.valid_noun))]
 
@@ -205,7 +220,7 @@ class Integer(Base):
             errors.append(Error('Value not <= {}'.format(self.lte)))
         return errors
 
-    def introspect(self):
+    def introspect(self):  # type: () -> Introspection
         return strip_none({
             'type': self.introspect_type,
             'description': self.description,
@@ -253,11 +268,11 @@ class UnicodeString(Base):
     description = attr.ib(default=None, validator=attr_is_optional(attr_is_string()))  # type: Optional[six.text_type]
     allow_blank = attr.ib(default=True, validator=attr_is_bool())  # type: bool
 
-    def __attrs_post_init__(self):
+    def __attrs_post_init__(self):  # type: () -> None
         if self.min_length is not None and self.max_length is not None and self.min_length > self.max_length:
             raise ValueError('min_length cannot be greater than max_length in UnicodeString')
 
-    def errors(self, value):
+    def errors(self, value):  # type: (AnyType) -> ListType[Error]
         if not isinstance(value, self.valid_type):
             return [Error('Not a {}'.format(self.valid_noun))]
         elif self.min_length is not None and len(value) < self.min_length:
@@ -268,7 +283,7 @@ class UnicodeString(Base):
             return [Error('String cannot be blank')]
         return []
 
-    def introspect(self):
+    def introspect(self):  # type: () -> Introspection
         return strip_none({
             'type': self.introspect_type,
             'description': self.description,
@@ -299,7 +314,7 @@ class UnicodeDecimal(Base):
 
     description = attr.ib(default=None, validator=attr_is_optional(attr_is_string()))  # type: Optional[six.text_type]
 
-    def errors(self, value):
+    def errors(self, value):  # type: (AnyType) -> ListType[Error]
         if not isinstance(value, six.text_type):
             return [
                 Error('Invalid decimal value (not unicode string)'),
@@ -312,7 +327,7 @@ class UnicodeDecimal(Base):
             ]
         return []
 
-    def introspect(self):
+    def introspect(self):  # type: () -> Introspection
         return strip_none({
             'type': self.introspect_type,
             'description': self.description,
diff --git a/conformity/fields/country.py b/conformity/fields/country.py
index 64e1328..a6ece0b 100644
--- a/conformity/fields/country.py
+++ b/conformity/fields/country.py
@@ -7,6 +7,7 @@ from typing import (  # noqa: F401 TODO Python 3
     Any as AnyType,
     AnyStr,
     Callable,
+    List as ListType,
 )
 
 import pycountry
@@ -37,6 +38,7 @@ class CountryCodeField(Constant):
         code_filter=lambda x: True,  # type: Callable[[AnyStr], bool]
         **kwargs  # type: AnyType
     ):
+        # type: (...) -> None
         """
         :param code_filter: If specified, will be called to further filter the available country codes
         :type code_filter: lambda x: bool
@@ -47,7 +49,7 @@ class CountryCodeField(Constant):
         super(CountryCodeField, self).__init__(*valid_country_codes, **kwargs)
         self._error_message = 'Not a valid country code'
 
-    def errors(self, value):
+    def errors(self, value):  # type: (AnyType) -> ListType[Error]
         if not isinstance(value, six.text_type):
             return [Error('Not a unicode string')]
         return super(CountryCodeField, self).errors(value)
diff --git a/conformity/fields/currency.py b/conformity/fields/currency.py
index 0fcb60c..e2ace9b 100644
--- a/conformity/fields/currency.py
+++ b/conformity/fields/currency.py
@@ -5,7 +5,9 @@ from __future__ import (
 
 from typing import (  # noqa: F401 TODO Python 3
     AbstractSet,
+    Any as AnyType,
     Iterable,
+    List as ListType,
     Optional,
 )
 
@@ -17,10 +19,11 @@ from conformity.error import (
     ERROR_CODE_INVALID,
     Error,
 )
-from conformity.fields.basic import (
+from conformity.fields.basic import (  # noqa: F401 TODO Python 3
     Base,
     Constant,
     Integer,
+    Introspection,
 )
 from conformity.fields.structures import Dictionary
 from conformity.utils import (
@@ -50,7 +53,7 @@ class Amount(Base):
     lte = attr.ib(default=None, validator=attr_is_optional(attr_is_int()))  # type: Optional[int]
     description = attr.ib(default=None, validator=attr_is_optional(attr_is_string()))  # type: Optional[six.text_type]
 
-    def errors(self, value):
+    def errors(self, value):  # type: (AnyType) -> ListType[Error]
         if not isinstance(value, currint.Amount):
             return [Error(
                 'Not a currint.Amount instance',
@@ -90,7 +93,7 @@ class Amount(Base):
             ))
         return errors
 
-    def introspect(self):
+    def introspect(self):  # type: () -> Introspection
         return strip_none({
             'type': self.introspect_type,
             'description': self.description,
@@ -114,9 +117,10 @@ class AmountDictionary(Dictionary):
         gte=None,  # type: int
         lt=None,  # type: int
         lte=None,  # type: int
-        *args,
-        **kwargs
+        *args,  # type: AnyType
+        **kwargs  # type: AnyType
     ):
+        # type: (...) -> None
         if valid_currencies is not None and (
             not hasattr(valid_currencies, '__iter__') or
             not all(isinstance(c, six.text_type) for c in valid_currencies)
diff --git a/conformity/fields/email.py b/conformity/fields/email.py
index dff8b55..28bed5e 100644
--- a/conformity/fields/email.py
+++ b/conformity/fields/email.py
@@ -7,12 +7,16 @@ import re
 from typing import (  # noqa: F401  TODO Python 3
     Any as AnyType,
     Iterable,
+    List as ListType,
 )
 
 import six
 
 from conformity.error import Error
-from conformity.fields.basic import UnicodeString
+from conformity.fields.basic import (  # noqa: F401 TODO Python 3
+    Introspection,
+    UnicodeString,
+)
 from conformity.fields.net import IPAddress
 from conformity.utils import strip_none
 
@@ -67,7 +71,7 @@ class EmailAddress(UnicodeString):
         if whitelist is not None:
             self.domain_whitelist = whitelist if isinstance(whitelist, frozenset) else frozenset(whitelist)
 
-    def errors(self, value):
+    def errors(self, value):  # type: (AnyType) -> ListType[Error]
         # Get any basic type errors
         result = super(EmailAddress, self).errors(value)
         if result:
@@ -90,7 +94,7 @@ class EmailAddress(UnicodeString):
             return [Error('Not a valid email address (invalid domain field)', pointer=domain_part)]
 
     @classmethod
-    def is_domain_valid(cls, domain_part):
+    def is_domain_valid(cls, domain_part):  # type: (six.text_type) -> bool
         if cls.domain_regex.match(domain_part):
             return True
 
@@ -103,7 +107,7 @@ class EmailAddress(UnicodeString):
                 return True
         return False
 
-    def introspect(self):
+    def introspect(self):  # type: () -> Introspection
         return strip_none({
             'type': self.introspect_type,
             'description': self.description,
diff --git a/conformity/fields/geo.py b/conformity/fields/geo.py
index 0a1cb3f..4d51a66 100644
--- a/conformity/fields/geo.py
+++ b/conformity/fields/geo.py
@@ -14,7 +14,7 @@ class Latitude(Float):
     Latitude coordinate on an ellipsoid or sphere
     """
 
-    def __attrs_post_init__(self):
+    def __attrs_post_init__(self):  # type: () -> None
         # Set end limits if they're not set (and clip any set ones to valid range)
         self.gte = max(-90, self.gte or -100)
         self.lte = min(90, self.lte or 100)
@@ -26,7 +26,7 @@ class Longitude(Float):
     Longitude coordinate on an ellipsoid or sphere
     """
 
-    def __attrs_post_init__(self):
+    def __attrs_post_init__(self):  # type: () -> None
         # Set end limits if they're not set (and clip any set ones to valid range)
         self.gte = max(-180, self.gte or -190)
         self.lte = min(180, self.lte or 190)
diff --git a/conformity/fields/meta.py b/conformity/fields/meta.py
index 5cdcfb7..2804217 100644
--- a/conformity/fields/meta.py
+++ b/conformity/fields/meta.py
@@ -29,8 +29,9 @@ from conformity.error import (
     ValidationError,
     update_error_pointer,
 )
-from conformity.fields.basic import (
+from conformity.fields.basic import (  # noqa: F401 TODO Python 3
     Base,
+    Introspection,
     attr_is_conformity_field,
 )
 from conformity.fields.structures import Dictionary
@@ -56,13 +57,13 @@ class Nullable(Base):
 
     field = attr.ib(validator=attr_is_conformity_field())  # type: Base
 
-    def errors(self, value):
+    def errors(self, value):  # type: (AnyType) -> ListType[Error]
         if value is None:
             return []
 
         return self.field.errors(value)
 
-    def introspect(self):
+    def introspect(self):  # type: () -> Introspection
         return {
             'type': self.introspect_type,
             'nullable': self.field.introspect(),
@@ -76,12 +77,12 @@ class Null(Base):
 
     introspect_type = 'null'
 
-    def errors(self, value):
+    def errors(self, value):  # type: (AnyType) -> ListType[Error]
         if value is not None:
             return [Error('Value is not null')]
         return []
 
-    def introspect(self):
+    def introspect(self):  # type: () -> Introspection
         return {'type': self.introspect_type}
 
 
@@ -98,7 +99,7 @@ class Polymorph(Base):
     contents_map = attr.ib(validator=attr_is_instance(dict))  # type: Mapping[HashableType, Base]
     description = attr.ib(default=None, validator=attr_is_optional(attr_is_string()))  # type: Optional[six.text_type]
 
-    def errors(self, value):
+    def errors(self, value):  # type: (AnyType) -> ListType[Error]
         # Get switch field value
         bits = self.switch_field.split('.')
         switch_value = value
@@ -114,7 +115,7 @@ class Polymorph(Base):
         # Run field errors
         return field.errors(value)
 
-    def introspect(self):
+    def introspect(self):  # type: () -> Introspection
         return strip_none({
             'type': self.introspect_type,
             'description': self.description,
@@ -137,12 +138,12 @@ class ObjectInstance(Base):
     valid_type = attr.ib(validator=attr_is_instance_or_instance_tuple(type))  # type: Union[Type, TupleType[Type, ...]]
     description = attr.ib(default=None, validator=attr_is_optional(attr_is_string()))  # type: Optional[six.text_type]
 
-    def errors(self, value):
+    def errors(self, value):  # type: (AnyType) -> ListType[Error]
         if not isinstance(value, self.valid_type):
-            return [Error('Not an instance of {}'.format(self.valid_type.__name__))]
+            return [Error('Not an instance of {}'.format(getattr(self.valid_type, '__name__', repr(self.valid_type))))]
         return []
 
-    def introspect(self):
+    def introspect(self):  # type: () -> Introspection
         return strip_none({
             'type': self.introspect_type,
             'description': self.description,
@@ -201,7 +202,7 @@ class PythonPath(Base):
 
         return []
 
-    def introspect(self):  # type: () -> Dict[six.text_type, AnyType]
+    def introspect(self):  # type: () -> Introspection
         return strip_none({
             'type': self.introspect_type,
             'description': self.description,
@@ -244,7 +245,7 @@ class TypeReference(Base):
     )  # type: Optional[Union[Type, TupleType[Type, ...]]]
     description = attr.ib(default=None, validator=attr_is_optional(attr_is_string()))  # type: Optional[six.text_type]
 
-    def errors(self, value):
+    def errors(self, value):  # type: (AnyType) -> ListType[Error]
         if not isinstance(value, type):
             return [Error('Not a type')]
 
@@ -253,7 +254,7 @@ class TypeReference(Base):
 
         return []
 
-    def introspect(self):
+    def introspect(self):  # type: () -> Introspection
         base_classes = None
         if self.base_classes:
             if isinstance(self.base_classes, type):
@@ -347,7 +348,7 @@ class ClassConfigurationSchema(Base):
     eager_default_validation = attr.ib(default=True, validator=attr_is_bool())  # type: bool
     add_class_object_to_dict = attr.ib(default=True, validator=attr_is_bool())  # type: bool
 
-    def __attrs_post_init__(self):
+    def __attrs_post_init__(self):  # type: () -> None
         self._schema_cache = {}  # type: Dict[six.text_type, Dictionary]
 
         if not self.base_class:
@@ -373,7 +374,7 @@ class ClassConfigurationSchema(Base):
             # If the default path is specified and eager validation is not disabled, validate the default path.
             self.initiate_cache_for(self.default_path)
 
-    def errors(self, value):
+    def errors(self, value):  # type: (AnyType) -> ListType[Error]
         if not isinstance(value, Mapping):
             return [Error('Not a mapping (dictionary)')]
 
@@ -451,11 +452,11 @@ class ClassConfigurationSchema(Base):
 
         return clazz(**configuration.get('kwargs', {}))
 
-    def introspect(self):
+    def introspect(self):  # type: () -> Introspection
         return strip_none({
             'type': self.introspect_type,
             'description': self.description,
-            'base_class': six.text_type(self.base_class.__name__),
+            'base_class': six.text_type(self.base_class.__name__) if self.base_class else None,
             'default_path': self.default_path,
             'switch_field': 'path',
             'switch_field_schema': self.switch_field_schema.introspect(),
@@ -501,7 +502,7 @@ class Any(Base):
         if kwargs:
             raise TypeError('Unknown keyword arguments: {}'.format(', '.join(kwargs.keys())))
 
-    def errors(self, value):
+    def errors(self, value):  # type: (AnyType) -> ListType[Error]
         result = []  # type: ListType[Error]
         for option in self.options:
             sub_errors = option.errors(value)
@@ -512,7 +513,7 @@ class Any(Base):
             result.extend(sub_errors)
         return result
 
-    def introspect(self):
+    def introspect(self):  # type: () -> Introspection
         return strip_none({
             'type': self.introspect_type,
             'description': self.description,
@@ -544,13 +545,13 @@ class All(Base):
         if kwargs:
             raise TypeError('Unknown keyword arguments: {}'.format(', '.join(kwargs.keys())))
 
-    def errors(self, value):
+    def errors(self, value):  # type: (AnyType) -> ListType[Error]
         result = []  # type: ListType[Error]
         for requirement in self.requirements:
             result.extend(requirement.errors(value) or [])
         return result
 
-    def introspect(self):
+    def introspect(self):  # type: () -> Introspection
         return strip_none({
             'type': self.introspect_type,
             'description': self.description,
@@ -572,7 +573,7 @@ class BooleanValidator(Base):
     error = attr.ib(validator=attr_is_string())  # type: six.text_type
     description = attr.ib(default=None, validator=attr_is_optional(attr_is_string()))  # type: Optional[six.text_type]
 
-    def errors(self, value):
+    def errors(self, value):  # type: (AnyType) -> ListType[Error]
         # Run the validator, but catch any errors and return them as an error.
         try:
             ok = self.validator(value)
@@ -584,7 +585,7 @@ class BooleanValidator(Base):
         else:
             return [Error(self.error)]
 
-    def introspect(self):
+    def introspect(self):  # type: () -> Introspection
         return strip_none({
             'type': self.introspect_type,
             'description': self.description,
diff --git a/conformity/fields/net.py b/conformity/fields/net.py
index 1cdad79..ed1e865 100644
--- a/conformity/fields/net.py
+++ b/conformity/fields/net.py
@@ -3,14 +3,20 @@ from __future__ import (
     unicode_literals,
 )
 
-import functools
 import re
+from typing import (  # noqa: F401 TODO Python 3
+    Any as AnyType,
+    List as ListType,
+)
 
 import attr
 import six
 
 from conformity.error import Error
-from conformity.fields.basic import UnicodeString
+from conformity.fields.basic import (  # noqa: F401 TODO Python 3
+    Introspection,
+    UnicodeString,
+)
 from conformity.fields.meta import Any
 from conformity.utils import strip_none
 
@@ -23,7 +29,7 @@ class IPv4Address(UnicodeString):
 
     introspect_type = 'ipv4_address'
 
-    def errors(self, value):
+    def errors(self, value):  # type: (AnyType) -> ListType[Error]
         # Get any basic type errors
         result = super(IPv4Address, self).errors(value)
         if result:
@@ -34,7 +40,7 @@ class IPv4Address(UnicodeString):
         else:
             return [Error('Not a valid IPv4 address')]
 
-    def introspect(self):
+    def introspect(self):  # type: () -> Introspection
         return strip_none({
             'type': self.introspect_type,
             'description': self.description,
@@ -46,7 +52,7 @@ class IPv6Address(UnicodeString):
 
     introspect_type = 'ipv6_address'
 
-    def errors(self, value):
+    def errors(self, value):  # type: (AnyType) -> ListType[Error]
         # Get any basic type errors
         result = super(IPv6Address, self).errors(value)
         if result:
@@ -93,7 +99,7 @@ class IPv6Address(UnicodeString):
         return []
 
     @staticmethod
-    def expand_ipv6_address(value):
+    def expand_ipv6_address(value):  # type: (six.text_type) -> six.text_type
         """
         Expands a potentially-shortened IPv6 address into its full length
         """
@@ -116,19 +122,17 @@ class IPv6Address(UnicodeString):
         # Now need to make sure every hextet is 4 lower case characters.
         # If a hextet is < 4 characters, we've got missing leading 0's.
         ret_ip = []
-        for hextet in new_ip:
-            ret_ip.append(('0' * (4 - len(hextet)) + hextet).lower())
+        for hextet_str in new_ip:
+            ret_ip.append(('0' * (4 - len(hextet_str)) + hextet_str).lower())
         return ':'.join(ret_ip)
 
-    def introspect(self):
+    def introspect(self):  # type: () -> Introspection
         return strip_none({
             'type': self.introspect_type,
             'description': self.description,
         })
 
 
-IPAddress = functools.partial(
-    Any,
-    IPv4Address(),
-    IPv6Address(),
-)
+class IPAddress(Any):
+    def __init__(self, **kwargs):  # type: (**AnyType) -> None
+        super(IPAddress, self).__init__(IPv4Address(), IPv6Address(), **kwargs)
diff --git a/conformity/fields/structures.py b/conformity/fields/structures.py
index bea92a7..1363f5a 100644
--- a/conformity/fields/structures.py
+++ b/conformity/fields/structures.py
@@ -9,6 +9,7 @@ from typing import (  # noqa: F401 TODO Python 3
     Dict,
     FrozenSet,
     Hashable as HashableType,
+    List as ListType,
     Mapping,
     Optional,
     Sized,
@@ -27,10 +28,11 @@ from conformity.error import (
     Error,
     update_error_pointer,
 )
-from conformity.fields.basic import (
+from conformity.fields.basic import (  # noqa: F401 TODO Python 3
     Anything,
     Base,
     Hashable,
+    Introspection,
 )
 from conformity.utils import (
     attr_is_instance,
@@ -58,11 +60,11 @@ class List(Base):
     introspect_type = type_noun  # type: six.text_type
     type_error = 'Not a list'  # type: six.text_type
 
-    def __attrs_post_init__(self):
+    def __attrs_post_init__(self):  # type: () -> None
         if self.min_length is not None and self.max_length is not None and self.min_length > self.max_length:
             raise ValueError('min_length cannot be greater than max_length in UnicodeString')
 
-    def errors(self, value):
+    def errors(self, value):  # type: (AnyType) -> ListType[Error]
         if not isinstance(value, self.valid_types):
             return [Error(self.type_error)]
 
@@ -89,7 +91,7 @@ class List(Base):
         # where the pointer is the value converted to a string instead of an index.
         return ((cls.LazyPointer(i, value), value) for i, value in enumerate(values))
 
-    def introspect(self):
+    def introspect(self):  # type: () -> Introspection
         return strip_none({
             'type': self.introspect_type,
             'contents': self.contents.introspect(),
@@ -142,7 +144,7 @@ class Dictionary(Base):
     allow_extra_keys = attr.ib(default=None)  # type: bool
     description = attr.ib(default=None, validator=attr_is_optional(attr_is_string()))  # type: Optional[six.text_type]
 
-    def __attrs_post_init__(self):
+    def __attrs_post_init__(self):  # type: () -> None
         if self.contents is None and getattr(self.__class__, 'contents', None) is not None:
             # If no contents were provided but a subclass has hard-coded contents, use those
             self.contents = self.__class__.contents
@@ -176,7 +178,7 @@ class Dictionary(Base):
         if self.description is not None and not isinstance(self.description, six.text_type):
             raise TypeError("'description' must be a unicode string")
 
-    def errors(self, value):
+    def errors(self, value):  # type: (AnyType) -> ListType[Error]
         if not isinstance(value, dict):
             return [Error('Not a dict')]
 
@@ -243,8 +245,8 @@ class Dictionary(Base):
             description=self.description if description is None else description,
         )
 
-    def introspect(self):
-        display_order = None
+    def introspect(self):  # type: () -> Introspection
+        display_order = None  # type: Optional[ListType[AnyType]]
         if isinstance(self.contents, OrderedDict):
             display_order = list(self.contents.keys())
 
@@ -279,11 +281,11 @@ class SchemalessDictionary(Base):
     min_length = attr.ib(default=None, validator=attr_is_optional(attr_is_int()))  # type: Optional[int]
     description = attr.ib(default=None, validator=attr_is_optional(attr_is_string()))  # type: Optional[six.text_type]
 
-    def __attrs_post_init__(self):
+    def __attrs_post_init__(self):  # type: () -> None
         if self.min_length is not None and self.max_length is not None and self.min_length > self.max_length:
             raise ValueError('min_length cannot be greater than max_length in UnicodeString')
 
-    def errors(self, value):
+    def errors(self, value):  # type: (AnyType) -> ListType[Error]
         if not isinstance(value, dict):
             return [Error('Not a dict')]
 
@@ -306,13 +308,13 @@ class SchemalessDictionary(Base):
 
         return result
 
-    def introspect(self):
+    def introspect(self):  # type: () -> Introspection
         result = {
             'type': self.introspect_type,
             'max_length': self.max_length,
             'min_length': self.min_length,
             'description': self.description,
-        }
+        }  # type: Introspection
         # We avoid using isinstance() here as that would also match subclass instances
         if not self.key_type.__class__ == Hashable:
             result['key_type'] = self.key_type.introspect()
@@ -342,7 +344,7 @@ class Tuple(Base):
         if kwargs:
             raise TypeError('Unknown keyword arguments: {}'.format(', '.join(kwargs.keys())))
 
-    def errors(self, value):
+    def errors(self, value):  # type: (AnyType) -> ListType[Error]
         if not isinstance(value, tuple):
             return [Error('Not a tuple')]
 
@@ -360,7 +362,7 @@ class Tuple(Base):
 
         return result
 
-    def introspect(self):
+    def introspect(self):  # type: () -> Introspection
         return strip_none({
             'type': self.introspect_type,
             'contents': [value.introspect() for value in self.contents],
diff --git a/conformity/fields/temporal.py b/conformity/fields/temporal.py
index 884f086..62bbd42 100644
--- a/conformity/fields/temporal.py
+++ b/conformity/fields/temporal.py
@@ -5,7 +5,9 @@ from __future__ import (
 
 import datetime
 from typing import (  # noqa: F401 TODO Python 3
+    Any as AnyType,
     FrozenSet,
+    List as ListType,
     Optional,
     Tuple as TupleType,
     Type,
@@ -16,7 +18,10 @@ import attr
 import six  # noqa: F401 TODO Python 3
 
 from conformity.error import Error
-from conformity.fields.basic import Base
+from conformity.fields.basic import (  # noqa: F401 TODO Python 3
+    Base,
+    Introspection,
+)
 from conformity.utils import (
     attr_is_optional,
     attr_is_string,
@@ -52,7 +57,7 @@ class TemporalBase(Base):
     lte = attr.ib(default=None)  # type: Union[datetime.date, datetime.time, datetime.datetime, datetime.timedelta]
     description = attr.ib(default=None, validator=attr_is_optional(attr_is_string()))  # type: Optional[six.text_type]
 
-    def __attrs_post_init__(self):
+    def __attrs_post_init__(self):  # type: () -> None
         if self.gt is not None and self._invalid(self.gt):
             raise TypeError("'gt' value {!r} cannot be used for comparisons in this type".format(self.gt))
         if self.gte is not None and self._invalid(self.gte):
@@ -68,7 +73,7 @@ class TemporalBase(Base):
             not cls.valid_isinstance or not isinstance(value, cls.valid_isinstance)
         )
 
-    def errors(self, value):
+    def errors(self, value):  # type: (AnyType) -> ListType[Error]
         if self._invalid(value):
             # using stricter type checking, because date is subclass of datetime, but they're not comparable
             return [Error('Not a {} instance'.format(self.valid_noun))]
@@ -84,14 +89,14 @@ class TemporalBase(Base):
             errors.append(Error('Value not <= {}'.format(self.lte)))
         return errors
 
-    def introspect(self):
+    def introspect(self):  # type: () -> Introspection
         return strip_none({
             'type': self.introspect_type,
             'description': self.description,
-            'gt': self.gt,
-            'gte': self.gte,
-            'lt': self.lt,
-            'lte': self.lte,
+            'gt': six.text_type(self.gt) if self.gt else None,
+            'gte': six.text_type(self.gte) if self.gte else None,
+            'lt': six.text_type(self.lt) if self.lt else None,
+            'lte': six.text_type(self.lte) if self.lte else None,
         })
 
 
diff --git a/conformity/utils.py b/conformity/utils.py
index fefbf54..13c1aba 100644
--- a/conformity/utils.py
+++ b/conformity/utils.py
@@ -4,13 +4,20 @@ from __future__ import (
 )
 
 import decimal
-from typing import Dict  # noqa: F401 TODO Python 3
+from typing import (  # noqa: F401 TODO Python 3
+    Dict,
+    TypeVar,
+)
 
 import attr
 import six
 
 
-def strip_none(value):  # type: (Dict) -> Dict
+KT = TypeVar('KT')
+VT = TypeVar('VT')
+
+
+def strip_none(value):  # type: (Dict[KT, VT]) -> Dict[KT, VT]
     """
     Takes a dict and removes all keys that have `None` values, used mainly for tidying up introspection responses. Take
     care not to use this on something that might legitimately contain a `None`.


Test patch:
diff --git a/tests/test_fields.py b/tests/test_fields.py
index 0efd449..265676f 100644
--- a/tests/test_fields.py
+++ b/tests/test_fields.py
@@ -15,6 +15,7 @@ import unittest
 import freezegun
 import pytest
 import pytz
+import six
 
 from conformity.error import (
     ERROR_CODE_MISSING,
@@ -170,19 +171,19 @@ class FieldTests(unittest.TestCase):
         self.assertEqual('dictionary', introspection['type'])
         self.assertFalse(introspection['allow_extra_keys'])
         self.assertEqual([], introspection['optional_keys'])
-        self.assertEqual(3, len(introspection['contents']))
-        self.assertIn('child_ids', introspection['contents'])
+        self.assertEqual(3, len(introspection['contents']))  # type: ignore
+        self.assertIn('child_ids', introspection['contents'])  # type: ignore
         self.assertEqual(
             {
                 'type': 'list',
                 'contents': {'gt': 0, 'type': 'integer'},
             },
-            introspection['contents']['child_ids'],
+            introspection['contents']['child_ids'],  # type: ignore
         )
-        self.assertIn('address', introspection['contents'])
-        self.assertEqual('dictionary', introspection['contents']['address']['type'])
-        self.assertFalse(introspection['contents']['address']['allow_extra_keys'])
-        self.assertEqual({'line2', 'state'}, set(introspection['contents']['address']['optional_keys']))
+        self.assertIn('address', introspection['contents'])  # type: ignore
+        self.assertEqual('dictionary', introspection['contents']['address']['type'])  # type: ignore
+        self.assertFalse(introspection['contents']['address']['allow_extra_keys'])  # type: ignore
+        self.assertEqual({'line2', 'state'}, set(introspection['contents']['address']['optional_keys']))  # type: ignore
         self.assertEqual(
             {
                 'city': {'type': 'unicode'},
@@ -192,14 +193,14 @@ class FieldTests(unittest.TestCase):
                 'postcode': {'type': 'unicode'},
                 'state': {'type': 'unicode'},
             },
-            introspection['contents']['address']['contents'],
+            introspection['contents']['address']['contents'],  # type: ignore
         )
         self.assertEqual(
             {
                 'type': 'set',
                 'contents': {'type': 'unicode'},
             },
-            introspection['contents']['unique_things'],
+            introspection['contents']['unique_things'],  # type: ignore
         )
 
     def test_dictionary_extension(self):  # type: () -> None
@@ -368,7 +369,7 @@ class FieldTests(unittest.TestCase):
             datetime_schema.introspect(),
             {
                 'type': 'datetime',
-                'gt': past1985,
+                'gt': six.text_type(past1985),
             },
         )
 
diff --git a/tests/test_settings.py b/tests/test_settings.py
index 95d22ff..32863d9 100644
--- a/tests/test_settings.py
+++ b/tests/test_settings.py
@@ -224,21 +224,21 @@ class TestSettingsOne(object):
 
         keys = settings.keys()
         assert isinstance(keys, KeysView if not six.PY2 else list)
-        keys = list(keys)
-        assert 'foo' in keys
-        assert 'bar' in keys
+        keys_list = list(keys)
+        assert 'foo' in keys_list
+        assert 'bar' in keys_list
 
         values = settings.values()
         assert isinstance(values, ValuesView if not six.PY2 else list)
-        values = list(values)
-        assert 'Cool' in values
-        assert True in values
+        values_list = list(values)
+        assert 'Cool' in values_list
+        assert True in values_list
 
         items = settings.items()
         assert isinstance(items, ItemsView if not six.PY2 else list)
-        items = list(items)
-        assert ('foo', 'Cool') in items
-        assert ('bar', True) in items
+        items_list = list(items)
+        assert ('foo', 'Cool') in items_list
+        assert ('bar', True) in items_list
 
         assert settings.get('foo') == 'Cool'
         assert settings.get('bar', False) is True