Repository: ramonhagenaars/nptyping

Issue:
"KeyError: 'u'" inside method for isinstance of NDArray
As mentioned in #37, I am having an issue with `isinstance(frame, NDArray[(self.var1, self.var2, 3)])` inside a class method.

```python
""" Drive a Matrix of LED's using a microcontroller.

This relies on a microcontroller that is flashed with specific firmware."""
import time
from pathlib import Path
from typing import Any

import numpy as np  # type:ignore
import serial  # type:ignore
from nptyping import NDArray, UInt8  # type:ignore


class LEDWall:
    def __init__(
        self,
        led_wall_port: serial.Serial,
        width: int,
        height: int,
        serpentine: bool = True,
    ):
        self.led_wall_port = led_wall_port

        self.width = width
        self.height = height
        self._frame_type = NDArray[(self.width, self.height, 3), UInt8]

        self.serpentine = serpentine

    # FIXME: https://github.com/ramonhagenaars/nptyping/issues/37
    def __call__(self, frame: NDArray[(Any, Any, 3), UInt8]) -> None:
        # if (
        #     not frame.shape == (self.width, self.height, 3)
        #     or not frame.dtype == np.uint8
        # ):
        if isinstance(frame, NDArray[(self.width, self.height, 3)]):
            raise RuntimeError("Incorrect frame dimensions")
```

is a (more or less) minimum reproduction of the issue.

The test:
```python
""" test the serial functionality of LEDWall"""

from contextlib import ExitStack as does_not_raise

import numpy as np
import pytest
from nptyping import NDArray

import led_wall_driver_software


class MockSerial:
    def __init__(self):
        self.written = None
        self.has_readline = False

    def write(self, input_):
        self.written = input_

    def readline(self):
        self.has_readline = True


TEST_DATA = [
    (
        1,
        1,
        False,
        np.arange(1 * 1 * 3).reshape(1, 1, 3).astype(np.uint8),
        b"\x00\x01\x02",
        does_not_raise(),
    ),
    (
        1,
        1,
        False,
        np.arange(1 * 2 * 3).reshape(1, 2, 3).astype(np.uint8),
        None,
        pytest.raises(RuntimeError),
    ),
    (
        1,
        1,
        False,
        np.arange(1 * 2 * 3).reshape(1, 2, 3).astype(np.uint16),
        None,
        pytest.raises(RuntimeError),
    ),
    (
        2,
        2,
        True,
        np.arange(2 * 2 * 3).reshape(2, 2, 3).astype(np.uint8),
        b"\x00\x01\x02\x03\x04\x05\x09\x0a\x0b\x06\x07\x08",
        does_not_raise(),
    ),
]


@pytest.mark.parametrize(
    "width,height,serpentinize,input_data,expected,raises", TEST_DATA
)
def test_serpentinize(width, height, serpentinize, input_data, expected, raises):
    with raises:
        mock_serial = MockSerial()
        led_wall = led_wall_driver_software.LEDWall(
            led_wall_port=mock_serial,
            width=width,
            height=height,
            serpentine=serpentinize,
        )

        led_wall(input_data)

        assert mock_serial.written == expected
        assert mock_serial.has_readline
```

the output of `pytest --cov=led_wall_driver_software --cov-config=setup.cfg -x -vvv`:

```
========================================================================== FAILURES ==========================================================================
_______________________________________________ test_serpentinize[1-1-False-input_data0-\x00\x01\x02-raises0] ________________________________________________

width = 1, height = 1, serpentinize = False, input_data = array([[[0, 1, 2]]], dtype=uint8), expected = b'\x00\x01\x02'
raises = <contextlib.ExitStack object at 0x7f5edc8e2ac8>

    @pytest.mark.parametrize(
        "width,height,serpentinize,input_data,expected,raises", TEST_DATA
    )
    def test_serpentinize(width, height, serpentinize, input_data, expected, raises):
        with raises:
            mock_serial = MockSerial()
            led_wall = led_wall_driver_software.LEDWall(
                led_wall_port=mock_serial,
                width=width,
                height=height,
                serpentine=serpentinize,
            )
    
>           led_wall(input_data)

tests/test_serial.py:73: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
led_wall_driver_software/__init__.py:35: in __call__
    if isinstance(frame, NDArray[(self.width, self.height, 3)]):
/home/bkirkbride/Projects/jukebox/led_wall_driver_software/venv/lib/python3.7/site-packages/nptyping/types/_ndarray_meta.py:64: in __instancecheck__
    cls, _NDArray[instance.shape, instance.dtype])
/home/bkirkbride/Projects/jukebox/led_wall_driver_software/venv/lib/python3.7/site-packages/typish/classes/_subscriptable_type.py:37: in __getitem__
    result._after_subscription(item)
/home/bkirkbride/Projects/jukebox/led_wall_driver_software/venv/lib/python3.7/site-packages/nptyping/types/_ndarray_meta.py:121: in _after_subscription
    method(item)
/home/bkirkbride/Projects/jukebox/led_wall_driver_software/venv/lib/python3.7/site-packages/typish/classes/_cls_function.py:68: in __call__
    return callable_(*args, **kwargs)
/home/bkirkbride/Projects/jukebox/led_wall_driver_software/venv/lib/python3.7/site-packages/nptyping/types/_ndarray_meta.py:156: in _sizes_and_type
    cls._only_type(item[1])
/home/bkirkbride/Projects/jukebox/led_wall_driver_software/venv/lib/python3.7/site-packages/nptyping/types/_ndarray_meta.py:134: in _only_type
    cls._type = Any if item is Any else get_type(item)
/home/bkirkbride/Projects/jukebox/led_wall_driver_software/venv/lib/python3.7/site-packages/nptyping/functions/_get_type.py:34: in get_type
    return ClsFunction(_delegates)(obj)
/home/bkirkbride/Projects/jukebox/led_wall_driver_software/venv/lib/python3.7/site-packages/typish/classes/_cls_function.py:68: in __call__
    return callable_(*args, **kwargs)
/home/bkirkbride/Projects/jukebox/led_wall_driver_software/venv/lib/python3.7/site-packages/nptyping/functions/_get_type.py:58: in _get_type_dtype
    return np_type_per_py_type[(py_type(dtype))](dtype)
/home/bkirkbride/Projects/jukebox/led_wall_driver_software/venv/lib/python3.7/site-packages/nptyping/functions/_py_type.py:29: in py_type
    return function(np_type)
/home/bkirkbride/Projects/jukebox/led_wall_driver_software/venv/lib/python3.7/site-packages/typish/classes/_cls_function.py:68: in __call__
    return callable_(*args, **kwargs)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

x = dtype('uint8')

>       np.dtype: lambda x: _TYPE_PER_KIND[x.kind],
        type: lambda x: py_type(np.dtype(x)),
    })
E   KeyError: 'u'

/home/bkirkbride/Projects/jukebox/led_wall_driver_software/venv/lib/python3.7/site-packages/nptyping/functions/_py_type.py:26: KeyError
```

Note that if I comment out the `isinstance` if statement, and un-comment the `if not ... or not ... :` statement everything works as expected.

Discussion:

Patch:
diff --git a/.github/workflows/pythonapp.yml b/.github/workflows/pythonapp.yml
index 0449509..7ecf1fc 100644
--- a/.github/workflows/pythonapp.yml
+++ b/.github/workflows/pythonapp.yml
@@ -7,7 +7,7 @@ jobs:
     runs-on: ${{ matrix.os }}
     strategy:
       matrix:
-        python-version: [ '3.5', '3.6', '3.7', '3.8' ]
+        python-version: [ '3.5', '3.6', '3.7', '3.8', '3.9' ]
         os: [ ubuntu-latest, macOS-latest, windows-latest ]
     name: Python ${{ matrix.python-version }} on ${{ matrix.os }}
     steps:
diff --git a/HISTORY.rst b/HISTORY.rst
index 6796073..d06ee10 100644
--- a/HISTORY.rst
+++ b/HISTORY.rst
@@ -3,6 +3,13 @@
 History
 -------
 
