Repository: JWock82/Pynite
Pull request #252: Added type hints throughout
Author: login: JBloss1517
Created: 2025-04-07T21:47:48Z | Merged: 2025-04-26T21:18:13Z

This add type hints to almost all class and method functions throughout library. I tried to scope new type related imports under the `if TYPE_CHECKING:` flag to minimize actual runtime performance hits from additional imports.

There were a few places where the types were a little ambiguous (mostly on the internal methods), so the types might be narrowed too much. For example, the argument x might permit a list of floats inl ieu of just a single float. 
 
Also, I Left type errors that show up in the editor after defining types on things largely untouched to keep scope of this PR strictly to adding type hints to the code. Future PRs can address these kind of warnings. These warnings were good to be warned about and should probably be fixed at some point to prevent errors popping up later.

I also largely left the imports as you have them (i.e. didn't remove unused imports), but I did try to sort them into PEP8 style order (standard lib -> 3rd party -> this package).

All tests are still passing.


Diff:
diff --git a/Pynite/Analysis.py b/Pynite/Analysis.py
index 96f69df0..9f751244 100644
--- a/Pynite/Analysis.py
+++ b/Pynite/Analysis.py
@@ -1,9 +1,21 @@
+from __future__ import annotations # Allows more recent type hints features
+from typing import TYPE_CHECKING
 from math import isclose
-from Pynite.LoadCombo import LoadCombo
+
 from numpy import array, atleast_2d, zeros, subtract, matmul, divide, seterr, nanmax
 from numpy.linalg import solve
 
-def _prepare_model(model):
+from Pynite.LoadCombo import LoadCombo
+
+if TYPE_CHECKING:
+    from typing import List, Tuple
+    from Pynite.FEModel3D import FEModel3D
+    from numpy import float64
+    from numpy.typing import NDArray
+    from scipy.sparse import lil_matrix
+
+
+def _prepare_model(model: FEModel3D) -> None:
     """Prepares a model for analysis by ensuring at least one load combination is defined, generating all meshes that have not already been generated, activating all non-linear members, and internally numbering all nodes and elements.
 
     :param model: The model being prepared for analysis.
@@ -43,7 +55,7 @@ def _prepare_model(model):
     # Assign an internal ID to all nodes and elements in the model. This number is different from the name used by the user to identify nodes and elements.
     _renumber(model)
 
-def _identify_combos(model, combo_tags=None):
+def _identify_combos(model: FEModel3D, combo_tags: List[str] | None = None) -> List[LoadCombo]:
     """Returns a list of load combinations that are to be run based on tags given by the user.
 
     :param model: The model being analyzed.
@@ -57,7 +69,7 @@ def _identify_combos(model, combo_tags=None):
     # Identify which load combinations to evaluate
     if combo_tags is None:
         # Evaluate all load combinations if not tags have been provided
-        combo_list = model.load_combos.values()
+        combo_list = list(model.load_combos.values())
     else:
         # Initialize the list of load combinations to be evaluated
         combo_list = []
@@ -71,7 +83,7 @@ def _identify_combos(model, combo_tags=None):
     # Return the list of load combinations to be evaluated
     return combo_list
 
-def _check_stability(model, K):
+def _check_stability(model: FEModel3D, K: NDArray[float64]) -> None:
     """
     Identifies nodal instabilities in a model's stiffness matrix.
     """
@@ -130,7 +142,7 @@ def _check_stability(model, K):
 
     return
 
-def _PDelta_step(model, combo_name, P1, FER1, D1_indices, D2_indices, D2, log=True, sparse=True, check_stability=False, max_iter=30, first_step=True):
+def _PDelta_step(model: FEModel3D, combo_name: str, P1: NDArray[float64], FER1: NDArray[float64], D1_indices: List[int], D2_indices: List[int], D2: NDArray[float64], log: bool = True, sparse: bool = True, check_stability: bool = False, max_iter: int = 30, first_step: bool = True) -> None:
     """Performs second order (P-Delta) analysis. This type of analysis is appropriate for most models using beams, columns and braces. Second order analysis is usually required by material-specific codes. The analysis is iterative and takes longer to solve. Models with slender members and/or members with combined bending and axial loads will generally have more significant P-Delta effects. P-Delta effects in plates/quads are not considered.
 
     :param combo_name: The name of the load combination to evaluate P-Delta effects for.
@@ -269,7 +281,7 @@ def _PDelta_step(model, combo_name, P1, FER1, D1_indices, D2_indices, D2, log=Tr
     # Flag the model as solved
     model.solution = 'P-Delta'
 
-def _pushover_step(model, combo_name, push_combo, step_num, P1, FER1, D1_indices, D2_indices, D2, log=True, sparse=True, check_stability=False):
+def _pushover_step(model: FEModel3D, combo_name: str, push_combo: str, step_num: int, P1: NDArray[float64], FER1: NDArray[float64], D1_indices: List[int], D2_indices: List[int], D2: NDArray[float64], log: bool = True, sparse: bool = True, check_stability: bool = False) -> None:
 
     # Run at least one iteration
     run_step = True
@@ -376,7 +388,7 @@ def _pushover_step(model, combo_name, push_combo, step_num, P1, FER1, D1_indices
     # Flag the model as solved
     model.solution = 'Pushover'
 
-def _unpartition_disp(model, D1, D2, D1_indices, D2_indices):
+def _unpartition_disp(model: FEModel3D, D1: NDArray[float64], D2: NDArray[float64], D1_indices: List[int], D2_indices: List[int]) -> NDArray[float64]:
     """Unpartitions displacements from the solver and returns them as a global displacement vector
 
     :param model: The finite element model being evaluated
@@ -412,7 +424,7 @@ def _unpartition_disp(model, D1, D2, D1_indices, D2_indices):
     # Return the displacement vector
     return D
 
-def _store_displacements(model, D1, D2, D1_indices, D2_indices, combo):
+def _store_displacements(model: FEModel3D, D1: NDArray[float64], D2: NDArray[float64], D1_indices: List[int], D2_indices: List[int], combo: LoadCombo) -> None:
     """Stores calculated displacements from the solver into the model's displacement vector `_D` and into each node object in the model
 
     :param model: The finite element model being evaluated.
@@ -445,7 +457,7 @@ def _store_displacements(model, D1, D2, D1_indices, D2_indices, combo):
         node.RY[combo.name] = D[node.ID*6 + 4, 0]
         node.RZ[combo.name] = D[node.ID*6 + 5, 0]
 
-def _sum_displacements(model, Delta_D1, Delta_D2, D1_indices, D2_indices, combo):
+def _sum_displacements(model: FEModel3D, Delta_D1: NDArray[float64], Delta_D2: NDArray[float64], D1_indices: List[int], D2_indices: List[int], combo: LoadCombo) -> None:
     """Sums calculated displacements for a load step from the solver into the model's displacement vector `_D` and into each node object in the model.
 
     :param model: The finite element model being evaluated.
@@ -478,7 +490,7 @@ def _sum_displacements(model, Delta_D1, Delta_D2, D1_indices, D2_indices, combo)
         node.RY[combo.name] += Delta_D[node.ID*6 + 4, 0]
         node.RZ[combo.name] += Delta_D[node.ID*6 + 5, 0]
 
-def _check_TC_convergence(model, combo_name="Combo 1", log=True, spring_tolerance=0, member_tolerance=0):
+def _check_TC_convergence(model: FEModel3D, combo_name: str = "Combo 1", log: bool = True, spring_tolerance: float = 0, member_tolerance: float = 0) -> bool:
 
     # Assume the model has converged until we find out otherwise
     convergence = True
@@ -571,7 +583,7 @@ def _check_TC_convergence(model, combo_name="Combo 1", log=True, spring_toleranc
     # Return whether the TC analysis has converged
     return convergence
   
-def _calc_reactions(model, log=False, combo_tags=None):
+def _calc_reactions(model: FEModel3D, log: bool = False, combo_tags: List[str] | None = None) -> None:
     """
     Calculates reactions internally once the model is solved.
 
@@ -838,7 +850,7 @@ def _calc_reactions(model, log=False, combo_tags=None):
                 RZ = node.RZ[combo.name]
                 node.RxnMZ[combo.name] += k*RZ
 
-def _check_statics(model, combo_tags=None):
+def _check_statics(model: FEModel3D, combo_tags: List[str] | None = None) -> None:
     '''
     Checks static equilibrium and prints results to the console.
 
@@ -933,7 +945,7 @@ def _check_statics(model, combo_tags=None):
     print(statics_table)
     print('')
     
-def _partition_D(model):
+def _partition_D(model: FEModel3D) -> Tuple[List[int], List[int], NDArray[float64]]:
     """Builds a list with known nodal displacements and with the positions in global stiffness matrix of known and unknown nodal displacements
 
     :return: A list of the global matrix indices for the unknown nodal displacements (D1_indices). A list of the global matrix indices for the known nodal displacements (D2_indices). A list of the known nodal displacements (D2).
@@ -1028,7 +1040,7 @@ def _partition_D(model):
     # Return the indices and the known displacements
     return D1_indices, D2_indices, D2
 
-def _partition(model, unp_matrix, D1_indices, D2_indices):
+def _partition(model: FEModel3D, unp_matrix: NDArray[float64] | lil_matrix, D1_indices: List[int], D2_indices: List[int]) -> Tuple[NDArray[float64], NDArray[float64]] | Tuple[NDArray[float64], NDArray[float64], NDArray[float64], NDArray[float64]]:
     """Partitions a matrix (or vector) into submatrices (or subvectors) based on degree of freedom boundary conditions.
 
     :param unp_matrix: The unpartitioned matrix (or vector) to be partitioned.
@@ -1058,7 +1070,7 @@ def _partition(model, unp_matrix, D1_indices, D2_indices):
         m22 = unp_matrix[D2_indices, :][:, D2_indices]
         return m11, m12, m21, m22
 
-def _renumber(model):
+def _renumber(model: FEModel3D) -> None:
     """
     Assigns node and element ID numbers to be used internally by the program. Numbers are
     assigned according to the order in which they occur in each dictionary.
diff --git a/Pynite/BeamSegY.py b/Pynite/BeamSegY.py
index e75515cd..74100a47 100644
--- a/Pynite/BeamSegY.py
+++ b/Pynite/BeamSegY.py
@@ -1,11 +1,12 @@
 from Pynite.BeamSegZ import BeamSegZ
 
+
 # %%
 class BeamSegY(BeamSegZ):
 
 #%% 
     # Returns the moment at a location on the segment
-    def moment(self, x, P_delta=False):
+    def moment(self, x: float, P_delta: bool = False) -> float:
         '''
         Returns the moment at a location on the segment.
 
@@ -32,7 +33,7 @@ def moment(self, x, P_delta=False):
         
         return M
 
-    def slope(self, x, P_delta=False):
+    def slope(self, x: float, P_delta: bool = False) -> float:
         """Returns the slope of the elastic curve at any point `x` along the segment.
 
         :param x: Location (relative to start of segment) where slope is to be calculated.
@@ -62,7 +63,7 @@ def slope(self, x, P_delta=False):
 
 #%%
     # Returns the deflection at a location on the segment
-    def deflection(self, x, P_delta=False):
+    def deflection(self, x: float, P_delta: bool = False) -> float:
         
         V1 = self.V1
         M1 = self.M1
@@ -109,7 +110,7 @@ def deflection(self, x, P_delta=False):
     
 #%%
     # Returns the maximum moment in the segment
-    def max_moment(self, P_delta=False):
+    def max_moment(self, P_delta: bool = False) -> float:
         
         w1 = self.w1
         w2 = self.w2
@@ -155,7 +156,7 @@ def max_moment(self, P_delta=False):
 
 #%%
     # Returns the minimum moment in the segment
-    def min_moment(self, P_delta=False):
+    def min_moment(self, P_delta: bool = False) -> float:
         
         w1 = self.w1
         w2 = self.w2
@@ -198,4 +199,3 @@ def min_moment(self, P_delta=False):
     
         # Return the minimum moment
         return min(M1, M2, M3, M4)
-    
\ No newline at end of file
diff --git a/Pynite/BeamSegZ.py b/Pynite/BeamSegZ.py
index dda58692..7b5267c8 100644
--- a/Pynite/BeamSegZ.py
+++ b/Pynite/BeamSegZ.py
@@ -4,8 +4,14 @@
 
 @author: D. Craig Brinck, SE
 """
+from __future__ import annotations # Allows more recent type hints features
+from typing import TYPE_CHECKING
+
 from numpy import full, array
 
+if TYPE_CHECKING:
+    from typing import Any, List
+    from numpy.typing import NDArray
 # %%
 # A mathematically continuous beam segment
 class BeamSegZ():
@@ -60,29 +66,29 @@ class BeamSegZ():
     
     """
 
-    def __init__(self):
+    def __init__(self) -> None:
         """
         Constructor
         """
         
-        self.x1 = None # Start location of beam segment (relative to start of beam)
-        self.x2 = None # End location of beam segment (relative to start of beam)
-        self.w1 = None # Linear distributed transverse load at start of segment
-        self.w2 = None # Linear distributed transverse load at end of segment
-        self.p1 = None # Linear distributed axial load at start of segment
-        self.p2 = None # Linear distributed axial load at end of segment
-        self.V1 = None # Internal shear force at start of segment
-        self.M1 = None # Internal moment at start of segment
-        self.P1 = None # Internal axial force at start of segment
-        self.T1 = None # Torsional moment at start of segment
-        self.theta1 = None # Slope at start of beam segment
-        self.delta1 = None # Displacement at start of beam segment
-        self.delta_x1 = None # Axial displacement at start of beam segment
-        self.EI = None # Flexural stiffness of the beam segment
-        self.EA = None # Axial stiffness of the beam segment
+        self.x1: float | None = None # Start location of beam segment (relative to start of beam)
+        self.x2: float | None = None # End location of beam segment (relative to start of beam)
+        self.w1: float | None = None # Linear distributed transverse load at start of segment
+        self.w2: float | None = None # Linear distributed transverse load at end of segment
+        self.p1: float | None = None # Linear distributed axial load at start of segment
+        self.p2: float | None = None # Linear distributed axial load at end of segment
+        self.V1: float | None = None # Internal shear force at start of segment
+        self.M1: float | None = None # Internal moment at start of segment
+        self.P1: float | None = None # Internal axial force at start of segment
+        self.T1: float | None = None # Torsional moment at start of segment
+        self.theta1: float | None = None # Slope at start of beam segment
+        self.delta1: float | None = None # Displacement at start of beam segment
+        self.delta_x1: float | None = None # Axial displacement at start of beam segment
+        self.EI: float | None = None # Flexural stiffness of the beam segment
+        self.EA: float | None = None # Axial stiffness of the beam segment
 
     # Returns the length of the segment
-    def Length(self):
+    def Length(self) -> float:
         """
         Returns the length of the segment
         """
@@ -90,7 +96,7 @@ def Length(self):
         return self.x2 - self.x1
 
     # Returns the shear force at a location 'x' on the segment
-    def Shear(self, x):
+    def Shear(self, x: float) -> float:
         
         V1 = self.V1
         w1 = self.w1
@@ -100,7 +106,7 @@ def Shear(self, x):
         return V1 + w1*x + x**2*(-w1 + w2)/(2*L)
 
     # Returns the moment at a location on the segment
-    def moment(self, x, P_delta=False):
+    def moment(self, x: float, P_delta: bool = False) -> float:
 
         V1 = self.V1
         M1 = self.M1
@@ -121,7 +127,7 @@ def moment(self, x, P_delta=False):
         return M
 
     # Returns the axial force at a location on the segment
-    def axial(self, x):
+    def axial(self, x: float) -> float:
         
         P1 = self.P1
         p1 = self.p1
@@ -130,7 +136,7 @@ def axial(self, x):
         
         return P1 + (p2 - p1)/(2*L)*x**2 + p1*x
 
-    def Torsion(self, x = 0):
+    def Torsion(self, x: float | List[float] = 0) -> float | None | NDArray[Any]:
         """
         Returns the torsional moment in the segment.
         """
@@ -145,7 +151,7 @@ def Torsion(self, x = 0):
         else:
             return full(len(x), self.T1)
     
-    def slope(self, x, P_delta=False):
+    def slope(self, x: float, P_delta: bool = False) -> float:
         """Returns the slope of the elastic curve at any point `x` along the segment.
 
         :param x: Location (relative to start of segment) where slope is to be calculated.
@@ -180,7 +186,7 @@ def slope(self, x, P_delta=False):
         return theta_x
 
     # Returns the deflection at a location on the segment
-    def deflection(self, x, P_delta=False):
+    def deflection(self, x: float, P_delta: bool = False) -> float:
         
         V1 = self.V1
         M1 = self.M1
@@ -227,7 +233,7 @@ def deflection(self, x, P_delta=False):
             # return delta1 + theta1*x - M1*x**2/(2*EI) + V1*x**3/(6*EI) + w1*x**4/(24*EI) + x**5*(-w1 + w2)/(120*EI*L)
             return delta_1 + theta_1*x + V1*x**3/(6*EI) + w1*x**4/(24*EI) + x**2*(-M1)/(2*EI) + x**5*(-w1 + w2)/(120*EI*L)
 
-    def AxialDeflection(self, x):
+    def AxialDeflection(self, x: float) -> float:
 
         delta_x1 = self.delta_x1
         P1 = self.P1
@@ -239,7 +245,7 @@ def AxialDeflection(self, x):
         return delta_x1 - 1/EA*(P1*x + p1*x**2/2 + (p2 - p1)*x**3/(6*L))
 
     # Returns the maximum shear in the segment
-    def max_shear(self):
+    def max_shear(self) -> float:
         
         w1 = self.w1
         w2 = self.w2
@@ -266,7 +272,7 @@ def max_shear(self):
         return max(V1, V2, V3)
 
     # Returns the minimum shear in the segment
-    def min_shear(self):
+    def min_shear(self) -> float:
         
         w1 = self.w1
         w2 = self.w2
@@ -293,7 +299,7 @@ def min_shear(self):
         return min(V1, V2, V3)
     
     # Returns the maximum moment in the segment
-    def max_moment(self, P_delta=False):
+    def max_moment(self, P_delta: bool = False) -> float:
         
         w1 = self.w1
         w2 = self.w2
@@ -338,7 +344,7 @@ def max_moment(self, P_delta=False):
         return max(M1, M2, M3, M4)
 
     # Returns the minimum moment in the segment
-    def min_moment(self, P_delta=False):
+    def min_moment(self, P_delta: bool = False) -> float:
         
         w1 = self.w1
         w2 = self.w2
@@ -383,7 +389,7 @@ def min_moment(self, P_delta=False):
         return min(M1, M2, M3, M4)
 
     # Returns the maximum axial force in the segment
-    def max_axial(self):
+    def max_axial(self) -> float:
         
         p1 = self.p1
         p2 = self.p2
@@ -410,7 +416,7 @@ def max_axial(self):
         return max(P1, P2, P3)
 
     # Returns the minimum axial force in the segment
-    def min_axial(self):
+    def min_axial(self) -> float:
         
         p1 = self.p1
         p2 = self. p2
@@ -436,7 +442,7 @@ def min_axial(self):
         # Return the minimum axial force
         return min(P1, P2, P3)
 
-    def MaxTorsion(self):
+    def MaxTorsion(self) -> float:
         """
         Returns the maximum torsional moment in the segment.
         """
@@ -446,7 +452,7 @@ def MaxTorsion(self):
         # This can be updated in the future for distributed torsional forces
         return self.T1
 
-    def MinTorsion(self):
+    def MinTorsion(self) -> float:
         """
         Returns the minimum torsional moment in the segment.
         """
diff --git a/Pynite/FEModel3D.py b/Pynite/FEModel3D.py
index 286bddf4..a6cb88f7 100644
--- a/Pynite/FEModel3D.py
+++ b/Pynite/FEModel3D.py
@@ -1,10 +1,10 @@
 # %%
 #futures import required to use bar operators for optional type annotations
-from __future__ import annotations
-
+from __future__ import annotations # Allows more recent type hints features
 from os import rename
 import warnings
 from math import isclose
+from typing import TYPE_CHECKING
 
 from numpy import array, zeros, matmul, divide, subtract, atleast_2d, all
 from numpy.linalg import solve
@@ -21,6 +21,10 @@
 from Pynite.Mesh import Mesh, RectangleMesh, AnnulusMesh, FrustrumMesh, CylinderMesh
 from Pynite import Analysis
 
+if TYPE_CHECKING:
+    from typing import Dict, List
+    from numpy import float64
+    from numpy.typing import NDArray
 
 # %%
 class FEModel3D():
@@ -28,7 +32,7 @@ class FEModel3D():
        and retrieve results from a finite element model.
     """
 
-    def __init__(self):
+    def __init__(self) -> None:
         """Creates a new 3D finite element model.
         """
         
@@ -36,36 +40,26 @@ def __init__(self):
         # the data types they store, and then those types will be removed. This will give us the
         # ability to get type-based hints when using the dictionaries.
 
-        self.nodes = {str:Node3D}          # A dictionary of the model's nodes
-        self.nodes.pop(str)
-        self.materials = {str:Material}    # A dictionary of the model's materials
-        self.materials.pop(str)
-        self.sections = {str:Section}      # A dictonary of the model's cross-sections
-        self.sections.pop(str)
-        self.springs = {str:Spring3D}      # A dictionary of the model's springs
-        self.springs.pop(str)
-        self.members = {str:PhysMember}    # A dictionary of the model's physical members
-        self.members.pop(str)
-        self.quads = {str:Quad3D}          # A dictionary of the model's quadiralterals
-        self.quads.pop(str)
-        self.plates = {str:Plate3D}        # A dictionary of the model's rectangular plates
-        self.plates.pop(str)
-        self.meshes = {str:Mesh}           # A dictionary of the model's meshes
-        self.meshes.pop(str)         
-        self.load_combos = {str:LoadCombo}  # A dictionary of the model's load combinations
-        self.load_combos.pop(str)
-        self._D = {str:[]}                 # A dictionary of the model's nodal displacements by load combination
-        self._D.pop(str)
-        
-        self.solution = None  # Indicates the solution type for the latest run of the model
+        self.nodes: Dict[str, Node3D] = {}             # A dictionary of the model's nodes
+        self.materials: Dict[str, Material] = {}       # A dictionary of the model's materials
+        self.sections: Dict[str, Section] = {}         # A dictonary of the model's cross-sections
+        self.springs: Dict[str, Spring3D] = {}         # A dictionary of the model's springs
+        self.members: Dict[str, PhysMember] = {}       # A dictionary of the model's physical members
+        self.quads: Dict[str, Quad3D] = {}             # A dictionary of the model's quadiralterals
+        self.plates: Dict[str, Plate3D] = {}           # A dictionary of the model's rectangular plates
+        self.meshes: Dict[str, Mesh] = {}              # A dictionary of the model's meshes
+        self.load_combos: Dict[str, LoadCombo] = {}    # A dictionary of the model's load combinations
+        self._D: Dict[str, NDArray[float64]] = {}      # A dictionary of the model's nodal displacements by load combination
+        
+        self.solution: str | None = None  # Indicates the solution type for the latest run of the model
 
     @property
-    def load_cases(self):
+    def load_cases(self) -> List[str]:
         """Returns a list of all the load cases in the model (in alphabetical order).
         """
         
         # Create an empty list of load cases
-        cases = []
+        cases: List[str] = []
 
         # Step through each node
         for node in self.nodes.values():
@@ -1820,14 +1814,14 @@ def Km(self, combo_name='Combo 1', push_combo='Push', step_num=1, log=False, spa
         # Return the global plastic reduction matrix
         return Km 
 
-    def FER(self, combo_name='Combo 1'):
+    def FER(self, combo_name='Combo 1') -> NDArray[float64]:
         """Assembles and returns the global fixed end reaction vector for any given load combo.
 
         :param combo_name: The name of the load combination to get the fixed end reaction vector
                            for. Defaults to 'Combo 1'.
         :type combo_name: str, optional
         :return: The fixed end reaction vector
-        :rtype: array
+        :rtype: NDArray[float64]
         """        
         
         # Initialize a zero vector to hold all the terms
@@ -1920,14 +1914,14 @@ def FER(self, combo_name='Combo 1'):
         # Return the global fixed end reaction vector
         return FER
     
-    def P(self, combo_name='Combo 1'):
+    def P(self, combo_name='Combo 1') -> NDArray[float64]:
         """Assembles and returns the global nodal force vector.
 
         :param combo_name: The name of the load combination to get the force vector for. Defaults
                            to 'Combo 1'.
         :type combo_name: str, optional
         :return: The global nodal force vector.
-        :rtype: array
+        :rtype: NDArray[float64]
         """
             
         # Initialize a zero vector to hold all the terms
@@ -1966,14 +1960,14 @@ def P(self, combo_name='Combo 1'):
         # Return the global nodal force vector
         return P
 
-    def D(self, combo_name='Combo 1'):
+    def D(self, combo_name='Combo 1') -> NDArray[float64]:
         """Returns the global displacement vector for the model.
 
         :param combo_name: The name of the load combination to get the results for. Defaults to
                            'Combo 1'.
         :type combo_name: str, optional
         :return: The global displacement vector for the model
-        :rtype: array
+        :rtype: NDArray[float64]
         """
  
         # Return the global displacement vector
@@ -2464,4 +2458,4 @@ def orphaned_nodes(self):
                 orphans.append(node.name)
         
         return orphans
-      
+
diff --git a/Pynite/FixedEndReactions.py b/Pynite/FixedEndReactions.py
index 237cd69b..c6b80303 100644
--- a/Pynite/FixedEndReactions.py
+++ b/Pynite/FixedEndReactions.py
@@ -5,22 +5,29 @@
 @author: D. Craig Brinck, SE
 """
 # %%
+from __future__ import annotations # Allows more recent type hints features
+from typing import TYPE_CHECKING
+
 from numpy import zeros
 
+if TYPE_CHECKING:
+    from typing import Literal
+    from numpy import float64
+    from numpy.typing import NDArray
 # %%
-def FER_PtLoad(P, x, L, Direction):
+def FER_PtLoad(P: float, x: float, L: float, Direction: Literal["Fy", "Fz"]) -> NDArray[float64]:
     """
     Returns the fixed end reaction vector for a point load
     
     Parameters:
     -----------
-    P : number
+    P : float
         The magnitude of the point load
-    x : number
+    x : float
         The location of the point load relative to the start of the member
-    L : number
+    L : float
         The length of the member
-    Direction : string
+    Direction : Literal["Fy", "Fz"]
         The direction of the point load. Must be one of the following:
             "Fy" = Force on the member's local y-axis
             "Fz" = Force on the member's local z-axis
@@ -46,17 +53,19 @@ def FER_PtLoad(P, x, L, Direction):
     return FER
     
 # %%
-def FER_Moment(M, x, L, Direction):
+def FER_Moment(M: float, x: float, L: float, Direction: Literal["My", "Mz"]) -> NDArray[float64]:
     """
     Returns the fixed end reaction vector for a concentrated moment
     
     Parameters
     ----------
-    M : number
+    M : float
         The magnitude of the moment
-    x : number
+    x : float
         The location of the moment relative to the start of the member
-    Direction : string
+    L : float
+        The length of the member
+    Direction : Literal["My", "Mz"]
         The direction of the moment. Must be one of the following:
             "My" = Moment applied about the local y-axis
             "Mz" = Moment applied about the local z-axis
@@ -83,7 +92,27 @@ def FER_Moment(M, x, L, Direction):
     
 # %%
 # Returns the fixed end reaction vector for a linear distributed load
-def FER_LinLoad(w1, w2, x1, x2, L, Direction):
+def FER_LinLoad(w1: float, w2: float, x1: float, x2: float, L: float, Direction: Literal["Fy", "Fz"]) -> NDArray[float64]:
+    """
+    Returns the fixed end reaction vector for a linear distributed load
+    
+    Parameters
+    ----------
+    w1 : float
+        The load magnitude at the start location
+    w2 : float
+        The load magnitude at the end location
+    x1 : float
+        The start location of the distributed load
+    x2 : float
+        The end location of the distributed load
+    L : float
+        The length of the member
+    Direction : Literal["Fy", "Fz"]
+        The direction of the distributed load. Must be one of the following:
+            "Fy" = Force on the member's local y-axis
+            "Fz" = Force on the member's local z-axis
+    """
         
     # Create the fixed end reaction vector
     FER = zeros((12, 1))
@@ -104,7 +133,19 @@ def FER_LinLoad(w1, w2, x1, x2, L, Direction):
 
 # %%
 # Returns the fixed end reaction vector for an axial point load
-def FER_AxialPtLoad(P, x, L):
+def FER_AxialPtLoad(P: float, x: float, L: float) -> NDArray[float64]:
+    """
+    Returns the fixed end reaction vector for an axial point load
+    
+    Parameters
+    ----------
+    P : float
+        The magnitude of the axial point load
+    x : float
+        The location of the axial point load relative to the start of the member
+    L : float
+        The length of the member
+    """
     
     # Create the fixed end reaction vector
     FER = zeros((12, 1))
@@ -117,7 +158,23 @@ def FER_AxialPtLoad(P, x, L):
 
 # %%
 # Returns the fixed end reaction vector for a distributed axial load
-def FER_AxialLinLoad(p1, p2, x1, x2, L):
+def FER_AxialLinLoad(p1: float, p2: float, x1: float, x2: float, L: float) -> NDArray[float64]:
+    """
+    Returns the fixed end reaction vector for a distributed axial load
+    
+    Parameters
+    ----------
+    p1 : float
+        The axial load magnitude at the start location
+    p2 : float
+        The axial load magnitude at the end location
+    x1 : float
+        The start location of the distributed axial load
+    x2 : float
+        The end location of the distributed axial load
+    L : float
+        The length of the member
+    """
     
     # Create the fixed end reaction vector
     FER = zeros((12, 1))
@@ -128,16 +185,18 @@ def FER_AxialLinLoad(p1, p2, x1, x2, L):
     
     return FER
  
-def FER_Torque(T, x, L):
+def FER_Torque(T: float, x: float, L: float) -> NDArray[float64]:
     """
     Returns the fixed end reaction vector for a concentrated torque
 
     Parameters
     ----------
-    T : number
+    T : float
         The magnitude of the torque
-    x : number
+    x : float
         The location of the torque relative to the start of the member
+    L : float
+        The length of the member
     """
 
     # Create the fixed end reaction vector
diff --git a/Pynite/LoadCombo.py b/Pynite/LoadCombo.py
index 2e2d0ae3..4ab0eee4 100644
--- a/Pynite/LoadCombo.py
+++ b/Pynite/LoadCombo.py
@@ -1,8 +1,11 @@
+from __future__ import annotations # Allows more recent type hints features
+from typing import Dict, List
+
 class LoadCombo():
     """A class that stores all the information necessary to define a load combination.
     """
 
-    def __init__(self, name, combo_tags=None, factors={}):
+    def __init__(self, name: str, combo_tags: List[str] | None = None, factors: Dict[str, float] = {}) -> None:
         """Initializes a new load combination.
 
         :param name: A unique name for the load combination.
@@ -13,20 +16,28 @@ def __init__(self, name, combo_tags=None, factors={}):
         :type factors: dict, optional
         """
         
-        self.name = name             # A unique user-defined name for the load combination
-        self.combo_tags = combo_tags   # Used to categorize the load combination (e.g. strength or serviceability)
-        self.factors = factors       # A dictionary containing each load case name and associated load factor
+        self.name: str = name             # A unique user-defined name for the load combination
+        self.combo_tags: List[str] | None = combo_tags   # Used to categorize the load combination (e.g. strength or serviceability)
+        self.factors: Dict[str, float] = factors       # A dictionary containing each load case name and associated load factor
     
-    def AddLoadCase(self, case_name, factor):
+    def AddLoadCase(self, case_name: str, factor: float) -> None:
         '''
         Adds a load case with its associated load factor
+
+        :param case_name: The name of the load case
+        :type case_name: str
+        :param factor: The load factor to apply to the load case
+        :type factor: float
         '''
 
         self.factors[case_name] = factor
     
-    def DeleteLoadCase(self, case_name):
+    def DeleteLoadCase(self, case_name: str) -> None:
         '''
         Deletes a load case with its associated load factor
+
+        :param case_name: The name of the load case to delete
+        :type case_name: str
         '''
 
         del self.factors[case_name]
diff --git a/Pynite/Material.py b/Pynite/Material.py
index ec1885d9..329204d7 100644
--- a/Pynite/Material.py
+++ b/Pynite/Material.py
@@ -1,4 +1,8 @@
-from typing import Optional
+from __future__ import annotations # Allows more recent type hints features
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from Pynite.FEModel3D import FEModel3D
 
 class Material():
     """
@@ -6,12 +10,29 @@ class Material():
 
     This class stores all properties related to the physical material of the element
     """
-    def __init__(self, model, name:str, E:float, G:float, nu:float, rho:float, fy:Optional[float] = None):
+    def __init__(self, model: FEModel3D, name: str, E: float, G: float, nu: float, rho: float, fy: float | None = None) -> None:
+        """Initialize a material object.
+
+        :param model: The finite element model this material belongs to
+        :type model: FEModel3D
+        :param name: A unique name for the material
+        :type name: str
+        :param E: Modulus of elasticity
+        :type E: float
+        :param G: Shear modulus
+        :type G: float
+        :param nu: Poisson's ratio
+        :type nu: float
+        :param rho: Density of the material
+        :type rho: float
+        :param fy: Yield strength of the material, defaults to None
+        :type fy: float, optional
+        """
 
-        self.model = model
-        self.name = name
-        self.E = E
-        self.G = G
-        self.nu = nu
-        self.rho = rho
-        self.fy = fy
\ No newline at end of file
+        self.model: FEModel3D = model
+        self.name: str = name
+        self.E: float = E
+        self.G: float = G
+        self.nu: float = nu
+        self.rho: float = rho
+        self.fy: float | None = fy
\ No newline at end of file
diff --git a/Pynite/Member3D.py b/Pynite/Member3D.py
index 3af19d35..1be823e4 100644
--- a/Pynite/Member3D.py
+++ b/Pynite/Member3D.py
@@ -1,11 +1,27 @@
-# %%
+from __future__ import annotations # Allows more recent type hints features
+from typing import TYPE_CHECKING
+from math import isclose, radians, cos, sin
+
 from numpy import array, zeros, add, subtract, matmul, insert, cross, divide, linspace, vstack, hstack, allclose, where, radians, sin, cos
 from numpy.linalg import inv, pinv
-from math import isclose
+
+import Pynite.FixedEndReactions
 from Pynite.BeamSegZ import BeamSegZ
 from Pynite.BeamSegY import BeamSegY
-import Pynite.FixedEndReactions
-import warnings
+
+if TYPE_CHECKING:
+    from typing import Dict, List, Tuple, Optional, Any, Literal
+
+    from numpy import float64
+    from numpy.typing import NDArray
+
+    from Pynite.FEModel3D import FEModel3D
+    from Pynite.Node3D import Node3D
+    from Pynite.Material import Material
+    from Pynite.Section import Section
+    from Pynite.LoadCombo import LoadCombo
+    from Pynite.BeamSegY import BeamSegY
+    from Pynite.BeamSegZ import BeamSegZ
 
 #%%
 class Member3D():
@@ -21,68 +37,91 @@ class Member3D():
     __plt = None
 
 #%%
-    def __init__(self, model, name, i_node, j_node, material_name, section_name, rotation=0.0,
-                 tension_only=False, comp_only=False):
+    def __init__(self, model: FEModel3D, name: str, i_node: Node3D, j_node: Node3D, 
+                 material_name: str, section_name: str, rotation: float = 0.0,
+                 tension_only: bool = False, comp_only: bool = False) -> None:
         """
         Initializes a new member.
-        """
-        self.name = name      # A unique name for the member given by the user
-        self.ID = None        # Unique index number for the member assigned by the program
-        self.i_node = i_node  # The element's i-node
-        self.j_node = j_node  # The element's j-node
+        
+        :param model: The finite element model this member belongs to
+        :type model: FEModel3D
+        :param name: A unique name for the member
+        :type name: str
+        :param i_node: The element's i-node (start node)
+        :type i_node: Node3D
+        :param j_node: The element's j-node (end node)
+        :type j_node: Node3D
+        :param material_name: The name of the material to use for this member
+        :type material_name: str
+        :param section_name: The name of the section to use for this member
+        :type section_name: str
+        :param rotation: Member rotation (degrees) about its local x-axis, defaults to 0.0
+        :type rotation: float, optional
+        :param tension_only: Indicates if the member is tension-only, defaults to False
+        :type tension_only: bool, optional
+        :param comp_only: Indicates if the member is compression-only, defaults to False
+        :type comp_only: bool, optional
+        """
+        self.name: str = name      # A unique name for the member given by the user
+        self.ID: int | None = None        # Unique index number for the member assigned by the program
+        self.i_node: Node3D = i_node  # The element's i-node
+        self.j_node: Node3D = j_node  # The element's j-node
 
         try:
-            self.material = model.materials[material_name] # The element's material
+            self.material: Material = model.materials[material_name] # The element's material
         except KeyError:
             raise NameError(f"No material named '{material_name}'")
         
         try:
-            self.section = model.sections[section_name] # The element's section
+            self.section: Section = model.sections[section_name] # The element's section
         except KeyError:
             raise NameError(f"No section names '{section_name}'")
         
         # Variables used to track nonlinear material member end forces
-        self._fxi = 0
-        self._myi = 0
-        self._mzi= 0
-        self._fxj = 0
-        self._myj = 0
-        self._mzj = 0
+        self._fxi: float = 0
+        self._myi: float = 0
+        self._mzi: float = 0
+        self._fxj: float = 0
+        self._myj: float = 0
+        self._mzj: float = 0
 
         # Variable used to track plastic load reveral
-        self.i_reversal = False
-        self.j_reversal = False
-
-        self.rotation = rotation  # Member rotation (degrees) about its local x-axis
-        self.PtLoads = []         # A list of point loads & moments applied to the element (Direction, P, x, case='Case 1') or (Direction, M, x, case='Case 1')
-        self.DistLoads = []       # A list of linear distributed loads applied to the element (Direction, w1, w2, x1, x2, case='Case 1')
-        self.SegmentsZ = []       # A list of mathematically continuous beam segments for z-bending
-        self.SegmentsY = []       # A list of mathematically continuous beam segments for y-bending
-        self.SegmentsX = []       # A list of mathematically continuous beam segments for torsion
-        self.Releases = [False, False, False, False, False, False, False, False, False, False, False, False]
-        self.tension_only = tension_only # Indicates whether the member is tension-only
-        self.comp_only = comp_only # Indicates whether the member is compression-only
+        self.i_reversal: bool = False
+        self.j_reversal: bool = False
+
+        self.rotation: float = rotation  # Member rotation (degrees) about its local x-axis
+        self.PtLoads: List[Tuple] = []         # A list of point loads & moments applied to the element (Direction, P, x, case='Case 1') or (Direction, M, x, case='Case 1')
+        self.DistLoads: List[Tuple] = []       # A list of linear distributed loads applied to the element (Direction, w1, w2, x1, x2, case='Case 1')
+        self.SegmentsZ: List[BeamSegZ] = []       # A list of mathematically continuous beam segments for z-bending
+        self.SegmentsY: List[BeamSegY] = []       # A list of mathematically continuous beam segments for y-bending
+        self.SegmentsX: List[BeamSegZ] = []       # A list of mathematically continuous beam segments for torsion
+        self.Releases: List[bool] = [False, False, False, False, False, False, False, False, False, False, False, False]
+        self.tension_only: bool = tension_only # Indicates whether the member is tension-only
+        self.comp_only: bool = comp_only # Indicates whether the member is compression-only
 
         # Members need to track whether they are active or not for any given load combination. They may become inactive for a load combination during a tension/compression-only analysis. This dictionary will be used when the model is solved.
-        self.active = {} # Key = load combo name, Value = True or False
+        self.active: Dict[str, bool] = {} # Key = load combo name, Value = True or False
         
         # The 'Member3D' object will store results for one load combination at a time. To reduce repetative calculations the '_solved_combo' variable will be used to track whether the member needs to be resegmented before running calculations for any given load combination.
-        self._solved_combo = None # The current solved load combination
+        self._solved_combo: LoadCombo | None = None # The current solved load combination
 
         # Members need a link to the model they belong to
-        self.model = model
+        self.model: FEModel3D = model
 
 #%%
-    def L(self):
+    def L(self) -> float:
         """
         Returns the length of the member.
+        
+        :return: The length of the member
+        :rtype: float
         """
 
         # Return the distance between the two nodes
         return self.i_node.distance(self.j_node)
 
 #%%
-    def _partition_D(self):
+    def _partition_D(self) -> Tuple[List[int], List[int]]:
         """
         Builds lists of unreleased and released degree of freedom indices for the member.
 
@@ -105,9 +144,12 @@ def _partition_D(self):
         return R1_indices, R2_indices
 
 #%%
-    def k(self):
+    def k(self) -> NDArray[Any]:
         """
         Returns the condensed (and expanded) local stiffness matrix for the member.
+        
+        :return: The condensed local stiffness matrix
+        :rtype: ndarray
         """
 
         # Partition the local stiffness matrix as 4 submatrices in
@@ -131,9 +173,12 @@ def k(self):
         return k_Condensed
 
 #%%
-    def _k_unc(self):
+    def _k_unc(self) -> NDArray[float64]:
         """
         Returns the uncondensed local stiffness matrix for the member.
+        
+        :return: The uncondensed local stiffness matrix
+        :rtype: NDArray[float64]
         """
 
         # Get the properties needed to form the local stiffness matrix
@@ -146,7 +191,7 @@ def _k_unc(self):
         L = self.L()
         
         # Create the uncondensed local stiffness matrix
-        k = array([[A*E/L,  0,             0,             0,      0,            0,            -A*E/L, 0,             0,             0,      0,            0],
+        k = array([ [A*E/L,  0,             0,             0,      0,            0,            -A*E/L, 0,             0,             0,      0,            0],
                     [0,      12*E*Iz/L**3,  0,             0,      0,            6*E*Iz/L**2,  0,      -12*E*Iz/L**3, 0,             0,      0,            6*E*Iz/L**2],
                     [0,      0,             12*E*Iy/L**3,  0,      -6*E*Iy/L**2, 0,            0,      0,             -12*E*Iy/L**3, 0,      -6*E*Iy/L**2, 0],
                     [0,      0,             0,             G*J/L,  0,            0,            0,      0,             0,             -G*J/L, 0,            0],
@@ -163,7 +208,7 @@ def _k_unc(self):
         return k
 
 #%%
-    def kg(self, P=0):
+    def kg(self, P: float = 0) -> NDArray[float64]:
         """
         Returns the condensed (expanded) local geometric stiffness matrix for the member.
 
@@ -171,6 +216,9 @@ def kg(self, P=0):
         ----------
         P : number, optional
             The axial force acting on the member (compression = +, tension = -)
+            
+        :return: The condensed local geometric stiffness matrix
+        :rtype: NDArray[float64]
         """
 
         # Get the properties needed to form the local geometric stiffness matrix
@@ -179,7 +227,7 @@ def kg(self, P=0):
         L = self.L()
         
         # Create the uncondensed local geometric stiffness matrix
-        kg = array([[0, 0,    0,     0,     0,         0,         0, 0,     0,    0,     0,         0],
+        kg = array([ [0, 0,    0,     0,     0,         0,         0, 0,     0,    0,     0,         0],
                      [0, 6/5,  0,     0,     0,         L/10,      0, -6/5,  0,    0,     0,         L/10],
                      [0, 0,    6/5,   0,     -L/10,     0,         0, 0,     -6/5, 0,     -L/10,     0],
                      [0, 0,    0,     Ip/A,  0,         0,         0, 0,     0,    -Ip/A, 0,         0],
@@ -218,7 +266,7 @@ def kg(self, P=0):
         # Return the local geomtric stiffness matrix, with end releases applied
         return kg_Condensed
     
-    def km(self, combo_name='Combo 1', push_combo='Push', step_num=1):
+    def km(self, combo_name: str = 'Combo 1', push_combo: str = 'Push', step_num: int = 1) -> NDArray[float64]:
         """Returns the local plastic reduction matrix for the element.
 
         :param combo_name: The name of the load combination to get the plastic reduction matrix for. Defaults to 'Combo 1'.
@@ -228,7 +276,7 @@ def km(self, combo_name='Combo 1', push_combo='Push', step_num=1):
         :param step_num: The pushover load step to consider for calculating the plastic reduciton matrix. Default is 1.
         :type step_num: int, optional
         :return: The plastic reduction matrix for the element
-        :rtype: array
+        :rtype: NDArray[float64]
         """
         
         # Get the elastic local stiffness matrix
@@ -264,7 +312,21 @@ def km(self, combo_name='Combo 1', push_combo='Push', step_num=1):
         else:
             return -ke @ G @ pinv(G.T @ ke @ G) @ G.T @ ke
     
-    def lamb(self, model_Delta_D, combo_name='Combo 1', push_combo='Push', step_num=1):
+    def lamb(self, model_Delta_D: NDArray[float64], combo_name: str = 'Combo 1', push_combo: str = 'Push', step_num: int = 1) -> NDArray[float64]:
+        """
+        Returns the lambda vector used in pushover analysis.
+        
+        :param model_Delta_D: The global displacement vector
+        :type model_Delta_D: ndarray
+        :param combo_name: The load combination name, defaults to 'Combo 1'
+        :type combo_name: str, optional
+        :param push_combo: The pushover load combination name, defaults to 'Push'
+        :type push_combo: str, optional
+        :param step_num: The current load step number, defaults to 1
+        :type step_num: int, optional
+        :return: The lambda vector
+        :rtype: NDArray[float64]
+        """
 
         # Obtain the change in the member's end displacements from the calculated displacement change vector
         Delta_D = array([model_Delta_D[self.i_node.ID*6 + 0],
@@ -312,14 +374,14 @@ def lamb(self, model_Delta_D, combo_name='Combo 1', push_combo='Push', step_num=
         return inv(G.T @ ke @ G) @ G.T @ ke @ Delta_d
 
 #%%
-    def fer(self, combo_name='Combo 1'):
+    def fer(self, combo_name: str = 'Combo 1') -> NDArray[float64]:
         """
         Returns the condensed (and expanded) local fixed end reaction vector for the member for the given load combination.
 
-        Parameters
-        ----------
-        combo : LoadCombo
-            The load combination to construct the fixed end reaction vector for.
+        :param combo_name: The name of the load combination to construct the fixed end reaction vector for
+        :type combo_name: str
+        :return: The fixed end reaction vector for the member
+        :rtype: NDArray[float64]
         """
         
         # Get the lists of unreleased and released degree of freedom indices
@@ -345,7 +407,7 @@ def fer(self, combo_name='Combo 1'):
         return ferCondensed
     
 #%%
-    def _fer_unc(self, combo_name='Combo 1'):
+    def _fer_unc(self, combo_name:str ='Combo 1') -> NDArray[float64]:
         """
         Returns the member's local fixed end reaction vector, ignoring the effects of end releases.
         Needed to apply the slope-deflection equation properly.
@@ -424,7 +486,7 @@ def _fer_unc(self, combo_name='Combo 1'):
         return fer
 
 #%%
-    def _partition(self, unp_matrix):
+    def _partition(self, unp_matrix: NDArray[float64])-> tuple[Any, Any] | tuple[Any, Any, Any, Any]:
         """
         Partitions a matrix into sub-matrices based on unreleased and released degree of freedom indices.
         """
@@ -445,7 +507,7 @@ def _partition(self, unp_matrix):
             return  m11, m12, m21, m22
 
 #%%   
-    def f(self, combo_name='Combo 1', push_combo='Push', step_num=1):
+    def f(self, combo_name:str='Combo 1', push_combo:str='Push', step_num=1) -> NDArray[float64]:
         """Returns the member's elastic local end force vector for the given load combination.
 
         :param combo_name: The load combination to get the local end for vector for. Defaults to 'Combo 1'.
@@ -466,7 +528,7 @@ def f(self, combo_name='Combo 1', push_combo='Push', step_num=1):
             return add(matmul(self.k(), self.d(combo_name)), self.fer(combo_name))
 
 #%%
-    def d(self, combo_name='Combo 1'):
+    def d(self, combo_name='Combo 1') -> NDArray[float64]:
         """
         Returns the member's local displacement vector.
 
@@ -481,7 +543,7 @@ def d(self, combo_name='Combo 1'):
         
 #%%  
     # Transformation matrix
-    def T(self):
+    def T(self) -> NDArray[float64]:
         """
         Returns the transformation matrix for the member.
         """
@@ -575,7 +637,7 @@ def T(self):
 
 #%%
     # Member global stiffness matrix
-    def K(self):
+    def K(self) -> NDArray[float64]:
         """Returns the global elastic stiffness matrix for the member.
 
         :return: The global elastic stiffness matrix for the member.
@@ -585,7 +647,7 @@ def K(self):
         # Calculate and return the stiffness matrix in global coordinates
         return matmul(matmul(inv(self.T()), self.k()), self.T())
 
-    def Kg(self, P=0.0):
+    def Kg(self, P: float=0.0):
         """Returns the global geometric stiffness matrix for the member. Used for P-Delta analysis.
 
         :param P: Member axial load. Defaults to 0.
@@ -597,7 +659,7 @@ def Kg(self, P=0.0):
         # Calculate and return the geometric stiffness matrix in global coordinates
         return matmul(matmul(inv(self.T()), self.kg(P)), self.T())
 
-    def Km(self, combo_name, push_combo, step_num):
+    def Km(self, combo_name:str, push_combo:str, step_num:int) -> NDArray[float64]:
         """Returns the global plastic reduction matrix for the member. Used to modify member behavior for plastic hinges at the ends.
 
         :param combo_name: The name of the load combination to get the plastic reduction matrix for.
@@ -613,7 +675,7 @@ def Km(self, combo_name, push_combo, step_num):
         # Calculate and return the plastic reduction matrix in global coordinates
         return matmul(matmul(inv(self.T()), self.km(combo_name, push_combo, step_num)), self.T())
     
-    def F(self, combo_name='Combo 1'):
+    def F(self, combo_name:str='Combo 1') -> NDArray[float64]:
         """
         Returns the member's global end force vector for the given load combination.
         """
@@ -621,7 +683,7 @@ def F(self, combo_name='Combo 1'):
         # Calculate and return the global force vector
         return matmul(inv(self.T()), self.f(combo_name))
     
-    def FER(self, combo_name='Combo 1'):
+    def FER(self, combo_name:str='Combo 1') -> NDArray[float64]:
         """
         Returns the global fixed end reaction vector
 
@@ -635,7 +697,7 @@ def FER(self, combo_name='Combo 1'):
         return matmul(inv(self.T()), self.fer(combo_name))
 
 #%%
-    def D(self, combo_name='Combo 1'):
+    def D(self, combo_name: str = 'Combo 1') -> NDArray[float64]:
         """
         Returns the member's global displacement vector.
 
@@ -672,7 +734,7 @@ def D(self, combo_name='Combo 1'):
         return D
 
 #%%
-    def shear(self, Direction, x, combo_name='Combo 1'):
+    def shear(self, Direction: Literal['Fy', 'Fz'], x: float, combo_name: str = 'Combo 1') -> float:
         """
         Returns the shear at a point along the member's length.
         
@@ -685,7 +747,7 @@ def shear(self, Direction, x, combo_name='Combo 1'):
         x : number
             The location at which to find the shear.
         combo_name : string
-            The name of the load combination to get the results for (not the combination itself).
+            The name of the load combination to get the results for (not the load combination itself).
         """
         
         # Only calculate results if the member is currently active
@@ -726,7 +788,7 @@ def shear(self, Direction, x, combo_name='Combo 1'):
             return 0
             
 #%%
-    def max_shear(self, Direction, combo_name='Combo 1'):
+    def max_shear(self, Direction: Literal['Fy', 'Fz'], combo_name: str = 'Combo 1') -> float:
         """
         Returns the maximum shear in the member for the given direction
         
@@ -775,7 +837,7 @@ def max_shear(self, Direction, combo_name='Combo 1'):
             return 0
     
 #%%
-    def min_shear(self, Direction, combo_name='Combo 1'):
+    def min_shear(self, Direction: Literal['Fy', 'Fz'], combo_name: str = 'Combo 1') -> float:
         """
         Returns the minimum shear in the member for the given direction
         
@@ -824,7 +886,7 @@ def min_shear(self, Direction, combo_name='Combo 1'):
             return 0
     
 #%%
-    def plot_shear(self, Direction, combo_name='Combo 1', n_points=20):
+    def plot_shear(self, Direction: Literal['Fy', 'Fz'], combo_name: str = 'Combo 1', n_points: int = 20) -> None:
         """
         Plots the shear diagram for the member
         
@@ -863,7 +925,7 @@ def plot_shear(self, Direction, combo_name='Combo 1', n_points=20):
         Member3D.__plt.show()    
         
 
-    def shear_array(self, Direction, n_points: int, combo_name='Combo 1', x_array=None):
+    def shear_array(self, Direction: Literal['Fy', 'Fz'], n_points: int, combo_name='Combo 1', x_array=None) -> NDArray[float64]:
         """
         Returns the array of the shear in the member for the given direction
         
@@ -905,7 +967,7 @@ def shear_array(self, Direction, n_points: int, combo_name='Combo 1', x_array=No
             raise ValueError(f"Direction must be 'Fy' or 'Fz'. {Direction} was given.")
 
 #%%
-    def moment(self, Direction, x, combo_name='Combo 1'):
+    def moment(self, Direction: Literal['My', 'Mz'], x: float, combo_name: str = 'Combo 1') -> float:
         """
         Returns the moment at a point along the member's length
         
@@ -971,7 +1033,7 @@ def moment(self, Direction, x, combo_name='Combo 1'):
             return 0
             
 #%%
-    def max_moment(self, Direction, combo_name='Combo 1'):
+    def max_moment(self, Direction: Literal['My', 'Mz'], combo_name: str = 'Combo 1') -> float:
         """
         Returns the maximum moment in the member for the given direction.
         
@@ -1028,7 +1090,7 @@ def max_moment(self, Direction, combo_name='Combo 1'):
             return 0
 
 #%%
-    def min_moment(self, Direction, combo_name='Combo 1'):
+    def min_moment(self, Direction: Literal['My', 'Mz'], combo_name: str = 'Combo 1') -> float:
         """
         Returns the minimum moment in the member for the given direction
         
@@ -1081,7 +1143,7 @@ def min_moment(self, Direction, combo_name='Combo 1'):
             return 0
 
 #%%
-    def plot_moment(self, Direction, combo_name='Combo 1', n_points=20):
+    def plot_moment(self, Direction: Literal['My', 'Mz'], combo_name: str = 'Combo 1', n_points: int = 20) -> None:
         """
         Plots the moment diagram for the member
         
@@ -1120,7 +1182,7 @@ def plot_moment(self, Direction, combo_name='Combo 1', n_points=20):
         Member3D.__plt.title('Member ' + self.name + '\n' + combo_name)
         Member3D.__plt.show()
     
-    def moment_array(self, Direction, n_points, combo_name='Combo 1', x_array = None):
+    def moment_array(self, Direction: Literal['My', 'Mz'], n_points: int, combo_name: str = 'Combo 1', x_array: Optional[NDArray[float64]] = None) -> NDArray[float64]:
         """
         Returns the array of the moment in the member for the given direction
         
@@ -1176,7 +1238,7 @@ def moment_array(self, Direction, n_points, combo_name='Combo 1', x_array = None
                 raise ValueError(f"Direction must be 'My' or 'Mz'. {Direction} was given.")
        
 #%%
-    def torque(self, x, combo_name='Combo 1'):
+    def torque(self, x: float, combo_name: str = 'Combo 1') -> float:
         """
         Returns the torsional moment at a point along the member's length
         
@@ -1310,7 +1372,7 @@ def plot_torque(self, combo_name='Combo 1', n_points=20):
         Member3D.__plt.title('Member ' + self.name + '\n' + combo_name)
         Member3D.__plt.show()
 
-    def torque_array(self, n_points, combo_name='Combo 1', x_array = None):
+    def torque_array(self, n_points, combo_name='Combo 1', x_array = None) -> NDArray[float64]:
         """
         Returns the array of the torque in the member
         
@@ -1340,7 +1402,7 @@ def torque_array(self, n_points, combo_name='Combo 1', x_array = None):
         return self._extract_vector_results(self.SegmentsX, x_array, 'torque')
 
         
-    def axial(self, x, combo_name='Combo 1'):
+    def axial(self, x: float, combo_name: str = 'Combo 1') -> float:
         """
         Returns the axial force at a point along the member's length.
         
@@ -1373,7 +1435,7 @@ def axial(self, x, combo_name='Combo 1'):
 
             return 0
 
-    def max_axial(self, combo_name='Combo 1'):
+    def max_axial(self, combo_name: str = 'Combo 1') -> float:
         """
         Returns the maximum axial force in the member
 
@@ -1405,7 +1467,7 @@ def max_axial(self, combo_name='Combo 1'):
 
             return 0
     
-    def min_axial(self, combo_name='Combo 1'):
+    def min_axial(self, combo_name: str = 'Combo 1') -> float:
         """
         Returns the minimum axial force in the member.
         
@@ -1437,7 +1499,7 @@ def min_axial(self, combo_name='Combo 1'):
 
             return 0
     
-    def plot_axial(self, combo_name='Combo 1', n_points=20):
+    def plot_axial(self, combo_name: str = 'Combo 1', n_points=20) -> None:
         """
         Plots the axial force diagram for the member.
         
@@ -1471,7 +1533,7 @@ def plot_axial(self, combo_name='Combo 1', n_points=20):
         Member3D.__plt.title('Member ' + self.name + '\n' + combo_name)
         Member3D.__plt.show()    
 
-    def axial_array(self, n_points, combo_name='Combo 1', x_array=None):
+    def axial_array(self, n_points: int, combo_name: str = 'Combo 1', x_array: Optional[NDArray[float64]] = None) -> NDArray[float64]:
         """
         Returns the array of the axial force in the member for the given direction
         
@@ -1501,7 +1563,7 @@ def axial_array(self, n_points, combo_name='Combo 1', x_array=None):
         return self._extract_vector_results(self.SegmentsZ, x_array, 'axial')
 
                         
-    def deflection(self, Direction, x, combo_name='Combo 1'):
+    def deflection(self, Direction: Literal['dx', 'dy', 'dz'], x: float, combo_name: str = 'Combo 1') -> float:
         """
         Returns the deflection at a point along the member's length.
         
@@ -1576,14 +1638,17 @@ def deflection(self, Direction, x, combo_name='Combo 1'):
 
             return 0
 
-    def max_deflection(self, Direction, combo_name='Combo 1'):
+    def max_deflection(self, Direction: Literal['dx', 'dy', 'dz'], combo_name: str = 'Combo 1') -> float:
         """
         Returns the maximum deflection in the member.
         
         Parameters
         ----------
-        Direction : {'dy', 'dz'}
-            The direction in which to find the maximum deflection.
+        Direction : string
+            The direction in which to find the deflection. Must be one of the following:
+                'dx' = Deflection in the local x-axis.
+                'dy' = Deflection in the local y-axis.
+                'dz' = Deflection in the local z-axis.
         combo_name : string
             The name of the load combination to get the results for (not the load combination itself).
         """
@@ -1612,14 +1677,17 @@ def max_deflection(self, Direction, combo_name='Combo 1'):
 
             return 0
     
-    def min_deflection(self, Direction, combo_name='Combo 1'):
+    def min_deflection(self, Direction: Literal['dx', 'dy', 'dz'], combo_name: str = 'Combo 1') -> float:
         """
         Returns the minimum deflection in the member.
         
         Parameters
         ----------
-        Direction : {'dy', 'dz'}
-            The direction in which to find the minimum deflection.
+        Direction : string
+            The direction in which to find the deflection. Must be one of the following:
+                'dx' = Deflection in the local x-axis.
+                'dy' = Deflection in the local y-axis.
+                'dz' = Deflection in the local z-axis.
         combo_name : string
             The name of the load combination to get the results for (not the load combination itself).
         """
@@ -1648,14 +1716,17 @@ def min_deflection(self, Direction, combo_name='Combo 1'):
 
             return 0
               
-    def plot_deflection(self, Direction, combo_name='Combo 1', n_points=20):
+    def plot_deflection(self, Direction: Literal['dx', 'dy', 'dz'], combo_name: str = 'Combo 1', n_points: int = 20) -> None:
         """
         Plots the deflection diagram for the member
         
         Parameters
         ----------
-        Direction : {'dy', 'dz'}
-            The direction in which to plot the deflection.
+        Direction : string
+            The direction in which to find the deflection. Must be one of the following:
+                'dx' = Deflection in the local x-axis.
+                'dy' = Deflection in the local y-axis.
+                'dz' = Deflection in the local z-axis.
         combo_name : string
             The name of the load combination to get the results for (not the load combination itself).
         n_points: int
@@ -1684,7 +1755,7 @@ def plot_deflection(self, Direction, combo_name='Combo 1', n_points=20):
         Member3D.__plt.title('Member ' + self.name + '\n' + combo_name)
         Member3D.__plt.show()
 
-    def deflection_array(self, Direction, n_points, combo_name='Combo 1', x_array=None):
+    def deflection_array(self, Direction: Literal['dx', 'dy', 'dz'], n_points: int, combo_name: str = 'Combo 1', x_array: Optional[NDArray[float64]] = None) -> NDArray[float64]:
         """
         Returns the array of the deflection in the member for the given direction
         
@@ -1743,7 +1814,7 @@ def deflection_array(self, Direction, n_points, combo_name='Combo 1', x_array=No
             else:
                 raise ValueError(f"Direction must be 'My' or 'Mz'. {Direction} was given.")
 
-    def rel_deflection(self, Direction, x, combo_name='Combo 1'):
+    def rel_deflection(self, Direction: Literal['dx', 'dy', 'dz'], x: float, combo_name: str = 'Combo 1') -> float:
         """
         Returns the relative deflection at a point along the member's length
         
@@ -1751,6 +1822,7 @@ def rel_deflection(self, Direction, x, combo_name='Combo 1'):
         ----------
         Direction : string
             The direction in which to find the relative deflection. Must be one of the following:
+                'dx' = Deflection in the local x-axis
                 'dy' = Deflection in the local y-axis
                 'dz' = Deflection in the local z-axis
         x : number
@@ -1806,14 +1878,17 @@ def rel_deflection(self, Direction, x, combo_name='Combo 1'):
 
             return 0
 
-    def plot_rel_deflection(self, Direction, combo_name='Combo 1', n_points=20):
+    def plot_rel_deflection(self, Direction: Literal['dx', 'dy', 'dz'], combo_name: str = 'Combo 1', n_points: int = 20) -> None:
         """
         Plots the deflection diagram for the member
         
         Parameters
         ----------
-        Direction : {'dy', 'dz'}
-            The direction in which to plot the deflection.
+        Direction : string
+            The direction in which to plot the deflection. Must be one of the following:
+                'dx' = Deflection in the local x-axis.
+                'dy' = Deflection in the local y-axis.
+                'dz' = Deflection in the local z-axis.
         combo_name : string
             The name of the load combination to get the results for (not the combination itself).
         """
@@ -1840,16 +1915,17 @@ def plot_rel_deflection(self, Direction, combo_name='Combo 1', n_points=20):
         Member3D.__plt.title('Member ' + self.name + '\n' + combo_name)
         Member3D.__plt.show()
 
-    def rel_deflection_array(self, Direction, n_points, combo_name='Combo 1', x_array=None):
+    def rel_deflection_array(self, Direction: Literal['dx', 'dy', 'dz'], n_points: int, combo_name: str = 'Combo 1', x_array: Optional[NDArray[float64]] = None) -> NDArray[float64]:
         """
         Returns the array of the relative deflection in the member for the given direction
         
         Parameters
         ----------
         Direction : string
-            The direction in which to find the relative deflection. Must be one of the following:
-                'dy' = Deflection in the local y-axis
-                'dz' = Deflection in the local z-axis
+            The direction in which to find the deflection. Must be one of the following:
+                'dx' = Deflection in the local x-axis.
+                'dy' = Deflection in the local y-axis.
+                'dz' = Deflection in the local z-axis.
         n_points: int
             The number of points in the array to generate over the full length of the member.
         combo_name : string
@@ -2073,14 +2149,14 @@ def _segment_member(self, combo_name='Combo 1'):
                         
                                 # Determine if the load is on this segment
                                 if round(x2,10) > round(x,10):
-                                                
+                                                            
                                     # Break up the load and place it on the segment
                                     # Note that 'w1' and 'w2' are really the axial loads 'p1' and 'p2' here
                                     SegmentsZ[i].p1 += (w2 - w1)/(x2 - x1)*(x - x1) + w1
                                     SegmentsZ[i].p2 += (w2 - w1)/(x2 - x1)*(SegmentsZ[i].x2 - x1) + w1
                                     SegmentsY[i].p1 += (w2 - w1)/(x2 - x1)*(x - x1) + w1
                                     SegmentsY[i].p2 += (w2 - w1)/(x2 - x1)*(SegmentsY[i].x2 - x1) + w1
-                            
+                                
                                     # Calculate the magnitude of the load at the start of the segment
                                     w2 = w1+(w2-w1)/(x2-x1)*(x-x1)
                                     x2 = x
@@ -2093,11 +2169,11 @@ def _segment_member(self, combo_name='Combo 1'):
                         
                                 # Determine if the load is on this segment
                                 if round(x2,10) > round(x,10):
-                                                
+                                                            
                                     # Break up the load and place it on the segment
                                     SegmentsZ[i].w1 += (w2 - w1)/(x2 - x1)*(x - x1) + w1
                                     SegmentsZ[i].w2 += (w2 - w1)/(x2 - x1)*(SegmentsZ[i].x2 - x1) + w1
-                            
+                                
                                     # Calculate the magnitude of the load at the start of the segment
                                     # This will be used as the 'x2' value for the load prior to the start of the segment
                                     w2 = w1 + (w2 - w1)/(x2 - x1)*(x - x1)
@@ -2111,11 +2187,11 @@ def _segment_member(self, combo_name='Combo 1'):
                         
                                 # Determine if the load is on this segment
                                 if round(x2,10) > round(x,10):
-                                                
+                                                            
                                     # Break up the load and place it on the segment
                                     SegmentsY[i].w1 += (w2 - w1)/(x2 - x1)*(SegmentsY[i].x1 - x1) + w1
                                     SegmentsY[i].w2 += (w2 - w1)/(x2 - x1)*(SegmentsY[i].x2 - x1) + w1
-                            
+                                
                                     # Calculate the magnitude of the load at the start of the segment
                                     w2 = w1 + (w2 - w1)/(x2 - x1)*(x - x1)
                                     x2 = x
@@ -2136,7 +2212,7 @@ def _segment_member(self, combo_name='Combo 1'):
 
                                 # Determine if the load is on this segment
                                 if round(x2, 10) > round(x, 10):
-                                                
+                                                            
                                     # Break up the load and place it on the segment
                                     SegmentsZ[i].p1 += (f2[0] - f1[0])/(x2 - x1)*(x - x1) + f1[0]
                                     SegmentsZ[i].p2 += (f2[0] - f1[0])/(x2 - x1)*(SegmentsZ[i].x2 - x1) + f1[0]
@@ -2164,7 +2240,9 @@ def _segment_member(self, combo_name='Combo 1'):
                                 SegmentsY[i].V1 += (f1[2] + f2[2])/2*(x2 - x1)
                                 SegmentsY[i].M1 += (x1 - x2)*(2*f1[2]*x1 - 3*f1[2]*x + f1[2]*x2 + f2[2]*x1 - 3*f2[2]*x + 2*f2[2]*x2)/6
                             
-    def _extract_vector_results(self, segments, x_array, result_name, P_delta=False):
+    def _extract_vector_results(self, segments: List, x_array: NDArray[float64], 
+                          result_name: Literal['moment', 'shear', 'axial', 'torque', 'deflection', 'axial_deflection'], 
+                          P_delta: bool = False) -> NDArray[float64]:
         """Extract results from the given segments using vectorised numpy functions"""
 
         # Initialize variables
diff --git a/Pynite/Mesh.py b/Pynite/Mesh.py
index 2df3f29c..e6c452bd 100644
--- a/Pynite/Mesh.py
+++ b/Pynite/Mesh.py
@@ -1,8 +1,14 @@
+from __future__ import annotations # Allows more recent type hints features
+from typing import TYPE_CHECKING
+from math import pi, sin, cos, ceil, isclose
+
 from Pynite.Node3D import Node3D
 from Pynite.Quad3D import Quad3D
 from Pynite.Plate3D import Plate3D
-from math import pi, sin, cos, ceil, isclose
-from bisect import bisect
+
+if TYPE_CHECKING:
+    from typing import List, Union, Dict
+    from Pynite.FEModel3D import FEModel3D
 
 #%%
 class Mesh():
@@ -10,7 +16,7 @@ class Mesh():
     A parent class for meshes to inherit from.
     """
 
-    def __init__(self, thickness, material_name, model, kx_mod=1, ky_mod=1, start_node='N1', start_element='Q1'):
+    def __init__(self, thickness: float, material_name: str, model: FEModel3D, kx_mod: float = 1, ky_mod: float = 1, start_node: str = 'N1', start_element: str = 'Q1') -> None:
 
         self.thickness = thickness          # Thickness
         self.material_name = material_name            # The name of the element material
@@ -21,21 +27,21 @@ def __init__(self, thickness, material_name, model, kx_mod=1, ky_mod=1, start_no
         self.last_node = None               # The name of the last node in the mesh
         self.start_element = start_element  # The name of the first element in the mesh
         self.last_element = None            # The name of the last element in the mesh
-        self.nodes = {}                     # A dictionary containing the nodes in the mesh
-        self.elements = {}                  # A dictionary containing the elements in the mesh
+        self.nodes: Dict[str, Node3D] = {}  # A dictionary containing the nodes in the mesh
+        self.elements: Dict[str, Union[Quad3D, Plate3D]] = {}  # A dictionary containing the elements in the mesh
         self.element_type = 'Quad'          # The type of element used in the mesh
         self._is_generated = False          # A flag indicating whether the mesh has been generated
     
-    def is_generated(self):
+    def is_generated(self) -> bool:
         return self._is_generated
     
-    def _rename_duplicates(self):
+    def _rename_duplicates(self) -> None:
         """Renames any nodes or elements in the mesh that are already in the model
         """
 
         # Initialize lists to track node and element name changes
-        revised_nodes = {}
-        revised_elements = {}
+        revised_nodes: Dict[str, Node3D] = {}
+        revised_elements: Dict[str, Union[Quad3D, Plate3D]] = {}
 
         # Step through each node in the mesh
         for node in self.nodes.values():
@@ -85,13 +91,13 @@ def _rename_duplicates(self):
         self.nodes = revised_nodes
         self.elements = revised_elements
             
-    def generate(self):
+    def generate(self) -> None:
         """
         A placeholder to be overwritten by subclasses inheriting from this class
         """
         pass
 
-    def max_shear(self, direction='Qx', combo=None):
+    def max_shear(self, direction: str = 'Qx', combo: str | None = None) -> float:
         """
         Returns the maximum shear in the mesh.
         
@@ -100,6 +106,7 @@ def max_shear(self, direction='Qx', combo=None):
 
         Parameters
         ----------
+
         direction : string, optional
             The direction to ge the maximum shear for. Options are 'Qx' or 'Qy'. Default
             is 'Qx'.
@@ -155,14 +162,14 @@ def max_shear(self, direction='Qx', combo=None):
                                      element.shear((xi + xj)/2, (yi + yn)/2, local, load_combo.name)[i, 0]])
 
                     # Determine if the maximum shear calculated is the largest encountered so far
-                    if Q_max == None or Q_max < Q_element:
+                    if Q_max is None or Q_max < Q_element:
                         # Save this value if it's the largest
                         Q_max = Q_element
             
         # Return the largest value encountered from all the elements
-        return Q_max
+        return 0.0 if Q_max is None else Q_max
     
-    def min_shear(self, direction='Qx', combo=None):
+    def min_shear(self, direction: str = 'Qx', combo: str | None = None) -> float:
         """
         Returns the minimum shear in the mesh.
         
@@ -171,6 +178,7 @@ def min_shear(self, direction='Qx', combo=None):
 
         Parameters
         ----------
+
         direction : string, optional
             The direction to ge the minimum shear for. Options are 'Qx' or 'Qy'. Default
             is 'Qx'.
@@ -226,14 +234,14 @@ def min_shear(self, direction='Qx', combo=None):
                                      element.shear((xi + xj)/2, (yi + yn)/2, local, load_combo.name)[i, 0]])
 
                     # Determine if the minimum shear calculated is the smallest encountered so far
-                    if Q_min == None or Q_min > Q_element:
+                    if Q_min is None or Q_min > Q_element:
                         # Save this value if it's the smallest
                         Q_min = Q_element
             
         # Return the smallest value encountered from all the elements
-        return Q_min
+        return 0.0 if Q_min is None else Q_min
 
-    def max_moment(self, direction='Mx', combo=None):
+    def max_moment(self, direction: str = 'Mx', combo: str | None = None) -> float:
         """
         Returns the maximum moment in the mesh.
         
@@ -242,6 +250,7 @@ def max_moment(self, direction='Mx', combo=None):
 
         Parameters
         ----------
+
         direction : string, optional
             The direction to ge the maximum moment for. Options are 'Mx', 'My', or 'Mxy'. Default
             is 'Mx'.
@@ -299,14 +308,14 @@ def max_moment(self, direction='Mx', combo=None):
                                      element.moment((xi + xj)/2, (yi + yn)/2, local, load_combo.name)[i, 0]])
 
                     # Determine if the maximum moment calculated is the largest encountered so far
-                    if M_max == None or M_max < M_element:
+                    if M_max is None or M_max < M_element:
                         # Save this value if it's the largest
                         M_max = M_element
             
         # Return the largest value encountered from all the elements
-        return M_max
+        return 0.0 if M_max is None else M_max
     
-    def min_moment(self, direction='Mx', combo=None):
+    def min_moment(self, direction: str = 'Mx', combo: str | None = None) -> float:
         """
         Returns the minimum moment in the mesh.
         
@@ -315,6 +324,7 @@ def min_moment(self, direction='Mx', combo=None):
 
         Parameters
         ----------
+
         direction : string, optional
             The direction to ge the minimum moment for. Options are 'Mx', 'My', or 'Mxy'. Default
             is 'Mx'.
@@ -372,14 +382,14 @@ def min_moment(self, direction='Mx', combo=None):
                                      element.moment((xi + xj)/2, (yi + yn)/2, local, load_combo.name)[i, 0]])
 
                     # Determine if the minimum moment calculated is the smallest encountered so far
-                    if M_min == None or M_min > M_element:
+                    if M_min is None or M_min > M_element:
                         # Save this value if it's the smallest
                         M_min = M_element
             
         # Return the smallest value encountered from all the elements
-        return M_min
+        return 0.0 if M_min is None else M_min
     
-    def max_membrane(self, direction='Sx', combo=None):
+    def max_membrane(self, direction: str = 'Sx', combo: str | None = None) -> float:
         """
         Returns the maximum membrane stress in the mesh.
         
@@ -388,6 +398,7 @@ def max_membrane(self, direction='Sx', combo=None):
 
         Parameters
         ----------
+
         direction : string, optional
             The direction to ge the maximum membrane force for. Options are 'Sx', 'Sy', or 'Sxy'. Default is 'Sx'.
         combo : string, optional
@@ -444,14 +455,14 @@ def max_membrane(self, direction='Sx', combo=None):
                                         element.membrane((xi + xj)/2, (yi + yn)/2, local, load_combo.name)[i, 0]])
 
                     # Determine if the maximum membrane stress calculated is the largest encountered so far
-                    if S_max == None or S_max < M_element:
+                    if S_max is None or S_max < M_element:
                         # Save this value if it's the smallest
                         S_max = M_element
             
         # Return the smallest value encountered from all the elements
-        return S_max
+        return 0.0 if S_max is None else S_max
     
-    def min_membrane(self, direction='Sx', combo=None):
+    def min_membrane(self, direction: str = 'Sx', combo: str | None = None) -> float:
         """
         Returns the minimum membrane stress in the mesh.
         
@@ -460,6 +471,7 @@ def min_membrane(self, direction='Sx', combo=None):
 
         Parameters
         ----------
+
         direction : string, optional
             The direction to ge the minimum membrane force for. Options are 'Sx', 'Sy', or 'Sxy'. Default is 'Sx'.
         combo : string, optional
@@ -516,22 +528,23 @@ def min_membrane(self, direction='Sx', combo=None):
                                         element.membrane((xi + xj)/2, (yi + yn)/2, local, load_combo.name)[i, 0]])
 
                     # Determine if the minimum membrane stress calculated is the smallest encountered so far
-                    if S_min == None or S_min > M_element:
+                    if S_min is None or S_min > M_element:
                         # Save this value if it's the smallest
                         S_min = M_element
             
         # Return the smallest value encountered from all the elements
-        return S_min
+        return 0.0 if S_min is None else S_min
     
 #%%
 class RectangleMesh(Mesh):
 
-    def __init__(self, mesh_size, width, height, thickness, material_name, model, kx_mod=1, ky_mod=1, origin=[0, 0, 0], plane='XY', x_control=None, y_control=None, start_node='N1', start_element='Q1', element_type='Quad'):
+    def __init__(self, mesh_size: float, width: float, height: float, thickness: float, material_name: str, model: FEModel3D, kx_mod: float = 1.0, ky_mod: float = 1.0, origin: List[float] = [0, 0, 0], plane: str = 'XY', x_control: List[float] | None = None, y_control: List[float] | None = None, start_node: str = 'N1', start_element: str = 'Q1', element_type: str = 'Quad') -> None:
         """
         A rectangular mesh of elements.
 
         Parameters
         ----------
+
         mesh_size : number
             Desired mesh size.
         width : number
@@ -570,6 +583,7 @@ def __init__(self, mesh_size, width, height, thickness, material_name, model, kx
 
         Returns
         -------
+
         A new rectangular mesh object.
 
         """
@@ -588,9 +602,9 @@ def __init__(self, mesh_size, width, height, thickness, material_name, model, kx
         else: self.y_control = y_control
 
         self.element_type = element_type
-        self.openings = {}
+        self.openings: Dict[str, RectOpening] = {}
     
-    def generate(self):
+    def generate(self) -> None:
 
         mesh_size = self.mesh_size
         width = self.width
@@ -850,7 +864,7 @@ def generate(self):
         # Flag the mesh as generated
         self._is_generated = True
 
-    def node_local_coords(self, node):
+    def node_local_coords(self, node: Node3D) -> tuple[float, float]:
         """
         Calculates a node's position in the mesh's local coordinate system
         """
@@ -867,12 +881,13 @@ def node_local_coords(self, node):
         
         return x, y
 
-    def add_rect_opening(self, name, x_left, y_bott, width, height):
+    def add_rect_opening(self, name: str, x_left: float, y_bott: float, width: float, height: float) -> None:
         """
         Adds a rectangular opening to the mesh.
 
         Parameters
         ----------
+
         name : string
             A unique name for the opening that can be used to access it later
             on.
@@ -903,10 +918,11 @@ class RectOpening():
     Represents a rectangular opening in a rectangular mesh.
     """
 
-    def __init__(self, x_left, y_bott, width, height):
+    def __init__(self, x_left: float, y_bott: float, width: float, height: float) -> None:
         """
         Parameters
         ----------
+
         x_left : number
             The x-coordinate for the left side of the opening in the mesh's
             local coordinate system.
@@ -930,8 +946,8 @@ class AnnulusMesh(Mesh):
     A mesh of quadrilaterals forming an annulus (a donut).
     """
 
-    def __init__(self, mesh_size, outer_radius, inner_radius, thickness, material_name, model, kx_mod=1,
-        ky_mod=1, origin=[0, 0, 0], axis='Y', start_node='N1', start_element='Q1'):
+    def __init__(self, mesh_size: float, outer_radius: float, inner_radius: float, thickness: float, material_name: str, model: FEModel3D, kx_mod: float = 1,
+        ky_mod: float = 1, origin: List[float] = [0, 0, 0], axis: str = 'Y', start_node: str = 'N1', start_element: str = 'Q1') -> None:
 
         super().__init__(thickness, material_name, model, kx_mod, ky_mod, start_node, start_element)
 
@@ -946,7 +962,7 @@ def __init__(self, mesh_size, outer_radius, inner_radius, thickness, material_na
         
         # self.generate()
     
-    def generate(self):
+    def generate(self) -> None:
         
         mesh_size = self.mesh_size
         r_outer = self.outer_radius
@@ -1027,8 +1043,8 @@ class AnnulusRingMesh(Mesh):
     A mesh of quadrilaterals forming an annular ring (a donut).
     """
 
-    def __init__(self, outer_radius, inner_radius, num_quads, thickness, material_name, model, kx_mod=1, ky_mod=1,
-                 origin=[0, 0, 0], axis='Y', start_node='N1', start_element='Q1'):
+    def __init__(self, outer_radius: float, inner_radius: float, num_quads: int, thickness: float, material_name: str, model: FEModel3D, kx_mod: float = 1, ky_mod: float = 1,
+                 origin: List[float] = [0, 0, 0], axis: str = 'Y', start_node: str = 'N1', start_element: str = 'Q1') -> None:
 
         super().__init__(thickness, material_name, model, kx_mod, ky_mod, start_node=start_node,
                          start_element=start_element)
@@ -1045,7 +1061,7 @@ def __init__(self, outer_radius, inner_radius, num_quads, thickness, material_na
         # Generate the nodes and elements
         self.generate()
 
-    def generate(self):
+    def generate(self) -> None:
 
         n = self.n  # Number of plates in the initial ring
 
@@ -1153,12 +1169,13 @@ class AnnulusTransRingMesh(Mesh):
     edge.
     """
 
-    def __init__(self, outer_radius, inner_radius, num_inner_quads, thickness, material_name, model,
-                 kx_mod=1, ky_mod=1, origin=[0, 0, 0], axis='Y', start_node='N1',
-                 start_element='Q1'):
+    def __init__(self, outer_radius: float, inner_radius: float, num_inner_quads: int, thickness: float, material_name: str, model: FEModel3D,
+                 kx_mod: float = 1, ky_mod: float = 1, origin: List[float] = [0, 0, 0], axis: str = 'Y', start_node: str = 'N1',
+                 start_element: str = 'Q1') -> None:
         """
         Parameters
         ----------
+
         direction : array
             A vector indicating the direction normal to the ring.
         """
@@ -1178,7 +1195,7 @@ def __init__(self, outer_radius, inner_radius, num_inner_quads, thickness, mater
         # Create the mesh
         self.generate()
 
-    def generate(self):
+    def generate(self) -> None:
 
         n = self.n  # Number of plates in the outside of the ring (coarse mesh)
 
@@ -1329,8 +1346,8 @@ class FrustrumMesh(AnnulusMesh):
     A mesh of quadrilaterals forming a frustrum (a cone intersected by a horizontal plane).
     """
 
-    def __init__(self, mesh_size, large_radius, small_radius, height, thickness, material_name, model, kx_mod=1, ky_mod=1,
-                 origin=[0, 0, 0], axis='Y', start_node='N1', start_element='Q1'):
+    def __init__(self, mesh_size: float, large_radius: float, small_radius: float, height: float, thickness: float, material_name: str, model: FEModel3D, kx_mod: float = 1, ky_mod: float = 1,
+                 origin: List[float] = [0, 0, 0], axis: str = 'Y', start_node: str = 'N1', start_element: str = 'Q1') -> None:
         
         # Create an annulus mesh
         super().__init__(mesh_size, large_radius, small_radius, thickness, material_name, model, kx_mod,
@@ -1338,7 +1355,7 @@ def __init__(self, mesh_size, large_radius, small_radius, height, thickness, mat
         
         self.height = height
     
-    def generate(self):
+    def generate(self) -> None:
 
         super().generate()
         
@@ -1381,6 +1398,7 @@ class CylinderMesh(Mesh):
 
     Parameters
     ----------
+
     mesh_size : number
         The desired mesh element edge size. This value will only be used to mesh vertically if `num_elements` is
         specified. Otherwise it will be used to mesh the circumference too.
@@ -1414,7 +1432,7 @@ class CylinderMesh(Mesh):
         The type of element to use for the mesh: 'Quad' or 'Rect'
     """
 
-    def __init__(self, mesh_size, radius, height, thickness, material_name, model, kx_mod=1, ky_mod=1,origin=[0, 0, 0], axis='Y', start_node='N1', start_element='Q1', num_elements=None, element_type='Quad'):
+    def __init__(self, mesh_size: float, radius: float, height: float, thickness: float, material_name: str, model: FEModel3D, kx_mod: float = 1, ky_mod: float = 1,origin: List[float] = [0, 0, 0], axis: str = 'Y', start_node: str = 'N1', start_element: str = 'Q1', num_elements: int | None = None, element_type: str = 'Quad') -> None:
 
         # Inherit properties and methods from the parent `Mesh` class
         super().__init__(thickness, material_name, model, kx_mod, ky_mod, start_node, start_element)
@@ -1440,7 +1458,7 @@ def __init__(self, mesh_size, radius, height, thickness, material_name, model, k
         # Generate the mesh
         self.generate()
     
-    def generate(self):
+    def generate(self) -> None:
         
         # Get the mesh thickness and the material name
         thickness = self.thickness
@@ -1522,6 +1540,7 @@ class CylinderRingMesh(Mesh):
 
     Parameters
     ----------
+
     radius : number
         Radius to the center of the plates in the cylindrical ring.
     height : number
@@ -1555,9 +1574,9 @@ class CylinderRingMesh(Mesh):
     
     """
 
-    def __init__(self, radius, height, num_elements, thickness, material_name, model, kx_mod=1, ky_mod=1,
-                 origin=[0, 0, 0], axis='Y', start_node='N1', start_element='Q1',
-                 element_type='Quad'):
+    def __init__(self, radius: float, height: float, num_elements: int, thickness: float, material_name: str, model: FEModel3D, kx_mod: float = 1, ky_mod: float = 1,
+                 origin: List[float] = [0, 0, 0], axis: str = 'Y', start_node: str = 'N1', start_element: str = 'Q1',
+                 element_type: str = 'Quad') -> None:
 
         super().__init__(thickness, material_name, model, kx_mod, ky_mod, start_node=start_node, start_element=start_element)
 
@@ -1573,7 +1592,7 @@ def __init__(self, radius, height, num_elements, thickness, material_name, model
         # Generate the nodes and elements
         self.generate()
 
-    def generate(self):
+    def generate(self) -> None:
         """
         Generates the nodes and elements in the mesh.
         """
@@ -1698,7 +1717,7 @@ def generate(self):
         # Flag the mesh as generated
         self._is_generated = True
         
-def check_mesh_integrity(mesh, console_log=True):
+def check_mesh_integrity(mesh: Mesh, console_log: bool = True) -> Union[str, List[str], None]:
     """Runs basic integrity checks to ensure the mesh is in sync with its model. Usually you don't
     want to run this check unless the mesh has been generated since generating the mesh is what
     syncs it to the model.
diff --git a/Pynite/Node3D.py b/Pynite/Node3D.py
index 90496197..853c7aad 100644
--- a/Pynite/Node3D.py
+++ b/Pynite/Node3D.py
@@ -4,13 +4,16 @@
 
 @author: D. Craig Brinck, SE
 """
+from __future__ import annotations # Allows more recent type hints features
 # %%      
+from typing import List, Tuple, Dict
+
 class Node3D():
     """
     A class representing a node in a 3D finite element model.
     """
     
-    def __init__(self, name, X, Y, Z):
+    def __init__(self, name: str, X: float, Y: float, Z: float):
         
         self.name = name    # A unique name for the node assigned by the user
         self.ID = None      # A unique index number for the node assigned by the program
@@ -19,52 +22,52 @@ def __init__(self, name, X, Y, Z):
         self.Y = Y          # Global Y coordinate
         self.Z = Z          # Global Z coordinate
         
-        self.NodeLoads = [] # A list of loads applied to the node (Direction, P, case) or (Direction, M, case)
+        self.NodeLoads: List[Tuple[str, float, str]] = [] # A list of loads applied to the node (Direction, P, case) or (Direction, M, case)
         
         # Initialize the dictionaries of calculated node displacements
-        self.DX = {}
-        self.DY = {}
-        self.DZ = {}
-        self.RX = {}
-        self.RY = {}
-        self.RZ = {}
+        self.DX: Dict[str, float] = {}
+        self.DY: Dict[str, float] = {}
+        self.DZ: Dict[str, float] = {}
+        self.RX: Dict[str, float] = {}
+        self.RY: Dict[str, float] = {}
+        self.RZ: Dict[str, float] = {}
         
         # Initialize the dictionaries of calculated node reactions
-        self.RxnFX = {}
-        self.RxnFY = {}
-        self.RxnFZ = {}
-        self.RxnMX = {}
-        self.RxnMY = {}
-        self.RxnMZ = {}
+        self.RxnFX: Dict[str, float] = {}
+        self.RxnFY: Dict[str, float] = {}
+        self.RxnFZ: Dict[str, float] = {}
+        self.RxnMX: Dict[str, float] = {}
+        self.RxnMY: Dict[str, float] = {}
+        self.RxnMZ: Dict[str, float] = {}
 
         # Initialize all support conditions to `False`
-        self.support_DX = False
-        self.support_DY = False
-        self.support_DZ = False
-        self.support_RX = False
-        self.support_RY = False
-        self.support_RZ = False
+        self.support_DX: bool = False
+        self.support_DY: bool = False
+        self.support_DZ: bool = False
+        self.support_RX: bool = False
+        self.support_RY: bool = False
+        self.support_RZ: bool = False
 
         # Inititialize all support springs
-        self.spring_DX = [None, None, None]  # [stiffness, direction, active]
-        self.spring_DY = [None, None, None]
-        self.spring_DZ = [None, None, None]
-        self.spring_RX = [None, None, None]
-        self.spring_RY = [None, None, None]
-        self.spring_RZ = [None, None, None]
+        self.spring_DX: List[float | str | bool | None] = [None, None, None]  # [stiffness, direction, active]
+        self.spring_DY: List[float | str | bool | None] = [None, None, None]
+        self.spring_DZ: List[float | str | bool | None] = [None, None, None]
+        self.spring_RX: List[float | str | bool | None] = [None, None, None]
+        self.spring_RY: List[float | str | bool | None] = [None, None, None]
+        self.spring_RZ: List[float | str | bool | None] = [None, None, None]
 
         # Initialize all enforced displacements to `None`
-        self.EnforcedDX = None
-        self.EnforcedDY = None
-        self.EnforcedDZ = None
-        self.EnforcedRX = None
-        self.EnforcedRY = None
-        self.EnforcedRZ = None
+        self.EnforcedDX: float | None = None
+        self.EnforcedDY: float | None = None
+        self.EnforcedDZ: float | None = None
+        self.EnforcedRX: float | None = None
+        self.EnforcedRY: float | None = None
+        self.EnforcedRZ: float | None = None
 
         # Initialize the color contour value for the node. This will be used for contour smoothing.
-        self.contour = []
+        self.contour: List[float] = []
 
-    def distance(self, other):
+    def distance(self, other: 'Node3D') -> float:
         """
         Returns the distance to another node.
 
diff --git a/Pynite/PhysMember.py b/Pynite/PhysMember.py
index f0feb96e..1499cdff 100644
--- a/Pynite/PhysMember.py
+++ b/Pynite/PhysMember.py
@@ -1,9 +1,16 @@
+from __future__ import annotations # Allows more recent type hints features
+from typing import Dict, List, Literal, Tuple, TYPE_CHECKING
+from Pynite.Member3D import Member3D
+
+if TYPE_CHECKING:
+    from Pynite.Node3D import Node3D
+    from Pynite.FEModel3D import FEModel3D
+    import numpy.typing as npt
+
 from numpy import array, dot, cross
 from numpy.linalg import norm
 from math import isclose, acos
 
-from Pynite.Member3D import Member3D
-
 class PhysMember(Member3D):
     """
     A physical member.
@@ -15,13 +22,13 @@ class PhysMember(Member3D):
     # '__plt' is used to store the 'pyplot' from matplotlib once it gets imported. Setting it to 'None' for now allows us to defer importing it until it's actually needed.
     __plt = None  
     
-    def __init__(self, model, name, i_node, j_node, material_name, section_name, rotation=0.0,
-                 tension_only=False, comp_only=False):
+    def __init__(self, model: FEModel3D, name: str, i_node: Node3D, j_node: Node3D, material_name: str, section_name: str, rotation: float = 0.0,
+                 tension_only: bool = False, comp_only: bool = False) -> None:
         
         super().__init__(model, name, i_node, j_node, material_name, section_name, rotation, tension_only, comp_only)
-        self.sub_members = {}
+        self.sub_members: Dict[str, Member3D] = {}
 
-    def descritize(self):
+    def descritize(self) -> None:
         """
         Subdivides the physical member into sub-members at each node along the physical member
         """
@@ -30,7 +37,7 @@ def descritize(self):
         self.sub_members = {}
 
         # Start a new list of nodes along the member
-        int_nodes = []
+        int_nodes: List[Tuple[Node3D, float]] = []
 
         # Create a vector from the i-node to the j-node
         Xi, Yi, Zi = self.i_node.X, self.i_node.Y, self.i_node.Z
@@ -38,8 +45,8 @@ def descritize(self):
         vector_ij = array([Xj-Xi, Yj-Yi, Zj-Zi])
 
         # Add the i-node and j-node to the list
-        int_nodes.append([self.i_node, 0])
-        int_nodes.append([self.j_node, norm(vector_ij)])
+        int_nodes.append((self.i_node, 0))
+        int_nodes.append((self.j_node, norm(vector_ij)))
 
         # Step through each node in the model
         for node in self.model.nodes.values():
@@ -61,7 +68,7 @@ def descritize(self):
                     if norm(vector_in) < norm(vector_ij):
 
                         # Add the node to the list of intermediate nodes
-                        int_nodes.append([node, norm(vector_in)])
+                        int_nodes.append((node, norm(vector_in)))
         
         # Create a list of sorted intermediate nodes by distance from the i-node
         int_nodes = sorted(int_nodes, key=lambda x: x[1])
@@ -146,7 +153,7 @@ def descritize(self):
             # Add the new sub-member to the sub-member dictionary for this physical member
             self.sub_members[name] = new_sub_member
     
-    def shear(self, Direction, x, combo_name='Combo 1'):
+    def shear(self, Direction: Literal['Fy', 'Fz'], x: float, combo_name: str = 'Combo 1') -> float:
         """
         Returns the shear at a point along the member's length.
         
@@ -165,7 +172,7 @@ def shear(self, Direction, x, combo_name='Combo 1'):
         member, x_mod = self.find_member(x)
         return member.shear(Direction, x_mod, combo_name)
     
-    def max_shear(self, Direction, combo_name='Combo 1'):
+    def max_shear(self, Direction: Literal['Fy', 'Fz'], combo_name: str = 'Combo 1') -> float:
         """
         Returns the maximum shear in the member for the given direction
         
@@ -186,7 +193,7 @@ def max_shear(self, Direction, combo_name='Combo 1'):
                 Vmax = V
         return Vmax
     
-    def min_shear(self, Direction, combo_name='Combo 1'):
+    def min_shear(self, Direction: Literal['Fy', 'Fz'], combo_name: str = 'Combo 1') -> float:
         """
         Returns the minimum shear in the member for the given direction
         
@@ -207,7 +214,7 @@ def min_shear(self, Direction, combo_name='Combo 1'):
                 Vmin = V
         return Vmin
    
-    def plot_shear(self, Direction, combo_name='Combo 1', n_points=20):
+    def plot_shear(self, Direction: Literal['Fy', 'Fz'], combo_name: str = 'Combo 1', n_points: int = 20) -> None:
         """
         Plots the shear diagram for the member
         
@@ -252,7 +259,7 @@ def plot_shear(self, Direction, combo_name='Combo 1', n_points=20):
         PhysMember.__plt.title('Member ' + self.name + '\n' + combo_name)
         PhysMember.__plt.show()
 
-    def moment(self, Direction, x, combo_name='Combo 1'):
+    def moment(self, Direction: Literal['My', 'Mz'], x: float, combo_name: str = 'Combo 1') -> float:
         """
         Returns the moment at a point along the member's length
         
@@ -271,7 +278,7 @@ def moment(self, Direction, x, combo_name='Combo 1'):
         member, x_mod = self.find_member(x)
         return member.moment(Direction, x_mod, combo_name)
     
-    def max_moment(self, Direction, combo_name='Combo 1'):
+    def max_moment(self, Direction: Literal['My', 'Mz'], combo_name: str = 'Combo 1') -> float:
         """
         Returns the maximum moment in the member for the given direction.
         
@@ -292,7 +299,7 @@ def max_moment(self, Direction, combo_name='Combo 1'):
                 Mmax = M
         return Mmax
     
-    def min_moment(self, Direction, combo_name='Combo 1'):
+    def min_moment(self, Direction: Literal['My', 'Mz'], combo_name: str = 'Combo 1') -> float:
         """
         Returns the minimum moment in the member for the given direction
         
@@ -313,12 +320,13 @@ def min_moment(self, Direction, combo_name='Combo 1'):
                 Mmin = M
         return Mmin
 
-    def plot_moment(self, Direction, combo_name='Combo 1', n_points=20):
+    def plot_moment(self, Direction: Literal['My', 'Mz'], combo_name: str = 'Combo 1', n_points: int = 20) -> None:
         """
         Plots the moment diagram for the member
         
         Parameters
         ----------
+
         Direction : string
             The direction in which to plot the moment. Must be one of the following:
                 'My' = Moment about the local y-axis.
@@ -358,7 +366,7 @@ def plot_moment(self, Direction, combo_name='Combo 1', n_points=20):
         PhysMember.__plt.title('Member ' + self.name + '\n' + combo_name)
         PhysMember.__plt.show()
     
-    def torque(self, x, combo_name='Combo 1'):
+    def torque(self, x: float, combo_name: str = 'Combo 1') -> float:
         """
         Returns the torsional moment at a point along the member's length
         
@@ -373,7 +381,7 @@ def torque(self, x, combo_name='Combo 1'):
         member, x_mod = self.find_member(x)
         return member.torque(x_mod, combo_name)
     
-    def max_torque(self, combo_name='Combo 1'):
+    def max_torque(self, combo_name: str = 'Combo 1') -> float:
         
         Tmax = None
         for member in self.sub_members.values():
@@ -382,7 +390,7 @@ def max_torque(self, combo_name='Combo 1'):
                 Tmax = T
         return Tmax
     
-    def min_torque(self, combo_name='Combo 1'):
+    def min_torque(self, combo_name: str = 'Combo 1') -> float:
         """
         Returns the minimum torsional moment in the member.
 
@@ -399,7 +407,7 @@ def min_torque(self, combo_name='Combo 1'):
                 Tmin = T
         return Tmin
 
-    def plot_torque(self, combo_name='Combo 1', n_points=20):
+    def plot_torque(self, combo_name: str = 'Combo 1', n_points: int = 20) -> None:
         """
         Plots the torque diagram for the member
         
@@ -440,7 +448,7 @@ def plot_torque(self, combo_name='Combo 1', n_points=20):
         PhysMember.__plt.title('Member ' + self.name + '\n' + combo_name)
         PhysMember.__plt.show()
 
-    def axial(self, x, combo_name='Combo 1'):
+    def axial(self, x: float, combo_name: str = 'Combo 1') -> float:
         """
         Returns the axial force at a point along the member's length.
         
@@ -455,7 +463,7 @@ def axial(self, x, combo_name='Combo 1'):
         member, x_mod = self.find_member(x)
         return member.axial(x_mod, combo_name)
     
-    def max_axial(self, combo_name='Combo 1'):
+    def max_axial(self, combo_name: str = 'Combo 1') -> float:
         
         Pmax = None
         for member in self.sub_members.values():
@@ -464,7 +472,7 @@ def max_axial(self, combo_name='Combo 1'):
                 Pmax = P
         return Pmax
     
-    def min_axial(self, combo_name='Combo 1'):
+    def min_axial(self, combo_name: str = 'Combo 1') -> float:
 
         Pmin = None
         for member in self.sub_members.values():
@@ -473,7 +481,7 @@ def min_axial(self, combo_name='Combo 1'):
                 Pmin = P
         return Pmin
 
-    def plot_axial(self, combo_name='Combo 1', n_points=20):
+    def plot_axial(self, combo_name: str = 'Combo 1', n_points: int = 20) -> None:
         """
         Plots the axial force diagram for the member
         
@@ -514,7 +522,7 @@ def plot_axial(self, combo_name='Combo 1', n_points=20):
         PhysMember.__plt.title('Member ' + self.name + '\n' + combo_name)
         PhysMember.__plt.show()
 
-    def deflection(self, Direction, x, combo_name='Combo 1'):
+    def deflection(self, Direction: Literal['dx', 'dy', 'dz'], x: float, combo_name: str = 'Combo 1') -> float:
         """
         Returns the deflection at a point along the member's length.
         
@@ -534,7 +542,7 @@ def deflection(self, Direction, x, combo_name='Combo 1'):
         member, x_mod = self.find_member(x)
         return member.deflection(Direction, x_mod, combo_name)
 
-    def max_deflection(self, Direction, combo_name='Combo 1'):
+    def max_deflection(self, Direction: Literal['dx', 'dy', 'dz'], combo_name: str = 'Combo 1') -> float:
         """
         Returns the maximum deflection in the member.
         
@@ -553,7 +561,7 @@ def max_deflection(self, Direction, combo_name='Combo 1'):
                 dmax = d
         return dmax
     
-    def min_deflection(self, Direction, combo_name='Combo 1'):
+    def min_deflection(self, Direction: Literal['dx', 'dy', 'dz'], combo_name: str = 'Combo 1') -> float:
         """
         Returns the minimum deflection in the member.
         
@@ -572,12 +580,13 @@ def min_deflection(self, Direction, combo_name='Combo 1'):
                 dmin = d
         return dmin
 
-    def rel_deflection(self, Direction, x, combo_name='Combo 1'):
+    def rel_deflection(self, Direction: Literal['dx', 'dy', 'dz'], x: float, combo_name: str = 'Combo 1') -> float:
         """
         Returns the relative deflection at a point along the member's length
         
         Parameters
         ----------
+
         Direction : string
             The direction in which to find the relative deflection. Must be one of the following:
                 'dy' = Deflection in the local y-axis
@@ -591,8 +600,7 @@ def rel_deflection(self, Direction, x, combo_name='Combo 1'):
         member, x_mod = self.find_member(x)
         return member.rel_deflection(Direction, x_mod, combo_name)
 
-    
-    def plot_deflection(self, Direction, combo_name='Combo 1', n_points=20):
+    def plot_deflection(self, Direction: Literal['dx', 'dy', 'dz'], combo_name: str = 'Combo 1', n_points: int = 20) -> None:
         """
         Plots the deflection diagram for the member
         
@@ -637,7 +645,7 @@ def plot_deflection(self, Direction, combo_name='Combo 1', n_points=20):
         PhysMember.__plt.title('Member ' + self.name + '\n' + combo_name)
         PhysMember.__plt.show()
 
-    def find_member(self, x):
+    def find_member(self, x: float) -> Tuple[Member3D, float]:
         """
         Returns the sub-member that the physical member's local point 'x' lies on, and 'x' modified for that sub-member's local coordinate system.
         """
@@ -662,4 +670,3 @@ def find_member(self, x):
                 break
         else:
             raise ValueError(f"Location x={x} does not lie on this member")
-            
\ No newline at end of file
diff --git a/Pynite/Plate3D.py b/Pynite/Plate3D.py
index d7c7da77..03b3f056 100644
--- a/Pynite/Plate3D.py
+++ b/Pynite/Plate3D.py
@@ -1,11 +1,21 @@
+from __future__ import annotations # Allows more recent type hints features
+from typing import List, Tuple, Optional,TYPE_CHECKING
+
 from numpy import zeros, array, matmul, cross, add
 from numpy.linalg import inv, norm, det
+from numpy.typing import NDArray
+
+if TYPE_CHECKING:
+    from numpy import float64
+    from Pynite.FEModel3D import FEModel3D
+    from Pynite.Node3D import Node3D
 
 #%%
 class Plate3D():
 
-    def __init__(self, name, i_node, j_node, m_node, n_node, t, material_name, model, kx_mod=1.0,
-                 ky_mod=1.0):
+    def __init__(self, name: str, i_node: Node3D, j_node: Node3D, m_node: Node3D, n_node: Node3D, 
+                 t: float, material_name: str, model: FEModel3D, kx_mod: float = 1.0,
+                 ky_mod: float = 1.0):
         """
         A rectangular plate element
 
@@ -35,45 +45,45 @@ def __init__(self, name, i_node, j_node, m_node, n_node, t, material_name, model
             The model the plate is a part of
         """
 
-        self.name = name
-        self.ID = None
-        self.type = 'Rect'
+        self.name: str = name
+        self.ID: Optional[int] = None
+        self.type: str = 'Rect'
 
-        self.i_node = i_node
-        self.j_node = j_node
-        self.m_node = m_node
-        self.n_node = n_node
+        self.i_node: Node3D = i_node
+        self.j_node: Node3D = j_node
+        self.m_node: Node3D = m_node
+        self.n_node: Node3D = n_node
 
-        self.t = t
+        self.t: float = t
         
-        self.kx_mod = kx_mod
-        self.ky_mod = ky_mod
+        self.kx_mod: float = kx_mod
+        self.ky_mod: float = ky_mod
 
-        self.pressures = []  # A list of surface pressures [pressure, case='Case 1']
+        self.pressures: List[Tuple[float, str]] = []  # A list of surface pressures [pressure, case='Case 1']
 
         # Plates need a link to the model they belong to
-        self.model = model
+        self.model: FEModel3D = model
 
         # Get material properties for the plate from the model
         try:
-            self.E = self.model.materials[material_name].E
-            self.nu = self.model.materials[material_name].nu
+            self.E: float = self.model.materials[material_name].E
+            self.nu: float = self.model.materials[material_name].nu
         except:
             raise KeyError('Please define the material ' + str(material_name) + ' before assigning it to plates.')
     
-    def width(self):
+    def width(self) -> float:
         """
         Returns the width of the plate along its local x-axis
         """
         return self.i_node.distance(self.j_node)
 
-    def height(self):
+    def height(self) -> float:
         """
         Returns the height of the plate along its local y-axis
         """
         return self.i_node.distance(self.n_node)
     
-    def Dm(self):
+    def Dm(self) -> NDArray[float64]:
         """
         Returns the plane stress constitutive matrix for orthotropic materials [Dm]
         """
@@ -99,7 +109,7 @@ def Dm(self):
         # Return the constitutive matrix [Dm]
         return Dm
 
-    def Db(self):
+    def Db(self) -> NDArray[float64]:
         """
         Returns the bending constitutive matrix for orthotropic materials [Db]
         """
@@ -120,7 +130,7 @@ def Db(self):
         # Return the constitutive matrix [Db]
         return Db
 
-    def J(self, r, s):
+    def J(self, r: float, s: float) -> NDArray[float64]:
         '''
         Returns the Jacobian matrix for the element
         '''
@@ -132,7 +142,7 @@ def J(self, r, s):
         return 1/4*array([[x1*(s - 1) - x2*(s - 1) + x3*(s + 1) - x4*(s + 1), y1*(s - 1) - y2*(s - 1) + y3*(s + 1) - y4*(s + 1)],
                           [x1*(r - 1) - x2*(r + 1) + x3*(r + 1) - x4*(r - 1), y1*(r - 1) - y2*(r + 1) + y3*(r + 1) - y4*(r - 1)]])
     
-    def B_m(self, r, s):
+    def B_m(self, r: float, s: float) -> NDArray[float64]:
 
         # Differentiate the interpolation functions
         # Row 1 = interpolation functions differentiated with respect to x
@@ -149,13 +159,13 @@ def B_m(self, r, s):
         
         return B_m
     
-    def k(self):
+    def k(self) -> NDArray[float64]:
         """
         returns the plate's local stiffness matrix
         """
         return add(self.k_b(), self.k_m())
     
-    def k_m(self):
+    def k_m(self) -> NDArray[float64]:
         '''
         Returns the local stiffness matrix for membrane (in-plane) stresses.
 
@@ -213,7 +223,7 @@ def k_m(self):
         
         return k_exp
     
-    def k_b(self):
+    def k_b(self) -> NDArray[float64]:
         """
         Returns the local stiffness matrix for bending
         """
@@ -300,7 +310,7 @@ def k_b(self):
         # Return the local stiffness matrix
         return k_exp
 
-    def f(self, combo_name='Combo 1'):
+    def f(self, combo_name: str = 'Combo 1') -> NDArray[float64]:
         """
         Returns the plate's local end force vector
         """
@@ -308,7 +318,7 @@ def f(self, combo_name='Combo 1'):
         # Calculate and return the plate's local end force vector
         return add(matmul(self.k(), self.d(combo_name)), self.fer(combo_name))
 
-    def fer(self, combo_name='Combo 1'):
+    def fer(self, combo_name: str = 'Combo 1') -> NDArray[float64]:
         """
         Returns the rectangle's local fixed end reaction vector.
 
@@ -379,7 +389,7 @@ def fer(self, combo_name='Combo 1'):
 
         return fer_exp
         
-    def d(self, combo_name='Combo 1'):
+    def d(self, combo_name: str = 'Combo 1') -> NDArray[float64]:
        """
        Returns the plate's local displacement vector
        """
@@ -387,7 +397,7 @@ def d(self, combo_name='Combo 1'):
        # Calculate and return the local displacement vector
        return matmul(self.T(), self.D(combo_name))
 
-    def F(self, combo_name='Combo 1'):
+    def F(self, combo_name: str = 'Combo 1') -> NDArray[float64]:
         """
         Returns the plate's global nodal force vector
         """
@@ -395,7 +405,7 @@ def F(self, combo_name='Combo 1'):
         # Calculate and return the global force vector
         return matmul(inv(self.T()), self.f(combo_name))
 
-    def D(self, combo_name='Combo 1'):
+    def D(self, combo_name: str = 'Combo 1') -> NDArray[float64]:
         """
         Returns the plate's global displacement vector for the given load combination.
         """
@@ -435,7 +445,7 @@ def D(self, combo_name='Combo 1'):
         # Return the global displacement vector
         return D
  
-    def T(self):
+    def T(self) -> NDArray[float64]:
         """
         Returns the plate's transformation matrix
         """
@@ -486,7 +496,7 @@ def T(self):
         
         return transMatrix
 
-    def K(self):
+    def K(self) -> NDArray[float64]:
         """
         Returns the plate's global stiffness matrix
         """
@@ -494,7 +504,7 @@ def K(self):
         # Calculate and return the stiffness matrix in global coordinates
         return matmul(matmul(inv(self.T()), self.k()), self.T())
 
-    def FER(self, combo_name='Combo 1'):
+    def FER(self, combo_name: str = 'Combo 1') -> NDArray[float64]:
         """
         Returns the global fixed end reaction vector.
 
@@ -508,7 +518,7 @@ def FER(self, combo_name='Combo 1'):
         # Calculate and return the fixed end reaction vector
         return matmul(inv(self.T()), self.fer(combo_name))
 
-    def _C(self):
+    def _C(self) -> NDArray[float64]:
         """
         Returns the plate's displacement coefficient matrix [C]
         """
@@ -543,7 +553,7 @@ def _C(self):
         # Return the coefficient matrix
         return C
 
-    def _Q(self, x, y):
+    def _Q(self, x: float, y: float) -> NDArray[float64]:
         """
         Calculates and returns the plate curvature coefficient matrix [Q] at a given point (x, y)
         in the plate's local system.
@@ -557,7 +567,7 @@ def _Q(self, x, y):
         # Return the [Q] coefficient matrix
         return Q
 
-    def _a(self, combo_name='Combo 1'):
+    def _a(self, combo_name: str = 'Combo 1') -> NDArray[float64]:
         """
         Returns the vector of plate bending constants for the displacement function.
 
@@ -574,7 +584,7 @@ def _a(self, combo_name='Combo 1'):
         # Return the plate bending constants
         return inv(self._C()) @ d
 
-    def moment(self, x, y, local=True, combo_name='Combo 1'):
+    def moment(self, x: float, y: float, local: bool = True, combo_name: str = 'Combo 1') -> NDArray[float64]:
         """
         Returns the internal moments (Mx, My, and Mxy) at any point (x, y) in the plate's local
         coordinate system
@@ -595,7 +605,7 @@ def moment(self, x, y, local=True, combo_name='Combo 1'):
         # Pynite's quadrilateral elements.
         return -self.Db() @ self._Q(x, y) @ self._a(combo_name)
  
-    def shear(self, x, y, local=True, combo_name='Combo 1'):
+    def shear(self, x: float, y: float, local: bool = True, combo_name: str = 'Combo 1') -> NDArray[float64]:
         """
         Returns the internal shears (Qx and Qy) at any point (x, y) in the plate's local
         coordinate system
@@ -640,7 +650,7 @@ def shear(self, x, y, local=True, combo_name='Combo 1'):
         return array([[Qx], 
                       [Qy]])
 
-    def membrane(self, x, y, local=True, combo_name='Combo 1'):
+    def membrane(self, x: float, y: float, local: bool = True, combo_name: str = 'Combo 1') -> NDArray[float64]:
         
         # Convert the (x, y) coordinates to (r, x) coordinates
         r = -1 + 2*x/self.width()
diff --git a/Pynite/Quad3D.py b/Pynite/Quad3D.py
index f225219f..7b030f75 100644
--- a/Pynite/Quad3D.py
+++ b/Pynite/Quad3D.py
@@ -3,11 +3,20 @@
 # 2. "Finite Element Procedures, 2nd Edition", Klaus-Jurgen Bathe
 # 3. "A First Course in the Finite Element Method, 4th Edition", Daryl L. Logan
 # 4. "Finite Element Analysis Fundamentals", Richard H. Gallagher
+from __future__ import annotations # Allows more recent type hints features
+from math import sin, cos
+from typing import TYPE_CHECKING, Literal
 
 import numpy as np
 from numpy import add
 from numpy.linalg import inv, det, norm
-from math import sin, cos
+
+if TYPE_CHECKING:
+    from typing import List, Tuple, Optional
+    from numpy import float64
+    from numpy.typing import NDArray
+    from Pynite.FEModel3D import FEModel3D
+    from Pynite.Node3D import Node3D
 
 class Quad3D():
     """
@@ -16,31 +25,32 @@ class Quad3D():
     This element performs well for thick and thin plates, and for skewed plates. Minor errors are introduced into the solution due to the drilling approximation. Orthotropic behavior is limited to acting along the plate's local axes.
     """
 
-    def __init__(self, name, i_node, j_node, m_node, n_node, t, material_name, model, kx_mod=1.0,
-                 ky_mod=1.0):
+    def __init__(self, name: str, i_node: Node3D, j_node: Node3D, m_node: Node3D, n_node: Node3D, 
+                 t: float, material_name: str, model: FEModel3D, kx_mod: float = 1.0,
+                 ky_mod: float = 1.0):
 
-        self.name = name
-        self.ID = None
-        self.type = 'Quad'
+        self.name: str = name
+        self.ID: Optional[int] = None
+        self.type: str = 'Quad'
 
-        self.i_node = i_node
-        self.j_node = j_node
-        self.m_node = m_node
-        self.n_node = n_node
+        self.i_node: Node3D = i_node
+        self.j_node: Node3D = j_node
+        self.m_node: Node3D = m_node
+        self.n_node: Node3D = n_node
 
-        self.t = t
-        self.kx_mod = kx_mod
-        self.ky_mod = ky_mod
+        self.t: float = t
+        self.kx_mod: float = kx_mod
+        self.ky_mod: float = ky_mod
 
-        self.pressures = []  # A list of surface pressures [pressure, case='Case 1']
+        self.pressures: List[Tuple[float, str]] = []  # A list of surface pressures [pressure, case='Case 1']
     
         # Quads need a link to the model they belong to
-        self.model = model
+        self.model: FEModel3D = model
 
         # Get material properties for the plate from the model
         try:
-            self.E = self.model.materials[material_name].E
-            self.nu = self.model.materials[material_name].nu
+            self.E: float = self.model.materials[material_name].E
+            self.nu: float = self.model.materials[material_name].nu
         except:
             raise KeyError('Please define the material ' + str(material_name) + ' before assigning it to plates.')
 
@@ -116,16 +126,16 @@ def _local_coords(self):
         y_axis = y_axis/norm(y_axis)
 
         # Calculate the local (x, y) coordinates for each node
-        self.x1 = 0
-        self.x2 = np.dot(vector_12, x_axis)
-        self.x3 = np.dot(vector_13, x_axis)
-        self.x4 = np.dot(vector_14, x_axis)
-        self.y1 = 0
-        self.y2 = np.dot(vector_12, y_axis)
-        self.y3 = np.dot(vector_13, y_axis)
-        self.y4 = np.dot(vector_14, y_axis)
-
-    def L_k(self, k):
+        self.x1: float = 0.0
+        self.x2: float = np.dot(vector_12, x_axis)
+        self.x3: float = np.dot(vector_13, x_axis)
+        self.x4: float = np.dot(vector_14, x_axis)
+        self.y1: float = 0.0
+        self.y2: float = np.dot(vector_12, y_axis)
+        self.y3: float = np.dot(vector_13, y_axis)
+        self.y4: float = np.dot(vector_14, y_axis)
+
+    def L_k(self, k: Literal[5, 6, 7, 8]) -> float:
         
         # Figures 3 and 5
         if k == 5:
@@ -139,7 +149,7 @@ def L_k(self, k):
         else:
             raise Exception('Invalid value for k. k must be 5, 6, 7, or 8.')
     
-    def dir_cos(self, k):
+    def dir_cos(self, k: Literal[5, 6, 7, 8]) -> Tuple[float, float]:
 
         L_k = self.L_k(k)
 
@@ -161,14 +171,14 @@ def dir_cos(self, k):
 
         return C, S
 
-    def phi_k(self, k):
+    def phi_k(self, k: Literal[5, 6, 7, 8]) -> float:
 
         kappa = 5/6
 
         # Equation 74
         return 2/(kappa*(1-self.nu))*(self.t/self.L_k(k))**2
 
-    def N_i(self, i, xi, eta):
+    def N_i(self, i: Literal[1, 2, 3, 4], xi: float, eta: float) -> float:
         """
         Returns the interpolation function for any given coordinate in the natural (xi, eta) coordinate system
         """
@@ -184,7 +194,7 @@ def N_i(self, i, xi, eta):
         else:
             raise Exception('Unable to calculate interpolation function. Invalid value specifed for i.')
     
-    def P_k(self, k, xi, eta):
+    def P_k(self, k: Literal[5, 6, 7, 8], xi: float, eta: float) -> float:
 
         if k == 5:
             return 1/2*(1 - xi**2)*(1 - eta)
@@ -197,7 +207,7 @@ def P_k(self, k, xi, eta):
         else:
             raise Exception('Unable to calculate shape function. Invalid value specified for k.')
     
-    def Co(self, xi, eta):
+    def Co(self, xi: float, eta: float) -> NDArray[float64]:
         """
         This alternate calculation of the Jacobian matrix follows "The development of DKMQ plate bending element for thick to thin shell analysis based on the Naghdi/Reissner/Mindlin shell theory" by Katili, Batoz, Maknun and Hamdouni (2015). In the reference "C^o" is used instead of "J" to refer to the Jacobian. This method does not seem to produce incorrect results, but will be kept for future reference. It is helpful for understanding this plate element and may prove a useful simplification to the code base if implemented correctly someday.
         """
@@ -258,7 +268,7 @@ def Co(self, xi, eta):
         
         return Co
 
-    def J(self, xi, eta):
+    def J(self, xi: float, eta: float) -> NDArray[float64]:
         """
         Returns the Jacobian matrix for the element
         """
@@ -270,13 +280,13 @@ def J(self, xi, eta):
         return 1/4*np.array([[x1*(eta - 1) - x2*(eta - 1) + x3*(eta + 1) - x4*(eta + 1), y1*(eta - 1) - y2*(eta - 1) + y3*(eta + 1) - y4*(eta + 1)],
                              [x1*(xi - 1)  - x2*(xi + 1)  + x3*(xi + 1)  - x4*(xi - 1),  y1*(xi - 1)  - y2*(xi + 1)  + y3*(xi + 1)  - y4*(xi - 1)]])
 
-    def N_gamma(self, xi, eta):
+    def N_gamma(self, xi: float, eta: float) -> NDArray[float64]:
 
         # Equation 44
         return np.array([[1/2*(1 - eta),       0,      1/2*(1 + eta),       0     ],
                          [     0,        1/2*(1 + xi),       0,       1/2*(1 - xi)]])
 
-    def A_gamma(self):
+    def A_gamma(self) -> NDArray[float64]:
 
         L5 = self.L_k(5)
         L6 = self.L_k(6)
@@ -289,7 +299,7 @@ def A_gamma(self):
                          [  0,    0,  -L7/2,    0 ],
                          [  0,    0,     0,  -L8/2]])
 
