Python type annotations
I'd like to suggest a potential improvement to the "template" or boilerplate used by the ksy compiler to generate Python classes. It's perfectly functional as is, but it's pretty verbose and does a few extra checks that might not actually be necessary.

As an example, I'm currently working on polishing the Kaitai definitions for all the files used by DataFlex flat-file databases. Here's a portion of the KSY for the table file's header:

```yaml
meta:
  id: table_file
  title: DataFlex v3 Database File
  application: DataFlex v3
  file-extension: dat
  ks-version: 0.9
  encoding: cp1252
  endian: le
  bit-endian: le
doc: |
    A DataFlex flat-file table.
doc-ref: https://hwiegman.home.xs4all.nl/fileformats/dat/DATAFLEX.txt
seq:
  - id: header
    size: 3072
    type: header
types:
  header:
    instances:
      higest_record_count_ever:
        pos: 0x00
        doc: |
          The total number of records that have ever been stored in a given table
        type: u4le
      first_reused_record:
        pos: 0x04
        doc: |
          Undeciphered value, has something to do with reuse of deleted space.
          Should be 0 if deleted space is not re-used, but could possibly be the
          first available record # otherwise.
        type: u4le
      record_count:
        pos: 0x08
        doc: |
          The total number of records currently in the table
        type: u4le
      max_record_count:
        pos: 0x0C
        doc: |
          The total number of records the table can store at any given time
        type: u4le
```

The generated Python code looks something like this:

```python
class TableFile(KaitaiStruct):
    """A DataFlex flat-file table.
    
    .. seealso::
       Source - https://web.archive.org/web/20210115231834/hwiegman.home.xs4all.nl/fileformats/dat/DATAFLEX.txt
    """

    class Header(KaitaiStruct):
        def __init__(self, _io, _parent=None, _root=None):
            self._io = _io
            self._parent = _parent
            self._root = _root if _root else self
            self._read()

        def _read(self):
            pass

        @property
        def higest_record_count_ever(self):
            """The total number of records that have ever been stored in a given table
            """
            if hasattr(self, '_m_higest_record_count_ever'):
                return self._m_higest_record_count_ever if hasattr(self, '_m_higest_record_count_ever') else None

            _pos = self._io.pos()
            self._io.seek(0)
            self._m_higest_record_count_ever = self._io.read_u4le()
            self._io.seek(_pos)
            return self._m_higest_record_count_ever if hasattr(self, '_m_higest_record_count_ever') else None

        @property
        def first_reused_record(self):
            """Un-deciphered value, has something to do with reuse of deleted space.
            Should be 0 if deleted space is not re-used, but could possibly be the
            first available record # otherwise.
            """
            if hasattr(self, '_m_first_reused_record'):
                return self._m_first_reused_record if hasattr(self, '_m_first_reused_record') else None

            _pos = self._io.pos()
            self._io.seek(4)
            self._m_first_reused_record = self._io.read_u4le()
            self._io.seek(_pos)
            return self._m_first_reused_record if hasattr(self, '_m_first_reused_record') else None

        @property
        def record_count(self):
            """The total number of records currently in the table
            """
            if hasattr(self, '_m_record_count'):
                return self._m_record_count if hasattr(self, '_m_record_count') else None

            _pos = self._io.pos()
            self._io.seek(8)
            self._m_record_count = self._io.read_u4le()
            self._io.seek(_pos)
            return self._m_record_count if hasattr(self, '_m_record_count') else None

        @property
        def max_record_count(self):
            """The total number of records the table can store at any given time
            """
            if hasattr(self, '_m_max_record_count'):
                return self._m_max_record_count if hasattr(self, '_m_max_record_count') else None

            _pos = self._io.pos()
            self._io.seek(12)
            self._m_max_record_count = self._io.read_u4le()
            self._io.seek(_pos)
            return self._m_max_record_count if hasattr(self, '_m_max_record_count') else None

    def __init__(self, _io, _parent=None, _root=None):
        self._io = _io
        self._parent = _parent
        self._root = _root if _root else self
        self._read()

    def _read(self):
        self._raw_header = self._io.read_bytes(3072)
        _io__raw_header = KaitaiStream(BytesIO(self._raw_header))
        self.header = TableFile.Header(_io__raw_header, self, self._root)

```