+1.4.0 (2020-12-23)
+++++++++++++++++++
+
+- Added ``SubArrayType``
+- Added ``StructuredType``
+- Added support for unsigned integers with ``py_type``.
+
 1.3.0 (2020-07-21)
 ++++++++++++++++++
 
@@ -53,4 +60,4 @@ History
 0.1.0 (2019-02-05)
 ++++++++++++++++++
 
-- Initial release
\ No newline at end of file
+- Initial release
diff --git a/README.md b/README.md
index ca6cb15..28954f4 100644
--- a/README.md
+++ b/README.md
@@ -132,6 +132,15 @@ NDArray[(3, 3, 5), Int[32]]
 
 ```
 
+A structured array:
+```python
+>>> import numpy as np
+
+>>> NDArray[(Any,...), np.dtype([('x',np.int32), ('y',np.int32)])]
+NDArray[(typing.Any, ...), StructuredType[Int[32], Int[32]]]
+
+```
+
 #### (❒) Checking your instances
 You can use `NDArray` with `isinstance` to dynamically check your arrays.
 
@@ -275,6 +284,28 @@ Object
 
 ```
 
+### (❒) StructuredType
+An nptyping equivalent of numpy structured dtypes.
+
+```python
+>>> from nptyping import StructuredType, Int
+
+>>> StructuredType[Int[32], Int[32]]
+StructuredType[Int[32], Int[32]]
+
+```
+
+### (❒) SubArrayType
+An nptyping equivalent of numpy subarray dtypes.
+
+```python
+>>> from nptyping import SubArrayType, Int
+
+>>> SubArrayType[Int[16], (4,2)]
+SubArrayType[Int[16], (4, 2)]
+
+```
+
 ### (❒) get_type
 With `get_type` you can get `nptyping` equivalent types for your arguments:
 
@@ -285,6 +316,8 @@ With `get_type` you can get `nptyping` equivalent types for your arguments:
 Int[32]
 >>> get_type('some string')
 Unicode[11]