-    def A_u(self):
+    def A_u(self) -> NDArray[float64]:
 
         # Calculate the length of each side of the quad
         L5 = self.L_k(5)
@@ -309,7 +319,7 @@ def A_u(self):
                              [  0,    0,  0,   0,    0,  0, -2/L7, C7, S7,  2/L7, C7, S7],
                              [ 2/L8, C8, S8,   0,    0,  0,   0,    0,  0, -2/L8, C8, S8]])
 
-    def A_Delta_inv_DKMQ(self):
+    def A_Delta_inv_DKMQ(self) -> NDArray[float64]:
 
         phi5 = self.phi_k(5)
         phi6 = self.phi_k(6)
@@ -321,7 +331,7 @@ def A_Delta_inv_DKMQ(self):
                               [   0,           0,      1/(1+phi7),     0     ],
                               [   0,           0,          0,      1/(1+phi8)]])
 
-    def A_phi_Delta(self):
+    def A_phi_Delta(self) -> NDArray[float64]:
 
         phi5 = self.phi_k(5)
         phi6 = self.phi_k(6)
@@ -333,7 +343,7 @@ def A_phi_Delta(self):
                          [      0,             0,       phi7/(1+phi7),       0      ],
                          [      0,             0,             0,       phi8/(1+phi8)]])
     
