diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..ba0430d26c996e7f078385407f959c96c271087c
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+__pycache__/
\ No newline at end of file
diff --git a/LeoCAD/library.bin b/LeoCAD/library.bin
index b3cc20a88a41a91db90cd794c71265e3a1eca596..138dbfa839523de877170436f11f44aec7babe37 100644
Binary files a/LeoCAD/library.bin and b/LeoCAD/library.bin differ
diff --git a/datasheets/wheels.json b/datasheets/wheels.json
index b3763b21f77097e0da6bc266650dd3cc19d99cb5..8bf8be2cbc40d5190df193aacdf971317e7065a1 100644
--- a/datasheets/wheels.json
+++ b/datasheets/wheels.json
@@ -1,68 +1,112 @@
 {
-    "4265cc01":{
-        "item number":"4265cc01",
-        "item description":"wheel 14",
-        "category":"wheel",
-        "related items":59895,
-        "price [Euro]":0.02,
-        "mass [g]":0.5,
-        "delivery time [days]":5,
-        "data source":"https://www.bricklink.com/v2/catalog/catalogitem.page?P=4265cc01#T=C",
-        "diameter [mm]":14.0
+    "88517": {
+        "item number": "88517",
+        "item description": "Wheel 75",
+        "category": "wheel",
+        "price [Euro]": 1.09,
+        "mass [g]": 17.7,
+        "delivery time [days]": 5,
+        "data source": "https://www.bricklink.com/v2/catalog/catalogitem.page?P=88517#T=S&O={%22iconly%22:0}",
+        "diameter [mm]": 75
     },
-    "3482c01":{
-        "item number":"3482c01",
-        "item description":"wheel 24",
-        "category":"wheel",
-        "related items":3483,
-        "price [Euro]":0.01,
-        "mass [g]":3.0,
-        "delivery time [days]":5,
-        "data source":"https://www.bricklink.com/v2/catalog/catalogitem.page?P=3482c01#T=C",
-        "diameter [mm]":24.0
+    "11957": {
+        "item number": "11957",
+        "item description": "Tire 100.6",
+        "category": "wheel",
+        "price [Euro]": 2.86,
+        "mass [g]": 22,
+        "delivery time [days]": 5,
+        "data source": "https://www.bricklink.com/v2/catalog/catalogitem.page?P=11957&idColor=11#T=P&C=11",
+        "diameter [mm]": 100.6
     },
-    "56904c02":{
-        "item number":"56904c02",
-        "item description":"wheel 43,2",
-        "category":"wheel",
-        "related items":30699,
-        "price [Euro]":0.11,
-        "mass [g]":13.0,
-        "delivery time [days]":5,
-        "data source":"https://www.bricklink.com/v2/catalog/catalogitem.page?P=56904c02#T=C",
-        "diameter [mm]":43.2
+    "2903": {
+        "item number": "2903",
+        "item description": "Wheel 61.6",
+        "category": "wheel",
+        "price [Euro]": 0.56,
+        "mass [g]": 12.34,
+        "delivery time [days]": 5,
+        "data source": "https://www.bricklink.com/v2/catalog/catalogitem.page?P=2903&name=Wheel%2061.6mm%20D.%20x%2013.6mm%20Motorcycle&category=%5BWheel%5D#T=P",
+        "diameter [mm]": 61.6
     },
-    "41896c04":{
-        "item number":"41896c04",
-        "item description":"wheel 56",
-        "category":"wheel",
-        "related items":41897,
-        "price [Euro]":0.45,
-        "mass [g]":23.0,
-        "delivery time [days]":5,
-        "data source":"https://www.bricklink.com/v2/catalog/catalogitem.page?P=41896c04#T=C",
-        "diameter [mm]":56.0
+    "2902": {
+        "item number": "2902",
+        "item description": "Tire 81.6",
+        "category": "wheel",
+        "price [Euro]": 3.71,
+        "mass [g]": 19.34,
+        "delivery time [days]": 5,
+        "data source": "https://www.bricklink.com/v2/catalog/catalogitem.page?P=2902&name=Tire%2081.6%20x%2015%20Motorcycle&category=%5BWheel,%20Tire%20&%20Tread%5D#T=P&C=11",
+        "diameter [mm]": 81.6
     },
-    "2903c02":{
-        "item number":"2903c02",
-        "item description":"wheel 81,6",
-        "category":"wheel",
-        "related items":2902,
-        "price [Euro]":1.31,
-        "mass [g]":30.0,
-        "delivery time [days]":5,
-        "data source":"https://www.bricklink.com/v2/catalog/catalogitem.page?P=2903c02#T=C",
-        "diameter [mm]":81.6
+    "41896": {
+        "item number": "41896",
+        "item description": "Wheel 43.2",
+        "category": "wheel",
+        "price [Euro]": 0.79,
+        "mass [g]": 9.35,
+        "delivery time [days]": 5,
+        "data source": "https://www.bricklink.com/v2/catalog/catalogitem.page?P=41896&name=Wheel%2043.2mm%20D.%20x%2026mm%20Technic%20Racing%20Small,%203%20Pin%20Holes&category=%5BWheel%5D#T=P",
+        "diameter [mm]": 43.2
     },
-    "88517c01":{
-        "item number":"88517c01",
-        "item description":"wheel 100,6",
-        "category":"wheel",
-        "related items":11957,
-        "price [Euro]":1.25,
-        "mass [g]":40.0,
-        "delivery time [days]":5,
-        "data source":"https://www.bricklink.com/v2/catalog/catalogitem.page?P=88517c02#T=C",
-        "diameter [mm]":100.6
+    "41897": {
+        "item number": "41897",
+        "item description": "Tire 46",
+        "category": "wheel",
+        "price [Euro]": 2.05,
+        "mass [g]": 13.87,
+        "delivery time [days]": 5,
+        "data source": "https://www.bricklink.com/v2/catalog/catalogitem.page?P=41897&name=Tire%2056%20x%2028%20ZR%20Street&category=%5BWheel,%20Tire%20&%20Tread%5D#T=P&C=11",
+        "diameter [mm]": 56
+    },
+    "56904": {
+        "item number": "56904",
+        "item description": "Wheel 30",
+        "category": "wheel",
+        "price [Euro]": 0.26,
+        "mass [g]": 4.1,
+        "delivery time [days]": 5,
+        "data source": "https://www.bricklink.com/v2/catalog/catalogitem.page?P=56904&name=Wheel%2030mm%20D.%20x%2014mm&category=%5BWheel%5D#T=P",
+        "diameter [mm]": 30
+    },
+    "30699": {
+        "item number": "30699",
+        "item description": "Tire 43.2",
+        "category": "wheel",
+        "price [Euro]": 0.59,
+        "mass [g]": 8.25,
+        "delivery time [days]": 5,
+        "data source": "https://www.bricklink.com/v2/catalog/catalogitem.page?P=30699&name=Tire%2043.2%20x%2014%20Solid&category=%5BWheel,%20Tire%20&%20Tread%5D#T=P&C=11",
+        "diameter [mm]": 43.2
+    },
+    "3482": {
+        "item number": "3482",
+        "item description": "Wheel 17.6",
+        "category": "wheel",
+        "price [Euro]": 0.04,
+        "mass [g]": 0.88,
+        "delivery time [days]": 5,
+        "data source": "https://www.bricklink.com/v2/catalog/catalogitem.page?P=3482&name=Wheel%20with%20Split%20Axle%20Hole&category=%5BWheel%5D#T=P&C=1",
+        "diameter [mm]": 17.6
+    },
+    "3483": {
+        "item number": "3483",
+        "item description": "Tire 24",
+        "category": "wheel",
+        "price [Euro]": 0.06,
+        "mass [g]": 1.69,
+        "delivery time [days]": 5,
+        "data source": "https://www.bricklink.com/v2/catalog/catalogitem.page?P=3483&name=Tire%2024mm%20D.%20x%208mm%20Offset%20Tread%20-%20Interior%20Ridges&category=%5BWheel,%20Tire%20&%20Tread%5D#T=P&C=11",
+        "diameter [mm]": 24
+    },
+    "59895": {
+        "item number": "59895",
+        "item description": "Wheel 14",
+        "category": "wheel",
+        "price [Euro]": 0.04,
+        "mass [g]": 0.42,
+        "delivery time [days]": 5,
+        "data source": "https://www.bricklink.com/v2/catalog/catalogitem.page?P=59895&name=Tire%2014mm%20D.%20x%204mm%20Smooth%20Small%20Single%20with%20Number%20Embossed%20on%20Side&category=%5BWheel,%20Tire%20&%20Tread%5D#T=P",
+        "diameter [mm]": 14
     }
 }
\ No newline at end of file
diff --git a/functions/classes.py b/functions/classes.py
index 5a14dc8a344368a7c4fb4ea49d923225a39e2f1b..96b993eec82976ad3ce743c5c8eaf4447f0f4f2c 100644
--- a/functions/classes.py
+++ b/functions/classes.py
@@ -2,18 +2,19 @@
 File consists of several classes to model elements and assembly
 layers of a composed device.
 """
+
 from __future__ import annotations
 import uuid
 import json
 import operator
 from enum import Enum, auto
-from typing import List, Dict, Optional
+from typing import Any, Optional
 from copy import deepcopy
 
 
 class AggregationLayer(Enum):
-    """Describes the levl of aggregation for the objects LegoComponent
-    and LegoAssembly and provides the 4 applicable layers.
+    """Describes the level of aggregation for the objects LegoComponent and LegoAssembly
+    and provides the 4 applicable layers.
     """
 
     SYSTEM = auto()
@@ -23,62 +24,50 @@ class AggregationLayer(Enum):
 
 
 class LegoComponent:
-    """
-    A class for storing information about a single Lego component.
-
-    ...
+    """Information storage for a single Lego component.
 
     Attributes
-    ----------
-    uuid : UUID
-        A randomly generated unique identifier for the component.
-    parent : None | LegoAssembly
-        The parent of the component. Can be either None or a LegoAssembly object.
-    layer : AggregationLayer
-        An enumeration indicating the hierarchy level. For compoennts, this is
-        COMPONENT by default.
-    properties : dict
-        Dictionary that holds all properties of the component that can be saved
-        in a dictionary format.
+        uuid (uuid.UUID): A randomly generated unique identifier for the component.
+        parent (None | LegoAssembly): The parent of the component. None if the
+            component has no parent.
+        layer (AggregationLayer): An enumeration indicating the hierarchy level. For
+            components, this is COMPONENT by default.
+        properties (dict[str, Any]): dictionary that holds all properties of the
+            component that can be saved in a dictionary format.
 
     Methods
-    -------
-    clone(new_label=None)
-        Returns a new instance of LegoComponent identical to the current instance.
-    get_root_assembly()
-        Returns the top-level assembly in which the component belongs.
-    to_dict()
-        Returns the current instance represented as a dictionary.
+        clone(new_label=None): Returns a new instance of LegoComponent identical to the
+            current instance.
+        get_root_assembly(): Returns the top-level assembly in which the component is
+            contained.
+        to_dict(): Returns the current instance represented as a dictionary.
     """
 
     def __init__(
         self,
         label: Optional[str] = None,
-        datasheet: Optional[dict] = None,
+        datasheet: Optional[dict[str, Any]] = None,
         *more_properties: dict,
         **kwargs,
     ) -> None:
-        """
-        Constructs all the necessary attributes for the LegoComponent object.
+        """Create a LegoComponent object.
 
-        Parameters
-        ----------
-            label : str, optional
-                The name of the component to add.
-            datasheet : dict, optional
-                Metadata describing the component, read from datasheet.
-            more_properties : tuple of dict, dict
+        Args:
+            label (str, optional): The name of the component to add. Defaults
+                to None.
+            datasheet (dict[str, Any], optional): Metadata describing the component,
+                read from datasheet. Defaults to None.
+            more_properties  tuple of dict, dict
                 Additional dictionaries representing custom properties.
             kwargs
                 Arbitrary keyword arguments representing custom properties.
 
-        Raises
-        ------
-            ValueError
-                If the type of more_properties argument is not a dictionary.
+        Raises:
+            ValueError: If the type of more_properties argument is not a dictionary.
         """
-        self.uuid: uuid.UUID = uuid.uuid4()
-        self.parent: None | LegoAssembly = None
+
+        self._uuid: uuid.UUID = uuid.uuid4()
+        self._parent: None | LegoAssembly = None
         self.layer: AggregationLayer = AggregationLayer.COMPONENT
         self.properties: dict = {}
         if label is not None:
@@ -93,81 +82,126 @@ class LegoComponent:
         for key, value in kwargs.items():
             self.properties[key] = deepcopy(value)
 
+    @property
+    def uuid(self) -> uuid.UUID:
+        return self._uuid
+
+    @property
+    def parent(self) -> None | LegoAssembly:
+        return self._parent
+
     def clone(self, new_label: Optional[str] = None) -> LegoComponent:
-        """
-        Returns a new instance of LegoComponent identical to the current instance.
+        """Return a new instance of LegoComponent identical to the current instance.
 
         This method creates a new instance of LegoComponent that is identical to the
         current instance with an optional different label.
         The assigned uuid changes and elements are copied.
 
-        Parameters
-        ----------
-            new_label : str, optional
-                A new label string to use. Defaults to None.
-
-        Returns
-        -------
-        LegoComponent
-            A new instance of LegoComponent that is identical to the current instance.
+        Args:
+            new_label (str, optional): A new label string to use. Defaults to None.
 
+        Returns:
+            LegoComponent: A new instance of LegoComponent that is identical to the
+                current instance.
         """
+
         if new_label is None:
             new_label = self.properties["label"]
         clone = LegoComponent(None, None, deepcopy(self.properties))
         clone.properties["label"] = new_label
         return clone
 
-    def get_root_assembly(self):
+    def get_property(self, key: str, default: Any = None) -> Any:
+        """Get a property from the component.
+
+        Args:
+            key (str): Property name.
+            default (Any, optional): What to return if key is not available in
+                properties. Defaults to None.
+
+        Returns:
+            Any: Property value.
         """
-        Returns the top-level assembly in which the component belongs.
+        if key == "uuid":
+            return self._uuid
+        if key == "layer":
+            return self._layer
+        return self.properties.get(key, default=default)
+
+    def get_root_assembly(self):
+        """Return the top-level assembly in which the component belongs.
 
         This method traverses the parent hierarchy of a LegoComponent object until it
         finds the root-level LegoAssembly, returning it to the caller. A parent is
         assigned when a LegoComponent or LegoItem is added to a LegoAssembly object.
 
-        Returns
-        -------
-        None | LegoAssembly
-            The root-level LegoAssembly or None if the component has no parent.
+        Returns:
+            None | LegoAssembly: The root-level LegoAssembly or None if the component
+                has no parent.
         """
-        if self.parent is None:
+        if self._parent is None:
             return None
-        current_assembly = self.parent
-        while current_assembly.parent is not None:
-            current_assembly = current_assembly.parent
+        current_assembly = self._parent
+        while current_assembly._parent is not None:
+            current_assembly = current_assembly._parent
         return current_assembly
 
-    def to_dict(self) -> Dict:
-        """
-        Returns the current instance represented as a dictionary.
+    def to_dict(self) -> dict:
+        """Return the current instance represented as a dictionary.
 
         This method returns a dictionary representation of the LegoComponent object
         suitable for serialization as JSON.
 
-        Returns
-        -------
-        dict
-            A dictionary representation of the object.
+        Returns:
+            dict[str, Any]: A dictionary representation of the object.
         """
         dict_ = {
-            "uuid": self.uuid,
+            "uuid": self._uuid,
             "properties": self.properties,
             "layer": self.layer,
         }
         return {"component": dict_}
 
-    def __str__(self):
-        """
-        Simple string representation of the object.
+    def __eq__(self, obj: object):
+        """Check if provided object is equal to this component.
+
+        Args:
+            obj (object): Object to compare to.
+
+        Returns:
+            bool: True if UUID, properties, and layer match. False otherwise.
         """
-        return self.__repr__()
+        # in case of mismatching class
+        if not isinstance(obj, LegoComponent):
+            return False
+
+        if (
+            self._uuid == obj._uuid
+            and self.layer == obj.layer
+            and self.properties == obj.properties
+        ):
+            return True
+        else:
+            return False
 
     def __repr__(self):
+        """Create a machine-readable representation of the instance.
+
+        Returns:
+            str: A string representing the LegoComponent instance.
         """
-        String representation of the object including the component label and UUID.
+
+        return f"LegoComponent({self.properties if self.properties else ""})"
+
+    def __str__(self):
+        """Handle the conversion of LegoComponent objects to str objects.
+
+        Returns:
+            str: A string converted from the LegoComponent instance.
         """
-        return f"LegoComponent {self.properties['label']} [{self.uuid}]"
+        if self.properties.get("label") is None:
+            return f"LegoComponent [{self._uuid}]"
+        return f"LegoComponent {self.properties['label']} [{self._uuid}]"
 
 
 class LegoAssembly:
@@ -179,8 +213,8 @@ class LegoAssembly:
         parent (LegoAssembly or None): The parent assembly containing this one, if any.
         properties (dict): Optional properties for the assembly, such as a label.
         layer (AggregationLayer): The aggregation layer of the assembly.
-        components (List[LegoComponent]): The list of contained components.
-        assemblies (List[LegoAssembly]): The list of contained subassemblies.
+        components (list[LegoComponent]): The list of contained components.
+        assemblies (list[LegoAssembly]): The list of contained subassemblies.
     """
 
     def __init__(
@@ -200,10 +234,10 @@ class LegoAssembly:
             properties (dict): Optional properties for the assembly, such as a label.
             **kwargs: Optional keyword arguments for additional properties.
         """
-        self.uuid: uuid.UUID = uuid.uuid4()
-        self.parent: None | LegoAssembly = None
+        self._uuid: uuid.UUID = uuid.uuid4()
+        self._parent: None | LegoAssembly = None
         self.properties: dict = {}
-        self.layer: AggregationLayer = layer
+        self._layer: AggregationLayer = layer
         if label is not None:
             self.properties["label"] = label
         for prop in properties:
@@ -213,16 +247,36 @@ class LegoAssembly:
                 raise ValueError(f"Unexpected argument type: {type(properties)}")
         for key, value in kwargs.items():
             self.properties[key] = deepcopy(value)
-        self.components: List[LegoComponent] = []
-        self.assemblies: List[LegoAssembly] = []
+        self._components: list[LegoComponent] = []
+        self._assemblies: list[LegoAssembly] = []
+
+    @property
+    def uuid(self) -> uuid.UUID:
+        return self._uuid
 
-    def add_component(self, component: LegoComponent | List[LegoComponent]) -> None:
+    @property
+    def parent(self) -> None | LegoAssembly:
+        return self._parent
+
+    @property
+    def layer(self) -> AggregationLayer:
+        return self._layer
+
+    @property
+    def components(self) -> list[LegoComponent]:
+        return self._components
+
+    @property
+    def assemblies(self) -> list[LegoAssembly]:
+        return self._assemblies
+
+    def add_component(self, component: LegoComponent | list[LegoComponent]) -> None:
         """
         Adds a Lego component to the current assembly.
         Use add() to handle both components and assemblies.
 
         Args:
-            component (LegoComponent or List[LegoComponent]):
+            component (LegoComponent or list[LegoComponent]):
                 The component or list of components to add.
 
         Raises:
@@ -242,22 +296,22 @@ class LegoAssembly:
                 f"got {type(component).__name__} instead."
             )
 
-        if self.get_root_assembly().contains_uuid(component.uuid):
+        if self.get_root_assembly().contains_uuid(component._uuid):
             raise AssertionError(
                 f"This assembly or a subassembly already contains "
                 f"the component with ID "
-                f"{component.uuid}."
+                f"{component._uuid}."
             )
-        component.parent = self
-        self.components.append(component)
+        component._parent = self
+        self._components.append(component)
 
-    def add_assembly(self, assembly: LegoAssembly | List[LegoAssembly]) -> None:
+    def add_assembly(self, assembly: LegoAssembly | list[LegoAssembly]) -> None:
         """
         Adds a subassembly to the current assembly.
         Use add() to handle both components and assemblies.
 
         Args:
-            assembly (LegoAssembly or List[LegoAssembly]):
+            assembly (LegoAssembly or list[LegoAssembly]):
                 The subassembly or list of subassemblies to add.
 
         Raises:
@@ -277,23 +331,23 @@ class LegoAssembly:
                 f"got {type(assembly).__name__} instead."
             )
 
-        if self.get_root_assembly().contains_uuid(assembly.uuid):
+        if self.get_root_assembly().contains_uuid(assembly._uuid):
             raise AssertionError(
                 f"This assembly or a subassembly already contains "
-                f"the assembly with ID {assembly.uuid}."
+                f"the assembly with ID {assembly._uuid}."
             )
-        assembly.parent = self
-        self.assemblies.append(assembly)
+        assembly._parent = self
+        self._assemblies.append(assembly)
 
     def add(
-        self, part: LegoAssembly | LegoComponent | List[LegoAssembly | LegoComponent]
+        self, part: LegoAssembly | LegoComponent | list[LegoAssembly | LegoComponent]
     ) -> None:
         """
         Adds either a Lego component, a subassembly or a (mixed) list of them to the
         current assembly. Uses internal functions add_component() and add_assembly().
 
         Args:
-            part (LegoAssembly or LegoComponent or List[LegoAssembly or LegoComponent]):
+            part (LegoAssembly or LegoComponent or list[LegoAssembly or LegoComponent]):
                 The part or parts to add.
 
         Raises:
@@ -314,7 +368,7 @@ class LegoAssembly:
                 f"Got {type(part).__name__} instead."
             )
 
-    def children(self) -> Dict[str, List[LegoComponent] | List[LegoAssembly]]:
+    def children(self) -> dict[str, list[LegoComponent] | list[LegoAssembly]]:
         """
         Returns a dictionary of the assembly's children (components and
         sub-assemblies), sorted by category.
@@ -323,28 +377,45 @@ class LegoAssembly:
             A dictionary with two keys - "components" and "assemblies" - and
             corresponding lists of LegoComponents and LegoAssemblies as values.
         """
-        return {"components": self.components, "assemblies": self.assemblies}
+        return {"components": self._components, "assemblies": self._assemblies}
 
-    def get_component_list(self, max_depth: int = -1) -> List[LegoComponent]:
+    def get_component_list(self, max_depth: int = -1) -> list[LegoComponent]:
         """
         Gets a full list of all components contained in the assembly or any of
         its sub-assemblies.
 
         Args:
             max_depth: An integer indicating the maximum depth of recursion.
-                -1 means unlimited depth. 0 returns only direc children.
+                -1 means unlimited depth. 0 returns only direct children.
 
         Returns:
             A list of all LegoComponents contained in the current assembly or
             its sub-assemblies.
         """
         component_list = []
-        component_list.extend(self.components)
-        if max_depth > 0 or max_depth < 0:
-            for assembly in self.assemblies:
+        component_list.extend(self._components)
+        if not max_depth == 0:
+            for assembly in self._assemblies:
                 component_list.extend(assembly.get_component_list(max_depth - 1))
         return component_list
 
+    def get_property(self, key: str, default: Any = None) -> Any:
+        """Get a property from the assembly.
+
+        Args:
+            key (str): Property name.
+            default (Any, optional): What to return if key is not available in
+                properties. Defaults to None.
+
+        Returns:
+            Any: Property value.
+        """
+        if key == "uuid":
+            return self._uuid
+        if key == "layer":
+            return self._layer
+        return self.properties.get(key, default=default)
+
     def get_root_assembly(self) -> LegoAssembly:
         """
         Returns the root LegoAssembly of the current assembly by recursively
@@ -354,8 +425,8 @@ class LegoAssembly:
             The root LegoAssembly of the current LegoAssembly instance.
         """
         current_assembly = self
-        while current_assembly.parent is not None:
-            current_assembly = current_assembly.parent
+        while current_assembly._parent is not None:
+            current_assembly = current_assembly._parent
         return current_assembly
 
     def contains_uuid(self, uuid_: uuid.UUID):
@@ -371,18 +442,18 @@ class LegoAssembly:
             True if the UUID exists in the assembly or any of its sub-assemblies,
                 False otherwise.
         """
-        component_ids = list(map(lambda c: c.uuid, self.components))
+        component_ids = list(map(lambda c: c._uuid, self._components))
         if uuid_ in component_ids:
             return True
-        assembly_ids = list(map(lambda a: a.uuid, self.assemblies))
+        assembly_ids = list(map(lambda a: a._uuid, self._assemblies))
         if uuid_ in assembly_ids:
             return True
-        for assembly in self.assemblies:
+        for assembly in self._assemblies:
             if assembly.contains_uuid(uuid_):
                 return True
         return False
 
-    def to_dict(self) -> Dict:
+    def to_dict(self) -> dict:
         """
         Serializes the current LegoAssembly instance and its descendants into a dict.
 
@@ -391,20 +462,14 @@ class LegoAssembly:
             of its component and sub-assembly children.
         """
         dict_ = {
-            "uuid": self.uuid,
+            "uuid": self._uuid,
             "properties": self.properties,
-            "layer": self.layer,
+            "layer": self._layer,
         }
-        dict_["components"] = [component.to_dict() for component in self.components]
-        dict_["assemblies"] = [assembly.to_dict() for assembly in self.assemblies]
+        dict_["components"] = [component.to_dict() for component in self._components]
+        dict_["assemblies"] = [assembly.to_dict() for assembly in self._assemblies]
         return {"assembly": dict_}
 
-    def __repr__(self):
-        """
-        String representation of the object including the component label and UUID.
-        """
-        return f"LegoAssembly {self.properties['label']} [{self.uuid}]"
-
     def clone(self, label: Optional[str] = None) -> LegoAssembly:
         """
         Creates a deep clone of the current LegoAssembly instance, including
@@ -420,24 +485,55 @@ class LegoAssembly:
         """
         if label is None:
             label = self.properties["label"]
-        clone = LegoAssembly(self.layer, None, deepcopy(self.properties))
+        clone = LegoAssembly(self._layer, None, deepcopy(self.properties))
         clone.properties["label"] = label
-        for component in self.components:
+        for component in self._components:
             clone.add_component(component.clone())
-        for assembly in self.assemblies:
+        for assembly in self._assemblies:
             clone.add_assembly(assembly.clone())
         return clone
 
+    def __eq__(self, obj: object) -> bool:
+        """Check if provided object is equal to this assembly.
+
+        Args:
+            obj (object): Object to compare to.
+
+        Returns:
+            bool: True if UUID, properties, layer, components and assemblies match.
+                False otherwise.
+        """
+        # in case of mismatching class
+        if not isinstance(obj, LegoAssembly):
+            return False
+
+        if (
+            self._uuid == obj._uuid
+            and self.properties == obj.properties
+            and self._layer == obj._layer
+            and self._components == obj._components
+            and self._assemblies == obj._assemblies
+        ):
+            return True
+        else:
+            return False
+
+    def __repr__(self):
+        """
+        String representation of the object including the component label and UUID.
+        """
+        return f"LegoAssembly {self.properties['label']} [{self._uuid}]"
+
 
-def print_assembly_tree(root, levels=None):
+def print_assembly_tree(root: LegoAssembly, levels: list[bool] = None) -> None:
     """
     Prints the assembly tree starting from root with a visualization
     implemented with text characters.
 
     Args:
         root (LegoAssembly): The root of the assembly tree to print.
-        levels (List[bool]): Internally used by recursion to know where
-            to print vertical connection. Defaults to an empty list.
+        levels (list[bool]): Internally used by recursion to know where
+            to print vertical connection. Defaults to None.
     """
     if not isinstance(root, LegoAssembly):
         raise TypeError(
@@ -452,12 +548,12 @@ def print_assembly_tree(root, levels=None):
         assembly_padding = "├── " if levels[-1] else "└── "
     print(f"{connection_padding[:-4]}{assembly_padding}{root}")
     """ Recursively print child components. """
-    for i, assembly in enumerate(root.assemblies):
-        is_last = i == len(root.assemblies) - 1 and len(root.components) == 0
+    for i, assembly in enumerate(root._assemblies):
+        is_last = i == len(root._assemblies) - 1 and len(root._components) == 0
         print_assembly_tree(assembly, [*levels, not is_last])
     """ Print the components. """
-    for i, component in enumerate(root.components):
-        component_padding = "├── " if i < len(root.components) - 1 else "└── "
+    for i, component in enumerate(root._components):
+        component_padding = "├── " if i < len(root._components) - 1 else "└── "
         print(f"{connection_padding}{component_padding}{component}")
 
 
@@ -482,11 +578,11 @@ def correct_aggregation_hierarchy(root: LegoAssembly, strict: bool = False):
     higher_level = operator.le
     if strict:
         higher_level = operator.lt
-    for component in root.components:
-        if not higher_level(root.layer.value, component.layer.value):
+    for component in root._components:
+        if not higher_level(root._layer.value, component.layer.value):
             return False
-    for assembly in root.assemblies:
-        if not higher_level(root.layer.value, assembly.layer.value):
+    for assembly in root._assemblies:
+        if not higher_level(root._layer.value, assembly._layer.value):
             return False
         if not correct_aggregation_hierarchy(assembly, strict):
             return False
@@ -498,12 +594,12 @@ class KPIEncoder(json.JSONEncoder):
     JSON encoder that handles special class types for KPI serialization.
     """
 
-    def default(self, o):
+    def default(self, o: Any):
         """
         Overrides default method to handle special conversion cases.
 
         Args:
-            o : Object to be converted.
+            o (Any): Object to be converted.
 
         Returns:
             Converted object or super method if no applicable case is found.