Python — linkml documentation

# Python

## Example Output

personinfo.py

## Overview

See also

using python

## Docs

### Command Line

#### gen-python

Generate python classes to represent a LinkML model

    gen-python [OPTIONS] YAMLFILE

Options

--head, --no-head

  Emit metadata heading

--genmeta, --no-genmeta

  Generating metamodel. Only use this for generating meta.py

--classvars, --no-classvars

  Generate CLASSVAR info

--slots, --no-slots

  Generate Slot information

-f, --format <format>

  Output format (default=py)

  Options

    py

--metadata, --no-metadata

  Include metadata in output (default=–metadata)

--useuris, --metauris

  Include metadata in output (default=–useuris)

-im, --importmap <importmap>

  Import mapping file

--log_level <log_level>

  Logging level (default=WARNING)

  Options

    CRITICAL | ERROR | WARNING | INFO | DEBUG

--mergeimports, --no-mergeimports

  Merge imports into source file (default=mergeimports)

Arguments

YAMLFILE

  Required argument

### Code

class linkml.generators.pythongen.PythonGenerator(schema: Union[str, TextIO, linkml_runtime.linkml_model.meta.SchemaDefinition], format: str = 'py', genmeta: bool = False, gen_classvars: bool = True, gen_slots: bool = True, **kwargs)

  Generates Python dataclasses from a LinkML model

  serialize(**kwargs) → str

    Generate output in the required format

    Parameters

      kwargs – Generater specific parameters

    Returns

      Generated output

## Additional Notes

## Code generation for LinkML slots

The generation of slot entries in class definitions is split into two parts: 1) The declaration of the slot type 2) The `__post_init__` processing transforms several allowable input forms into single, consistent internal representation.

The elements that control the python for slot generation include * Whether `range` references an instance of a LinkML `type` or a LinkML `class` * If the slot range is a `class`: * Whether the class has a key (`key: <type>`), an identifier (`identifier: <type>`), or neither * If the class has a `key` or `identifier`, whether the instances are inlined as a dictionary (`inlined: true`), inlined as a list (`inlined_as_list: true`) or are referenced elsewhere in the model (Default). * Whether the slot is or concrete (`abstract: false`) or abstract (`abstract: true` or `mixin: true`) (Default: `false`) * Whether a slot is required (`required: true`) or optional (`required: false`) (Default: `false`) * Whether a slot is single-valued (`multivalued: false`) or multi-valued (`multivalued: true`) (Default: `false`) * The `ifabsent` attribute (not covered in this document) * The `default` value (not covered in this document)

These various situations are described more detail below: ### 1) Slot range is a LinkML `type` definition LinkML type definitions can take one of three forms: 1) Builtin python type `yaml types: <type>: base: <builtin python type> (e.g. 'str', 'int', 'float', etc) ...` 2) Defined type `yaml types: <type>: base: <type defined in linkml/linkml_runtime/utils/metamodelcore.py> (e.g. URIorCURIE, Date, NCName, etc) ...`

3. Inherited type `yaml types: <type>: typeof: <parent type>` Each of these are outlined below #### 1) Python generation for basic python Type. For this example, we show basic permutations on the python `int` type as a base YAML:

    types:
      integer:
        uri: xsd:integer
        base: int
        description: An integer

    classes:
        Integers:
          description: various permutations of the integer type
          attributes:
            opt_integer:
              range: integer
            mand_integer:
              range: integer
              required: true
            opt_multi_integer:
              range: integer
              multivalued: true
            mand_multi_integer:
              range: integer
              multivalued: true
              required: true

Generated Python:

    ...

    class Integer(int):
        """ An integer """
        type_class_uri = XSD.integer
        type_class_curie = "xsd:integer"
        type_name = "integer"
        type_model_uri = METATYPE.Integer

    @dataclass
    class Integers(YAMLRoot):
        """
        various permutations of the integer type
        """
        ...

        mand_integer: int = None
        mand_multi_integer: Union[int, List[int]] = None
        opt_integer: Optional[int] = None
        opt_multi_integer: Optional[Union[int, List[int]]] = empty_list()

        def __post_init__(self, **kwargs: Dict[str, Any]):
            if self.mand_integer is None:
                raise ValueError("mand_integer must be supplied")
            if not isinstance(self.mand_integer, int):
                self.mand_integer = int(self.mand_integer)

            if self.mand_multi_integer is None:
                raise ValueError("mand_multi_integer must be supplied")
            elif not isinstance(self.mand_multi_integer, list):
                self.mand_multi_integer = [self.mand_multi_integer]
            elif len(self.mand_multi_integer) == 0:
                raise ValueError(f"mand_multi_integer must be a non-empty list")
            self.mand_multi_integer = [v if isinstance(v, int) else int(v) for v in self.mand_multi_integer]

            if self.opt_integer is not None and not isinstance(self.opt_integer, int):
                self.opt_integer = int(self.opt_integer)

            if self.opt_multi_integer is None:
                self.opt_multi_integer = []
            if not isinstance(self.opt_multi_integer, list):
                self.opt_multi_integer = [self.opt_multi_integer]
            self.opt_multi_integer = [v if isinstance(v, int) else int(v) for v in self.opt_multi_integer]

            super().__post_init__(**kwargs)

The python first emits a class declaration for the base class – in this case, the class `Integer` which derives from the builtin type `int`. Four class variables are included in the generation: * `type_class_uri` - the URIRef for the type itself * `type_class_curie` - the string CURIE representation of the type * `type_name` - the non-mangled name assigned to the type in the original definition * `type_model_uri` - the URIRef of the type definition in the default LinkML namespace

The python then emits a class definition for the `Integers` classe, where we have defined four slot type permutations:

1. `mand_integer` - a single valued required type:

   - The type, `int`, is from the `base` attribute. The python generator does make use of strictly positional parameters, so all type declarations will have an appropriate default, which is `None` in this case:

     `mand_integer: int = None`

   - Check for the presence of the value itself: `if self.mand_integer is None: raise ValueError("mand_integer must be supplied")`

   - Coerce the input value to `int` if it isn’t one already: `if not isinstance(self.mand_integer, int): self.mand_integer = int(self.mand_integer)`