-    def B_b_beta(self, xi, eta):
+    def B_b_beta(self, xi: float, eta: float) -> NDArray[float64]:
 
         # Get the inverse of the Jacobian matrix
         J_inv = inv(self.J(xi, eta))
@@ -367,7 +377,7 @@ def B_b_beta(self, xi, eta):
                          [0,  0,  N1y, 0,  0,  N2y, 0,  0,  N3y, 0,  0,  N4y],
                          [0, N1y, N1x, 0, N2y, N2x, 0, N3y, N3x, 0, N4y, N4x]])
     
-    def B_b_Delta_beta(self, xi, eta):
+    def B_b_Delta_beta(self, xi: float, eta: float) -> NDArray[float64]:
 
         # Get the inverse of the Jacobian matrix
         J_inv = inv(self.J(xi, eta))
@@ -406,7 +416,7 @@ def B_b_Delta_beta(self, xi, eta):
                          [    P5y*S5,          P6y*S6,          P7y*S7,          P8y*S8,    ],
                          [P5y*C5 + P5x*S5, P6y*C6 + P6x*S6, P7y*C7 + P7x*S7, P8y*C8 + P8x*S8]])
     
-    def B_b(self, xi, eta):
+    def B_b(self, xi: float, eta: float) -> NDArray[float64]:
         """
         Returns the [B_b] matrix for bending
         """