Again, it's perfectly functional, it's just... verbose. Also, it doesn't take advantage of the fact that ~~practically~~ everything in the KSY has type information associated with it. 

Basically, my proposition would be to take advantage of the fact that the Python dev team has committed pretty hardcore to building in type annotations since Python 3.7 to dramatically improve the readability and maintainability of the generated parsing classes. Ideally, the Kaitai definition above would spit out something more like this:

```python
"""DataFlex Table File Parser."""

# This is a generated file! Please edit source .ksy file and use kaitai-struct-compiler to rebuild

from __future__ import annotations
from typing import Any, Optional

from pkg_resources import parse_version
import kaitaistruct
from kaitaistruct import KaitaiStruct, KaitaiStream, BytesIO


__all__ = ("TableFile",)


if parse_version(kaitaistruct.__version__) < parse_version("0.9"):
    raise RuntimeError(
        f"Incompatible Kaitai Struct Python API: 0.9 or later is required, but you have {kaitaistruct.__version__}"
    )


class Header(KaitaiStruct):
    """The table's header section."""

    _io: KaitaiStream
    _parent: "TableFile"
    _root: "TableFile"

    # Storage for lazy-loaded attributes
    __highest_record_count_ever: Optional[int] = None
    __first_reused_record: Optional[int] = None
    __record_count: Optional[int] = None
    __max_record_count: Optional[int] = None

    def __init__(self, _io, _parent=None, _root=None):
        self._io = _io
        self._parent = _parent
        self._root = _root if _root else self
        self._read()

    def _read(self):
        """Read the table data."""
        pass

    @property
    def highest_record_count_ever(self) -> int:
        """The total number of records that have ever been stored in a given table"""
        if self.__highest_record_count_ever is None:
            _pos = self._io.pos()
            self._io.seek(0)
            self.__highest_record_count_ever = self._io.read_u4le()
            self._io.seek(_pos)
        return self.__highest_record_count_ever

    @property
    def first_reused_record(self) -> int:
        """Un-deciphered value, has something to do with reuse of deleted space.
        Should be 0 if deleted space is not re-used, but could possibly be the
        first available record # otherwise.
        """
        if self.__first_reused_record is None:
            _pos = self._io.pos()
            self._io.seek(4)
            self.__first_reused_record = self._io.read_u4le()
            self._io.seek(_pos)
        return self.__first_reused_record

    @property
    def record_count(self) -> int:
        """The total number of records currently in the table"""
        if self.__record_count is None:
            _pos = self._io.pos()
            self._io.seek(8)
            self.__record_count = self._io.read_u4le()
            self._io.seek(_pos)
        return self.__record_count

    @property
    def max_record_count(self) -> int:
        """The total number of records the table can store at any given time"""
        if self.__max_record_count is None:
            _pos = self._io.pos()
            self._io.seek(12)
            self.__max_record_count = self._io.read_u4le()
            self._io.seek(_pos)
        return self.__max_record_count


class TableFile(KaitaiStruct):
    """A DataFlex flat-file table.

    .. seealso::
       Source - https://web.archive.org/web/20210115231834/hwiegman.home.xs4all.nl/fileformats/dat/DATAFLEX.txt
    """

    header: Header
    _io: KaitaiStream
    _parent: Optional[Any] = None
    _root: "TableFile"
    _raw_header: bytes

    def __init__(self, _io, _parent=None, _root=None):
        self._io = _io
        self._parent = _parent
        self._root = _root if _root else self
        self._read()

    def _read(self):
        self._raw_header = self._io.read_bytes(3072)
        _io__raw_header = KaitaiStream(BytesIO(self._raw_header))
        self.header = Header(_io__raw_header, self, self._root)
```

I won't go on at length or anything, but hopefully the difference is evident.

I can't tell you how much I *love* Kaitai, and I **really, really, really** do not want my suggestion to be taken as any kind of criticism because it is absolutely not. There a million ways to skin a proverbial cat in Python and absolutely every Python programmer has a different idea of what "Pythonic" means.

I don't expect to just toss out a proposal and have someone just *do* it for me.  I am more than willing (though potentially questionably capable) of writing the new code for the compiler myself. The only thing stopping me really is the fact that I have no idea where to look or begin. If you'd point me in the right direction (and if you think it's something you'd accept a pull request for) then I'll happily get to work on it myself.

In either case, I'm eager to hear any thoughts or feedback anyone would care to share.