2. `mand_multi_integer` - a mandatory list of integers

   - Accept either a list of integers or a single integer, which will be converted to a list:

     `mand_multi_integer: Union[int, List[int]] = None`

   - Check that the value is present. Note: LinkML treats an empty list as the same as an absent value.

         if self.mand_multi_integer is None:
             raise ValueError("mand_multi_integer must be supplied")

   - Convert a non-list value into a list. This allows constructors to be lazy when it comes to single valued entries. Note, however, that this does require that list-of-list constructs to be explicitly declared because, while the code treats `x = C(1)` as the same as `x = C([1])` in the multivalued case, it does not attempt to differentiate `x = C([1])` and `x = C([[1]])`. This could be done if there is a compelling reason to, however:

         elif not isinstance(self.mand_multi_integer, list):
             self.mand_multi_integer = [self.mand_multi_integer]

   - Check that the mandatory list has at least one element (our definition of `mandatory list`: `elif len(self.mand_multi_integer) == 0: raise ValueError(f"mand_multi_integer must be a non-empty list")`

   - Coerce all of the member of the list into the target type (`int`):

     `self.mand_multi_integer = [v if isinstance(v, int) else int(v) for v in self.mand_multi_integer]`

3. `opt_integer` - Optional non-list.

   - Accept either an `int` or `None`, the latter being entailed in the `Optional` type: `opt_integer: Optional[int] = None`

   - Coerce to an integer if the value is present, otherwise let it be: `if self.opt_integer is not None and not isinstance(self.opt_integer, int): self.opt_integer = int(self.opt_integer)`

   - Type is `Optional[<type>]`

   - `__post_init__` adds type coercion if necessary

4. `opt_multi_integer`: list:

   - Accept one of `None`, `int`, list of `int`, or anything that can be coerced into `List[int]`:

     `opt_multi_integer: Optional[Union[int, List[int]]] = empty_list()`

   - The absence of a list property is always represented as an emtpy list: `if self.opt_multi_integer is None: self.opt_multi_integer = []`

   - Coerce any other non-list into a list: `if not isinstance(self.opt_multi_integer, list): self.opt_multi_integer = [self.opt_multi_integer]`

   - Coerce all the members of the list into the range type `self.opt_multi_integer = [v if isinstance(v, int) else int(v) for v in self.opt_multi_integer]`

YAML:

    types:
      date:
        uri: xsd:date
        base: XSDDate
        repr: str
        description: a date (year, month and day) in an idealized calendar

    classes:
      Dates:
        description: various permutations of the date type
        attributes:
          opt_date:
            range: date
          mand_date:
            range: date
            required: true
          opt_multi_date:
            range: date
            multivalued: true
          mand_multi_date:
            range: date
            multivalued: true
            required: true

python:

    class Date(XSDDate):
        """ a date (year, month and day) in an idealized calendar """
        type_class_uri = XSD.date
        type_class_curie = "xsd:date"
        type_name = "date"
        type_model_uri = METATYPE.Date

    @dataclass
    class Dates(YAMLRoot):
        """
        various permutations of the date type
        """
        ...

        mand_date: Union[str, XSDDate] = None
        mand_multi_date: Union[Union[str, XSDDate], List[Union[str, XSDDate]]] = None
        opt_date: Optional[Union[str, XSDDate]] = None
        opt_multi_date: Optional[Union[Union[str, XSDDate], List[Union[str, XSDDate]]]] = empty_list()

        def __post_init__(self, **kwargs: Dict[str, Any]):
            if self.mand_date is None:
                raise ValueError("mand_date must be supplied")
            if not isinstance(self.mand_date, XSDDate):
                self.mand_date = XSDDate(self.mand_date)

            if self.mand_multi_date is None:
                raise ValueError("mand_multi_date must be supplied")
            elif not isinstance(self.mand_multi_date, list):
                self.mand_multi_date = [self.mand_multi_date]
            elif len(self.mand_multi_date) == 0:
                raise ValueError(f"mand_multi_date must be a non-empty list")
            self.mand_multi_date = [v if isinstance(v, XSDDate) else XSDDate(v) for v in self.mand_multi_date]

            if self.opt_date is not None and not isinstance(self.opt_date, XSDDate):
                self.opt_date = XSDDate(self.opt_date)

            if self.opt_multi_date is None:
                self.opt_multi_date = []
            if not isinstance(self.opt_multi_date, list):
                self.opt_multi_date = [self.opt_multi_date]
            self.opt_multi_date = [v if isinstance(v, XSDDate) else XSDDate(v) for v in self.opt_multi_date]

            super().__post_init__(**kwargs)

The generated python for this type is identical to that of the builtin type with the exception that type declarations are the `Union` of the class representing the element and the `repr` element (e.g. `XSDDate` and `str`):

`int` type:

    mand_multi_integer: Union[int, List[int]] = None

`XSDDate` type:

    mand_multi_date: Union[Union[str, XSDDate], List[Union[str, XSDDate]]] = None

### 3) Python generation for an inherited Type

The following YAML shows three inherited types - one that inherits from a builtin type, a second from a metamodelcore type and a third from a the inherited type.

    types:
      InheritedType:
        typeof: integer

      InheritedType2:
        typeof: uriorcurie

      InheritedType3:
        typeof: InheritedType2

Python:

    # Types
    class InheritedType(Integer):
        type_class_uri = XSD.integer
        type_class_curie = "xsd:integer"
        type_name = "InheritedType"
        type_model_uri = PTYPES.InheritedType

    class InheritedType2(Uriorcurie):
        type_class_uri = XSD.anyURI
        type_class_curie = "xsd:anyURI"
        type_name = "InheritedType2"
        type_model_uri = PTYPES.InheritedType2

    class InheritedType3(InheritedType2):
        type_class_uri = XSD.anyURI
        type_class_curie = "xsd:anyURI"
        type_name = "InheritedType3"
        type_model_uri = PTYPES.InheritedType3

         ...

    @dataclass
    class InheritedTypes(YAMLRoot):
        """
        various permutations of a typeof referencing a builtin
        """
        ...

        mand_InheritedType: Union[int, InheritedType] = None
        mand_multi_InheritedType: Union[Union[int, InheritedType], List[Union[int, InheritedType]]] = None
        opt_InheritedType: Optional[Union[int, InheritedType]] = None
        opt_multi_InheritedType: Optional[Union[Union[int, InheritedType], List[Union[int, InheritedType]]]] = empty_list()

        def __post_init__(self, **kwargs: Dict[str, Any]):
            if self.mand_InheritedType is None:
                raise ValueError("mand_InheritedType must be supplied")
            if not isinstance(self.mand_InheritedType, InheritedType):
                self.mand_InheritedType = InheritedType(self.mand_InheritedType)

            if self.mand_multi_InheritedType is None:
                raise ValueError("mand_multi_InheritedType must be supplied")
            elif not isinstance(self.mand_multi_InheritedType, list):
                self.mand_multi_InheritedType = [self.mand_multi_InheritedType]
            elif len(self.mand_multi_InheritedType) == 0:
                raise ValueError(f"mand_multi_InheritedType must be a non-empty list")
            self.mand_multi_InheritedType = [v if isinstance(v, InheritedType) else InheritedType(v) for v in self.mand_multi_InheritedType]

            if self.opt_InheritedType is not None and not isinstance(self.opt_InheritedType, InheritedType):
                self.opt_InheritedType = InheritedType(self.opt_InheritedType)

            if self.opt_multi_InheritedType is None:
                self.opt_multi_InheritedType = []
            if not isinstance(self.opt_multi_InheritedType, list):
                self.opt_multi_InheritedType = [self.opt_multi_InheritedType]
            self.opt_multi_InheritedType = [v if isinstance(v, InheritedType) else InheritedType(v) for v in self.opt_multi_InheritedType]

            super().__post_init__(**kwargs)

The Python patterns for each of the inherited types (Inherited from basic (e.g. `int`), inherited from metamodelcore.py (e.g. `uriorcurie`) or from another type are identical to those generated above for `XSDDate`

#### 2) Slot range is a LinkML `class` definition

The previous section described the Python that was generated for slot ranges that reference LinkML Type definitions. This section addresses the second use case, where the slot range references another LinkML Class. Classes are a more complex example, because, as opposed to types, classes can either be represented by value or, if they have been declared to have unique identifiers, by reference. In addition, classes that are represented by value can either be represented as lists of values or dictionaries.

The following attributes control how the python is generated for a LinkML `class` definition: * The slot `abstract` setting * The slot `required` setting * The slot `multivalued` setting * Whether a class includes a slot identified as a “key” (`"key": true`), an “identifier” (`"identifier": true`) or neither. Note that at the moment, a class may have at most one key or identifier slot. Two keys, two identifiers or a key and an identifier are all considered errors. It is anticipated that a future version of LinkML will allow multiple key slots, directly implementing the notion of a “compound key”. * The slot `inlined` and `inlined_as_list` properties * The slot `ifabsent` attribute * The slot `default` value

Various permutations of these cases are described below:

### 1) Non-keyed, non-identified classes

Classes without keys or identifiers cannot only be realized inline. The following code shows examples of various permutations of this type of class:

    classes:
      OptionalOneElementRange:
        description: Range is a optional class that contains one non-key/non-identifier element
        attributes:
          v1:
            range: OneElementClass

      RequiredOneElementRange:
        description: Range is a required class that contains one non-key/non-identifier element
        is_a: OptionalOneElementRange
        attributes:
          v1:
            range: OneElementClass
            required: true

      OptionalOneElementRangeList:
        description: Range is a optional list of a class that contain one non-key/non-identifier element
        attributes:
          v1:
            range: OneElementClass
            multivalued: true

      RequiredThreeElementRangeList:
        description: Range is a required list of a class that contain one non-key/non-identifier element
        attributes:
          v1:
            range: ThreeElementClass
            multivalued: true
            required: true

An optional, single element class generates the following code:

    @dataclass
        class OptionalOneElementRange(YAMLRoot):
            """
            Range is a optional class that contains one non-key/non-identifier element
            """
            ...

            v1: Optional[Union[dict, OneElementClass]] = None

            def __post_init__(self, **kwargs: Dict[str, Any]):
                if self.v1 is not None and not isinstance(self.v1, OneElementClass):
                    self.v1 = OneElementClass(**self.v1)

                super().__post_init__(**kwargs)

`v1` can be one of: * `None` - as in indicated by the `Optional` type * any python `dict`, which is the type that underlies all class representations * an instance of `OneElementClass`

The only additional initialization code coerces the input to a OneElementClass if it exists (is not `None`) and it isn’t already an instance.

Switching from optional to required produces:

    @dataclass
    class RequiredOneElementRange(YAMLRoot):
        """
        Range is a required class that contains one non-key/non-identifier element
        """
        ...

        v1: Union[dict, OneElementClass] = None

        def __post_init__(self, **kwargs: Dict[str, Any]):
            if self.v1 is None:
                raise ValueError("v1 must be supplied")
            if not isinstance(self.v1, OneElementClass):
                self.v1 = OneElementClass(**self.v1)

            super().__post_init__(**kwargs)

Which is the same as the previous example, with the exception that the value, `v1` must be supplied.

The third example, an optional list of `OneElementClass` instances:

    @dataclass
    class OptionalOneElementRangeList(YAMLRoot):
        """
        Range is a optional list of a class that contain one non-key/non-identifier element
        """
        ...

        v1: Optional[Union[Union[dict, OneElementClass], List[Union[dict, OneElementClass]]]] = empty_list()

        def __post_init__(self, **kwargs: Dict[str, Any]):
            if self.v1 is None:
                self.v1 = []
            if not isinstance(self.v1, list):
                self.v1 = [self.v1]
            self.v1 = [v if isinstance(v, OneElementClass) else OneElementClass(**v) for v in self.v1]

            super().__post_init__(**kwargs)

Accepts any of: * `None` - the list is not present * An empty list (`[]`) - same semantics as `None` * A dictionary * An instance of `OneElementClass` * A list of dictionaries and/or `OneElementClass` instances

The code: 1) translates the `None` value into the equivalent empty list 2) converts a naked dict or `OneElementClass` instance into a single element list 3) coerces the list of zero or more items into a list of `OneElementClass` instances