@@ -414,7 +424,7 @@ def B_b(self, xi, eta):
         # Return the [B] matrix for bending
         return add(self.B_b_beta(xi, eta), self.B_b_Delta_beta(xi, eta) @ self.A_Delta_inv_DKMQ() @ self.A_u())
 
-    def B_s(self, xi, eta):
+    def B_s(self, xi: float, eta: float) -> NDArray[float64]:
         """
         Returns the [B_s] matrix for shear
         """
@@ -422,7 +432,7 @@ def B_s(self, xi, eta):
         # Return the [B] matrix for shear
         return inv(self.J(xi, eta)) @ self.N_gamma(xi, eta) @ self.A_gamma() @ self.A_phi_Delta() @ self.A_u()
 
-    def B_s_gamma(self, xi, eta):
+    def B_s_gamma(self, xi:float , eta: float) -> None:
         """Returns the [B_s_gamma] matrix for shear (Equation 39 in Reference 1)
 
         :param xi: _description_
@@ -430,8 +440,9 @@ def B_s_gamma(self, xi, eta):
         :param eta: _description_
         :type eta: _type_
         """
+        raise NotImplementedError('This function is not implemented yet. It is not needed for the current implementation of the Quad3D element.')
 
-    def B_m(self, xi, eta):
+    def B_m(self, xi: float, eta: float) -> NDArray[float64]:
 
         # Differentiate the interpolation functions
         # Row 1 = interpolation functions differentiated with respect to x
