diff --git a/polygraph/types/schema.py b/polygraph/types/schema.py
new file mode 100644
index 0000000..6c98fed
--- /dev/null
+++ b/polygraph/types/schema.py
@@ -0,0 +1,61 @@
+from polygraph.exceptions import PolygraphSchemaError
+from polygraph.types.api import typedef
+from polygraph.types.definitions import TypeKind
+from polygraph.types.object_type import ObjectType
+from polygraph.utils.strict_dict import StrictDict
+
+
+def typename(type_):
+    return typedef(type_).name
+
+
+def build_type_map(type_list):
+    type_map = StrictDict()
+
+    for type_ in type_list:
+        typedef = type_.__type
+        kind = typedef.kind
+        type_map[typename(type_)] = type_
+        if kind in (TypeKind.SCALAR, TypeKind.ENUM):
+            continue
+        elif kind in (TypeKind.LIST, TypeKind.NON_NULL):
+            of_type = typedef.of_type
+            subtype_map = build_type_map([of_type])
+            type_map.update(subtype_map)
+
+        elif kind == TypeKind.UNION:
+            possible_types = typedef.possible_types
+            subtype_map = build_type_map(possible_types)
+            type_map.update(subtype_map)
+
+        elif kind == TypeKind.OBJECT:
+            field_types = []
+            for field in typedef.fields:
+                field_types.append(field.return_type)
+                field_types.extend(field.arg_types.values() if field.arg_types else [])
+            subtype_map = build_type_map(field_types)
+            type_map.update(subtype_map)
+
+        elif kind == TypeKind.INPUT_OBJECT:
+            # FIXME
+            pass
+
+        elif kind == TypeKind.INTERFACE:
+            # FIXME
+            pass
+
+    return type_map
+
+
+class Schema:
+    def __init__(self, query: ObjectType, mutation=None, directives=None, additional_types=None):
+        if not issubclass(query, ObjectType):
+            raise PolygraphSchemaError("Query must be an ObjectType")
+        self.query = query
+        self.mutation = mutation
+        self.directives = directives
+        initial_types = additional_types or []
+        initial_types.append(query)
+        if mutation:
+            initial_types.append(mutation)
+        self.type_map = build_type_map(initial_types)
diff --git a/polygraph/types/type_builder.py b/polygraph/types/type_builder.py
index 943b40d..2b20577 100644
--- a/polygraph/types/type_builder.py
+++ b/polygraph/types/type_builder.py
@@ -77,7 +77,7 @@ class Union(PolygraphOutputType, PolygraphType):
         return type(name, bases, attrs)
 
 