In the final case, we’ve attempted to show both a) what a required inline list looks like and b) show that the target type is irrelevant as long as it is a LinkML `Class` and has no `key` or `identifier` slots

    @dataclass
    class RequiredThreeElementRangeList(YAMLRoot):
        """
        Range is a required list of a class that contain two non-key/non-identifier elements
        """
        ...

        v1: Union[Union[dict, ThreeElementClass], List[Union[dict, ThreeElementClass]]] = None

        def __post_init__(self, **kwargs: Dict[str, Any]):
            if self.v1 is None:
                raise ValueError("v1 must be supplied")
            elif not isinstance(self.v1, list):
                self.v1 = [self.v1]
            elif len(self.v1) == 0:
                raise ValueError(f"v1 must be a non-empty list")
            self.v1 = [v if isinstance(v, ThreeElementClass) else ThreeElementClass(**v) for v in self.v1]

            super().__post_init__(**kwargs)

The structural differences between the above class and its predecessor are: 1) The `v1` signature does not include `None` as an option (i.e. does not begin with `Optional[]`) 2) A `None` value is flagged as an error rather than converted to an empty list 3) An empty list is flagged as an error.

#### Keyed or Identified Classes

When the range of a slot is a LinkML class that has a `key` or an `identifier`, additional options present themselves: 1) Should elements be represented inline or as references? 2) If inline, should the elements be represented as lists or as dictionaries, whose key is the `key` or `identifier` value.