@@ -448,7 +459,7 @@ def B_m(self, xi, eta):
 
         return B_m
 
-    def Hb(self):
+    def Hb(self) -> NDArray[float64]:
         '''
         Returns the stress-strain matrix for plate bending.
         '''
@@ -464,7 +475,7 @@ def Hb(self):
         
         return Hb
 
-    def Hs(self):
+    def Hs(self) -> NDArray[float64]:
         '''
         Returns the stress-strain matrix for shear.
         '''
@@ -479,7 +490,7 @@ def Hs(self):
 
         return Hs
 
-    def Cm(self):
+    def Cm(self) -> NDArray[float64]:
         """
         Returns the stress-strain matrix for an isotropic or orthotropic plane stress element
         """
@@ -504,7 +515,7 @@ def Cm(self):
         
         return Cm
 
-    def k_b(self):
+    def k_b(self) -> NDArray[float64]:
         '''
         Returns the local stiffness matrix for bending stresses
         '''
@@ -614,7 +625,7 @@ def k_b(self):
 
         return k_exp
 
-    def k_m(self):
+    def k_m(self) -> NDArray[float64]:
         '''
         Returns the local stiffness matrix for membrane (in-plane) stresses.
 
@@ -672,7 +683,7 @@ def k_m(self):
         
         return k_exp
 
-    def k(self):
+    def k(self) -> NDArray[float64]:
         '''
         Returns the quad element's local stiffness matrix.
         '''
@@ -683,7 +694,7 @@ def k(self):
         # Sum the bending and membrane stiffness matrices
         return np.add(self.k_b(), self.k_m())
    
-    def f(self, combo_name='Combo 1'):
+    def f(self, combo_name:str='Combo 1') -> NDArray[float64]:
         """
         Returns the quad element's local end force vector
         """
@@ -691,7 +702,7 @@ def f(self, combo_name='Combo 1'):
         # Calculate and return the plate's local end force vector
         return np.add(self.k() @ self.d(combo_name), self.fer(combo_name))
 
-    def fer(self, combo_name='Combo 1'):
+    def fer(self, combo_name:str='Combo 1') -> NDArray[float64]:
         """
         Returns the quadrilateral's local fixed end reaction vector.
 
@@ -757,7 +768,7 @@ def fer(self, combo_name='Combo 1'):
 
         return fer_exp
 
-    def d(self, combo_name='Combo 1'):
+    def d(self, combo_name='Combo 1') -> NDArray[float64]:
        """
        Returns the quad element's local displacement vector
        """
@@ -765,7 +776,7 @@ def d(self, combo_name='Combo 1'):
        # Calculate and return the local displacement vector
        return self.T() @ self.D(combo_name)
 
-    def F(self, combo_name='Combo 1'):
+    def F(self, combo_name:str='Combo 1') -> NDArray[float64]:
         """
         Returns the quad element's global force vector
 
@@ -778,7 +789,7 @@ def F(self, combo_name='Combo 1'):
         # Calculate and return the global force vector
         return inv(self.T()) @ self.f(combo_name)
 
-    def D(self, combo_name='Combo 1'):
+    def D(self, combo_name:str='Combo 1') -> NDArray[float64]:
         '''
         Returns the quad element's global displacement vector for the given
         load combination.