+>>> get_type(np.dtype([('x', np.int32), ('y', np.int32)]))
+StructuredType[Int[32], Int[32]]
 
 ```
 
diff --git a/nptyping/__init__.py b/nptyping/__init__.py
index e15aaac..ba09683 100644
--- a/nptyping/__init__.py
+++ b/nptyping/__init__.py
@@ -26,5 +26,7 @@ from nptyping.types._number import (
     Float64,
 )
 from nptyping.types._object import Object
+from nptyping.types._subarray_type import SubArrayType
+from nptyping.types._structured_type import StructuredType
 from nptyping.types._timedelta64 import Timedelta64
 from nptyping.types._unicode import Unicode
diff --git a/nptyping/_meta.py b/nptyping/_meta.py
index 22d55e0..300b135 100644
--- a/nptyping/_meta.py
+++ b/nptyping/_meta.py
@@ -1,5 +1,5 @@
 __title__ = 'nptyping'
-__version__ = '1.3.0'
+__version__ = '1.4.0'
 __author__ = 'Ramon Hagenaars'
 __author_email__ = 'ramon.hagenaars@gmail.com'
 __description__ = 'Type hints for Numpy.'
diff --git a/nptyping/functions/_get_type.py b/nptyping/functions/_get_type.py
index a1286a4..c570ec3 100644
--- a/nptyping/functions/_get_type.py
+++ b/nptyping/functions/_get_type.py
@@ -19,6 +19,8 @@ from nptyping.types._number import (
     DEFAULT_FLOAT_BITS,
 )
 from nptyping.types._object import Object
+from nptyping.types._subarray_type import SubArrayType, is_subarray_type
+from nptyping.types._structured_type import StructuredType, is_structured_type
 from nptyping.types._timedelta64 import Timedelta64
 from nptyping.types._unicode import Unicode
 
@@ -44,6 +46,10 @@ def _get_type_type(type_: type) -> Type['NPType']:
 
 def _get_type_dtype(dtype: numpy.dtype) -> Type['NPType']:
     # Return the nptyping type of a numpy dtype.
+    if is_subarray_type(dtype):
+        return get_subarray_type(dtype)
+    if is_structured_type(dtype):
+        return get_structured_type(dtype)
     np_type_per_py_type = {
         type: _get_type_type,
         bool: get_type_bool,
@@ -184,6 +190,26 @@ def get_type_complex(_: Any) -> Type[Complex128]:
     return Complex128
 
 
+# Library private.
+def get_structured_type(dtype: numpy.dtype) -> Type[StructuredType]:
+    """
+    Return the NPType that corresponds to dtype of a structured array.
+    :param dtype: a dtype of a structured NumPy array
+    :return: a StructuredType type.
+    """
+    return StructuredType[dtype]
+
+
+# Library private.
+def get_subarray_type(dtype: numpy.dtype) -> Type[SubArrayType]:
+    """
+    Return the NPType that corresponds to dtype of a subarray.
+    :param dtype: a dtype of a NumPy subarray
+    :return: a SubArrayType type.
+    """
+    return SubArrayType[dtype]
+
+
 _delegates = [
     (NPType, lambda x: x),
     (type, _get_type_type),
diff --git a/nptyping/functions/_py_type.py b/nptyping/functions/_py_type.py
index ebbc800..41a6ec5 100644
--- a/nptyping/functions/_py_type.py
+++ b/nptyping/functions/_py_type.py
@@ -31,6 +31,7 @@ def py_type(np_type: Union[np.dtype, type, Literal[Any]]) -> type:
 
 _TYPE_PER_KIND = {
     'i': int,
+    'u': int,
     'f': float,
     'U': str,
     'O': object,
diff --git a/nptyping/types/_ndarray_meta.py b/nptyping/types/_ndarray_meta.py
index a1af12e..790c028 100644
--- a/nptyping/types/_ndarray_meta.py
+++ b/nptyping/types/_ndarray_meta.py
@@ -99,6 +99,7 @@ class _NDArrayMeta(SubscriptableType):
 class _NDArray(NPType, metaclass=_NDArrayMeta):
     _shape = (Any, ...)  # type: Union[Tuple[int, ...], Tuple[Any, EllipsisType]]  # noqa
     _type = Any
+    _special = True  # Added to be able to compile types with sphinx.
 
     @classmethod
     def _after_subscription(cls, item: Any) -> None:
diff --git a/nptyping/types/_structured_type.py b/nptyping/types/_structured_type.py
new file mode 100644
index 0000000..771581c
--- /dev/null
+++ b/nptyping/types/_structured_type.py
@@ -0,0 +1,58 @@
+from typing import Any
+
+import numpy as np
+from typish import SubscriptableType
+
+from nptyping.types._nptype import NPType
+
+
+def is_structured_type(dtype: np.dtype) -> bool:
+    """Detect if the type is a structured array type."""
+    return hasattr(dtype, 'fields') and dtype.fields is not None
+
+
+class _StructuredTypeMeta(SubscriptableType):
+    def __hash__(cls) -> int:
+        return hash(cls.fields)
+
+    def __repr__(cls) -> str:
+        if cls.fields is not None:
+            field_strs = ', '.join([str(f) for f in cls.fields])
+            return 'StructuredType[{}]'.format(field_strs)
+        return 'StructuredType'
+
+    def __eq__(cls, instance: Any) -> bool:
+        if hasattr(instance, 'fields'):
+            return (instance.fields == cls.fields
+                    or tuple(instance.fields) == tuple(cls.fields))
+        return False
+
+    def __instancecheck__(cls, instance: Any) -> bool:
+        from nptyping.functions._get_type import get_type
+
+        return (is_structured_type(instance)
+                and (cls == instance or cls == get_type(instance)))
+
+    __str__ = __repr__
+    __subclasscheck__ = __eq__
+
+
+class StructuredType(NPType, metaclass=_StructuredTypeMeta):
+    """
+    Corresponds to the dtype of a structured NumPy array.
+    """
+    fields = None
+
+    @classmethod
+    def _after_subscription(cls, args: Any) -> None:
+        from nptyping.functions._get_type import get_type
+
+        if isinstance(args, np.dtype):
+            cls.fields = tuple(get_type(args.fields[n][0]) for n in args.names)
+        elif isinstance(args, tuple):
+            cls.fields = tuple(get_type(t) for t in args)
+        elif isinstance(args, type):
+            cls.fields = (get_type(args),)
+        else:
+            raise Exception(
+                'Incompatible arguments to StructuredType: {}'.format(args))
diff --git a/nptyping/types/_subarray_type.py b/nptyping/types/_subarray_type.py
new file mode 100644
index 0000000..66d25ef
--- /dev/null
+++ b/nptyping/types/_subarray_type.py
@@ -0,0 +1,61 @@
+from typing import Any
+
+import numpy as np
+from typish import SubscriptableType
+
+from nptyping.types._nptype import NPType
+
+
+def is_subarray_type(dtype: np.dtype) -> bool:
+    """Detect if the type is a subarray."""
+    return (hasattr(dtype, 'shape')
+            and isinstance(dtype.shape, tuple)
+            and len(dtype.shape) != 0)
+
+
+class _SubArrayTypeMeta(SubscriptableType):
+    def __hash__(cls) -> int:
+        return hash((cls.base, cls.shape))
+
+    def __repr__(cls) -> str:
+        if cls.base is not None:
+            return 'SubArrayType[{0}, {1}]'.format(cls.base, cls.shape)
+        return 'SubArrayType'
+
+    def __eq__(cls, instance: Any) -> bool:
+        if hasattr(instance, 'base') and hasattr(instance, 'shape'):
+            return instance.base == cls.base and instance.shape == cls.shape
+        return False
+
+    def __instancecheck__(cls, instance: Any) -> bool:
+        from nptyping.functions._get_type import get_type
+
+        if is_subarray_type(instance):
+            if cls == instance:
+                return True
+            return cls == get_type(instance)
+        return False
+
+    __str__ = __repr__
+    __subclasscheck__ = __eq__
+
+
+class SubArrayType(NPType, metaclass=_SubArrayTypeMeta):
+    """
+    Corresponds to the dtype of a NumPy subarray.
+    """
+    base = None
+    shape = None
+
+    @classmethod
+    def _after_subscription(cls, args: Any) -> None:
+        from nptyping.functions._get_type import get_type
+
+        if isinstance(args, np.dtype):
+            cls.base = get_type(args.base)
+            cls.shape = args.shape
+        elif isinstance(args, tuple):
+            cls.base, cls.shape = args
+        else:
+            raise Exception(
+                'Incompatible arguments to SubArrayType: {}'.format(args))
diff --git a/setup.cfg b/setup.cfg
index 39f9ffb..ceef6be 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,5 +1,13 @@
 [pylint]
-disable = too-many-instance-attributes,too-many-arguments
-          ,missing-module-docstring,too-few-public-methods,fixme
-          ,invalid-name,import-outside-toplevel,unidiomatic-typecheck
-          ,cyclic-import,duplicate-code
+disable =
+    too-many-instance-attributes,
+    too-many-arguments,
+    missing-module-docstring,
+    too-few-public-methods,
+    fixme,
+    invalid-name,
+    import-outside-toplevel,
+    unidiomatic-typecheck,
+    cyclic-import,
+    duplicate-code,
+    unsubscriptable-object
diff --git a/setup.py b/setup.py
index 12e7a22..f8fe05c 100644
--- a/setup.py
+++ b/setup.py
@@ -13,7 +13,7 @@ with open('README.md', mode='r', encoding='utf-8') as f:
 
 requirements = [
     'numpy',
-    'typish>=1.5.2',
+    'typish>=1.7.0',
 ],
 
 test_requirements = [
@@ -55,6 +55,7 @@ setup(
         'Programming Language :: Python :: 3.5',
         'Programming Language :: Python :: 3.6',
         'Programming Language :: Python :: 3.7',
-        'Programming Language :: Python :: 3.8'
+        'Programming Language :: Python :: 3.8',
+        'Programming Language :: Python :: 3.9',
     ]
 )


Test patch:
diff --git a/tests/test_functions/test_get_type.py b/tests/test_functions/test_get_type.py
index f040e70..f32630b 100644
--- a/tests/test_functions/test_get_type.py
+++ b/tests/test_functions/test_get_type.py
@@ -24,6 +24,8 @@ from nptyping import (
     Bool,
     Datetime64,
     Complex128,
+    StructuredType,
+    SubArrayType,
 )
 from nptyping.types._object import Object
 from nptyping.types._timedelta64 import Timedelta64
@@ -118,3 +120,9 @@ class TestGetType(TestCase):
             ...
 
         self.assertEqual(Object, get_type(SomeRandomClass()))
+
+    def test_get_type_structured_type(self):
+        self.assertEqual(StructuredType[Int[32]], get_type(np.dtype([('x', np.int32)])))
+
+    def test_get_type_subarray_type(self):
+        self.assertEqual(SubArrayType[Int[32], (3,)], get_type(np.dtype((np.int32, 3))))
diff --git a/tests/test_functions/test_py_type.py b/tests/test_functions/test_py_type.py
index f23ae2a..052fe8c 100644
--- a/tests/test_functions/test_py_type.py
+++ b/tests/test_functions/test_py_type.py
@@ -19,6 +19,11 @@ class TestPyType(TestCase):
         self.assertIs(int, py_type(np.int32))
         self.assertIs(int, py_type(np.int64))
 
+        self.assertIs(int, py_type(np.uint))
+        self.assertIs(int, py_type(np.uint16))
+        self.assertIs(int, py_type(np.uint32))
+        self.assertIs(int, py_type(np.uint64))
+
         self.assertIs(float, py_type(np.float))
         self.assertIs(float, py_type(np.float16))
         self.assertIs(float, py_type(np.float32))
diff --git a/tests/test_types/test_ndarray.py b/tests/test_types/test_ndarray.py
index 0f9a1da..9b8f733 100644
--- a/tests/test_types/test_ndarray.py
+++ b/tests/test_types/test_ndarray.py
@@ -2,8 +2,9 @@ from typing import Any, Optional
 from unittest import TestCase
 
 import numpy as np
+from numpy.core.arrayprint import SubArrayFormat
 
-from nptyping import NDArray, DEFAULT_INT_BITS, Int, Bool, Datetime64
+from nptyping import NDArray, DEFAULT_INT_BITS, Int, Bool, Datetime64, StructuredType, SubArrayType
 from nptyping.types._timedelta64 import Timedelta64
 
 
@@ -104,7 +105,7 @@ class TestNDArray(TestCase):
 
     def test_instance_check_types(self):
         arr2x2x2_float = np.array([[[1.0, 2.0], [3.0, 4.0]],
-                                  [[5.0, 6.0], [7.0, 8.0]]])
+                                   [[5.0, 6.0], [7.0, 8.0]]])
         arr2x2x2 = np.array([[[1, 2], [3, 4]], [[5, 6], [7, 8]]])
 
         self.assertTrue(isinstance(arr2x2x2_float, NDArray[(2, 2, 2), float]))
@@ -112,7 +113,7 @@ class TestNDArray(TestCase):
 
     def test_instance_check_types_any(self):
         arr2x2x2_float = np.array([[[1.0, 2.0], [3.0, 4.0]],
-                                  [[5.0, 6.0], [7.0, 8.0]]])
+                                   [[5.0, 6.0], [7.0, 8.0]]])
         arr2x2x2 = np.array([[[1, 2], [3, 4]], [[5, 6], [7, 8]]])
         arr2x2 = np.array([[1, 2], [3, 4]])
 
@@ -202,3 +203,10 @@ class TestNDArray(TestCase):
         self.assertNotIsInstance(np.array([[True, False], [True, 42]]), NDArray[(Any, ...), Bool])
         self.assertIsInstance(np.array([np.datetime64()]), NDArray[(Any, ...), Datetime64])
         self.assertIsInstance(np.array([np.timedelta64()]), NDArray[(Any, ...), Timedelta64])
+
+    def test_instance_check_with_structured_types(self):
+        some_dtype = np.dtype([('x', np.int32), ('y', np.int32, 4)])
+        some_other_dtype = np.dtype([('x', np.int32), ('y', np.int32, 3)])
+        self.assertIsInstance(np.zeros((1,), dtype=some_dtype), NDArray[(Any, ...), some_dtype])
+        self.assertNotIsInstance(np.zeros((1,), dtype=some_dtype), NDArray[(Any, ...), some_other_dtype])
+        self.assertIsInstance(np.zeros((1,), dtype=some_dtype), NDArray[(Any, ...), StructuredType[Int[32], SubArrayType[Int[32], (4,)]]])
diff --git a/tests/test_types/test_structured_type.py b/tests/test_types/test_structured_type.py
new file mode 100644
index 0000000..bca78a3
--- /dev/null
+++ b/tests/test_types/test_structured_type.py
@@ -0,0 +1,59 @@
+from unittest import TestCase
+
+import numpy as np
+from nptyping import Int, StructuredType, Unicode, SubArrayType
+from nptyping.types._structured_type import is_structured_type
+
+
+class TestStructuredType(TestCase):
+
+    def test_equality_of_basic_object_with_no_structure(self):
+        self.assertEqual(StructuredType, StructuredType)
+
+    def test_equality_of_two_structured_types(self):
+        self.assertEqual(StructuredType[Int[32], Unicode[8]], StructuredType[Int[32], Unicode[8]])
+
+    def test_inequality_of_two_structured_types(self):
+        self.assertNotEqual(StructuredType[Int[32], Unicode[8], Int[16]], StructuredType[Int[32], Unicode[8]])
+
+    def test_dtype_constructed_by_hand_is_an_instance_of_structured_type(self):
+        self.assertIsInstance(np.dtype([('x', np.int32), ('y', np.int32)]), StructuredType[Int[32], Int[32]])
+
+    def test_names_dont_matter_when_comparing_two_structured_types(self):
+        self.assertEqual(StructuredType[np.dtype([('x', np.int32), ('y', np.int32)])], StructuredType[np.dtype([('a', np.int32), ('b', np.int32)])])
+
+    def test_dtype_constructed_by_hand_is_not_an_instance_of_structured_type_if_fields_differ(self):
+        self.assertNotIsInstance(np.dtype([('x', np.int32), ('y', np.int16)]), StructuredType[Int[32], Int[32]])
+
+    def test_number_is_not_a_structured_type(self):
+        self.assertFalse(is_structured_type(np.int32))
+
+    def test_structured_array_is_a_structured_type(self):
+        self.assertTrue(is_structured_type(np.dtype([('x', np.int32), ('y', np.int32, (2, 3))])))
+
+    def test_structured_type_by_hand_instance_check(self):
+        some_dtype = np.dtype([('x', np.int32), ('y', np.int32, 4)])
+        self.assertIsInstance(some_dtype, StructuredType[Int[32], SubArrayType[Int[32], (4,)]])
+
+    def test_structured_type_repr_with_no_parameters(self):
+        self.assertEqual(str(StructuredType), 'StructuredType')
+
+    def test_structured_type_repr_with_parameters(self):
+        self.assertEqual(str(StructuredType[Int[16], SubArrayType[Int[32], (4,)]]),
+                         'StructuredType[Int[16], SubArrayType[Int[32], (4,)]]')
+
+    def test_structured_type_single_field(self):
+        self.assertIsInstance(np.dtype([('x', np.int32)]), StructuredType[Int[32]])
+
+    def test_other_type_is_not_an_instance_of_structured_type(self):
+        self.assertNotIsInstance(Int[32], StructuredType[Int[32], Int[32]])
+
+    def test_structured_type_derived_from_dtype_passes_instance_check(self):
+        self.assertIsInstance(StructuredType[np.dtype([('x', np.int32), ('y', np.int32)])], StructuredType[Int[32], Int[32]])
+
+    def test_structured_type_from_heterogenous_base_types(self):
+        self.assertEqual(StructuredType[Int[32], np.int32], StructuredType[Int[32], Int[32]])
+
+    def test_incompatible_parameters_to_structured_type_raise_exception(self):
+        with self.assertRaises(Exception):
+            StructuredType['bad arg']
diff --git a/tests/test_types/test_subarray_type.py b/tests/test_types/test_subarray_type.py
new file mode 100644
index 0000000..59e5efb
--- /dev/null
+++ b/tests/test_types/test_subarray_type.py
@@ -0,0 +1,56 @@
+from unittest import TestCase
+
+import numpy as np
+from nptyping import Int, SubArrayType
+from nptyping.types._subarray_type import is_subarray_type
+
+
+class TestSubArrayType(TestCase):
+    def test_number_is_not_a_subarray_type(self):
+        self.assertFalse(is_subarray_type(np.int32))
+
+    def test_subarray_is_a_subarray_type(self):
+        self.assertTrue(is_subarray_type(np.dtype((int, 4))))
+
+    def test_structured_array_is_not_a_subarray_type(self):
+        self.assertFalse(is_subarray_type(np.dtype([('x', int), ('y', int, (2, 3))])))
+
+    def test_subarray_element_of_structured_array_is_a_structured_type(self):
+        some_type = np.dtype([('x', int), ('y', int, (2, 3))])
+        self.assertTrue(is_subarray_type(some_type.fields['y'][0]))
+
+    def test_subarray_type_constructed_by_hand_is_equal_to_constructed_from_dtype(self):
+        self.assertEqual(SubArrayType[Int[32], (4,)], SubArrayType[np.dtype((np.int32, 4))])
+
+    def test_subarray_type_constructed_by_hand_is_not_equal_to_constructed_from_dtype_if_shape_doesnt_match(self):
+        self.assertNotEqual(SubArrayType[np.int32, (4,)], SubArrayType[np.dtype((np.int32, (4, 1)))])
+
+    def test_subarray_type_constructed_by_hand_is_not_equal_to_constructed_from_dtype_if_base_doesnt_match(self):
+        self.assertNotEqual(SubArrayType[np.int16, (4,)], SubArrayType[np.dtype((np.int32, 4))])
+
+    def test_subarray_dtype_is_an_instance_of_subarray_type(self):
+        self.assertIsInstance(np.dtype((np.int32, 4)), SubArrayType[np.int32, (4,)])
+
+    def test_subarray_dtype_is_not_an_instance_of_subarray_type_if_shape_doesnt_match(self):
+        self.assertNotIsInstance(np.dtype((np.int32, 4)), SubArrayType[np.int32, (4, 1)])
+
+    def test_subarray_dtype_is_not_an_instance_of_subarray_type_if_base_doesnt_match(self):
+        self.assertNotIsInstance(np.dtype((np.int32, 4)), SubArrayType[np.int16, 4])
+
+    def test_subarray_type_repr_with_no_parameters(self):
+        self.assertEqual(str(SubArrayType), 'SubArrayType')
+
+    def test_subarray_type_repr_with_parameters(self):
+        self.assertEqual(str(SubArrayType[Int[16], (4,)]), 'SubArrayType[Int[16], (4,)]')
+
+    def test_other_type_is_not_an_instance_of_subarray_type(self):
+        self.assertNotIsInstance(Int[32], SubArrayType[Int[32], (1,)])
+
+    def test_other_type_does_not_equal_subarray_type(self):
+        self.assertNotEqual(SubArrayType[Int[32], (1,)], Int[32])
+
+    def test_incompatible_parameters_to_subarray_type_raise_exception(self):
+        with self.assertRaises(Exception):
+            SubArrayType[Int[32]]
+        with self.assertRaises(Exception):
+            SubArrayType['bad arg']