We discuss each of these options below

### Key or identified class references

Using the following classes as building blocks

    KeyedThreeElementClass:
      description: A keyed class with an additional integer and date
      attributes:
        name:
          range: string
          key: true
        value:
          range: integer
        modifier:
          range: date

    IdentifiedThreeElementClass:
      description: A identified class with an additional integer and date
      attributes:
        name:
          range: string
          identifier: true
        value:
          range: integer
        modifier:
          range: date

Python:

    class KeyedThreeElementClassName(extended_str):
        pass

    class IdentifiedThreeElementClassName(extended_str):
        pass

    @dataclass
    class KeyedThreeElementClass(YAMLRoot):
        """
        A keyed class with an additional integer and date
        """
        ...

        name: Union[str, KeyedThreeElementClassName] = None
        value: Optional[int] = None
        modifier: Optional[Union[str, XSDDate]] = None

        def __post_init__(self, **kwargs: Dict[str, Any]):
            if self.name is None:
                raise ValueError("name must be supplied")
            if not isinstance(self.name, KeyedThreeElementClassName):
                self.name = KeyedThreeElementClassName(self.name)

            if self.value is not None and not isinstance(self.value, int):
                self.value = int(self.value)

            if self.modifier is not None and not isinstance(self.modifier, XSDDate):
                self.modifier = XSDDate(self.modifier)

            super().__post_init__(**kwargs)

    @dataclass
    class IdentifiedThreeElementClass(YAMLRoot):
        """
        A identified class with an additional integer and date
        """
        ...

        name: Union[str, IdentifiedThreeElementClassName] = None
        value: Optional[int] = None
        modifier: Optional[Union[str, XSDDate]] = None

        def __post_init__(self, **kwargs: Dict[str, Any]):
            if self.name is None:
                raise ValueError("name must be supplied")
            if not isinstance(self.name, IdentifiedThreeElementClassName):
                self.name = IdentifiedThreeElementClassName(self.name)

            if self.value is not None and not isinstance(self.value, int):
                self.value = int(self.value)

            if self.modifier is not None and not isinstance(self.modifier, XSDDate):
                self.modifier = XSDDate(self.modifier)

            super().__post_init__(**kwargs)

We can now begin with the following LinkML class definitions:

    OptionalKeyedThreeElementRange:
      description: Range is a optional class that contains one key and two regular elements
      attributes:
        v1:
          range: KeyedThreeElementClass

    OptionalKeyedThreeElementRangeList:
      description: Range is a optional list of a class that contains one key and two regular elements
      attributes:
        v1:
          range: KeyedThreeElementClass
          multivalued: true

    RequiredIdentifiedThreeElementRange:
      description: Range is a required class that contains one identifier and two regular elements
      attributes:
        v1:
          range: IdentifiedThreeElementClass
          required: true

    RequiredIdentifiedThreeElementRangeList:
      description: Range is a optional list of a class that contains one identifier and two regular elements
      attributes:
        v1:
          range: IdentifiedThreeElementClass
          multivalued: true
          required: true

Python equivalents

    @dataclass
    class OptionalKeyedThreeElementRange(YAMLRoot):
        """
        Range is a optional class that contains one key and two regular elements
        """
        ...

        v1: Optional[Union[str, KeyedThreeElementClassName]] = None

        def __post_init__(self, **kwargs: Dict[str, Any]):
            if self.v1 is not None and not isinstance(self.v1, KeyedThreeElementClassName):
                self.v1 = KeyedThreeElementClassName(self.v1)

            super().__post_init__(**kwargs)

In the above example, `v1` is a single, optional reference to a `KeyedThreeElementClass` instance. The constructor can take one of: 1) `None` - because the link is optional 2) `str` - the type of the key to the `KeyedThreeElementClass` 3) `ThreeElementClassName` - an instance of the actual key. 4) Any python object that can be coerced into a `KeyedThreeElementClassName`

The key is recorded in the class. Note that the Python code, as it exists today, does NOT check that there actually `exists` an instance of `KeyedThreeElementClass` with the name in `v1` and, as such, there is currently no way to get directly from an instance of the class, `OptionalKeyedThreeElementRange` to an instance of `KeyedThreeElementClass`. This check and capability may be added to a later version of the LinkML tooling. The second example, below, shows an optional list

    @dataclass
    class OptionalKeyedThreeElementRangeList(YAMLRoot):
        """
        Range is a optional list of a class that contains one key and two regular elements
        """
        ...

        v1: Optional[Union[Union[str, KeyedThreeElementClassName], List[Union[str, KeyedThreeElementClassName]]]] = empty_list()

        def __post_init__(self, **kwargs: Dict[str, Any]):
            if self.v1 is None:
                self.v1 = []
            if not isinstance(self.v1, (list, dict)):
                self.v1 = [self.v1]
            self.v1 = [v if isinstance(v, KeyedThreeElementClassName) else KeyedThreeElementClassName(v) for v in self.v1]

            super().__post_init__(**kwargs)