@@ -825,7 +836,7 @@ def D(self, combo_name='Combo 1'):
         # Return the global displacement vector
         return D
 
-    def K(self):
+    def K(self) -> NDArray[float64]:
         '''
         Returns the quad element's global stiffness matrix
         '''
@@ -837,7 +848,7 @@ def K(self):
         return inv(T) @ self.k() @ T
  
     # Global fixed end reaction vector
-    def FER(self, combo_name='Combo 1'):
+    def FER(self, combo_name:str='Combo 1') -> NDArray[float64]:
         '''
         Returns the global fixed end reaction vector.
 
@@ -851,7 +862,7 @@ def FER(self, combo_name='Combo 1'):
         # Calculate and return the fixed end reaction vector
         return inv(self.T()) @ self.fer(combo_name)
   
-    def T(self):
+    def T(self) -> NDArray[float64]:
         """
         Returns the coordinate transformation matrix for the quad element.
         """
@@ -984,7 +995,7 @@ def T(self):
     #     # Return the transformation matrix.
     #     return T
     
-    def shear(self, xi=0, eta=0, local=True, combo_name='Combo 1'):
+    def shear(self, xi:float=0.0, eta:float=0.0, local:bool=True, combo_name:str='Combo 1') -> NDArray[float64]:
         """
         Returns the interal shears at any point in the quad element.
 
@@ -993,9 +1004,9 @@ def shear(self, xi=0, eta=0, local=True, combo_name='Combo 1'):
 
         Parameters
         ----------
-        xi : number
+        xi : float
             The xi-coordinate. Default is 0.
-        eta : number
+        eta : float
             The eta-coordinate. Default is 0.
         
         Returns
@@ -1049,7 +1060,7 @@ def shear(self, xi=0, eta=0, local=True, combo_name='Combo 1'):
                                                   Qy,
                                                   [0]]))
    
-    def moment(self, xi=0, eta=0, local=True, combo_name='Combo 1'):
+    def moment(self, xi:float=0.0, eta:float=0.0, local:bool=True, combo_name:str='Combo 1') -> NDArray[float64]:
         """
         Returns the interal moments at any point in the quad element.
 
@@ -1058,9 +1069,9 @@ def moment(self, xi=0, eta=0, local=True, combo_name='Combo 1'):
 
         Parameters
         ----------
-        xi : number
+        xi : float
             The xi-coordinate. Default is 0.
-        eta : number
+        eta : float
             The eta-coordinate. Default is 0.
         
         Returns
@@ -1121,7 +1132,7 @@ def moment(self, xi=0, eta=0, local=True, combo_name='Combo 1'):
 
             return M_global
 
-    def membrane(self, xi=0, eta=0, local=True, combo_name='Combo 1'):
+    def membrane(self, xi:float=0, eta: float=0, local:bool=True, combo_name:str='Combo 1') -> NDArray[float64]:
         
         # Get the plate's local displacement vector. Slice out terms not related to membrane stresses.
         d = self.d(combo_name)[[0, 1, 6, 7, 12, 13, 18, 19], :]
diff --git a/Pynite/Rendering.py b/Pynite/Rendering.py
index 6bb43fab..e0a1f4ff 100644
--- a/Pynite/Rendering.py
+++ b/Pynite/Rendering.py
@@ -1,12 +1,21 @@
-
+from __future__ import annotations # Allows more recent type hints features
 from json import load
 import warnings
+from typing import TYPE_CHECKING
 
 from IPython.display import Image
 import numpy as np
 import pyvista as pv
 import math
 
+# For type checking only - these imports are only used during type checking
+if TYPE_CHECKING:
+    from typing import List, Union, Tuple, Optional
+    from Pynite.Node3D import Node3D
+    from Pynite.Member3D import Member3D
+    from Pynite.Spring3D import Spring3D
+    from Pynite.FEModel3D import FEModel3D
+
 # Allow for 3D interaction within jupyter notebook using trame
 try:
     pv.global_theme.trame.jupyter_extension_enabled = True
@@ -19,27 +28,27 @@ class Renderer:
     """Used to render finite element models.
     """
 
-    scalar = None
+    scalar: Optional[str] = None
 
-    def __init__(self, model):
+    def __init__(self, model: FEModel3D) -> None:
 
-        self.model = model
+        self.model: FEModel3D = model
 
         # Default settings for rendering
-        self._annotation_size = 5
-        self._deformed_shape = False
-        self._deformed_scale = 30
-        self._render_nodes = True
-        self._render_loads = True
-        self._color_map = None
-        self._combo_name = 'Combo 1'
-        self._case = None
-        self._labels = True
-        self._scalar_bar = False
-        self._scalar_bar_text_size = 24
-        self.theme = 'default'
-
-        self.plotter = pv.Plotter()
+        self._annotation_size: float = 5.0
+        self._deformed_shape: bool = False
+        self._deformed_scale: float = 30.0
+        self._render_nodes: bool = True
+        self._render_loads: bool = True
+        self._color_map: Optional[str] = None
+        self._combo_name: Optional[str] = 'Combo 1'
+        self._case: Optional[str] = None
+        self._labels: bool = True
+        self._scalar_bar: bool = False
+        self._scalar_bar_text_size: int = 24
+        self.theme: str = 'default'
+
+        self.plotter: pv.Plotter = pv.Plotter()
         self.plotter.set_background('white')  # Setting background color
         # self.plotter.add_logo_widget('./Resources/Full Logo No Buffer - Transparent.png')
         # self.plotter.view_isometric()
@@ -47,130 +56,127 @@ def __init__(self, model):
         self.plotter.show_axes()
 
         # Initialize load labels
-        self._load_label_points = []
-        self._load_labels = []
+        self._load_label_points: List[List[float]] = []
+        self._load_labels: List[Union[str, float, int]] = []
 
         # Initialize spring labels
-        self._spring_label_points = []
-        self._spring_labels = []
+        self._spring_label_points: List[List[float]] = []
+        self._spring_labels: List[str] = []
 
     @property
-    def window_width(self):
+    def window_width(self) -> int:
         return self.plotter.window_size[0]
 
     @window_width.setter
-    def window_width(self, width):
+    def window_width(self, width: int) -> None:
         height = self.plotter.window_size[1]
         self.plotter.window_size = (width, height)
 
     @property
-    def window_height(self):
+    def window_height(self) -> int:
         return self.plotter.window_size[1]
 
     @window_height.setter
-    def window_height(self, height):
+    def window_height(self, height: int) -> None:
         width = self.plotter.window_size[0]
         self.plotter.window_size = (width, height)
 
     @property
-    def annotation_size(self):
+    def annotation_size(self) -> float:
         return self._annotation_size
     
     @annotation_size.setter
-    def annotation_size(self, size):
+    def annotation_size(self, size: float) -> None:
         self._annotation_size = size
     
     @property
-    def deformed_shape(self):
+    def deformed_shape(self) -> bool:
         return self._deformed_shape
     
     @deformed_shape.setter
-    def deformed_shape(self, deformed_shape):
+    def deformed_shape(self, deformed_shape: bool) -> None:
         self._deformed_shape = deformed_shape
     
     @property
-    def deformed_scale(self):
+    def deformed_scale(self) -> float:
         return self._deformed_scale
     
     @deformed_scale.setter
-    def deformed_scale(self, scale):
+    def deformed_scale(self, scale: float) -> None:
         self._deformed_scale = scale
     
     @property
-    def render_nodes(self):
+    def render_nodes(self) -> bool:
         return self._render_nodes
     
     @render_nodes.setter
-    def render_nodes(self, render_nodes):
+    def render_nodes(self, render_nodes: bool) -> None:
         self._render_nodes = render_nodes
 
     @property
-    def render_loads(self):
+    def render_loads(self) -> bool:
         return self._render_loads
     
     @render_loads.setter
-    def render_loads(self, render_loads):
+    def render_loads(self, render_loads: bool) -> None:
         self._render_loads = render_loads
     
     @property
-    def color_map(self):
+    def color_map(self) -> Optional[str]:
         return self._color_map
     
     @color_map.setter
-    def color_map(self, color_map):
+    def color_map(self, color_map: Optional[str]) -> None:
         self._color_map = color_map
     
     @property
-    def combo_name(self):
+    def combo_name(self) -> Optional[str]:
         return self._combo_name
     
     @combo_name.setter
-    def combo_name(self, combo_name):
+    def combo_name(self, combo_name: Optional[str]) -> None:
         self._combo_name = combo_name
         self._case = None
     
     @property
-    def case(self):
+    def case(self) -> Optional[str]:
         return self._case
     
     @case.setter
-    def case(self, case):
+    def case(self, case: Optional[str]) -> None:
         self._case = case
         self._combo_name = None
     
     @property
-    def show_labels(self):
+    def show_labels(self) -> bool:
         return self._labels
     
     @show_labels.setter
-    def show_labels(self, show_labels):
+    def show_labels(self, show_labels: bool) -> None:
         self._labels = show_labels
     
     @property
-    def scalar_bar(self):
+    def scalar_bar(self) -> bool:
         return self._scalar_bar
     
     @scalar_bar.setter
-    def scalar_bar(self, scalar_bar):
+    def scalar_bar(self, scalar_bar: bool) -> None:
         self._scalar_bar = scalar_bar
     
     @property
-    def scalar_bar_text_size(self):
+    def scalar_bar_text_size(self) -> int:
         return self._scalar_bar_text_size
     
     @scalar_bar_text_size.setter
-    def scalar_bar_text_size(self, text_size):
+    def scalar_bar_text_size(self, text_size: int) -> None:
         self._scalar_bar_text_size = text_size
 
-    def render_model(self, reset_camera=True):
+    def render_model(self, reset_camera: bool = True) -> None:
         """
         Renders the model in a window
 
         Parameters
         ----------
-        interact : bool
-            Suppresses interacting with the window if set to `False`. This can be used to capture a
-            screenshot without pausing the program for the user to interact. Default is `True`.
         reset_camera : bool
             Resets the camera if set to `True`. Default is `True`.
         """
@@ -181,7 +187,7 @@ def render_model(self, reset_camera=True):
         # Render the model (code execution will pause here until the user closes the window)
         self.plotter.show(title='Pynite - Simple Finite Element Analysis for Python', auto_close=False)
 
-    def screenshot(self, filepath='./Pynite_Image.png', interact=True, reset_camera=True):
+    def screenshot(self, filepath: str = './Pynite_Image.png', interact: bool = True, reset_camera: bool = True) -> None:
         """Saves a screenshot of the rendered model. Important: Press `q` to capture the screenshot after positioning the view. Pressing the `X` button in the corner of the window will ignore the positioning and shut down the entire renderer against further use once the screenshot is taken.
 
         :param filepath: The filepath to write the image to. When set to 'jupyter', the resulting plot is placed inline in a jupyter notebook. Defaults to 'jupyter'.
@@ -204,7 +210,7 @@ def screenshot(self, filepath='./Pynite_Image.png', interact=True, reset_camera=
         # Save the screenshot to the specified filepath. Note that `auto_close` shuts down the entire plotter after the screenshot is taken, rather than just closing the window. We'll set `auto_close=False` to allow the plotter to remain active. Note that the window must be closed by pressing `q`. Closing it with the 'X' button in the window's corner will close the whole plotter down.
         self.plotter.show(title='Pynite - Simple Finite Element Anlaysis for Python', screenshot=filepath, auto_close=False)
 
-    def update(self, reset_camera=True):
+    def update(self, reset_camera: bool = True) -> None:
         """
         Builds or rebuilds the pyvista plotter
 
@@ -307,7 +313,7 @@ def update(self, reset_camera=True):
         if reset_camera:
             self.plotter.reset_camera()
     
-    def plot_node(self, node, color='grey'):
+    def plot_node(self, node: Node3D, color: str = 'grey') -> None:
         """Adds a node to the plotter
 
         :param node: node
@@ -475,7 +481,7 @@ def plot_node(self, node, color='grey'):
                                               z_length=self.annotation_size*0.6),
                                       color=color)
 
-    def plot_member(self, member, theme='default'):
+    def plot_member(self, member: Member3D, theme: str = 'default') -> None:
         """
         Adds a member to the plotter. This method generates a line representing a structural member between two nodes, and adds it to the plotter with specified theme settings.
         
@@ -502,7 +508,7 @@ def plot_member(self, member, theme='default'):
 
         self.plotter.add_mesh(line, color='black', line_width=2)
 
-    def plot_spring(self, spring, color='grey', deformed=False):
+    def plot_spring(self, spring: Spring3D, color: str = 'grey', deformed: bool = False) -> None:
         """
         Adds a spring to the plotter. This method generates a zig-zag line representing a spring between two nodes, and adds it to the plotter with specified theme settings.
         """
@@ -585,7 +591,7 @@ def plot_spring(self, spring, color='grey', deformed=False):
         self._spring_label_points.append([(Xi + Xj) / 2, (Yi + Yj) / 2, (Zi + Zj) / 2])
 
             
-    def plot_plates(self, deformed_shape, deformed_scale, color_map, combo_name):
+    def plot_plates(self, deformed_shape: bool, deformed_scale: float, color_map: Optional[str], combo_name: Optional[str]) -> None:
         
         # Start a list of vertices
         plate_vertices = []
@@ -672,7 +678,7 @@ def plot_plates(self, deformed_shape, deformed_scale, color_map, combo_name):
         else:
             self.plotter.add_mesh(plate_polydata)
       
-    def plot_deformed_node(self, node, scale_factor, color='grey'):
+    def plot_deformed_node(self, node: Node3D, scale_factor: float, color: str = 'grey') -> None:
 
         # Calculate the node's deformed position
         newX = node.X + scale_factor * (node.DX[self.combo_name])
@@ -685,7 +691,7 @@ def plot_deformed_node(self, node, scale_factor, color='grey'):
         # Add the mesh to the plotter
         self.plotter.add_mesh(sphere, color=color)
   
-    def plot_deformed_member(self, member, scale_factor):
+    def plot_deformed_member(self, member: Member3D, scale_factor: float) -> None:
         
         # Determine if this member is active for each load combination
         if member.active:
@@ -740,7 +746,8 @@ def plot_deformed_member(self, member, scale_factor):
                 line = pv.Line(D_plot[i], D_plot[i+1])
                 self.plotter.add_mesh(line, color='red', line_width=2)
 
-    def plot_pt_load(self, position, direction, length, label_text=None, color='green'):
+    def plot_pt_load(self, position: Tuple[float, float, float], direction: Union[Tuple[float, float, float], np.ndarray], 
+                    length: float, label_text: Optional[Union[str, float, int]] = None, color: str = 'green') -> None:
 
         # Create a unit vector in the direction of the 'direction' vector
         unitVector = direction/np.linalg.norm(direction)
@@ -777,7 +784,10 @@ def plot_pt_load(self, position, direction, length, label_text=None, color='gree
         # Plot the shaft
         self.plotter.add_mesh(shaft, line_width=2, color=color)                         
 
-    def plot_dist_load(self, position1, position2, direction, length1, length2, label_text1, label_text2, color='green'):
+    def plot_dist_load(self, position1: Tuple[float, float, float], position2: Tuple[float, float, float], 
+                      direction: Union[np.ndarray, Tuple[float, float, float]], length1: float, length2: float,
+                      label_text1: Optional[Union[str, float, int]], label_text2: Optional[Union[str, float, int]], 
+                      color: str = 'green') -> None:
 
         # Calculate the length of the distributed load
         load_length = ((position2[0] - position1[0])**2 + (position2[1] - position1[1])**2 + (position2[2] - position1[2])**2)**0.5
@@ -827,7 +837,8 @@ def plot_dist_load(self, position1, position2, direction, length1, length2, labe
         # Combine all geometry into a single PolyData object
         self.plotter.add_mesh(tail_line, color=color)
 
-    def plot_moment(self, center, direction, radius, label_text=None, color='green'):
+    def plot_moment(self, center: Tuple[float, float, float], direction: Union[Tuple[float, float, float], np.ndarray], 
+                    radius: float, label_text: Optional[Union[str, float, int]] = None, color: str = 'green') -> None:
 
         # Convert the direction vector into a unit vector
         v1 = direction/np.linalg.norm(direction)
diff --git a/Pynite/Reporting.py b/Pynite/Reporting.py
index dd0dceb8..ff5b2c62 100644
--- a/Pynite/Reporting.py
+++ b/Pynite/Reporting.py
@@ -1,7 +1,12 @@
 # Import libraries necessary for report printing
+from __future__ import annotations # Allows more recent type hints features
+from typing import TYPE_CHECKING
+
 from jinja2 import Environment, PackageLoader
 import pdfkit
 
+if TYPE_CHECKING:
+    from Pynite import FEModel3D
 
 # Determine the filepath to the local Pynite installation
 from pathlib import Path
@@ -15,14 +20,14 @@
 # Get the report template
 template = env.get_template('Report_Template.html')
 
-def create_report(model, output_filepath=path/'./Pynite Report.pdf', **kwargs):
+def create_report(model: FEModel3D, output_filepath:Path=path/'./Pynite Report.pdf', **kwargs):
     """Creates a pdf report for a given finite element model.
 
     :param model: The model to generate the report for.
     :type model: ``FEModel3D``
     :param output_filepath: The filepath to send the report to. Defaults to 'Pynite Report.pdf' in your ``PYTHONPATH``
     :type output_filepath: ``str``, optional
-    :param \**kwargs: See below for a list of valid arguments.
+    :param \\**kwargs: See below for a list of valid arguments.
     :Keyword Arguments:
         * *node_table* (``bool``) -- Set to ``True`` if you want node data included in the report. Defaults to ``True``.
         * *member_table* (``bool``) -- Set to ``True`` if you want member data included in the report. Defaults to ``True``.
diff --git a/Pynite/Section.py b/Pynite/Section.py
index 8e0c5119..aa276a5b 100644
--- a/Pynite/Section.py
+++ b/Pynite/Section.py
@@ -1,5 +1,12 @@
+from __future__ import annotations # Allows more recent type hints features
 
 import numpy as np
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from numpy import float64
+    from numpy.typing import NDArray
+    from Pynite.FEModel3D import FEModel3D
 
 class Section():
     """
@@ -7,7 +14,7 @@ class Section():
 
     This class stores all properties related to the geometry of the member
     """
-    def __init__(self, model, name:str, A:float, Iy:float, Iz:float, J:float) -> None:
+    def __init__(self, model: 'FEModel3D', name: str, A: float, Iy: float, Iz: float, J: float) -> None:
         """
         :param model: The finite element model to which this section belongs
         :type model: FEModel3D
@@ -22,25 +29,43 @@ def __init__(self, model, name:str, A:float, Iy:float, Iz:float, J:float) -> Non
         :param J: The torsion constant of the section
         :type J: float
         """        
-        self.model = model
-        self.name = name
-        self.A = A
-        self.Iy = Iy
-        self.Iz = Iz
-        self.J = J
+        self.model: 'FEModel3D' = model
+        self.name: str = name
+        self.A: float = A
+        self.Iy: float = Iy
+        self.Iz: float = Iz
+        self.J: float = J
     
-    def Phi(self):
-        pass
+    def Phi(self, fx: float = 0, my: float = 0, mz: float = 0):
+        """
+        Method to be overridden by subclasses for determining whether the cross section is
+        elastic or plastic.
+        
+        :param fx: Axial force
+        :type fx: float
+        :param my: y-axis (weak) moment
+        :type my: float
+        :param mz: z-axis (strong) moment
+        :type mz: float
+        :return: The stress ratio
+        :rtype: float
+        """
+        raise NotImplementedError("Phi method must be implemented in subclasses.")
 
-    def G(self, fx, my, mz):
+    def G(self, fx: float, my: float, mz: float) -> NDArray:
         """
-<<<<<<< HEAD
-        Returns the gradient to the yield surface at a given point using numerical differentiation. This is a default solution. For a better solution, overwrite this method with a more precise one in the material/shape specific child class that inherits from this class.
-=======
-        Returns the gradient to the yield surface at a given point using numerical differentiation.
-        This is a default solution. For a better solution, overwrite this method with a more precies
+        Returns the gradient to the yield surface at a given point using numerical differentiation. 
+        This is a default solution. For a better solution, overwrite this method with a more precise 
         one in the material/shape specific child class that inherits from this class.
->>>>>>> e49bff2e7890fa6b4069c6c9e2b013a2a90fe7e1
+        
+        :param fx: Axial force at the cross-section
+        :type fx: float
+        :param my: y-axis (weak) moment at the cross-section
+        :type my: float
+        :param mz: z-axis (strong) moment at the cross-section
+        :type mz: float
+        :return: The gradient to the yield surface at the cross-section
+        :rtype: NDArray
         """
         
         # Small increment for numerical differentiation
@@ -61,20 +86,43 @@ def G(self, fx, my, mz):
 
 class SteelSection(Section):
 
-    def __init__(self, model, name, A, Iy, Iz, J, Zy, Zz, material_name):
+    def __init__(self, model: 'FEModel3D', name: str, A: float, Iy: float, Iz: float, J: float, 
+                 Zy: float, Zz: float, material_name: str) -> None:
+        """
+        Initialize a steel section
+
+        :param model: The finite element model to which this section belongs
+        :type model: FEModel3D
+        :param name: Name of the section
+        :type name: str
+        :param A: Cross-sectional area of the section
+        :type A: float
+        :param Iy: The second moment of area the section about the Y (minor) axis
+        :type Iy: float
+        :param Iz: The second moment of area the section about the Z (major) axis
+        :type Iz: float
+        :param J: The torsion constant of the section
+        :type J: float
+        :param Zy: Plastic section modulus about the Y (minor) axis
+        :type Zy: float
+        :param Zz: Plastic section modulus about the Z (major) axis
+        :type Zz: float
+        :param material_name: Name of the material used for this section
+        :type material_name: str
+        """
 
         # Basic section properties
-        super().__init__(model, name, A, Iy, Iz, J, material_name)
+        super().__init__(model, name, A, Iy, Iz, J)
 
         # Additional section properties for steel
-        self.ry = (Iy/A)**0.5
-        self.rz = (Iz/A)**0.5
-        self.Zy = Zy
-        self.Zz = Zz
+        self.ry: float = (Iy/A)**0.5
+        self.rz: float = (Iz/A)**0.5
+        self.Zy: float = Zy
+        self.Zz: float = Zz
 
         self.material = model.materials[material_name]
     
-    def Phi(self, fx, my, mz):
+    def Phi(self, fx: float = 0, my: float = 0, mz: float = 0) -> float:
         """
         A method used to determine whether the cross section is elastic or plastic. 
         Values less than 1 indicate the section is elastic.
@@ -102,7 +150,7 @@ def Phi(self, fx, my, mz):
         # "Matrix Structural Analysis, 2nd Edition", Equation 10.18
         return p**2 + m_z**2 + m_y**4 + 3.5*p**2*m_z**2 + 3*p**6*m_y**2 + 4.5*m_z**4*m_y**2
     
-    def G(self, fx, my, mz):
+    def G(self, fx: float, my: float, mz: float) -> NDArray[float64]:
         """Returns the gradient to the material's yield surface for the given load. Used to construct the plastic reduction matrix for nonlinear behavior.
 
         :param fx: Axial force at the cross-section.
@@ -112,7 +160,7 @@ def G(self, fx, my, mz):
         :param mz: z-axis (strong) moment at the cross-section.
         :type mz: float
         :return: The gradient to the material's yield surface at the cross-section.
-        :rtype: array
+        :rtype: NDArray
         """
         
         # Plastic strengths for material nonlinearity
diff --git a/Pynite/ShearWall.py b/Pynite/ShearWall.py
index 9b38a3cc..d2028367 100644
--- a/Pynite/ShearWall.py
+++ b/Pynite/ShearWall.py
@@ -1,6 +1,8 @@
+from __future__ import annotations # Allows more recent type hints features
 from math import isclose
 from numpy import average
 import io
+from typing import TYPE_CHECKING
 
 from Pynite.FEModel3D import FEModel3D
 from prettytable import PrettyTable
@@ -8,83 +10,88 @@
 import matplotlib.pyplot as plt
 from matplotlib.patches import Rectangle
 
+if TYPE_CHECKING:
+    from typing import List, Dict, Tuple
+    from Pynite.Quad3D import Quad3D
+    import matplotlib.figure
+
 class ShearWall():
     """Creates a new shear wall model that allows for modeling of complex shear walls. Shear wall models are 2D (aside from flanges) standalone models. You can add openings and flanges (wall returns). Diaphragm levels can be defined in order to apply shear forces along the length of the wall. Diaphgrams can be full or partial length. Supports can be applied at any level in the shear wall. Supports can also be full or partial length. A `ky_mod` factor is built in to account for cracking. Shear walls can automatically detect shear wall piers and coupling beams, and sum internal forces in those components.
     """
 
-    def __init__(self):
-
-        self.model = FEModel3D()
-        self._L = None
-        self._H = None
-        self._t = None
-        self._ky_mod = 0.35
-        self._mesh_size = 1
-        self._openings = []
-        self._flanges = []
-        self._supports = []
-        self._stories = []
-        self._shears = []
-        self._axials = []
-        self._materials = []
-        self.piers = {}
-        self.coupling_beams = {}
+    def __init__(self) -> None:
+
+        self.model: FEModel3D = FEModel3D()
+        self._L: float | None = None
+        self._H: float | None = None
+        self._t: float | None = None
+        self._ky_mod: float = 0.35
+        self._mesh_size: float = 1
+        self._openings: List[List[str | float | None]] = []
+        self._flanges: List[List[str | float]] = []
+        self._supports: List[List[float]] = []
+        self._stories: List[List[str | float]] = []
+        self._shears: List[List[str | float]] = []
+        self._axials: List[List[str | float]] = []
+        self._materials: List[List[str | float]] = []
+        self.piers: Dict[str, Pier] = {}
+        self.coupling_beams: Dict[str, CouplingBeam] = {}
     
     @property
-    def L(self):
+    def L(self) -> float | None:
         return self._L
 
     @L.setter
-    def L(self, value):
+    def L(self, value: float) -> None:
         self._L = value
     
     @property
-    def H(self):
+    def H(self) -> float | None:
         return self._H
 
     @H.setter
-    def H(self, value):
+    def H(self, value: float) -> None:
         self._H = value
 
     @property
-    def mesh_size(self):
+    def mesh_size(self) -> float:
         return self._mesh_size
     
     @mesh_size.setter
-    def mesh_size(self, value):
+    def mesh_size(self, value: float) -> None:
         self._mesh_size = value
     
     @property
-    def ky_mod(self):
+    def ky_mod(self) -> float:
         return self._ky_mod
     
     @ky_mod.setter
-    def ky_mod(self, value):
+    def ky_mod(self, value: float) -> None:
         self._ky_mod = value
     
-    def add_load_combo(self, name, factors, combo_type='strength'):
+    def add_load_combo(self, name: str, factors: Dict[str, float], combo_type: str = 'strength') -> None:
         self.model.add_load_combo(name, factors, combo_type)
 
-    def add_material(self, name, E, G, nu, rho, t, x_start=None, x_end=None, y_start=None, y_end=None):
+    def add_material(self, name: str, E: float, G: float, nu: float, rho: float, t: float, x_start: float | None = None, x_end: float | None = None, y_start: float | None = None, y_end: float | None = None) -> None:
         if x_start is None: x_start = 0
         if x_end is None: x_end = self._L
         if y_start is None: y_start = 0
         if y_end is None: y_end = self._H
         self._materials.append([name, E, G, nu, rho, t, x_start, x_end, y_start, y_end])
 
-    def add_opening(self, name, x_start, y_start, width, height, tie=None):
+    def add_opening(self, name: str, x_start: float, y_start: float, width: float, height: float, tie: float | None = None) -> None:
         self._openings.append([name, x_start, y_start, width, height, None])
     
-    def add_flange(self, thickness, width, x, y_start, y_end, material, side):
+    def add_flange(self, thickness: float, width: float, x: float, y_start: float, y_end: float, material: str, side: str) -> None:
         self._flanges.append([thickness, width, x, y_start, y_end, material, side])
     
-    def add_support(self, elevation=None, x_start=None, x_end=None):
+    def add_support(self, elevation: float | None = None, x_start: float | None = None, x_end: float | None = None) -> None:
         if elevation is None: elevation = 0
         if x_start is None: x_start = 0
         if x_end is None: x_end = self._L
         self._supports.append([elevation, x_start, x_end])
         
-    def add_story(self, story_name, elevation, x_start=None, x_end=None):
+    def add_story(self, story_name: str, elevation: float, x_start: float | None = None, x_end: float | None = None) -> None:
 
         # Validate input
         if elevation is None: elevation = self._H
@@ -100,13 +107,13 @@ def add_story(self, story_name, elevation, x_start=None, x_end=None):
         # Add a 100 kip story shear to the model to use when calculating the story's stiffness
         self.add_shear(story_name, 100, case=story_name)
 
-    def add_shear(self, story_name, force, case='Case 1'):
+    def add_shear(self, story_name: str, force: float, case: str = 'Case 1') -> None:
         self._shears.append([story_name, force, case])
     
-    def add_axial(self, story_name, force, case='Case 1'):
+    def add_axial(self, story_name: str, force: float, case: str = 'Case 1') -> None:
         self._axials.append([story_name, force, case])
 
-    def generate(self):
+    def generate(self) -> None:
 
         # Add materials to the model
         for material in self._materials:
@@ -114,8 +121,8 @@ def generate(self):
             self.model.add_material(name, E, G, nu, rho)
         
         # Identify mesh control points
-        x_control = [0, self._L]
-        y_control = [0, self._H]
+        x_control: List[float] = [0, self._L]
+        y_control: List[float] = [0, self._H]
 
         for material in self._materials:
             x_control.append(material[6])
@@ -123,7 +130,7 @@ def generate(self):
             y_control.append(material[8])
             y_control.append(material[9])
 
-        z_control = [0]
+        z_control: List[float] = [0]
         for flg in self._flanges:
             if flg[6] == 'NS': z_control.append(flg[1])
             else: z_control.append(-flg[1])
@@ -275,14 +282,14 @@ def generate(self):
         self._identify_coupling_beams()
 
 
-    def _identify_piers(self):
+    def _identify_piers(self) -> None:
         
         # Reset all piers in the wall
         self.piers = {}
 
         # Create a list of x and y coordinates that represent the edges of the wall
-        x_vals = [0, self._L]
-        y_vals = [0, self._H]
+        x_vals: List[float] = [0, self._L]
+        y_vals: List[float] = [0, self._H]
 
         # Add the edges of the openings to the lists
         for opng in self._openings:
@@ -296,7 +303,7 @@ def _identify_piers(self):
         y_vals = sorted(y_vals)
 
         # Remove duplicate (or near duplicate) values
-        unique_list = []
+        unique_list: List[float] = []
         for i in range(len(x_vals) - 1):
             # Only keep the value at `i` if it's not a duplicate or near duplicate of the next value
             if not isclose(x_vals[i], x_vals[i+1]):
@@ -322,7 +329,7 @@ def _identify_piers(self):
             self.piers['P' + str(i+1)] = Pier('P' + str(i+1), x, y, width, height)
 
         # Divide the strip piers further into rectanglular piers using the top and bottom of each opening as pier boundaries
-        new_piers = {}
+        new_piers: Dict[str, Pier] = {}
         pier_count = 1
         for pier in self.piers.values():
             for i in range(len(y_vals) - 1):
@@ -335,7 +342,7 @@ def _identify_piers(self):
         self.piers = new_piers
 
         # Delete any piers that fall within an opening
-        delete_list = []
+        delete_list: List[str] = []
         for pier in self.piers.values():
            # Check if this pier is inside any of the openings
            for opng in self._openings:
@@ -411,7 +418,7 @@ def _identify_piers(self):
                     break
 
         # Generate a list of new keys in ascending order
-        new_keys = [f'P{i+1}' for i in range(len(self.piers))]
+        new_keys: List[str] = [f'P{i+1}' for i in range(len(self.piers))]
 
         # Replace the old dicionary with one that has updated keys
         self.piers = dict(zip(new_keys, self.piers.values()))
@@ -429,14 +436,14 @@ def _identify_piers(self):
                     and round(Y_avg, 10) <= round (pier.y + pier.height, 10)):
                     pier.plates.append(plate)
     
-    def _identify_coupling_beams(self):
+    def _identify_coupling_beams(self) -> None:
         
         # Reset all coupling beams in the wall
         self.coupling_beams = {}
 
         # Create a list of x and y coordinates that represent the edges of the wall
-        x_vals = [0, self._L]
-        y_vals = [0, self._H]
+        x_vals: List[float] = [0, self._L]
+        y_vals: List[float] = [0, self._H]
 
         # Add the edges of the openings to the lists
         for opng in self._openings:
@@ -450,7 +457,7 @@ def _identify_coupling_beams(self):
         y_vals = sorted(y_vals)
 
         # Remove duplicate (or near duplicate) values
-        unique_list = []
+        unique_list: List[float] = []
         for i in range(len(x_vals) - 1):
             # Only keep the value at `i` if it's not a duplicate or near duplicate of the next value
             if not isclose(x_vals[i], x_vals[i+1]):
@@ -476,7 +483,7 @@ def _identify_coupling_beams(self):
             self.coupling_beams['B' + str(i+1)] = CouplingBeam('B' + str(i+1), x, y, length, height)
 
         # Divide the strips further into rectanglular beams using the left and right of each opening as beam boundaries
-        new_beams = {}
+        new_beams: Dict[str, CouplingBeam] = {}
         beam_count = 1
         for beam in self.coupling_beams.values():
             for i in range(len(x_vals) - 1):
@@ -489,7 +496,7 @@ def _identify_coupling_beams(self):
         self.coupling_beams = new_beams
 
         # Delete any beams that fall within an opening
-        delete_list = []
+        delete_list: List[str] = []
         for beam in self.coupling_beams.values():
            
            # Check if this beam is inside any of the openings
@@ -576,7 +583,7 @@ def _identify_coupling_beams(self):
             del self.coupling_beams[beam]
 
         # Generate a list of new keys in ascending order
-        new_keys = [f'B{i + 1}' for i in range(len(self.coupling_beams))]
+        new_keys: List[str] = [f'B{i + 1}' for i in range(len(self.coupling_beams))]
 
         # Replace the old dicionary with one that has updated keys
         self.coupling_beams = dict(zip(new_keys, self.coupling_beams.values()))
@@ -594,7 +601,7 @@ def _identify_coupling_beams(self):
                     and round(Y_avg, 10) <= round (beam.y + beam.height, 10)):
                     beam.plates.append(plate)
 
-    def draw_piers(self, show=False):
+    def draw_piers(self, show: bool = False) -> None | matplotlib.figure.Figure:
         
         fig, ax = plt.subplots()
 
@@ -613,7 +620,7 @@ def draw_piers(self, show=False):
         if show == True: plt.show()
         else: return plt
     
-    def draw_coupling_beams(self, show=False):
+    def draw_coupling_beams(self, show: bool = False) -> None | matplotlib.figure.Figure:
         
         fig, ax = plt.subplots()
 
@@ -639,7 +646,7 @@ def draw_coupling_beams(self, show=False):
         if show == True: plt.show()
         else: return plt
 
-    def _add_rectangle(self, ax, x, y, w, h, name, color='white'):
+    def _add_rectangle(self, ax: matplotlib.axes.Axes, x: float, y: float, w: float, h: float, name: str, color: str = 'white') -> None:
         """Adds a rectangle to the pyplot
         """
 
@@ -654,7 +661,7 @@ def _add_rectangle(self, ax, x, y, w, h, name, color='white'):
         ax.set_xlim(0, max(ax.get_xlim()[1], x + w))
         ax.set_ylim(0, max(ax.get_ylim()[1], y + h))
     
-    def _sort_openings(self):
+    def _sort_openings(self) -> None:
 
         # Sort the openings based on y-coordinates
         n = len(self._openings)
@@ -670,7 +677,7 @@ def _sort_openings(self):
                 if self._openings[j][1] > self._openings[j+1][1]:
                     self._openings[j], self._openings[j+1] = self._openings[j+1], self._openings[j]
 
-    def stiffness(self, story_name):
+    def stiffness(self, story_name: str) -> float:
 
         # TODO: Validate that the specified story exists in the shear wall
 
@@ -701,7 +708,7 @@ def stiffness(self, story_name):
         # Return the story's stiffness:
         return V/(d_max*12)
 
-    def render(self, color_map='Txy', combo_name='Combo 1'):
+    def render(self, color_map: str = 'Txy', combo_name: str = 'Combo 1') -> None:
         
         from Pynite.Visualization import Renderer
         renderer = Renderer(self.model)
@@ -715,7 +722,7 @@ def render(self, color_map='Txy', combo_name='Combo 1'):
         renderer.labels = False
         renderer.render_model()
     
-    def screenshots(self, combo_name='Combo 1', dir_path='./'):
+    def screenshots(self, combo_name: str = 'Combo 1', dir_path: str = './') -> None:
 
         from Pynite.Rendering import Renderer
         
@@ -742,7 +749,7 @@ def screenshots(self, combo_name='Combo 1', dir_path='./'):
         pier_sketch = self.draw_piers(show=False)
         pier_sketch.savefig(dir_path + '/shear_wall_piers.png', format='png')
 
-    def print_piers(self, combo_name='Combo 1'):
+    def print_piers(self, combo_name: str = 'Combo 1') -> None:
         """Tabulates and prints pier results for the shear wall
         """
 
@@ -763,7 +770,7 @@ def print_piers(self, combo_name='Combo 1'):
         print('+-------------------+')
         print(table)
     
-    def print_coupling_beams(self, combo_name='Combo 1'):
+    def print_coupling_beams(self, combo_name: str = 'Combo 1') -> None:
         """Tabulates and prints coupling beam results for the shear wall
         """
 
@@ -787,15 +794,15 @@ def print_coupling_beams(self, combo_name='Combo 1'):
 #%%
 class Pier():
     
-    def __init__(self, name, x, y, width, height):
-        self.name = name
-        self.x = x  # The location of the left side of the pier
-        self.y = y  # The height of the bottom of the pier
-        self.width = width
-        self.height = height
-        self.plates = []
+    def __init__(self, name: str, x: float, y: float, width: float, height: float) -> None:
+        self.name: str = name
+        self.x: float = x  # The location of the left side of the pier
+        self.y: float = y  # The height of the bottom of the pier
+        self.width: float = width
+        self.height: float = height
+        self.plates: List[Quad3D] = []
     
-    def sum_forces(self, combo_name='Combo 1'):
+    def sum_forces(self, combo_name: str = 'Combo 1') -> Tuple[float, float, float, float]:
 
         # Initialize the forces in the plate
         P, M, V = 0, 0, 0
@@ -837,15 +844,15 @@ def sum_forces(self, combo_name='Combo 1'):
 #%%
 class CouplingBeam():
     
-    def __init__(self, name, x, y, length, height):
-        self.name = name
-        self.x = x  # The location of the left side of the coupling beam
-        self.y = y  # The height to the bottom of the coupling beam
-        self.length = length
-        self.height = height
-        self.plates = []
+    def __init__(self, name: str, x: float, y: float, length: float, height: float) -> None:
+        self.name: str = name
+        self.x: float = x  # The location of the left side of the coupling beam
+        self.y: float = y  # The height to the bottom of the coupling beam
+        self.length: float = length
+        self.height: float = height
+        self.plates: List[Quad3D] = []
     
-    def sum_forces(self, combo_name='Combo 1'):
+    def sum_forces(self, combo_name: str = 'Combo 1') -> Tuple[float, float, float, float]:
 
         # Initialize plate forces to zero
         P, M, V = 0, 0, 0
diff --git a/Pynite/Spring3D.py b/Pynite/Spring3D.py
index a33b4f7b..01312579 100644
--- a/Pynite/Spring3D.py
+++ b/Pynite/Spring3D.py
@@ -1,9 +1,17 @@
 # %%
+from __future__ import annotations # Allows more recent type hints features
 from numpy import zeros, array, add, subtract, matmul, insert, cross, divide
 from numpy.linalg import inv
 from math import isclose
 import Pynite.FixedEndReactions
 from Pynite.LoadCombo import LoadCombo
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from typing import Dict, Optional
+    from numpy import float64
+    from numpy.typing import NDArray
+    from Pynite.Node3D import Node3D
 
 # %%
 class Spring3D():
@@ -14,27 +22,28 @@ class Spring3D():
     __plt = None
 
 #%%
-    def __init__(self, name, i_node, j_node, ks, LoadCombos={'Combo 1':LoadCombo('Combo 1', factors={'Case 1':1.0})},
-                 tension_only=False, comp_only=False):
+    def __init__(self, name: str, i_node: Node3D, j_node: Node3D, ks: float, 
+                 LoadCombos: Dict[str, LoadCombo] = {'Combo 1': LoadCombo('Combo 1', factors={'Case 1': 1.0})},
+                 tension_only: bool = False, comp_only: bool = False) -> None:
         '''
         Initializes a new spring.
         '''
-        self.name = name      # A unique name for the spring given by the user
-        self.ID = None        # Unique index number for the spring assigned by the program
-        self.i_node = i_node  # The spring's i-node
-        self.j_node = j_node  # The spring's j-node
-        self.ks = ks          # The spring constant (force/displacement)
-        self.load_combos = LoadCombos # The dictionary of load combinations in the model this spring belongs to
-        self.tension_only = tension_only # Indicates whether the spring is tension-only
-        self.comp_only = comp_only # Indicates whether the spring is compression-only
+        self.name: str = name      # A unique name for the spring given by the user
+        self.ID: Optional[int] = None        # Unique index number for the spring assigned by the program
+        self.i_node: Node3D = i_node  # The spring's i-node
+        self.j_node: Node3D = j_node  # The spring's j-node
+        self.ks: float = ks          # The spring constant (force/displacement)
+        self.load_combos: Dict[str, LoadCombo] = LoadCombos # The dictionary of load combinations in the model this spring belongs to
+        self.tension_only: bool = tension_only # Indicates whether the spring is tension-only
+        self.comp_only: bool = comp_only # Indicates whether the spring is compression-only
 
         # Springs need to track whether they are active or not for any given load combination.
         # They may become inactive for a load combination during a tension/compression-only
         # analysis. This dictionary will be used when the model is solved.
-        self.active = {} # Key = load combo name, Value = True or False
+        self.active: Dict[str, bool] = {} # Key = load combo name, Value = True or False
 
 #%%
-    def L(self):
+    def L(self) -> float:
         '''
         Returns the length of the spring.
         '''
@@ -43,7 +52,7 @@ def L(self):
         return self.i_node.distance(self.j_node)
 
 #%%
-    def k(self):
+    def k(self) -> NDArray[float64]:
         '''
         Returns the local stiffness matrix for the spring.
         '''
@@ -70,7 +79,7 @@ def k(self):
         return k
 
 #%%   
-    def f(self, combo_name='Combo 1'):
+    def f(self, combo_name: str = 'Combo 1') -> NDArray[float64]:
         '''
         Returns the spring's local end force vector for the given load combination.
 