-class List(PolygraphType):
+class List(PolygraphOutputType, PolygraphType):
     """
     A GraphQL list is a special collection type which declares the
     type of each item in the List (referred to as the item type of
diff --git a/polygraph/utils/strict_dict.py b/polygraph/utils/strict_dict.py
new file mode 100644
index 0000000..98f7dc7
--- /dev/null
+++ b/polygraph/utils/strict_dict.py
@@ -0,0 +1,11 @@
+class StrictDict(dict):
+    """
+    A dictionary that raises an exception if attempting to associate a known
+    key with a different value
+    """
+    def __setitem__(self, key, item):
+        if key in self and self[key] != item:
+            raise ValueError(
+                "Attempting to set {} to a different value {}".format(key, item)
+            )
+        return super().__setitem__(key, item)

Typemap builder
diff --git a/polygraph/types/tests/test_type_map.py b/polygraph/types/tests/test_type_map.py
new file mode 100644
index 0000000..78476f0
--- /dev/null
+++ b/polygraph/types/tests/test_type_map.py
@@ -0,0 +1,41 @@
+from unittest import TestCase
+
+from polygraph.types.decorators import field
+from polygraph.types.object_type import ObjectType
+from polygraph.types.scalar import ID, Boolean, Float, Int, String
+from polygraph.types.schema import Schema
+from polygraph.types.type_builder import List, NonNull, Union
+
+
+class Person(ObjectType):
+    @field()
+    def name(self) -> NonNull(String):
+        pass
+
+    @field()
+    def age(year: Int) -> String:
+        pass
+
+
+class Animal(ObjectType):
+    @field()
+    def can_walk(self) -> Boolean:
+        pass
+
+
+class Query(ObjectType):
+    @field()
+    def characters(self) -> List(Union(Animal, Person)):
+        pass
+
+
+class TypeMapTest(TestCase):
+    def test_type_map_builder(self):
+        schema = Schema(query=Query, additional_types=[ID])
+        type_map = schema.type_map
+        self.assertEqual(type_map["Animal"], Animal)
+        self.assertEqual(type_map["Person"], Person)
+        self.assertEqual(type_map["Animal|Person"], Union(Animal, Person))
+        self.assertEqual(type_map["Boolean"], Boolean)
+        self.assertEqual(type_map["ID"], ID)
+        self.assertNotIn(Float, type_map.values())  # Float was not defined anywhere
diff --git a/polygraph/utils/tests/test_strict_dict.py b/polygraph/utils/tests/test_strict_dict.py
new file mode 100644
index 0000000..b000dab
--- /dev/null
+++ b/polygraph/utils/tests/test_strict_dict.py
@@ -0,0 +1,20 @@
+from unittest import TestCase
+
+from polygraph.utils.strict_dict import StrictDict
+
+
+class StrictDictTest(TestCase):
+    def test_cannot_update_same_key_with_different_value(self):
+        d = StrictDict()
+        d["George"] = "Washington"
+        d["John"] = "Adams"
+        with self.assertRaises(ValueError):
+            d["George"] = "Bush"
+        self.assertEqual(d["George"], "Washington")
+
+    def test_can_update_same_key_with_same_value(self):
+        d = StrictDict()
+        d["George"] = "Bush"
+        d["Bill"] = "Clinton"
+        d["George"] = "Bush"
+        self.assertEqual(d["George"], "Bush")

pip install -e .[dev]
parse_log_pytest
requirements.txt
pytest
apt-get update apt-get install -y gcc
3.9
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
args==0.1.0
asttokens==3.0.0
autopep8==2.3.2
certifi==2025.1.31
charset-normalizer==3.4.1
clint==0.5.1
coverage==7.8.0
decorator==5.2.1
distlib==0.3.9
exceptiongroup==1.2.2
executing==2.2.0
filelock==3.18.0
flake8==7.2.0
graphql-core==3.2.6
idna==3.10
iniconfig==2.1.0
ipython==8.18.1
isort==6.0.1
jedi==0.19.2
matplotlib-inline==0.1.7
mccabe==0.7.0
packaging==24.2
parso==0.8.4
pbr==6.1.1
pexpect==4.9.0
pkginfo==1.12.1.2
platformdirs==4.3.7
pluggy==1.5.0
-e git+https://github.com/polygraph-python/polygraph.git@a3023ad2c3207477fc4a0798d2e17da42ee8e6d2#egg=polygraph
prompt_toolkit==3.0.50
ptyprocess==0.7.0
pudb==2017.1.2
pure_eval==0.2.3
pycodestyle==2.13.0
pyflakes==3.3.1
Pygments==2.19.1
pytest==8.3.5
requests==2.32.3
requests-toolbelt==1.0.0
stack-data==0.6.3
stevedore==5.4.1
tomli==2.2.1
traitlets==5.14.3
twine==1.8.1
typing_extensions==4.13.0
urllib3==2.3.0
urwid==2.6.16
virtualenv==20.29.3
virtualenv-clone==0.5.7
virtualenvwrapper==6.1.1
wcwidth==0.2.13

name: polygraph
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - pip=25.0=py39h06a4308_0
  - python=3.9.21=he870216_1
  - readline=8.2=h5eee18b_0
  - setuptools=75.8.0=py39h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tzdata=2025a=h04d1e81_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - args==0.1.0
      - asttokens==3.0.0
      - autopep8==2.3.2
      - certifi==2025.1.31
      - charset-normalizer==3.4.1
      - clint==0.5.1
      - coverage==7.8.0
      - decorator==5.2.1
      - distlib==0.3.9
      - exceptiongroup==1.2.2
      - executing==2.2.0
      - filelock==3.18.0
      - flake8==7.2.0
      - graphql-core==3.2.6
      - idna==3.10
      - iniconfig==2.1.0
      - ipython==8.18.1
      - isort==6.0.1
      - jedi==0.19.2
      - matplotlib-inline==0.1.7
      - mccabe==0.7.0
      - packaging==24.2
      - parso==0.8.4
      - pbr==6.1.1
      - pexpect==4.9.0
      - pkginfo==1.12.1.2
      - platformdirs==4.3.7
      - pluggy==1.5.0
      - prompt-toolkit==3.0.50
      - ptyprocess==0.7.0
      - pudb==2017.1.2
      - pure-eval==0.2.3
      - pycodestyle==2.13.0
      - pyflakes==3.3.1
      - pygments==2.19.1
      - pytest==8.3.5
      - requests==2.32.3
      - requests-toolbelt==1.0.0
      - stack-data==0.6.3
      - stevedore==5.4.1
      - tomli==2.2.1
      - traitlets==5.14.3
      - twine==1.8.1
      - typing-extensions==4.13.0
      - urllib3==2.3.0
      - urwid==2.6.16
      - virtualenv==20.29.3
      - virtualenv-clone==0.5.7
      - virtualenvwrapper==6.1.1
      - wcwidth==0.2.13
prefix: /opt/conda/envs/polygraph

polygraph/types/tests/test_type_map.py::TypeMapTest::test_type_map_builder polygraph/utils/tests/test_strict_dict.py::StrictDictTest::test_can_update_same_key_with_same_value polygraph/utils/tests/test_strict_dict.py::StrictDictTest::test_cannot_update_same_key_with_different_value
polygraph/utils/strict_dict.py polygraph/types/type_builder.py polygraph/types/schema.py
polygraph/utils/strict_dict.py polygraph/types/type_builder.py polygraph/types/schema.py