The above signature is a bit confusing. It allows: 1) `None` - no list has been supplied 2) `str` - the type of the key to a single `KeyedThreeElementClass` 3) `ThreeElementClassName` - an instance of an actual key 4) Any python object that can be coerced into a `KeyedThreeElementClass` (not explicit in the signature) 4) A list consisting of any combination of `str`, `ThreeElementClassNames`, or other objects that can be coerced into `KeyedThreeElementClassName`

The remaining two non-line examples are similar enough to the above that they are included below without comment. Note, in particular, that there is no difference between keyed and identified class representation.

    @dataclass
    class RequiredIdentifiedThreeElementRange(YAMLRoot):
        """
        Range is a required class that contains one identifier and two regular elements
        """
        ...

        v1: Union[str, IdentifiedThreeElementClassName] = None

        def __post_init__(self, **kwargs: Dict[str, Any]):
            if self.v1 is None:
                raise ValueError("v1 must be supplied")
            if not isinstance(self.v1, IdentifiedThreeElementClassName):
                self.v1 = IdentifiedThreeElementClassName(self.v1)

            super().__post_init__(**kwargs)

    @dataclass
    class RequiredIdentifiedThreeElementRangeList(YAMLRoot):
        """
        Range is a optional list of a class that contains one identifier and two regular elements
        """
        ...

        v1: Union[Union[str, IdentifiedThreeElementClassName], List[Union[str, IdentifiedThreeElementClassName]]] = None

        def __post_init__(self, **kwargs: Dict[str, Any]):
            if self.v1 is None:
                raise ValueError("v1 must be supplied")
            elif not isinstance(self.v1, (list, dict)):
                self.v1 = [self.v1]
            if len(self.v1) == 0:
                raise ValueError(f"v1 must be a non-empty list dictionary or class")
            self.v1 = [v if isinstance(v, IdentifiedThreeElementClassName) else IdentifiedThreeElementClassName(v) for v in self.v1]

            super().__post_init__(**kwargs)

#### Python generation options for single-value (non-list) slots

The table below describes the various python generation options for single-valued slots (`"multivalued": false`)

ca se # | multiv alued | inlined | inlined_as_list | range class has key or identif ier | res ult
--- | --- | --- | --- | --- | ---
1. 1 | false (defau lt) |  |  | N | slo t val ue in of con tai nin g cla ss (i. e. inl ine )
1. 2 | false | N | N | Y | slo t key or ide nti fie r is in con tai nin g cla ss
1. 3 | false | Y |  | Y | slo t val ue in of con tai nin g cla ss (i. e. inl ine )
1. 4 | false |  | Y | Y | slo t val ue in of con tai nin g cla ss (i. e. inl ine )

The YAML examples below the above options for both optional and required slots.

    OptionalThreeElementRange:
      description: Case 1.1(o) -- single values optional slot - range has no keys or identifiers
      attributes:
        v1:
          range: ThreeElementClass

    RequiredThreeElementRange:
      description: Case 1.1(r) -- single values optional slot - range has no keys or identifiers
      attributes:
        v1:
          range: ThreeElementClass
          required: true

    OptionalIdentifiedThreeElementRange:
      description: Case 1.2(o) -- single values optional slot - range has an identifier
      attributes:
        v1:
          range: IdentifiedThreeElementClass

    RequiredKeyedThreeElementRange:
      description: Case 1.2(r) -- single values optional slot - range has a key
      attributes:
        v1:
          range: KeyedThreeElementClass
          required: true

    OptionalInlinedKeyedThreeElementRange:
      description: Case 1.3(o) -- single values optional slot - range has an identifier
      attributes:
        v1:
          range: KeyedThreeElementClass
          inlined: true

    RequiredInlinedIdentifiedThreeElementRange:
      description: Case 1.3(r) -- single values optional slot - range has a key
      attributes:
        v1:
          range: IdentifiedThreeElementClass
          required: true
          inlined: true

    OptionalInlinedAsListKeyedThreeElementRange:
      description: Case 1.4(o) -- single values optional slot - range has an identifier
      attributes:
        v1:
          range: KeyedThreeElementClass
          inlined_as_list: true

    RequiredInlinedAsListIdentifiedThreeElementRange:
      description: Case 1.4(r) -- single values optional slot - range has a key
      attributes:
        v1:
          range: IdentifiedThreeElementClass
          required: true
          inlined_as_list: true