@@ -84,7 +93,7 @@ def f(self, combo_name='Combo 1'):
         return matmul(self.k(), self.d(combo_name))
 
 #%%
-    def d(self, combo_name='Combo 1'):
+    def d(self, combo_name: str = 'Combo 1') -> NDArray[float64]:
         '''
         Returns the spring's local displacement vector.
 
@@ -98,7 +107,7 @@ def d(self, combo_name='Combo 1'):
         return matmul(self.T(), self.D(combo_name))
         
 #%%
-    def T(self):
+    def T(self) -> NDArray[float64]:
         '''
         Returns the transformation matrix for the spring.
         '''
@@ -175,7 +184,7 @@ def T(self):
         return transMatrix
 
 #%%
-    def K(self):
+    def K(self) -> NDArray[float64]:
         '''
         Spring global stiffness matrix
         '''
@@ -184,7 +193,7 @@ def K(self):
         return matmul(matmul(inv(self.T()), self.k()), self.T())
 
 #%%
-    def F(self, combo_name='Combo 1'):
+    def F(self, combo_name: str = 'Combo 1') -> NDArray[float64]:
         '''
         Returns the spring's global end force vector for the given load combination.
         '''
@@ -193,7 +202,7 @@ def F(self, combo_name='Combo 1'):
         return matmul(inv(self.T()), self.f(combo_name))
 
 #%%
-    def D(self, combo_name='Combo 1'):
+    def D(self, combo_name: str = 'Combo 1') -> NDArray[float64]:
         '''
         Returns the spring's global displacement vector.
 
