diff --git a/muppet/puppet/ast.py b/muppet/puppet/ast.py
index 2ce3c35263d793d5021e1345c9857d9b4f27ca2b..b527c23d1c8c1535a8fec20fd763c1d38046ef29 100644
--- a/muppet/puppet/ast.py
+++ b/muppet/puppet/ast.py
@@ -722,3 +722,107 @@ def build_ast(form: Any) -> Puppet:
         case default:
             logger.warning("Unhandled item: %a", default)
             return PuppetParseError(default)
+
+
+def generate_template() -> None:
+    """
+    Output code for a formater class.
+
+    Each formatter needs to implement a rather large number of
+    methods, and keeping track of them all is hard. This generates a
+    ready-to-use template.
+
+    .. code-block:: sh
+
+        python -m muppet.puppet.ast > muppet/puppet/format/NAME.py
+    """
+
+    def pyind(level: int) -> str:
+        """Indent string for python code."""
+        return ' ' * 4 * level
+
+    subclasses = sorted(subclass.__name__ for subclass in Puppet.__subclasses__())
+
+    def tokenize_class(s: str) -> list[str]:
+        """Split a camel or pascal cased string into words."""
+        out: list[str] = []
+        current: str = ''
+        for c in s:
+            if c.isupper():
+                if current.isupper():
+                    current += c
+                else:
+                    out.append(current)
+                    current = c
+            else:
+                current += c
+        out.append(current)
+
+        if out[0] == '':
+            out = out[1:]
+
+        return out
+
+    def setup_override() -> None:
+        print('''
+from typing import (
+    TypeVar,
+    Callable,
+)
+
+
+F = TypeVar('F', bound=Callable[..., object])
+
+# TODO replace this decorator with
+# from typing import override
+# once the target python version is changed to 3.12
+
+
+def override(f: F) -> F:
+    """
+    Return function unchanged.
+
+    Placeholder @override annotator if the actual annotation isn't
+    implemented in the current python version.
+    """
+    return f
+
+
+'''.lstrip())
+
+    print('''"""
+__
+
+TODOFormatter
+TODO_RETURN_TYPE
+"""''')
+
+    print()
+    print('import logging')
+    print('from .base import Serializer')
+    print('from muppet.puppet.ast import (')
+    for subclass in subclasses:
+        print(pyind(1) + subclass + ',')
+    print(')')
+
+    setup_override()
+
+    print()
+    print()
+    print('logger = logging.getLogger(__name__)')
+    print()
+    print()
+
+    print('class TODOFormatter(Serializer[TODO_RETURN_TYPE]):')
+    print(pyind(1) + '"""TODO document me!!!"""')
+    for subclass in subclasses:
+        func_name = '_'.join(tokenize_class(subclass)).lower()
+        print()
+        print(pyind(1) + '@override')
+        print(pyind(1) + '@classmethod')
+        decl = f"def _{func_name}(cls, it: {subclass}, indent: int) -> TODO_RETURN_TYPE:"
+        print(pyind(1) + decl)
+
+
+if __name__ == '__main__':
+    generate_template()