diff --git a/Documentation/release/dev/python-numpy-integration.md b/Documentation/release/dev/python-numpy-integration.md
new file mode 100644
index 0000000000000000000000000000000000000000..28e0fed873b35790343c7bbfdba1dd88b53414c7
--- /dev/null
+++ b/Documentation/release/dev/python-numpy-integration.md
@@ -0,0 +1,13 @@
+## Add Python logic to enable module import at vtk module load
+
+This feature is mostly driven by the @override capability in Python to automatically enhance native vtk class with some Python one.
+
+By default we have added those following dependencies:
+- vtkCommonDataModel: vtkmodules.util.data_model
+- vtkCommonExecutionModel: vtkmodules.util.execution_model
+
+But now a user is able to add to it by calling `vtkmodules.register_vtk_module_dependencies(vtk_module_name, *import_strings)` to automate imports at vtk module loading.
+
+## Make numpy optional for vtkmodules.util.data_model
+
+`vtkmodules.util.data_model` has been added to enhance vtkDataModel API for Python using the @override infrastructure to mainly handle numpy in/out manipulation. But since numpy is an optional dependency for VTK, we are providing a downgraded version when numpy is not available so we can keep automatically load it at module startup regardless of numpy presence.
diff --git a/Wrapping/Python/vtkmodules/__init__.py.in b/Wrapping/Python/vtkmodules/__init__.py.in
index 25bf15447e8fc801c39766cd40618b1a2b534106..42bcce62f8b57751bcd7cf553edc14a0fa04078b 100644
--- a/Wrapping/Python/vtkmodules/__init__.py.in
+++ b/Wrapping/Python/vtkmodules/__init__.py.in
@@ -3,6 +3,51 @@ Currently, this package is experimental and may change in the future.
 """
 from __future__ import absolute_import
 import sys
+import importlib.util
+
+from pathlib import Path
+
+from importlib.abc import MetaPathFinder
+from importlib.machinery import ExtensionFileLoader
+
+LOADING_STACK = []
+
+def find_lib_path(paths, vtk_module_name):
+    for ext in [".pyd", ".so"]:
+        for base_path in paths:
+            for f in Path(base_path).glob(f"{vtk_module_name}.*{ext}"):
+                return str(f.resolve())
+
+
+class VTKMetaHook(MetaPathFinder):
+    """Attach a custom  loaded for vtk native library loading to defer loading of pure python dependencies"""
+    def find_spec(self, fullname, path, target=None):
+        if fullname.startswith("vtkmodules.vtk"):
+            vtk_module_name = fullname.split(".")[1]
+            module_path = find_lib_path(path, vtk_module_name)
+            if module_path is None:
+                return None
+
+            LOADING_STACK.append(fullname)
+            return importlib.util.spec_from_file_location(fullname, module_path, loader=VTKLoader(fullname, module_path))
+
+        return None
+
+
+class VTKLoader(ExtensionFileLoader):
+    """Flush any pending dependency load once initialize() phase is done"""
+    def exec_module(self, module):
+        super().exec_module(module)
+
+        # Process pending dependencies only if the module match the first load request
+        if len(LOADING_STACK) and LOADING_STACK[0] == module.__name__:
+            LOADING_STACK.clear()
+            on_vtk_module_init_completed()
+
+
+
+# Register our hook for vtk library loader
+sys.meta_path.insert(0, VTKMetaHook())
 
 
 def _windows_dll_path():
@@ -58,8 +103,48 @@ def _load_vtkmodules_static():
 #------------------------------------------------------------------------------
 # list the contents
 __all__ = [
-@_vtkmodules_all@]
-
+    @_vtkmodules_all@
+]
 #------------------------------------------------------------------------------
 # get the version
 __version__ = "@VTK_MAJOR_VERSION@.@VTK_MINOR_VERSION@.@VTK_BUILD_VERSION@"
+
+#------------------------------------------------------------------------------
+# describe import dependencies to properly define Python @override
+MODULE_MAPPER = {
+    "vtkCommonDataModel": [
+        "vtkmodules.util.data_model",
+    ],
+    "vtkCommonExecutionModel": [
+        "vtkmodules.util.execution_model",
+    ],
+}
+LOADED_MODULES = set()
+PENDING_LOADED_MODULES = set()
+
+def register_vtk_module_dependencies(vtk_module_name, *import_names):
+    """Method to call for registering external override on vtkmodule load"""
+    MODULE_MAPPER.setdefault(vtk_module_name, []).extend(import_names)
+
+    # If already loaded let's make sure we import it now
+    if vtk_module_name in LOADED_MODULES:
+        for import_name in import_names:
+            importlib.import_module(import_name)
+
+
+def on_vtk_module_init(module_name):
+    """Automatically called by vtkmodule when they are loaded"""
+    if module_name in LOADED_MODULES:
+        return
+
+    PENDING_LOADED_MODULES.add(module_name)
+
+
+def on_vtk_module_init_completed():
+    pending = list(PENDING_LOADED_MODULES)
+    PENDING_LOADED_MODULES.clear()
+
+    for module_name in pending:
+        LOADED_MODULES.add(module_name)
+        for import_name in MODULE_MAPPER.get(module_name, []):
+            importlib.import_module(import_name)
diff --git a/Wrapping/Python/vtkmodules/util/data_model.py b/Wrapping/Python/vtkmodules/util/data_model.py
index e83a6cf4a3e118a2a6cf480691037abd6f3fce19..d5553afd01f862ab6742c3c92d473fda6913b5af 100644
--- a/Wrapping/Python/vtkmodules/util/data_model.py
+++ b/Wrapping/Python/vtkmodules/util/data_model.py
@@ -1,8 +1,8 @@
 """This module provides classes that allow numpy style access
 to VTK datasets. See examples at bottom.
 """
-
-from vtkmodules.vtkCommonCore import vtkPoints
+from contextlib import suppress
+from vtkmodules.vtkCommonCore import vtkPoints, vtkAbstractArray
 from vtkmodules.vtkCommonDataModel import (
     vtkCellArray,
     vtkDataObject,
@@ -17,10 +17,15 @@ from vtkmodules.vtkCommonDataModel import (
     vtkPartitionedDataSet
 )
 
-from vtkmodules.numpy_interface import dataset_adapter as dsa
-import numpy
 import weakref
 
+NUMPY_AVAILABLE = False
+
+with suppress(ImportError):
+    import numpy
+    from vtkmodules.numpy_interface import dataset_adapter as dsa
+    NUMPY_AVAILABLE = True
+
 class FieldDataBase(object):
     def __init__(self):
         self.association = None
@@ -39,6 +44,10 @@ class FieldDataBase(object):
         if isinstance(idx, int) and idx >= self.GetNumberOfArrays():
             raise IndexError("array index out of range")
         vtkarray = super().GetArray(idx)
+
+        if not NUMPY_AVAILABLE:
+            return vtkarray if vtkarray else self.GetAbstractArray(idx)
+
         if not vtkarray:
             vtkarray = self.GetAbstractArray(idx)
             if vtkarray:
@@ -70,6 +79,12 @@ class FieldDataBase(object):
 
     def set_array(self, name, narray):
         """Appends a new array to the dataset attributes."""
+        if not NUMPY_AVAILABLE:
+            if isinstance(narray, vtkAbstractArray):
+                narray.SetName(name)
+                self.AddArray(narray)
+            return
+
         if narray is dsa.NoneArray:
             # if NoneArray, nothing to do.
             return
@@ -221,6 +236,10 @@ class CompositeDataSetAttributes(object):
 
     def set_array(self, name, narray):
         """Appends a new array to the composite dataset attributes."""
+        if not NUMPY_AVAILABLE:
+            # don't know how to handle composite dataset attribute when numpy not around
+            raise NotImplementedError("Only available with numpy")
+
         if narray is dsa.NoneArray:
             # if NoneArray, nothing to do.
             return
@@ -246,6 +265,11 @@ class CompositeDataSetAttributes(object):
     def get_array(self, idx):
         """Given a name, returns a VTKCompositeArray."""
         arrayname = idx
+
+        if not NUMPY_AVAILABLE:
+            # don't know how to handle composite dataset attribute when numpy not around
+            raise NotImplementedError("Only available with numpy")
+
         if arrayname not in self.ArrayNames:
             return dsa.NoneArray
         if arrayname not in self.Arrays or self.Arrays[arrayname]() is None:
@@ -291,12 +315,23 @@ class PointSet(DataSet):
     @property
     def points(self):
         pts = self.GetPoints()
+
+        if not NUMPY_AVAILABLE:
+            return pts
+
         if not pts or not pts.GetData():
             return None
         return dsa.vtkDataArrayToVTKArray(pts.GetData())
 
     @points.setter
     def points(self, points):
+        if isinstance(points, vtkPoints):
+            self.SetPoints(points)
+            return
+
+        if not NUMPY_AVAILABLE:
+            raise ValueError("Expect vtkPoints")
+
         pts = dsa.numpyTovtkDataArray(points, "points")
         vtkpts = vtkPoints()
         vtkpts.SetData(pts)
@@ -308,20 +343,33 @@ class vtkUnstructuredGrid(PointSet, vtkUnstructuredGrid):
     def cells(self):
         ca = self.GetCells()
         conn_vtk = ca.GetConnectivityArray()
-        conn = dsa.vtkDataArrayToVTKArray(conn_vtk)
         offsets_vtk = ca.GetOffsetsArray()
-        offsets = dsa.vtkDataArrayToVTKArray(offsets_vtk)
         ct_vtk = self.GetCellTypesArray()
+
+        if not NUMPY_AVAILABLE:
+            return {
+               'connectivity' : conn_vtk,
+               'offsets' : offsets_vtk,
+               'cell_types' : ct_vtk,
+            }
+
+        conn = dsa.vtkDataArrayToVTKArray(conn_vtk)
+        offsets = dsa.vtkDataArrayToVTKArray(offsets_vtk)
         ct = dsa.vtkDataArrayToVTKArray(ct_vtk)
         return { 'connectivity' : conn, 'offsets' : offsets , 'cell_types' : ct}
 
     @cells.setter
     def cells(self, cells):
         ca = vtkCellArray()
+
+        if not NUMPY_AVAILABLE:
+            ca.SetData(cells['offsets'], cells['connectivity'])
+            self.SetCells(cells['cell_types'], ca)
+            return
+
         conn_vtk = dsa.numpyTovtkDataArray(cells['connectivity'])
         offsets_vtk = dsa.numpyTovtkDataArray(cells['offsets'])
         cell_types_vtk = dsa.numpyTovtkDataArray(cells['cell_types'])
-        print(cells['cell_types'][1])
         ca.SetData(offsets_vtk, conn_vtk)
         self.SetCells(cell_types_vtk, ca)
 
@@ -335,8 +383,15 @@ class vtkPolyData(PointSet, vtkPolyData):
     def polygons(self):
         ca = self.GetPolys()
         conn_vtk = ca.GetConnectivityArray()
-        conn = dsa.vtkDataArrayToVTKArray(conn_vtk)
         offsets_vtk = ca.GetOffsetsArray()
+
+        if not NUMPY_AVAILABLE:
+            return {
+                'connectivity' : conn_vtk,
+                'offsets' : offsets_vtk,
+            }
+
+        conn = dsa.vtkDataArrayToVTKArray(conn_vtk)
         offsets = dsa.vtkDataArrayToVTKArray(offsets_vtk)
         return { 'connectivity' : conn, 'offsets' : offsets }
 
@@ -406,7 +461,7 @@ class CompositeDataSetBase(object):
     def cell_data(self):
         "Returns the cell data as a DataSetAttributes instance."
         if self._CellData is None or self._CellData() is None:
-            cdata = self.get_attributes(DataObject.CELL)
+            cdata = self.get_attributes(vtkDataObject.CELL)
             self._CellData = weakref.ref(cdata)
         return self._CellData()
 
@@ -414,13 +469,17 @@ class CompositeDataSetBase(object):
     def field_data(self):
         "Returns the field data as a DataSetAttributes instance."
         if self._FieldData is None or self._FieldData() is None:
-            fdata = self.get_attributes(DataObject.FIELD)
+            fdata = self.get_attributes(vtkDataObject.FIELD)
             self._FieldData = weakref.ref(fdata)
         return self._FieldData()
 
     @property
     def points(self):
         "Returns the points as a VTKCompositeDataArray instance."
+        if not NUMPY_AVAILABLE:
+            # don't know how to handle composite dataset when numpy not around
+            raise NotImplementedError("Only available with numpy")
+
         if self._Points is None or self._Points() is None:
             pts = []
             for ds in self:
@@ -430,11 +489,11 @@ class CompositeDataSetBase(object):
                     _pts = None
 
                 if _pts is None:
-                    pts.append(NoneArray)
+                    pts.append(dsa.NoneArray)
                 else:
                     pts.append(_pts)
-            if len(pts) == 0 or all([a is NoneArray for a in pts]):
-                cpts = NoneArray
+            if len(pts) == 0 or all([a is dsa.NoneArray for a in pts]):
+                cpts = dsa.NoneArray
             else:
                 cpts = dsa.VTKCompositeDataArray(pts, dataset=self)
             self._Points = weakref.ref(cpts)
@@ -444,3 +503,15 @@ class CompositeDataSetBase(object):
 class vtkPartitionedDataSet(CompositeDataSetBase, vtkPartitionedDataSet):
     def append(self, dataset):
         self.SetPartition(self.GetNumberOfPartitions(), dataset)
+
+# -----------------------------------------------------------------------------
+# Handle pickle registration
+# -----------------------------------------------------------------------------
+with suppress(ImportError):
+    import copyreg
+    from vtkmodules.util.pickle_support import serialize_VTK_data_object
+
+    copyreg.pickle(vtkPolyData, serialize_VTK_data_object)
+    copyreg.pickle(vtkUnstructuredGrid, serialize_VTK_data_object)
+    copyreg.pickle(vtkImageData, serialize_VTK_data_object)
+    copyreg.pickle(vtkPartitionedDataSet, serialize_VTK_data_object)
diff --git a/Wrapping/Python/vtkmodules/util/pickle_support.py b/Wrapping/Python/vtkmodules/util/pickle_support.py
index 6abb472778c722a64bccb3dde5d9429f2d4aa8c5..8fcbf2d958d939d99fa64890afde79a4e0d8d979 100644
--- a/Wrapping/Python/vtkmodules/util/pickle_support.py
+++ b/Wrapping/Python/vtkmodules/util/pickle_support.py
@@ -22,7 +22,7 @@ objects in the global dispatch table used by pickle. NumPy is required as well s
 try:
     import copyreg, pickle, numpy
 except ImportError:
-    raise RuntimeError("This module depends on the pickle, copyreg, and numpy modules.\
+    raise ImportError("This module depends on the pickle, copyreg, and numpy modules.\
  Please make sure that it is installed properly.")
 
 from ..vtkParallelCore import vtkCommunicator
diff --git a/Wrapping/PythonCore/vtkPythonUtil.cxx b/Wrapping/PythonCore/vtkPythonUtil.cxx
index f401af3668b43ad0a9d7b01dba594725f1d8cc1e..66631b58b5933b21a6b25b838029e06ceb7ec36a 100644
--- a/Wrapping/PythonCore/vtkPythonUtil.cxx
+++ b/Wrapping/PythonCore/vtkPythonUtil.cxx
@@ -1041,6 +1041,17 @@ bool vtkPythonUtil::ImportModule(const char* fullname, PyObject* globals)
 void vtkPythonUtil::AddModule(const char* name)
 {
   vtkPythonMap->ModuleList->push_back(name);
+
+  // Register module name into pending list for defered side module loading
+  PyObject* pModule = PyImport_ImportModule("vtkmodules");
+  PyObject* pFunc = PyObject_GetAttrString(pModule, "on_vtk_module_init");
+  PyObject* pArgs = PyTuple_New(1);
+  PyTuple_SetItem(pArgs, 0, PyUnicode_FromString(name));
+  PyObject* execVal = PyObject_CallObject(pFunc, pArgs);
+  Py_DECREF(execVal);
+  Py_DECREF(pArgs);
+  Py_DECREF(pFunc);
+  Py_DECREF(pModule);
 }
 
 //------------------------------------------------------------------------------