The emitted python is as described for types with the exception of case 1.2 – non-inlined keyed or identified class, where the identifier or key of the class is the range of the `v1` slot in Python:

    @dataclass
    class OptionalThreeElementRange(YAMLRoot):
        """
        Case 1.1(o) -- single values optional slot - range has no keys or identifiers
        """
        ...
        v1: Optional[Union[dict, ThreeElementClass]] = None

        def __post_init__(self, **kwargs: Dict[str, Any]):
            if self.v1 is not None and not isinstance(self.v1, ThreeElementClass):
                self.v1 = ThreeElementClass(**self.v1)

            super().__post_init__(**kwargs)

    @dataclass
    class RequiredThreeElementRange(YAMLRoot):
        """
        Case 1.1(r) -- single values optional slot - range has no keys or identifiers
        """
        ...
        v1: Union[dict, ThreeElementClass] = None

        def __post_init__(self, **kwargs: Dict[str, Any]):
            if self.v1 is None:
                raise ValueError("v1 must be supplied")
            if not isinstance(self.v1, ThreeElementClass):
                self.v1 = ThreeElementClass(**self.v1)

            super().__post_init__(**kwargs)

    @dataclass
    class OptionalIdentifiedThreeElementRange(YAMLRoot):
        """
        Case 1.2(o) -- single values optional slot - range has an identifier
        """
        ...
        v1: Optional[Union[str, IdentifiedThreeElementClassName]] = None

        def __post_init__(self, **kwargs: Dict[str, Any]):
            if self.v1 is not None and not isinstance(self.v1, IdentifiedThreeElementClassName):
                self.v1 = IdentifiedThreeElementClassName(self.v1)

            super().__post_init__(**kwargs)

    @dataclass
    class RequiredKeyedThreeElementRange(YAMLRoot):
        """
        Case 1.2(r) -- single values optional slot - range has a key
        """
        ...
        v1: Union[str, KeyedThreeElementClassName] = None

        def __post_init__(self, **kwargs: Dict[str, Any]):
            if self.v1 is None:
                raise ValueError("v1 must be supplied")
            if not isinstance(self.v1, KeyedThreeElementClassName):
                self.v1 = KeyedThreeElementClassName(self.v1)

            super().__post_init__(**kwargs)

    @dataclass
    class OptionalInlinedKeyedThreeElementRange(YAMLRoot):
        """
        Case 1.3(o) -- single values optional slot - range has an identifier
        """
        ...
        v1: Optional[Union[dict, KeyedThreeElementClass]] = None

        def __post_init__(self, **kwargs: Dict[str, Any]):
            if self.v1 is not None and not isinstance(self.v1, KeyedThreeElementClass):
                self.v1 = KeyedThreeElementClass(self.v1)

            super().__post_init__(**kwargs)

    @dataclass
    class RequiredInlinedIdentifiedThreeElementRange(YAMLRoot):
        """
        Case 1.3(r) -- single values optional slot - range has a key
        """
        ...
        v1: Union[dict, IdentifiedThreeElementClass] = None

        def __post_init__(self, **kwargs: Dict[str, Any]):
            if self.v1 is None:
                raise ValueError("v1 must be supplied")
            if not isinstance(self.v1, IdentifiedThreeElementClass):
                self.v1 = IdentifiedThreeElementClass(self.v1)

            super().__post_init__(**kwargs)

    @dataclass
    class OptionalInlinedAsListKeyedThreeElementRange(YAMLRoot):
        """
        Case 1.4(o) -- single values optional slot - range has an identifier
        """
        ...
        v1: Optional[Union[dict, KeyedThreeElementClass]] = None

        def __post_init__(self, **kwargs: Dict[str, Any]):
            if self.v1 is not None and not isinstance(self.v1, KeyedThreeElementClass):
                self.v1 = KeyedThreeElementClass(self.v1)

            super().__post_init__(**kwargs)

    @dataclass
    class RequiredInlinedAsListIdentifiedThreeElementRange(YAMLRoot):
        """
        Case 1.4(r) -- single values optional slot - range has a key
        """
        ...
        v1: Union[dict, IdentifiedThreeElementClass] = None

        def __post_init__(self, **kwargs: Dict[str, Any]):
            if self.v1 is None:
                raise ValueError("v1 must be supplied")
            if not isinstance(self.v1, IdentifiedThreeElementClass):
                self.v1 = IdentifiedThreeElementClass(self.v1)

            super().__post_init__(**kwargs)

#### Python generation options for multivalued slots

The python generated for multivalued slots is a more interesting situation.

ca se # | multiv alued | inlined | inlined_as_list | range class has key or identif ier | res ult
--- | --- | --- | --- | --- | ---
2. 1 | true |  |  | N | Slo t is inl ine d as a lis t
2. 2 | true | false (default) | false (default) | Y | Slo t is a lis t of ran ge key s or ide nti fie rs
2. 3 | true |  | true |  | Slo t is inl ine d as a lis t
2. 4 | true | true | false | Y | Slo t is inl ine d as a dic tio nar y, key ed by the slo t key or ide nti fie r

The first two cases (2.1) and (2.2) do not differ significantly from what was described earlier

    OptionalMultivaluedThreeElementRange:
        description: Case 2.1(o) -- multivalued optional slot - range has no key or identifier
        attributes:
          v1:
            range: ThreeElementClass
            multivalued: true

      RequiredMultivaluedThreeElementRange:
        description: Case 2.1(r) -- multivalued optional slot - range has no key or identifier
        attributes:
          v1:
            range: ThreeElementClass
            multivalued: true
            required: true

      OptionalMultivaluedKeyedThreeElementRange:
        description: Case 2.2(o) -- multivalued optional slot - range has a key
        attributes:
          v1:
            range: KeyedThreeElementClass
            multivalued: true

      RequiredMultivaluedIdentifiedThreeElementRange:
        description: Case 2.2(r) -- multivalued optional slot - range has an identifier
        attributes:
          v1:
            range: IdentifiedThreeElementClass
            multivalued: true
            required: true

    @dataclass
    class OptionalMultivaluedThreeElementRange(YAMLRoot):
        """
        Case 2.1(o) -- multivalued optional slot - range has no key or identifier
        """
        ...
        v1: Optional[Union[Union[dict, ThreeElementClass], List[Union[dict, ThreeElementClass]]]] = empty_list()

        def __post_init__(self, **kwargs: Dict[str, Any]):
            if self.v1 is None:
                self.v1 = []
            if not isinstance(self.v1, list):
                self.v1 = [self.v1]
            self.v1 = [v if isinstance(v, ThreeElementClass) else ThreeElementClass(**v) for v in self.v1]

            super().__post_init__(**kwargs)

    @dataclass
    class RequiredMultivaluedThreeElementRange(YAMLRoot):
        """
        Case 2.1(r) -- multivalued optional slot - range has no key or identifier
        """
        ...
        v1: Union[Union[dict, ThreeElementClass], List[Union[dict, ThreeElementClass]]] = None

        def __post_init__(self, **kwargs: Dict[str, Any]):
            if self.v1 is None:
                raise ValueError("v1 must be supplied")
            elif not isinstance(self.v1, list):
                self.v1 = [self.v1]
            elif len(self.v1) == 0:
                raise ValueError(f"v1 must be a non-empty list")
            self.v1 = [v if isinstance(v, ThreeElementClass) else ThreeElementClass(**v) for v in self.v1]

            super().__post_init__(**kwargs)

    @dataclass
    class OptionalMultivaluedKeyedThreeElementRange(YAMLRoot):
        """
        Case 2.2(o) -- multivalued optional slot - range has a key
        """
        ...
        v1: Optional[Union[Union[str, KeyedThreeElementClassName], List[Union[str, KeyedThreeElementClassName]]]] = empty_list()

        def __post_init__(self, **kwargs: Dict[str, Any]):
            if self.v1 is None:
                self.v1 = []
            if not isinstance(self.v1, list):
                self.v1 = [self.v1]
            self.v1 = [v if isinstance(v, KeyedThreeElementClassName) else KeyedThreeElementClassName(v) for v in self.v1]

            super().__post_init__(**kwargs)

    @dataclass
    class RequiredMultivaluedIdentifiedThreeElementRange(YAMLRoot):
        """
        Case 2.2(r) -- multivalued required slot - range has an identifier
        """
        ...
        v1: Union[Union[str, IdentifiedThreeElementClassName], List[Union[str, IdentifiedThreeElementClassName]]] = None

        def __post_init__(self, **kwargs: Dict[str, Any]):
            if self.v1 is None:
                raise ValueError("v1 must be supplied")
            elif not isinstance(self.v1, list):
                self.v1 = [self.v1]
            elif len(self.v1) == 0:
                raise ValueError(f"v1 must be a non-empty list")
            self.v1 = [v if isinstance(v, IdentifiedThreeElementClassName) else IdentifiedThreeElementClassName(v) for v in self.v1]

            super().__post_init__(**kwargs)