@@ -229,7 +238,7 @@ def D(self, combo_name='Combo 1'):
         return D
 
 #%%
-    def axial(self, combo_name='Combo 1'):
+    def axial(self, combo_name: str = 'Combo 1') -> float:
         '''
         Returns the axial force in the spring.
         
diff --git a/Pynite/Tri3D.py b/Pynite/Tri3D.py
index c5434226..0b3f3222 100644
--- a/Pynite/Tri3D.py
+++ b/Pynite/Tri3D.py
@@ -1,11 +1,20 @@
-from numpy import zeros, array, matmul, cross, add
+from __future__ import annotations # Allows more recent type hints features
+from typing import List, TYPE_CHECKING
+
+from numpy import zeros, array, matmul, cross, add, float64
 from numpy.linalg import inv, norm, det
 
+if TYPE_CHECKING:
+    from numpy.typing import NDArray
+    from Pynite.Node3D import Node3D
+    from Pynite.FEModel3D import FEModel3D
+
 #%%
 class Tri3D():
 
-    def __init__(self, name, i_node, j_node, k_node, t, material_name, model, kx_mod=1.0,
-                 ky_mod=1.0):
+    def __init__(self, name: str, i_node: Node3D, j_node: Node3D, k_node: Node3D, 
+                 t: float, material_name: str, model: FEModel3D, kx_mod: float = 1.0,
+                 ky_mod: float = 1.0) -> None:
         """
         A rectangular plate element
 
@@ -53,8 +62,8 @@ def __init__(self, name, i_node, j_node, k_node, t, material_name, model, kx_mod
 
         # Get material properties for the plate from the model
         try:
-            self.E = self.model.Materials[material_name].E
-            self.nu = self.model.Materials[material_name].nu
+            self.E = self.model.materials[material_name].E
+            self.nu = self.model.materials[material_name].nu
         except:
             raise KeyError('Please define the material ' + str(material_name) + ' before assigning it to plates.')
     
@@ -64,13 +73,13 @@ def width(self):
         """
         return self.i_node.distance(self.j_node)
 
-    def height(self):
+    def height(self) -> float:
         """
         Returns the height of the plate along its local y-axis
         """
         return self.i_node.distance(self.n_node)
     
-    def Dm(self):
+    def Dm(self) -> NDArray[float64]:
         """
         Returns the plane stress constitutive matrix for orthotropic materials [Dm]
         """
@@ -96,7 +105,7 @@ def Dm(self):
         # Return the constitutive matrix [Dm]
         return Dm
 
-    def Db(self):
+    def Db(self) -> NDArray[float64]:
         """
         Returns the bending constitutive matrix for orthotropic materials [Db]
         """
@@ -117,7 +126,7 @@ def Db(self):
         # Return the constitutive matrix [Db]
         return Db
 
-    def J(self, r, s):
+    def J(self, r: float, s: float) -> NDArray[float64]:
         '''
         Returns the Jacobian matrix for the element
         '''
@@ -146,13 +155,13 @@ def B_m(self, r, s):
         
         return B_m
     
-    def k(self):
+    def k(self) -> NDArray[float64]:
         """
         returns the plate's local stiffness matrix
         """
         return add(self.k_b(), self.k_m())
     
-    def k_m(self):
+    def k_m(self) -> NDArray[float64]:
         '''
         Returns the local stiffness matrix for membrane (in-plane) stresses.
 
@@ -210,7 +219,7 @@ def k_m(self):
         
         return k_exp
     
-    def k_b(self):
+    def k_b(self) -> NDArray[float64]:
         """
         Returns the local stiffness matrix for bending
         """
@@ -297,7 +306,7 @@ def k_b(self):
         # Return the local stiffness matrix
         return k_exp
 
-    def f(self, combo_name='Combo 1'):
+    def f(self, combo_name='Combo 1') -> NDArray[float64]:
         """
         Returns the plate's local end force vector
         """
@@ -319,7 +328,7 @@ def fer(self, combo_name='Combo 1'):
         fer = zeros((12, 1))
 
         # Get the requested load combination
-        combo = self.model.LoadCombos[combo_name]
+        combo = self.model.load_combos[combo_name]
 
         # Initialize the element's surface pressure to zero
         p = 0
@@ -376,7 +385,7 @@ def fer(self, combo_name='Combo 1'):
 
         return fer_exp
         
-    def d(self, combo_name='Combo 1'):
+    def d(self, combo_name: str='Combo 1') -> NDArray[float64]:
        """
        Returns the plate's local displacement vector
        """
@@ -384,7 +393,7 @@ def d(self, combo_name='Combo 1'):
        # Calculate and return the local displacement vector
        return matmul(self.T(), self.D(combo_name))
 
-    def F(self, combo_name='Combo 1'):
+    def F(self, combo_name:str ='Combo 1') -> NDArray[float64]:
         """
         Returns the plate's global nodal force vector
         """
@@ -392,7 +401,7 @@ def F(self, combo_name='Combo 1'):
         # Calculate and return the global force vector
         return matmul(inv(self.T()), self.f(combo_name))
 
-    def D(self, combo_name='Combo 1'):
+    def D(self, combo_name:str ='Combo 1') -> NDArray[float64]:
         """
         Returns the plate's global displacement vector for the given load combination.
         """
@@ -432,7 +441,7 @@ def D(self, combo_name='Combo 1'):
         # Return the global displacement vector
         return D
  
-    def T(self):
+    def T(self) -> NDArray[float64]:
         """
         Returns the plate's transformation matrix
         """
@@ -483,7 +492,7 @@ def T(self):
         
         return transMatrix
 
-    def K(self):
+    def K(self) -> NDArray[float64]:
         """
         Returns the plate's global stiffness matrix
         """
@@ -491,7 +500,7 @@ def K(self):
         # Calculate and return the stiffness matrix in global coordinates
         return matmul(matmul(inv(self.T()), self.k()), self.T())
 
-    def FER(self, combo_name='Combo 1'):
+    def FER(self, combo_name='Combo 1') -> NDArray[float64]:
         """
         Returns the global fixed end reaction vector.
 
@@ -505,7 +514,7 @@ def FER(self, combo_name='Combo 1'):
         # Calculate and return the fixed end reaction vector
         return matmul(inv(self.T()), self.fer(combo_name))
 
-    def _C(self):
+    def _C(self) -> NDArray[float64]:
         """
         Returns the plate's displacement coefficient matrix [C]
         """
@@ -540,7 +549,7 @@ def _C(self):
         # Return the coefficient matrix
         return C
 
-    def _Q(self, x, y):
+    def _Q(self, x:float, y:float) -> NDArray[float64]:
         """
         Calculates and returns the plate curvature coefficient matrix [Q] at a given point (x, y)
         in the plate's local system.
@@ -554,7 +563,7 @@ def _Q(self, x, y):
         # Return the [Q] coefficient matrix
         return Q
 
-    def _a(self, combo_name='Combo 1'):
+    def _a(self, combo_name:str ='Combo 1') -> NDArray[float64]:
         """
         Returns the vector of plate bending constants for the displacement function.
 
@@ -571,7 +580,7 @@ def _a(self, combo_name='Combo 1'):
         # Return the plate bending constants
         return inv(self._C()) @ d
 
-    def moment(self, x, y, combo_name='Combo 1'):
+    def moment(self, x:float, y:float, combo_name:str ='Combo 1') -> NDArray[float64]:
         """
         Returns the internal moments (Mx, My, and Mxy) at any point (x, y) in the plate's local
         coordinate system
@@ -592,7 +601,7 @@ def moment(self, x, y, combo_name='Combo 1'):
         # Pynite's quadrilateral elements.
         return -self.Db() @ self._Q(x, y) @ self._a(combo_name)
  
-    def shear(self, x, y, combo_name='Combo 1'):
+    def shear(self, x:float, y:float, combo_name:str='Combo 1') -> NDArray[float64]:
         """
         Returns the internal shears (Qx and Qy) at any point (x, y) in the plate's local
         coordinate system
@@ -637,7 +646,7 @@ def shear(self, x, y, combo_name='Combo 1'):
         return array([[Qx], 
                        [Qy]])
 
-    def membrane(self, x, y, combo_name='Combo 1'):
+    def membrane(self, x:float, y:float, combo_name:str='Combo 1') -> NDArray[float64]:
         
         # Convert the (x, y) coordinates to (r, x) coordinates
         r = -1 + 2*x/self.width()
diff --git a/Pynite/Visualization.py b/Pynite/Visualization.py
index f7599f50..2e07953d 100644
--- a/Pynite/Visualization.py
+++ b/Pynite/Visualization.py
@@ -1,4 +1,4 @@
-
+from __future__ import annotations # Allows more recent type hints features
 from json import load
 import warnings
 
@@ -864,7 +864,7 @@ class VisPtLoad():
     Creates a point load for the viewer
     '''
     
-    def __init__(self, position, direction, length, label_text=None, annotation_size=5, color=None):
+    def __init__(self, position, direction, length, label_text: str | None = None, annotation_size=5, color: str | None = None):
         '''
         Constructor.
       
@@ -878,7 +878,7 @@ def __init__(self, position, direction, length, label_text=None, annotation_size
           The length of the load arrow.
         tip_length : number
           The height of the arrow head.
-        label_text : string
+        label_text : string | None
           Text that will show up at the tail of the arrow. If set to 'None' no text will be displayed.
         '''
       


Reviews:
user: login: JWock82
state: COMMENTED
author_association: OWNER
submitted_at: 2025-04-10T00:54:24Z
user: login: JBloss1517
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-11T18:19:27Z

Review comments:
diff_hunk: @@ -1,9 +1,17 @@
 # %%
+from __future__ import annotations # Allows more recent type hints features
 from numpy import zeros, array, add, subtract, matmul, insert, cross, divide
 from numpy.linalg import inv
 from math import isclose
 import Pynite.FixedEndReactions
 from Pynite.LoadCombo import LoadCombo
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from typing import Dict, Optional
+    from numpy import float64
+    from numpy.typing import NDArray
path: Pynite/Spring3D.py
user: login: JWock82
body: `from typing import NDArray, Tuple`
created_at: 2025-04-10T00:54:24Z
author_association: OWNER
diff_hunk: @@ -1,9 +1,17 @@
 # %%
+from __future__ import annotations # Allows more recent type hints features
 from numpy import zeros, array, add, subtract, matmul, insert, cross, divide
 from numpy.linalg import inv
 from math import isclose
 import Pynite.FixedEndReactions
 from Pynite.LoadCombo import LoadCombo
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from typing import Dict, Optional
+    from numpy import float64
+    from numpy.typing import NDArray
path: Pynite/Spring3D.py
user: login: JBloss1517
body: Thank you for reviewing this. You are right, I need to add Tuple to the `from typing import ...` line. I didn't run flake8 on the repo before pushing it up. But will be sure to run it locally before pushing up the change to make sure I didn't miss anything obvious.
created_at: 2025-04-11T18:19:26Z
author_association: COLLABORATOR

PR comments:
user: login: JWock82
created_at: 2025-04-10T00:55:27Z
body: Wow! This is a very helpful PR! Thanks for doing this! It looks the tests didn't pass due to an import error in `ShearWall.py`. I've commented with what I think should be the corrected code.
author_association: OWNER
user: login: JWock82
created_at: 2025-04-26T21:19:06Z
body: @JBloss1517 you have been through a lot of the code on Pynite. Would you like to become a maintainer for Pynite?
author_association: OWNER
user: login: KwentiN-ui
created_at: 2025-04-27T10:41:56Z
body: I really appreciate this PR! It makes working with Pynite a lot more comfortable. Thank you for your time!

I've noticed that a bunch of files use conditional imports for types, which is a great way to get around circular imports. However using these types (especially in function definitions) can introduce a lot of headaches when not encapsulating them in quotations. I've whipped up a minimal example.

When running this file on Python 3.12, this will result in an import error:
```python
from typing import TYPE_CHECKING
if TYPE_CHECKING:
    from typing import Tuple

def foo(a:Tuple[int,int]):
    return a
```
This is a know problem with conditional type imports and can easily be avoided, by supplying the types as a string in quotation marks:
```python
from typing import TYPE_CHECKING
if TYPE_CHECKING:
    from typing import Tuple

def foo(a:"Tuple[int,int]"):
    return a
```
This still allows the LSP to parse the types correctly, without introducing weird behaviour.
I havent run into any issues yet, but this might be a useful thing to add to improve stability. If you don't mind the mixing of features I could also add the quotation marks to my PR #254.

I wish you all a nice weekend!
author_association: CONTRIBUTOR
user: login: JBloss1517
created_at: 2025-04-27T14:51:33Z
body: Thanks @KwentiN-ui for your kind words! I'm glad you find it helpful. When I was adding the type hints, I considered the string approach like you mentioned, but as strictly a matter of preference, I liked to keep them off. 

To avoid the runtime errors you mention you have two options:

1. wrap all type hints with quotes (this is what Python does under the hood anyway). Most editors are smart enough to understand what is going on and still work with them.

2. Add the following line at the top of the file: `from __future__ import annotations`. This allows you to not wrap your types in quotes and to also use more recent typing features like Union operators `|` instead of `Union[..., ...]` and lower case dict instead of Dict, etc. 

Ultimately I went with option 2 because it gained us some niceties and also avoids all the additional quotes. In Python 3.14 neither quotes nor the top __future__ import will be required 🎉, but it will be sometime before we can use this. 
You can check out https://peps.python.org/pep-0563/ for more background on this. 
author_association: COLLABORATOR
user: login: KwentiN-ui
created_at: 2025-04-27T16:25:50Z
body: @JBloss1517 Thank you for the explanation. You obviously put some thought into the decision. I was not aware that `from __future__ import annotations` was this versatile, it seems to be pretty powerful regarding supporting older python versions.
author_association: CONTRIBUTOR
user: login: JBloss1517
created_at: 2025-04-29T03:19:28Z
body: > @JBloss1517 you have been through a lot of the code on Pynite. Would you like to become a maintainer for Pynite?

I would be honored to help in a more official capacity. My work (Apex Engineers) is currently using PyNite on some internal tooling and are willing to donate some of my time to help support this library for the broader community. I would probably stay clear of touching some of the matrix pieces until I have a better understanding of these subjects, but I can help in many other areas in the meantime.
author_association: COLLABORATOR