Case 2.3 (Inlined as list) shows a new pattern:

    OptionalMultivaluedInlinedListIdentifiedThreeElementRange:
      description: 2.3(o) Range is an optional identified three element class that is represented as an inlined list
      attributes:
        v1:
          range: IdentifiedThreeElementClass
          multivalued: true
          inlined_as_list: true

    RequiredMultivaluedInlinedListKeyedThreeElementRangeList:
      description: 2.3(r) Range is a required keyed three element class that is represented as an inlined list
      attributes:
        v1:
          range: KeyedThreeElementClass
          multivalued: true
          inlined_as_list: true
          required: true

The Python that is generated from the above entries will require some more description:

    @dataclass
    class OptionalMultivaluedInlinedListIdentifiedThreeElementRange(YAMLRoot):
        """
        2.3(o) Range is an optional identified three element class that is represented as an inlined list
        """
        ...
        v1: Optional[Union[Dict[Union[str, IdentifiedThreeElementClassName], Union[dict, IdentifiedThreeElementClass]], List[Union[dict, IdentifiedThreeElementClass]]]] = empty_list()

        def __post_init__(self, **kwargs: Dict[str, Any]):
            if self.v1 is None:
                self.v1 = []
            if not isinstance(self.v1, (list, dict)):
                self.v1 = [self.v1]
            self._normalize_inlined_slot(slot_name="v1", slot_type=IdentifiedThreeElementClass, key_name="name", inlined_as_list=True, keyed=True)

            super().__post_init__(**kwargs)

Starting with the signature:

`v1: Optional[Union[Dict[Union[str, IdentifiedThreeElementClassName], Union[dict, IdentifiedThreeElementClass]], List[Union[dict, IdentifiedThreeElementClass]]]] = empty_list()`

The outermost declaration: `Optional[...] = empty_list()` reflects: 1) `v1` is not required: `None` is a valid value 2) the internal storage structure is going to be a list. Note that None, [] and {} (Missing scalar, empty list and empty dictionary all indicate absence of a value)

Moving inwards, `Union[Dict[...], List[...]]` means that the inputs can either be in the form of a dictionary or a list.

### Input in the form of a dictionary

If the input is in the form of a dictionary, its signature is “`Dict[Union[str, IdentifiedThreeElementClassName], Union[dict, IdentifiedThreeElementClass]]`”, that is a dictionary whose keys are valid `IdentifiedThreeElementClassNames` and whose values are instances of `IdentifiedThreeElementClass`. The `str` part of the key signature and the `dict` component of the value indicate that, raw strings can be substituted for class names and plain dictionaries for class values.

Noting the definition for `KeyedThreeElementClass` is:

    KeyedThreeElementClass:
      description: A keyed class with an additional integer and date
      attributes:
        name:
          range: string
          key: true
        value:
          range: integer
        modifier:
          range: date

One can represent instances of `OptionalMultivaluedInlinedListIdentifiedThreeElementRange` in YAML as:

    element1:
      value: 17
      modifier: 2012-03-11
    element2:
    element3:
      name: element3
      value: 42

or in python as:

    ktec_examples = OptionalMultivaluedInlinedListIdentifiedThreeElementRange(
        {
            element1: {value: 17, modifier: "2012-03-11"},
            KeyedThreeElementClassKey('element2'): {},
            element3: {name: 'element3', value: 42}
        }
    )

Note that the following is not valid because, while the `name` attribute (the key) is not required, as it has already been established by the key, if present it must match the key itself

    element3:
      name: test
      value: 42

One of the reasons for the explicit key types (e.g. `KeyedThreeElementClassKey`) is to provide some level of checking for potential link mismatches. The following code snippet:

    itc = IdentifiedThreeElementClass('entry1', 17)
    ktec_examples: OptionalMultivaluedInlinedListIdentifiedThreeElementRange = {
        itc.name: {value: 42}
    }

Would result in an error, as `itc.name` is an instance of `IdentifiedThreeElementClassKey` while `KeyedThreeElementClass` expects a `KeyedThreeElementClassKey`

### Input in the form of a list

Starting with the skeleton we visited earlier, Moving inwards, `Union[Dict[...], List[...]]` the second option, `List[Union[dict, IdentifiedThreeElementClass]]` indicates that the input to the constructor can also be a list of dictionaries that conform to the `IdentifiedThreeElementClass` model. Referencing the examples that we used above, we could also use:

    - name: element1
      value: 17
      modifier: 2012-03-11
    - name: element2
    - name: element3
      value: 42

And the Python equivalent would be:

    ktec_examples = OptionalMultivaluedInlinedListIdentifiedThreeElementRange(
        [ {name: 'element1', value: 17, modifier: "2012-03-11"},
          {name: KeyedThreeElementClassKey('element2')},
          {name: 'element3', value: 42}
        ]
    )

We then move on to the generated code:

    def __post_init__(self, **kwargs: Dict[str, Any]):
        if self.v1 is None:
            self.v1 = []
        if not isinstance(self.v1, (list, dict)):
            self.v1 = [self.v1]

The first two tests are pretty close to what we saw earlier, with the exception that you can’t pass an isolated dictionary. You can construct a list of integers with a single value:

    my_entries = ListOfIntegers(17)

Which would, if the class `ListOfIntegers`’s first variable was a multivalued slot with a range of intgers, result in `my_entries.v == [17]`. When dealing with classes, however, the following:

    my_entries = ListOfClasses(dict(name='element1', value=17))

would fail, as it would try to convert this into a list with two `IdentifiedThreeElementClass` entries.

The final line in the `__post_init__` section:

    self._normalize_inlined_slot(slot_name="v1", slot_type=IdentifiedThreeElementClass, key_name="name", inlined_as_list=True, keyed=True)

passes the remainder of the post initilization normalization to a function defined in `YAMLRoot`.

The final case, Case 2.4 (Inlined as dictionary) is essentially the same, with the exception that the target type is a dictionary rather than a list.

    OptionalMultivaluedInlinedIdentifiedThreeElementRangeList:
        description: 2.4(o) Range is an optional identified three element class that is represented as an inlined dictionary
        attributes:
          v1:
            range: IdentifiedThreeElementClass
            multivalued: true
            inlined: true

      RequiredMultivaluedInlinedKeyedThreeElementRange:
        description: 2.4(r) Range is a required keyed three element class that is represented as an inlined dictionary
        attributes:
          v1:
            range: KeyedThreeElementClass
            multivalued: true
            inlined: true
            required: true

    @dataclass
    class OptionalMultivaluedInlinedIdentifiedThreeElementRangeList(YAMLRoot):
        """
        2.4(o) Range is an optional identified three element class that is represented as an inlined dictionary
        """
        ...
        v1: Optional[Union[Dict[Union[str, IdentifiedThreeElementClassName], Union[dict, IdentifiedThreeElementClass]], List[Union[dict, IdentifiedThreeElementClass]]]] = empty_dict()

        def __post_init__(self, **kwargs: Dict[str, Any]):
            if self.v1 is None:
                self.v1 = []
            if not isinstance(self.v1, (list, dict)):
                self.v1 = [self.v1]
            self._normalize_inlined_slot(slot_name="v1", slot_type=IdentifiedThreeElementClass, key_name="name", inlined_as_list=None, keyed=True)

            super().__post_init__(**kwargs)

    @dataclass
    class RequiredMultivaluedInlinedKeyedThreeElementRange(YAMLRoot):
        """
        2.4(r) Range is a required keyed three element class that is represented as an inlined dictionary
        """
        ...
        v1: Union[Dict[Union[str, KeyedThreeElementClassName], Union[dict, KeyedThreeElementClass]], List[Union[dict, KeyedThreeElementClass]]] = empty_dict()

        def __post_init__(self, **kwargs: Dict[str, Any]):
            if self.v1 is None:
                raise ValueError("v1 must be supplied")
            elif not isinstance(self.v1, (list, dict)):
                self.v1 = [self.v1]
            if len(self.v1) == 0:
                raise ValueError(f"v1 must be a non-empty list, dictionary, or class")
            self._normalize_inlined_slot(slot_name="v1", slot_type=KeyedThreeElementClass, key_name="name", inlined_as_list=None, keyed=True)

            super().__post_init__(**kwargs)

The generated code is identical to the previous case (2.3) with the exception that the default initializer is `empty_dict()` instead of `empty_list()`, and the `_normalized_inlined_slot` function is passed a false `inlined_as_list` value (well, ‘None’ but…).

As a final note on the `inlined_as_list` vs. just plain `inlined`, while the internal storage structure is different, the initialization code is exactly the same – YAML emitted from a list structure can be loaded as a dictionary and visa-versa.

#### Slots

One bit that we haven’t touched on here is the `slots` section of the emitted Python.

At the moment, the following code is emitted:

    # Slots
    class slots:
        pass

        ...

    slots.RequiredInlinedKeyedTwoElementRange_v1 = Slot(uri=LISTS_AND_KEYS.v1, name="RequiredInlinedKeyedTwoElementRange_v1", curie=LISTS_AND_KEYS.curie('v1'),
                       model_uri=LISTS_AND_KEYS.RequiredInlinedKeyedTwoElementRange_v1, domain=RequiredInlinedKeyedTwoElementRange, range=Union[dict, KeyedTwoElementClass])

    slots.RequiredInlinedKeyedTwoElementRangeList_v1 = Slot(uri=LISTS_AND_KEYS.v1, name="RequiredInlinedKeyedTwoElementRangeList_v1", curie=LISTS_AND_KEYS.curie('v1'),
                       model_uri=LISTS_AND_KEYS.RequiredInlinedKeyedTwoElementRangeList_v1, domain=RequiredInlinedKeyedTwoElementRangeList, range=Union[Dict[Union[str, KeyedTwoElementClassName], Union[dict, KeyedTwoElementClass]], List[Union[dict, KeyedTwoElementClass]]])

    slots.RequiredInlinedKeyedThreeElementRange_v1 = Slot(uri=LISTS_AND_KEYS.v1, name="RequiredInlinedKeyedThreeElementRange_v1", curie=LISTS_AND_KEYS.curie('v1'),
                       model_uri=LISTS_AND_KEYS.RequiredInlinedKeyedThreeElementRange_v1, domain=RequiredInlinedKeyedThreeElementRange, range=Union[dict, KeyedThreeElementClass])

    slots.RequiredInlinedKeyedThreeElementRangeList_v1 = Slot(uri=LISTS_AND_KEYS.v1, name="RequiredInlinedKeyedThreeElementRangeList_v1", curie=LISTS_AND_KEYS.curie('v1'),
                       model_uri=LISTS_AND_KEYS.RequiredInlinedKeyedThreeElementRangeList_v1, domain=RequiredInlinedKeyedThreeElementRangeList, range=Union[Dict[Union[str, KeyedThreeElementClassName], Union[dict, KeyedThreeElementClass]], List[Union[dict, KeyedThreeElementClass]]])

The `slots` class is here to keep the module namespace from getting too confusing, the idea that, if you want to reference a slot, you use `slots.RequiredInlinedTwoElementRange_v1` (which is the mangling of the owner class name and the actual slot)

At the moment, you have the following elements:

- uri - the SLOT URI

- name - the mangled name

- curie - the SLOT CURIE

- model_uri - the slot URI where the base is the Model URI vs. one assigned via `slot_uri`

- domain - the slot domain

- range - the range signature

Note, however, that the slots area is still being enhanced, so keep an eye on entries like Issue #272