Repository: pyccel/pyccel (language: python)

Issue:
Classes
Implementation of classes can be roughly split into 4 phases:

# Phase 1 - Bring C support up to the same level as the Fortran support
- [x] #1439 Implement a structure to hold data
- [x] #1441 Implement basic class functions
- [x] Implement class constructors
- [x] #1443 Implement class destructors
- [x] #1444 Implement functions which return classes
- [x] #1442 Implement class interfaces

This stage is complete when all the test cases in the [`tests/pyccel/scripts/classes/`](https://github.com/pyccel/pyccel/tree/devel/tests/pyccel/scripts/classes) can be translated.

# Phase 2 - Clean up and document current implementation
- [ ] #1471 Flexible instance naming
- [x] #1469 Capturing all attributes
- [x] #1468 Duplication of DottedVariables
- [x] #1491 Document everything that is implemented (this should mention that it is generation only for now)
- [x] #1492 Add support for type annotation to match what is done with functions
- [x] #1527 
- [x] #1507 
- [ ] #1541 
- [x] #1577 
- [x] #1586 
- [x] #1684 

# Phase 3 - Add wrappers to expose the classes to Python

This stage will allow the implementation of tests in the tests/epyccel folder. It is recommended to tackle these issues in the order they are listed. However it may be useful to fix the Fortran-C wrapper and the C-Python Fortran corrections together:

- [x] Add a C-Python wrapper for the class object which does the following:
    - [x] #1493 Add printing for a Python API class which saves a pointer to the C object
    - [x] #1494 Add code to function wrapping so that the pointer is packed into the API class when returned from a function
    - [x] #1495 Add code to function wrapping so that the pointer is unpacked from the API class to allow instances to be passed as arguments
    - [x] #1605 Add methods to the API class
    - [x] #1607 Add constructor/destructor to the API class
    - [x] #1618 Expose variables through the API class
- [x] Add a Fortran-C wrapper for the class object which does the following:
    - [x] #1604 Improve wrappers for functions to return a C-pointer when a class object is returned (similarly to what is done for arrays)
    - [x] #1606 Improve wrappers for functions to unpack a C-pointer to a class object argument (similarly to what is done for arrays)
    - [x] #1607 Add wrappers for the class methods (including constructor/destructor)
    - [ ] #1618 Add wrappers for the class variables
- [x] #1650 

# Phase 4 - Improve support for classes
- [ ] #1610
- [ ] Properties
- [ ] Inheritance
- [ ] Polymorphism (e.g. via abstract methods)
- [ ] More magic methods
- [ ] Class variables
- [ ] Inline class methods


Discussion / hints:
Cwrapper reference: https://docs.python.org/3/extending/newtypes_tutorial.html
Additional cwrapper reference for class variable getters and setters (this mimicks the property decorator) : https://docs.python.org/3/c-api/structures.html#c.PyGetSetDef

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2905b9df6e..5d1ca23690 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -30,6 +30,7 @@ All notable changes to this project will be documented in this file.
 -   \[INTERNALS\] In `ast.numpyext` rename `Shape` as `NumpyShape`, `NumpyArraySize` as `NumpySize`
 -   \[INTERNALS\] In `ast.internals` rename `PyccelArraySize` as `PyccelArraySizeElement`, create new `PyccelArraySize` w/out `index` argument
 -   \[INTERNALS\] Make `NumpySize` a factory class (which cannot be instantiated)
+-   \[INTERNALS\] Re-write C-Python API wrapping stage (#1477)
 
 ### Deprecated
 
diff --git a/developer_docs/wrapper_stage.md b/developer_docs/wrapper_stage.md
index 5560f09dd5..2ecc359535 100644
--- a/developer_docs/wrapper_stage.md
+++ b/developer_docs/wrapper_stage.md
@@ -47,14 +47,655 @@ Scalar module variables are already accessible from C so no extra work is done h
 
 Arrays are not compatible with C. Instead the wrapper prints a function which returns a pointer to the module variable as well as its size information to make the information available from C.
 
+#### Example
+
+When the following code is translated to Fortran:
+```python
+import numpy as np
+x = np.empty(6)
+```
+
+The following Fortran translation is obtained:
+```fortran
+module tmp
+
+
+  use, intrinsic :: ISO_C_Binding, only : b4 => C_BOOL , f64 => C_DOUBLE &
+        , i64 => C_INT64_T
+  implicit none
+
+  integer(i64), bind(c) :: n
+  real(f64), allocatable, target :: x(:)
+  logical(b4), private :: initialised = .False._b4
+
+  contains
+
+  !........................................
+  subroutine tmp__init()
+
+    implicit none
+
+    if (.not. initialised) then
+      n = 6_i64
+      allocate(x(0:n - 1_i64))
+      initialised = .True._b4
+    end if
+
+  end subroutine tmp__init
+  !........................................
+
+  !........................................
+  subroutine tmp__free()
+
+    implicit none
+
+    if (initialised) then
+      if (allocated(x)) then
+        deallocate(x)
+      end if
+      initialised = .False._b4
+    end if
+
+  end subroutine tmp__free
+  !........................................
+
+end module tmp
+```
+
+The array `x` is wrapped as follows:
+```fortran
+  subroutine bind_c_x(bound_x, x_shape_1) bind(c)
+
+    use tmp, only: x
+
+    implicit none
+
+    type(c_ptr), intent(out) :: bound_x
+    integer(i64), intent(out) :: x_shape_1
+
+    x_shape_1 = size(x, kind=i64)
+    bound_x = c_loc(x)
+
+  end subroutine bind_c_x
+```
+
 ### Functions
 
 In the simplest case a wrapper around a function takes the same arguments, calls the function and then returns the result. The only difference between this function and the original function is the body (where the original function is called) and the `bind(c)` tag which ensures that the function name is accessible from C and is not mangled.
 
 More complex cases include functions with array arguments or results, optional variables and functions as arguments.
 
-Array arguments and results are handled similarly to array module variables, by adding additional variables for the shape and stride information.
+Array arguments and results are handled similarly to array module variables, by adding additional variables for the shape and stride information. Strides are not used for results as pointers cannot be returned from functions.
 
 Optional arguments are passed as C pointers. An if/else block then determines whether the pointer is assigned or not. This can be quite lengthy, however it is unavoidable for compilation with intel, or nvidia. It is also unavoidable for arrays as it is important not to index an array (to access the strides) which is not present.
 
 Finally the most complex cases such as functions as arguments are simply not printed. Instead these cases raise warnings or errors to alert the user that support is missing.
+
+#### Example 1 : function with scalar arguments
+
+The following function:
+
+```python
+def f(x : int):
+    return x + 3
+```
+
+is translated to the following Fortran code:
+```fortran
+  function f(x) result(Out_0001)
+
+    implicit none
+
+    integer(i64) :: Out_0001
+    integer(i64), value :: x
+
+    Out_0001 = x + 3_i64
+    return
+
+  end function f
+```
+
+which is then wrapped as follows:
+```fortran
+  function bind_c_f(x) bind(c) result(Out_0001)
+
+    implicit none
+
+    integer(i64) :: Out_0001
+    integer(i64), value :: x
+
+    Out_0001 = f(x = x)
+
+  end function bind_c_f
+```
+
+This function has the following prototype in C:
+```c
+int64_t bind_c_f(int64_t);
+```
+
+#### Example 2 : function with array arguments
+
+The following function:
+```python
+def f(x : 'int[:]'):
+    return x + 3
+```
+
+is translated to the following Fortran code:
+```fortran
+  subroutine f(x, Out_0001)
+
+    implicit none
+
+    integer(i64), allocatable, intent(out) :: Out_0001(:)
+    integer(i64), intent(in) :: x(0:)
+
+    allocate(Out_0001(0:size(x, kind=i64) - 1_i64))
+    Out_0001 = x + 3_i64
+    return
+
+  end subroutine f
+```
+
+which is then wrapped as follows:
+```fortran
+  subroutine bind_c_f(bound_x, bound_x_shape_1, bound_x_stride_1, &
+        bound_Out_0001, Out_0001_shape_1) bind(c)
+
+    implicit none
+
+    type(c_ptr), intent(out) :: bound_Out_0001
+    integer(i64), intent(out) :: Out_0001_shape_1
+    type(c_ptr), value :: bound_x
+    integer(i64), value :: bound_x_shape_1
+    integer(i64), value :: bound_x_stride_1
+    integer(i64), pointer :: x(:)
+    integer(i64), allocatable :: Out_0001(:)
+    integer(i64), pointer :: Out_0001_ptr(:)
+
+    call C_F_Pointer(bound_x, x, [bound_x_shape_1 * bound_x_stride_1])
+    call f(x = x(1_i64::bound_x_stride_1), Out_0001 = Out_0001)
+    Out_0001_shape_1 = size(Out_0001, kind=i64)
+    allocate(Out_0001_ptr(0:Out_0001_shape_1 - 1_i64))
+    Out_0001_ptr = Out_0001
+    bound_Out_0001 = c_loc(Out_0001_ptr)
+
+  end subroutine bind_c_f
+```
+
+This function has the following prototype in C:
+```c
+int bind_c_f(void*, int64_t, int64_t, void*, int64_t*);
+```
+
+#### Example 3 : function with optional scalar arguments
+
+The following function:
+```python
+def f(x : int = None):
+    if x is None:
+        return 2
+    else:
+        return x + 3
+```
+
+is translated to the following Fortran code:
+```fortran
+  function f(x) result(Out_0001)
+
+    implicit none
+
+    integer(i64) :: Out_0001
+    integer(i64), optional, value :: x
+
+    if (.not. present(x)) then
+      Out_0001 = 2_i64
+      return
+    else
+      Out_0001 = x + 3_i64
+      return
+    end if
+
+  end function f
+```
+
+which is then wrapped as follows:
+```fortran
+  function bind_c_f(bound_x) bind(c) result(Out_0001)
+
+    implicit none
+
+    integer(i64) :: Out_0001
+    type(c_ptr), value :: bound_x
+    integer(i64), pointer :: x
+
+    if (c_associated(bound_x)) then
+      call C_F_Pointer(bound_x, x)
+      Out_0001 = f(x = x)
+    else
+      Out_0001 = f()
+    end if
+
+  end function bind_c_f
+```
+
+This function has the following prototype in C:
+```c
+int64_t bind_c_f(void*);
+```
+
+#### Example 4 : function with optional array arguments
+
+The following function:
+```python
+def f(x : 'float[:]' = None):
+    import numpy as np
+    if x is None:
+        return np.ones(3)
+    else:
+        return x + 3
+```
+
+is translated to the following Fortran code:
+```fortran
+  subroutine f(x, Out_0001)
+
+    implicit none
+
+    real(f64), allocatable, intent(out) :: Out_0001(:)
+    real(f64), optional, intent(in) :: x(0:)
+
+    if (.not. present(x)) then
+      allocate(Out_0001(0:2_i64))
+      Out_0001 = 1.0_f64
+      return
+    else
+      allocate(Out_0001(0:size(x, kind=i64) - 1_i64))
+      Out_0001 = x + 3_i64
+      return
+    end if
+    if (allocated(Out_0001)) then
+      deallocate(Out_0001)
+    end if
+
+  end subroutine f
+```
+
+which is then wrapped as follows:
+```fortran
+  subroutine bind_c_f(bound_x, bound_x_shape_1, bound_x_stride_1, &
+        bound_Out_0001, Out_0001_shape_1) bind(c)
+
+    implicit none
+
+    type(c_ptr), intent(out) :: bound_Out_0001
+    integer(i64), intent(out) :: Out_0001_shape_1
+    type(c_ptr), value :: bound_x
+    integer(i64), value :: bound_x_shape_1
+    integer(i64), value :: bound_x_stride_1
+    real(f64), pointer :: x(:)
+    real(f64), allocatable :: Out_0001(:)
+    real(f64), pointer :: Out_0001_ptr(:)
+
+    if (c_associated(bound_x)) then
+      call C_F_Pointer(bound_x, x, [bound_x_shape_1 * bound_x_stride_1])
+      call f(x = x(1_i64::bound_x_stride_1), Out_0001 = Out_0001)
+    else
+      call f(Out_0001 = Out_0001)
+    end if
+    Out_0001_shape_1 = size(Out_0001, kind=i64)
+    allocate(Out_0001_ptr(0:Out_0001_shape_1 - 1_i64))
+    Out_0001_ptr = Out_0001
+    bound_Out_0001 = c_loc(Out_0001_ptr)
+
+  end subroutine bind_c_f
+```
+
+This function has the following prototype in C:
+```c
+int bind_c_f(void*, int64_t, int64_t, void*, int64_t*);
+```
+
+### Class module variables
+
+**Class module variables are not yet wrapped. This describes the future implementation**
+
+Unlike Fortran, C does not have classes in the language. The wrapper therefore cannot pass the class to C via a description. Instead the wrapper should print a function which returns a pointer to the module variable.
+
+Additionally the class method functions will be wrapped as described for functions. The attributes of the class will be exposed via wrapper functions.
+
+## C To Python
+
+The C to Python wrapper wraps C code to make it callable from Python. This module relies heavily on the [Python-C API](https://docs.python.org/3/c-api/index.html).
+
+### Functions
+
+A function that can be called from Python must have the following prototype:
+```c
+PyObject* func_name(PyObject* self, PyObject* args, PyObject* kwargs);
+```
+
+The arguments and keyword arguments are unpacked into individual `PyObject` pointers.
+Each of these objects is checked to verify the type. If the type does not match the expected type then an error is raised as described in the [C-API documentation](https://docs.python.org/3/c-api/intro.html#exceptions).
+If the type does match then the value is unpacked into a C object. This is done using custom functions defined in `pyccel/stdlib/cwrapper/` or `pyccel/stdlib/cwrapper_ndarrays/` (see these files for more details).
+
+Once C objects have been retrieved the function is called normally.
+
+Finally all the arguments are packed into a Python tuple stored in a `PyObject` and are returned.
+
+The wrapper is attached to the module via a `PyMethodDef` (see C-API [docs](https://docs.python.org/3/c-api/structures.html#c.PyMethodDef)).
+
+#### Example
+
+The following Python code:
+```python
+def f(x : 'float[:]', y : float = 3):
+    return x + y
+```
+
+leads to C code with the following prototype:
+```c
+t_ndarray f(t_ndarray x, double y);
+```
+
+which is then wrapped as follows:
+```c
+PyObject* f_wrapper(PyObject* self, PyObject* args, PyObject* kwargs)
+{
+    PyObject* x_obj;
+    PyObject* y_obj;
+    t_ndarray x = {.shape = NULL};
+    double y;
+    t_ndarray Out_0001 = {.shape = NULL};
+    PyObject* Out_0001_obj;
+    // Initialise any optional arguments
+    y_obj = Py_None;
+    // Declare the names of the arguments so they can be found when the function is called
+    static char *kwlist[] = {
+        "x",
+        "y",
+        NULL
+    };
+    // Unpack the Python arguments into individual PyObjects (e.g. x_obj, y_obj)
+    // The vertical line splits compulsory arguments from optional arguments
+    if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O|O", kwlist, &x_obj, &y_obj))
+    {
+        return NULL;
+    }
+    // Unpack the PyObject (x_obj) into a C object (x)
+    if (pyarray_check(x_obj, NPY_DOUBLE, INT64_C(1), NO_ORDER_CHECK))
+    {
+        x = pyarray_to_ndarray(x_obj);
+    }
+    else
+    {
+        // Return in case of an error (raised if the type doesn't match)
+        return NULL;
+    }
+    // Initialise default values
+    y = INT64_C(3);
+    // Unpack the PyObject (y_obj) into a C object (y)
+    if (PyIs_NativeFloat(y_obj))
+    {
+        y = PyDouble_to_Double(y_obj);
+    }
+    else if (y_obj != Py_None)
+    {
+        // Return in case of an error (raised if the type doesn't match)
+        return NULL;
+    }
+    // Call the function
+    Out_0001 = f(x, y);
+    // Free memory allocated for the arguments
+    free_pointer(&x);
+    // Pack the results into a PyObject
+    Out_0001_obj = ndarray_to_pyarray(Out_0001);
+    // Free memory allocated for the results
+    free_pointer(&Out_0001);
+    return Out_0001_obj;
+}
+```
+
+The function is linked to the module via a `PyMethodDef` as follows:
+```c
+static PyMethodDef tmp_methods[] = {
+    {
+        "f", // Function name
+        (PyCFunction)f_wrapper, // Function implementation
+        METH_VARARGS | METH_KEYWORDS, // Indicates that the function accepts args and kwargs
+        "" // function docstring
+    },
+    { NULL, NULL, 0, NULL}
+};
+```
+
+If the code was translated to Fortran the prototype is:
+```c
+int bind_c_f(void*, int64_t, int64_t, double, void*, int64_t*);
+```
+
+which is then wrapped as follows:
+```c
+PyObject* f_wrapper(PyObject* self, PyObject* args, PyObject* kwargs)
+{
+    PyObject* bound_x_obj;
+    PyObject* y_obj;
+    t_ndarray x = {.shape = NULL};
+    void* bound_x;
+    int64_t bound_x_shape_1;
+    int64_t bound_x_stride_1;
+    double y;
+    void* bound_Out_0001;
+    int64_t Out_0001_shape_1;
+    t_ndarray Out_0001 = {.shape = NULL};
+    PyObject* bound_Out_0001_obj;
+    // Initialise any optional arguments
+    y_obj = Py_None;
+    // Declare the names of the arguments so they can be found when the function is called
+    static char *kwlist[] = {
+        "x",
+        "y",
+        NULL
+    };
+    // Unpack the Python arguments into individual PyObjects (e.g. x_obj, y_obj)
+    // The vertical line splits compulsory arguments from optional arguments
+    if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O|O", kwlist, &bound_x_obj, &y_obj))
+    {
+        return NULL;
+    }
+    // Unpack the PyObject (x_obj) into a C object (x)
+    if (pyarray_check(bound_x_obj, NPY_DOUBLE, INT64_C(1), NO_ORDER_CHECK))
+    {
+        x = pyarray_to_ndarray(bound_x_obj);
+    }
+    else
+    {
+        // Return in case of an error (raised if the type doesn't match)
+        return NULL;
+    }
+    // Unpack the C object into the individual arguments needed to call a Fortran function
+    bound_x = nd_data(&x);
+    bound_x_shape_1 = nd_ndim(&x, INT64_C(0));
+    bound_x_stride_1 = nd_nstep_F(&x, INT64_C(0));
+    // Initialise default values
+    y = INT64_C(3);
+    // Unpack the PyObject (y_obj) into a C object (y)
+    if (PyIs_NativeFloat(y_obj))
+    {
+        y = PyDouble_to_Double(y_obj);
+    }
+    else if (y_obj != Py_None)
+    {
+        // Return in case of an error (raised if the type doesn't match)
+        return NULL;
+    }
+    // Call the function
+    bind_c_f(bound_x, bound_x_shape_1, bound_x_stride_1, y, &bound_Out_0001, &Out_0001_shape_1);
+    // Free memory allocated for the arguments
+    free_pointer(&x);
+    // Pack the results into a `ndarray`
+    Out_0001 = array_create(1, (int64_t[]){Out_0001_shape_1}, nd_double, true, order_c);
+    Out_0001.raw_data = bound_Out_0001;
+    // Pack the results into a PyObject
+    bound_Out_0001_obj = ndarray_to_pyarray(Out_0001);
+    // Free memory allocated for the results
+    free_pointer(&Out_0001);
+    return bound_Out_0001_obj;
+}
+```
+
+### Interfaces
+
+Interfaces are functions which accept more than one type.
+These functions are handled via multiple functions in the wrapper:
+1.  A function which can be called from Python with the prototype:
+    ```c
+    PyObject* func_name(PyObject* self, PyOject* args, PyObject* kwargs);
+    ```
+
+2.  A function which determines which combination of types were used in the call
+
+3.  A function for each combination of types which calls the translated function
+
+#### Example
+
+The following Python code:
+```python
+@template('T', [int, float])
+def f(x : 'T'):
+    return x + 2
+```
+
+leads to C code with the following prototypes:
+```c
+double f_00(double x);
+int64_t f_01(int64_t x);
+```
+
+which is then wrapped as follows:
+```c
+/*........................................*/
+PyObject* f_00_wrapper(PyObject* x_obj)
+{
+    double x;
+    double Out_0001;
+    PyObject* Out_0001_obj;
+    x = PyDouble_to_Double(x_obj);
+    Out_0001 = f_00(x);
+    Out_0001_obj = Double_to_PyDouble(&Out_0001);
+    return Out_0001_obj;
+}
+/*........................................*/
+
+/*........................................*/
+PyObject* f_01_wrapper(PyObject* x_obj)
+{
+    int64_t x;
+    int64_t Out_0001;
+    PyObject* Out_0001_obj;
+    x = PyInt64_to_Int64(x_obj);
+    Out_0001 = f_01(x);
+    Out_0001_obj = Int64_to_PyLong(&Out_0001);
+    return Out_0001_obj;
+}
+/*........................................*/
+/*_________________________________CommentBlock_________________________________*/
+/*Assess the types. Raise an error for unexpected types and calculate an integer */
+/*which indicates which function should be called.                               */
+/*_______________________________________________________________________________*/
+int64_t f_type_check(PyObject* x_obj)
+{
+    int64_t type_indicator;
+    type_indicator = INT64_C(0);
+    if (PyIs_NativeFloat(x_obj))
+    {
+        type_indicator += INT64_C(0);
+    }
+    else if (PyIs_NativeInt(x_obj))
+    {
+        type_indicator += INT64_C(1);
+    }
+    else
+    {
+        PyErr_SetString(PyExc_TypeError, "Unexpected type for argument x");
+        return -INT64_C(1);
+    }
+    return type_indicator;
+}
+/*........................................*/
+/*........................................*/
+PyObject* f_wrapper(PyObject* self, PyObject* args, PyObject* kwargs)
+{
+    PyObject* x_obj;
+    int64_t type_indicator;
+    static char *kwlist[] = {
+        "x",
+        NULL
+    };
+    if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O", kwlist, &x_obj))
+    {
+        return NULL;
+    }
+    type_indicator = f_type_check(x_obj);
+    if (type_indicator == INT64_C(0))
+    {
+        return f_00_wrapper(x_obj);
+    }
+    else if (type_indicator == INT64_C(1))
+    {
+        return f_01_wrapper(x_obj);
+    }
+    else
+    {
+        PyErr_SetString(PyExc_TypeError, "Unexpected type combination");
+        return NULL;
+    }
+}
+/*........................................*/
+```
+
+The function is linked to the module via a `PyMethodDef` as follows:
+```c
+static PyMethodDef tmp_methods[] = {
+    {
+        "f", // Function name
+        (PyCFunction)f_wrapper, // Function implementation
+        METH_VARARGS | METH_KEYWORDS, // Indicates that the function accepts args and kwargs
+        "" // function docstring
+    },
+    { NULL, NULL, 0, NULL}
+};
+```
+
+### Variables
+
+In order to import variables from a module, they must be attached to the module using the function [`PyModule_AddObject`](https://docs.python.org/3/c-api/module.html#c.PyModule_AddObject).
+In order to do this the value of the variable must be placed in a `PyObject`.
+This is done in a function which is executed when the module is imported for the first time.
+
+#### Example
+
+The following Python code:
+```python
+x = 3
+```
+
+leads to the following module initialisation function:
+```c
+int32_t tmp_exec_func(PyObject* mod)
+{
+    PyObject* Dummy_0001;
+    // Call the C function to initialise the module
+    tmp__init();
+    // Pack the module variable into a PyObject
+    Dummy_0001 = Int64_to_PyLong(&x);
+    // Add the PyObject to the Python module.
+    if (PyModule_AddObject(mod, "x", Dummy_0001) < INT64_C(0))
+    {
+        // Raise an error in case of problems
+        return -INT64_C(1);
+    }
+    return INT64_C(0);
+}
+```
diff --git a/pyccel/ast/bind_c.py b/pyccel/ast/bind_c.py
index 07f945abd5..b33f82fab4 100644
--- a/pyccel/ast/bind_c.py
+++ b/pyccel/ast/bind_c.py
@@ -13,17 +13,38 @@
 from pyccel.ast.core import FunctionDef
 from pyccel.ast.core import FunctionDefArgument, FunctionDefResult
 from pyccel.ast.datatypes import DataType, NativeInteger
+from pyccel.ast.variable import Variable
 
 __all__ = (
+    'BindCArrayVariable',
     'BindCFunctionDef',
     'BindCFunctionDefArgument',
     'BindCFunctionDefResult',
     'BindCModule',
     'BindCPointer',
+    'BindCVariable',
     'CLocFunc',
     'C_F_Pointer',
 )
 
+# =======================================================================================
+#                                    Datatypes
+# =======================================================================================
+
+class BindCPointer(DataType):
+    """
+    Datatype representing a C pointer in Fortran.
+
+    Datatype representing a C pointer in Fortran. This data type is defined
+    in the iso_c_binding module.
+    """
+    __slots__ = ()
+    _name = 'bindcpointer'
+
+# =======================================================================================
+#                                   Wrapper classes
+# =======================================================================================
+
 
 class BindCFunctionDef(FunctionDef):
     """
@@ -352,6 +373,10 @@ class BindCModule(Module):
     variable_wrappers : list of BindCFunctionDef
         A list containing all the functions which expose module variables to C.
 
+    removed_functions : list of FunctionDef
+        A list of any functions which weren't translated to BindCFunctionDef
+        objects (e.g. private functions).
+
     **kwargs : dict
         See `pyccel.ast.core.Module`.
 
@@ -361,12 +386,13 @@ class BindCModule(Module):
         The class from which BindCModule inherits which contains all details
         about the args and kwargs.
     """
-    __slots__ = ('_orig_mod','_variable_wrappers')
-    _attribute_nodes = ('_orig_mod','_variable_wrappers')
+    __slots__ = ('_orig_mod','_variable_wrappers', '_removed_functions')
+    _attribute_nodes = Module._attribute_nodes + ('_orig_mod','_variable_wrappers', '_removed_functions')
 
-    def __init__(self, *args, original_module, variable_wrappers = (), **kwargs):
+    def __init__(self, *args, original_module, variable_wrappers = (), removed_functions = None, **kwargs):
         self._orig_mod = original_module
         self._variable_wrappers = variable_wrappers
+        self._removed_functions = removed_functions
         super().__init__(*args, **kwargs)
 
     @property
@@ -387,18 +413,134 @@ def variable_wrappers(self):
         """
         return self._variable_wrappers
 
+    @property
+    def removed_functions(self):
+        """
+        Get the functions which weren't translated to BindCFunctionDef objects.
+
+        Get a list of the functions which weren't translated to BindCFunctionDef objects.
+        This includes private functions and objects for which wrapper support is lacking.
+        """
+        return self._removed_functions
+
+    @property
+    def declarations(self):
+        """
+        Get the declarations of all module variables.
+
+        In the case of a BindCModule no variables should be declared. Basic variables
+        are used directly from the original module and more complex variables require
+        wrapper functions.
+        """
+        return ()
+
 # =======================================================================================
 
-class BindCPointer(DataType):
+class BindCVariable(Variable):
     """
-    Datatype representing a C pointer in Fortran.
+    A class which wraps a compatible variable from Fortran to make it available in C.
 
-    Datatype representing a C pointer in Fortran. This data type is defined
-    in the iso_c_binding module.
+    A class which wraps a compatible variable from Fortran to make it available in C.
+    A compatible variable is a variable which can be exposed to C simply using
+    iso_c_binding (i.e. no wrapper function is required).
+
+    Parameters
+    ----------
+    *args : tuple
+        See Variable.
+
+    **kwargs : dict
+        See Variable.
+
+    See Also
+    --------
+    Variable : The super class.
     """
-    __slots__ = ()
-    _name = 'bindcpointer'
+    __slots__ = ('_f_name',)
 
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self._f_name = self._name.lower()
+
+    @property
+    def name(self):
+        """
+        The name of the external variable that should be printed in C.
+
+        The name of the external variable that should be printed in C.
+        In order to be compatible with Fortran the name must be printed
+        in lower case letters.
+        """
+        return self._f_name
+
+    @property
+    def indexed_name(self):
+        """
+        The name under which the variable is indexed in the scope.
+
+        The name under which the variable is indexed in the scope. This is
+        important in order to be able to collect the original Python name
+        used by the user in case of collisions.
+        """
+        return self._name
+
+# =======================================================================================
+
+class BindCArrayVariable(Variable):
+    """
+    A class which wraps an array from Fortran to make it available in C.
+
+    A class which wraps an array from Fortran to make it available in C.
+
+    Parameters
+    ----------
+    *args : tuple
+        See Variable.
+
+    wrapper_function : FunctionDef
+        The function which can be used to access the array.
+
+    original_variable : Variable
+        The original variable in the Fortran code.
+
+    **kwargs : dict
+        See Variable.
+
+    See Also
+    --------
+    Variable : The super class.
+    """
+    __slots__ = ('_wrapper_function', '_original_variable')
+    _attribute_nodes = ('_wrapper_function', '_original_variable')
+    def __init__(self, *args, wrapper_function, original_variable, **kwargs):
+        self._original_variable = original_variable
+        self._wrapper_function = wrapper_function
+        super().__init__(*args, **kwargs)
+
+    @property
+    def original_variable(self):
+        """
+        The original variable in the Fortran code.
+
+        The original variable in the Fortran code. This is important in
+        order to access the correct type and other details about the
+        Variable.
+        """
+        return self._original_variable
+
+    @property
+    def wrapper_function(self):
+        """
+        The function which can be used to access the array.
+
+        The function which can be used to access the array. The function
+        must return the pointer to the raw data and information about
+        the shape.
+        """
+        return self._wrapper_function
+
+# =======================================================================================
+#                                   Utility functions
 # =======================================================================================
 
 class CLocFunc(Basic):
diff --git a/pyccel/ast/core.py b/pyccel/ast/core.py
index a3280ac57d..ebecb54657 100644
--- a/pyccel/ast/core.py
+++ b/pyccel/ast/core.py
@@ -2060,6 +2060,8 @@ class FunctionCall(PyccelAstNode):
 
     def __init__(self, func, args, current_function=None):
 
+        for a in args:
+            assert not isinstance(a, FunctionDefArgument)
         # Ensure all arguments are of type FunctionCallArgument
         args = [a if isinstance(a, FunctionCallArgument) else FunctionCallArgument(a) for a in args]
 
@@ -2693,17 +2695,29 @@ def set_recursive(self):
         """ Mark the function as a recursive function """
         self._is_recursive = True
 
-    def clone(self, newname):
+    def clone(self, newname, **new_kwargs):
         """
-        Create an identical FunctionDef with name
-        newname.
+        Create an almost identical FunctionDef with name `newname`.
+
+        Create an almost identical FunctionDef with name `newname`.
+        Additional parameters can be passed to alter the resulting
+        FunctionDef.
 
         Parameters
         ----------
-        newname: str
-            new name for the FunctionDef
+        newname : str
+            New name for the FunctionDef.
+
+        **new_kwargs : dict
+            Any new keyword arguments to be passed to the new FunctionDef.
+
+        Returns
+        -------
+        FunctionDef
+            The clone of the function definition.
         """
         args, kwargs = self.__getnewargs__()
+        kwargs.update(new_kwargs)
         cls = type(self)
         new_func = cls(*args, **kwargs)
         new_func.rename(newname)
diff --git a/pyccel/ast/cwrapper.py b/pyccel/ast/cwrapper.py
index d25b008b3a..35a9377363 100644
--- a/pyccel/ast/cwrapper.py
+++ b/pyccel/ast/cwrapper.py
@@ -18,10 +18,13 @@
 from .datatypes import NativeBool, NativeString
 
 from .core      import FunctionDefArgument, FunctionDefResult
-from .core      import FunctionCall, FunctionDef, FunctionAddress
+from .core      import FunctionDef
+from .core      import Module, Interface
 
 from .internals import get_final_precision
 
+from .literals  import LiteralString
+
 from .variable  import Variable
 
 from .c_concepts import ObjectAddress
@@ -33,6 +36,9 @@
 #
 # --------- CLASSES -----------
 #
+    'PyFunctionDef',
+    'PyInterface',
+    'PyModule',
     'PyccelPyObject',
     'PyArgKeywords',
     'PyArg_ParseTupleNode',
@@ -45,18 +51,18 @@
     'flags_registry',
 #----- C / PYTHON FUNCTIONS ---
     'Py_DECREF',
-    'set_python_error_message',
-#----- CHECK FUNCTIONS ---
-    'generate_datatype_error',
-    'scalar_object_check',
 )
 
 #-------------------------------------------------------------------
 #                        Python DataTypes
 #-------------------------------------------------------------------
 class PyccelPyObject(DataType):
-    """ Datatype representing a PyObject which is the
-    class used to hold python objects"""
+    """
+    Datatype representing a `PyObject`.
+
+    Datatype representing a `PyObject` which is the
+    class used to hold Python objects in `Python.h`.
+    """
     __slots__ = ()
     _name = 'pyobject'
 
@@ -98,75 +104,53 @@ def arg_names(self):
         """
         return self._arg_names
 
-#using the documentation of PyArg_ParseTuple() and Py_BuildValue https://docs.python.org/3/c-api/arg.html
-pytype_parse_registry = {
-    (NativeInteger(), 4)       : 'i',
-    (NativeInteger(), 8)       : 'l',
-    (NativeInteger(), 2)       : 'h',
-    (NativeInteger(), 1)       : 'b',
-    (NativeFloat(), 8)         : 'd',
-    (NativeFloat(), 4)         : 'f',
-    (NativeComplex(), 4)       : 'O',
-    (NativeComplex(), 8)       : 'O',
-    (NativeBool(), -1)         : 'p',
-    (NativeString(), 0)        : 's',
-    (PyccelPyObject(), 0)      : 'O',
-    }
-
+#-------------------------------------------------------------------
 class PyArg_ParseTupleNode(Basic):
     """
-    Represents a call to the function from Python.h which collects the expected arguments
+    Represents a call to the function `PyArg_ParseTupleNode`.
+
+    Represents a call to the function `PyArg_ParseTupleNode` from `Python.h`.
+    This function collects the expected arguments from `self`, `args`, `kwargs`
+    and packs them into variables with datatype `PyccelPyObject`.
 
     Parameters
     ----------
-    python_func_args: Variable
-        Args provided to the function in python
-    python_func_kwargs: Variable
-        Kwargs provided to the function in python
-    c_func_args: list of Variable
-        List of expected arguments. This helps determine the expected output types
-    parse_args: list of Variable
-        List of arguments into which the result will be collected
+    python_func_args : Variable
+        Args provided to the function in Python.
+    python_func_kwargs : Variable
+        Kwargs provided to the function in Python.
+    c_func_args : list of Variable
+        List of expected arguments. This helps determine the expected output types.
+    parse_args : list of Variable
+        List of arguments into which the result will be collected.
     arg_names : list of str
-        A list of the names of the function arguments
-    is_interface : boolean
-        Default value False and True when working with interface functions
+        A list of the names of the function arguments.
     """
     __slots__ = ('_pyarg','_pykwarg','_parse_args','_arg_names','_flags')
     _attribute_nodes = ('_pyarg','_pykwarg','_parse_args','_arg_names')
 
     def __init__(self, python_func_args,
                         python_func_kwargs,
-                        c_func_args, parse_args,
+                        c_func_args,
+                        parse_args,
                         arg_names):
         if not isinstance(python_func_args, Variable):
             raise TypeError('Python func args should be a Variable')
         if not isinstance(python_func_kwargs, Variable):
             raise TypeError('Python func kwargs should be a Variable')
-        if not all(isinstance(c, FunctionDefArgument) for c in c_func_args):
-            raise TypeError('C func args should be a list of Arguments')
         if not isinstance(parse_args, list) and any(not isinstance(c, Variable) for c in parse_args):
             raise TypeError('Parse args should be a list of Variables')
         if not isinstance(arg_names, PyArgKeywords):
             raise TypeError('Parse args should be a list of Variables')
-        if len(parse_args) != len(c_func_args):
-            raise TypeError('There should be the same number of c_func_args and parse_args')
 
-        self._flags      = ''
         i = 0
-
         while i < len(c_func_args) and not c_func_args[i].has_default:
-            self._flags += self.get_pytype(c_func_args[i], parse_args[i])
             i+=1
+        self._flags = 'O'*i
+
         if i < len(c_func_args):
             self._flags += '|'
-        while i < len(c_func_args):
-            self._flags += self.get_pytype(c_func_args[i], parse_args[i])
-            i+=1
-        # Restriction as of python 3.8
-        if any([isinstance(a, (Variable, FunctionAddress)) and a.is_kwonly for a in c_func_args]):
-            errors.report('Kwarg only arguments without default values will not raise an error if they are not passed',
-                          symbol=c_func_args, severity='warning')
+            self._flags += 'O'*(len(c_func_args)-i)
 
         self._pyarg      = python_func_args
         self._pykwarg    = python_func_kwargs
@@ -174,17 +158,6 @@ def __init__(self, python_func_args,
         self._arg_names  = arg_names
         super().__init__()
 
-    def get_pytype(self, c_arg, parse_arg):
-        """Return the needed flag to parse or build value
-        """
-        if isinstance(c_arg, FunctionAddress):
-            return 'O'
-        else:
-            try:
-                return pytype_parse_registry[(parse_arg.dtype, get_final_precision(parse_arg))]
-            except KeyError as e:
-                raise NotImplementedError("Type not implemented for argument collection : "+str(type(parse_arg))) from e
-
     @property
     def pyarg(self):
         """ The  variable containing all positional arguments
@@ -201,9 +174,11 @@ def pykwarg(self):
 
     @property
     def flags(self):
-        """ The flags indicating the types of the objects to
-        be collected from the python arguments passed to the
-        function
+        """
+        The flags indicating the types of the objects.
+
+        The flags indicating the types of the objects to be collected from
+        the Python arguments passed to the function.
         """
         return self._flags
 
@@ -221,6 +196,7 @@ def arg_names(self):
         """
         return self._arg_names
 
+#-------------------------------------------------------------------
 class PyBuildValueNode(PyccelAstNode):
     """
     Represents a call to the function PyBuildValueNode.
@@ -284,7 +260,7 @@ class PyModule_AddObject(PyccelAstNode):
     _shape = None
 
     def __init__(self, mod_name, name, variable):
-        if not isinstance(name, str):
+        if not isinstance(name, LiteralString):
             raise TypeError("Name must be a string")
         if not isinstance(variable, Variable) or \
                 variable.dtype is not PyccelPyObject():
@@ -312,6 +288,200 @@ def variable(self):
         """
         return self._var
 
+#-------------------------------------------------------------------
+class PyModule(Module):
+    """
+    Class to hold a module which is accessible from Python.
+
+    Class to hold a module which is accessible from Python. This class
+    adds external functions and external declarations to the basic
+    Module. However its main utility is in order to differentiate
+    itself such that a different `_print` function can be implemented
+    to handle it.
+
+    Parameters
+    ----------
+    *args : tuple
+        See Module.
+
+    external_funcs : iterable of FunctionDef
+        A list of external functions.
+
+    declarations : iterable
+        Any declarations of (external) variables which should be made in the module.
+
+    **kwargs : dict
+        See Module.
+
+    See Also
+    --------
+    Module : The super class from which the class inherits.
+    """
+    __slots__ = ('_external_funcs', '_declarations')
+    _attribute_nodes = Module._attribute_nodes + ('_external_funcs', '_declarations')
+    def __init__(self, *args, external_funcs = (), declarations = (), **kwargs):
+        self._external_funcs = external_funcs
+        self._declarations = declarations
+        super().__init__(*args, **kwargs)
+
+    @property
+    def external_funcs(self):
+        """
+        A list of external functions.
+
+        The external functions which should be declared at the start of the module.
+        This is useful for declaring the existence of Fortran functions whose
+        definition and declaration is inaccessible from C.
+        """
+        return self._external_funcs
+
+    @external_funcs.setter
+    def external_funcs(self, funcs):
+        for f in self._external_funcs:
+            f.remove_user_node(self)
+        self._external_funcs = funcs
+        for f in funcs:
+            f.set_current_user_node(self)
+
+    @property
+    def declarations(self):
+        """
+        All declarations that need printing in the module.
+
+        All declarations that need printing in the module. This usually includes
+        any variables coming from a non-C language for which compatibility with C
+        exists.
+        """
+        return self._declarations
+
+    @declarations.setter
+    def declarations(self, decs):
+        for d in self._declarations:
+            d.remove_user_node(self)
+        self._declarations = decs
+        for d in decs:
+            d.set_current_user_node(self)
+
+#-------------------------------------------------------------------
+class PyFunctionDef(FunctionDef):
+    """
+    Class to hold a FunctionDef which is accessible from Python.
+
+    Contains the Python-compatible version of the function which is
+    used for the wrapper.
+    As compared to a normal FunctionDef, this version contains
+    arguments for the shape of arrays. It should be generated by
+    calling `codegen.wrapper.CToPythonWrapper.wrap`.
+
+    Parameters
+    ----------
+    *args : list
+        See FunctionDef.
+
+    original_function : FunctionDef
+        The function from which the Python-compatible version was created.
+
+    **kwargs : dict
+        See FunctionDef.
+
+    See Also
+    --------
+    pyccel.ast.core.FunctionDef
+        The class from which BindCFunctionDef inherits which contains all
+        details about the args and kwargs.
+    """
+    __slots__ = ('_original_function',)
+    _attribute_nodes = (*FunctionDef._attribute_nodes, '_original_function')
+
+    def __init__(self, *args, original_function, **kwargs):
+        self._original_function = original_function
+        super().__init__(*args, **kwargs)
+
+    @property
+    def original_function(self):
+        """
+        The function which is wrapped by this PyFunctionDef.
+
+        The original function which would be printed in pure C which is not
+        compatible with Python.
+        """
+        return self._original_function
+
+#-------------------------------------------------------------------
+class PyInterface(Interface):
+    """
+    Class to hold an Interface which is accessible from Python.
+
+    A class which holds the Python-compatible Interface. It contains functions for
+    determining the type of the arguments passed to the Interface and the functions
+    called through the interface.
+
+    Parameters
+    ----------
+    name : str
+        The name of the interface. See Interface.
+
+    functions : iterable of FunctionDef
+        The functions of the interface. See Interface.
+
+    interface_func : FunctionDef
+        The function which Python will call to access the interface.
+
+    type_check_func : FunctionDef
+        The helper function which will determine the types of the arguments passed.
+
+    original_interface : Interface
+        The interface being wrapped.
+
+    **kwargs : dict
+        See Interface.
+
+    See Also
+    --------
+    Interface : The super class.
+    """
+    __slots__ = ('_interface_func', '_type_check_func', '_original_interface')
+    _attribute_nodes = Interface._attribute_nodes + ('_interface_func', '_type_check_func',
+                        '_original_interface')
+
+    def __init__(self, name, functions, interface_func, type_check_func, original_interface, **kwargs):
+        self._interface_func = interface_func
+        self._type_check_func = type_check_func
+        self._original_interface = original_interface
+        for f in functions:
+            if not isinstance(f, PyFunctionDef):
+                raise TypeError("PyInterface functions should be instances of the class PyFunctionDef.")
+        super().__init__(name, functions, False, **kwargs)
+
+    @property
+    def interface_func(self):
+        """
+        The function which is exposed to Python.
+
+        The function which receives the Python arguments `self`, `args`, and `kwargs` and calls
+        the appropriate function.
+        """
+        return self._interface_func
+
+    @property
+    def type_check_func(self):
+        """
+        The function which determines the types which were passed to the Interface.
+
+        The function which takes the arguments passed to the function and returns an integer
+        indicating which function was called.
+        """
+        return self._type_check_func
+
+    @property
+    def original_function(self):
+        """
+        The Interface which is wrapped by this PyInterface.
+
+        The original interface which would be printed in C.
+        """
+        return self._original_interface
+
 #-------------------------------------------------------------------
 #                      Python.h Constants
 #-------------------------------------------------------------------
@@ -329,41 +499,25 @@ def variable(self):
                         arguments = [FunctionDefArgument(Variable(dtype=PyccelPyObject(), name='o', memory_handling='alias'))],
                         results = [])
 
+#using the documentation of PyArg_ParseTuple() and Py_BuildValue https://docs.python.org/3/c-api/arg.html
+pytype_parse_registry = {
+    (NativeInteger(), 4)       : 'i',
+    (NativeInteger(), 8)       : 'l',
+    (NativeInteger(), 2)       : 'h',
+    (NativeInteger(), 1)       : 'b',
+    (NativeFloat(), 8)         : 'd',
+    (NativeFloat(), 4)         : 'f',
+    (NativeComplex(), 4)       : 'O',
+    (NativeComplex(), 8)       : 'O',
+    (NativeBool(), -1)         : 'p',
+    (NativeString(), 0)        : 's',
+    (PyccelPyObject(), 0)      : 'O',
+    }
+
 #-------------------------------------------------------------------
 #                      cwrapper.h functions
 #-------------------------------------------------------------------
 
-def Python_to_C(c_object):
-    """
-    Create a FunctionDef responsible for casting scalar Python argument to C.
-
-    Creates a FunctionDef node which contains all the code necessary
-    for casting a PythonObject to a C object whose characteristics
-    match that of the object passed as an argument.
-
-    Parameters
-    ----------
-    c_object : Variable
-        The variable needed for the generation of the cast_function.
-
-    Returns
-    -------
-    FunctionDef
-        The function which casts the Python object to C.
-    """
-    dtype = c_object.dtype
-    prec  = get_final_precision(c_object)
-    try :
-        cast_function = py_to_c_registry[(dtype, prec)]
-    except KeyError:
-        errors.report(PYCCEL_RESTRICTION_TODO, symbol=dtype,severity='fatal')
-    cast_func = FunctionDef(name = cast_function,
-                       body      = [],
-                       arguments = [FunctionDefArgument(Variable(dtype=PyccelPyObject(), name = 'o', memory_handling='alias'))],
-                       results   = [FunctionDefResult(Variable(dtype=dtype, name = 'v', precision = prec))])
-
-    return cast_func
-
 # Functions definitions are defined in pyccel/stdlib/cwrapper/cwrapper.c
 py_to_c_registry = {
     (NativeBool(), -1)     : 'PyBool_to_Bool',
@@ -396,15 +550,17 @@ def C_to_Python(c_object):
     """
     if c_object.rank != 0:
         cast_function = 'ndarray_to_pyarray'
+        memory_handling = 'stack'
     else:
         try :
             cast_function = c_to_py_registry[(c_object.dtype, c_object.precision)]
         except KeyError:
             errors.report(PYCCEL_RESTRICTION_TODO, symbol=c_object.dtype,severity='fatal')
+        memory_handling = 'alias'
 
     cast_func = FunctionDef(name = cast_function,
                        body      = [],
-                       arguments = [FunctionDefArgument(Variable(dtype=c_object.dtype, name = 'v', precision = c_object.precision))],
+                       arguments = [FunctionDefArgument(c_object.clone('v', is_argument = True, memory_handling=memory_handling))],
                        results   = [FunctionDefResult(Variable(dtype=PyccelPyObject(), name = 'o', memory_handling='alias'))])
 
     return cast_func
@@ -441,69 +597,8 @@ def C_to_Python(c_object):
                            FunctionDefArgument(Variable(dtype = NativeString(), name = 's'))],
               results   = [])
 
-def set_python_error_message(exception, message):
-    """
-    Generate a function call which sets the Python error.
-
-    Generate a function call of C/Python API PyErr_SetString
-    https://docs.python.org/3/c-api/exceptions.html#c.PyErr_SetString
-    with a defined error message used to set the error indicator.
-
-    Parameters
-    ----------
-    exception : str
-        The error exception type.
-    message : str
-        The message which will be shown.
-
-    Returns
-    -------
-    FunctionCall
-        The FunctionCall which raises the error.
-    """
-    func = FunctionDef(name = 'PyErr_SetString',
-                  body      = [],
-                  arguments = [FunctionDefArgument(Variable(dtype = PyccelPyObject(), name = 'o')),
-                               FunctionDefArgument(Variable(dtype = NativeString(), name = 's'))],
-                  results   = [])
-
-    exception = Variable(PyccelPyObject(), name = exception)
-
-    return FunctionCall(func, [exception, message])
-
-
-def generate_datatype_error(variable):
-    """
-    Generate TypeError exception from the variable information.
-
-    Generate a TypeError exception indicated that the variable passed
-    as an argument does not have the right datatype/precision.
-
-    Parameters
-    ----------
-    variable : Variable
-        The variable which indicates the correct datatype/precision.
-
-    Returns
-    -------
-    FunctionCall
-        Call to PyErr_SetString with TypeError as exception and custom message.
-    """
-    dtype     = variable.dtype
-
-    if variable.precision == -1:
-        precision = 'native '
-    elif isinstance(dtype, NativeBool):
-        precision = ''
-    elif isinstance(dtype, NativeComplex):
-        precision = '{} bit '.format(variable.precision * 2 * 8)
-    else:
-        precision = '{} bit '.format(variable.precision * 8)
-
-    message = '"Argument must be {precision}{dtype}"'.format(
-            precision = precision,
-            dtype     = variable.dtype)
-    return set_python_error_message('PyExc_TypeError', message)
+PyNotImplementedError = Variable(PyccelPyObject(), name = 'PyExc_NotImplementedError')
+PyTypeError = Variable(PyccelPyObject(), name = 'PyExc_TypeError')
 
 
 # Functions definitions are defined in pyccel/stdlib/cwrapper/cwrapper.c
@@ -520,36 +615,3 @@ def generate_datatype_error(variable):
     (NativeComplex(), -1)  : 'PyIs_NativeComplex',
     (NativeComplex(), 4)   : 'PyIs_Complex64',
     (NativeComplex(), 8)   : 'PyIs_Complex128'}
-
-def scalar_object_check(py_object, c_object):
-    """
-    Create FunctionCall responsible for checking Python argument data type.
-
-    Create a FunctionCall which checks whether the Python argument
-    passed as an argument is a scalar with a type which matches the
-    type of the C object.
-
-    Parameters
-    ----------
-    py_object : Variable
-        The Python argument of the check function.
-    c_object : Variable
-        The variable needed for the generation of the type check.
-
-    Returns
-    -------
-    FunctionCall
-        Check type FunctionCall.
-    """
-
-    try :
-        check_type = check_type_registry[c_object.dtype, c_object.precision]
-    except KeyError:
-        errors.report(PYCCEL_RESTRICTION_TODO, symbol=c_object.dtype,severity='fatal')
-
-    check_func = FunctionDef(name = check_type,
-                    body      = [],
-                    arguments = [FunctionDefArgument(Variable(dtype=PyccelPyObject(), name = 'o', memory_handling = 'alias'))],
-                    results   = [FunctionDefResult(Variable(dtype=NativeBool(), name = 'r'))])
-
-    return FunctionCall(check_func, [py_object])
diff --git a/pyccel/ast/numpy_wrapper.py b/pyccel/ast/numpy_wrapper.py
index 07bf400772..1edf438641 100644
--- a/pyccel/ast/numpy_wrapper.py
+++ b/pyccel/ast/numpy_wrapper.py
@@ -103,26 +103,26 @@ def get_numpy_max_acceptable_version_file():
 # Return the shape of the n-th dimension : function definition in pyccel/stdlib/cwrapper/cwrapper_ndarrays.c
 array_get_dim  = FunctionDef(name    = 'nd_ndim',
                            body      = [],
-                           arguments = [FunctionDefArgument(Variable(dtype=NativeVoid(), name = 'o', memory_handling='alias')),
+                           arguments = [FunctionDefArgument(Variable(dtype=NativeVoid(), name = 'o', is_optional = True)),
                                         FunctionDefArgument(Variable(dtype=NativeInteger(), name = 'idx'))],
                            results   = [FunctionDefResult(Variable(dtype=NativeInteger(), name = 'd'))])
 
 # Return the stride of the n-th dimension : function definition in pyccel/stdlib/cwrapper/cwrapper_ndarrays.c
 array_get_c_step = FunctionDef(name    = 'nd_nstep_C',
                            body      = [],
-                           arguments = [FunctionDefArgument(Variable(dtype=NativeVoid(), name = 'o', memory_handling='alias')),
+                           arguments = [FunctionDefArgument(Variable(dtype=NativeVoid(), name = 'o', is_optional = True)),
                                         FunctionDefArgument(Variable(dtype=NativeInteger(), name = 'idx'))],
                            results   = [FunctionDefResult(Variable(dtype=NativeInteger(), name = 'd'))])
 array_get_f_step = FunctionDef(name    = 'nd_nstep_F',
                            body      = [],
-                           arguments = [FunctionDefArgument(Variable(dtype=NativeVoid(), name = 'o', memory_handling='alias')),
+                           arguments = [FunctionDefArgument(Variable(dtype=NativeVoid(), name = 'o', is_optional = True)),
                                         FunctionDefArgument(Variable(dtype=NativeInteger(), name = 'idx'))],
                            results   = [FunctionDefResult(Variable(dtype=NativeInteger(), name = 'd'))])
 
 # Return the data of ndarray : function definition in pyccel/stdlib/cwrapper/cwrapper_ndarrays.c
 array_get_data  = FunctionDef(name   = 'nd_data',
                            body      = [],
-                           arguments = [FunctionDefArgument(Variable(dtype=NativeVoid(), name = 'o', memory_handling='alias'))],
+                           arguments = [FunctionDefArgument(Variable(dtype=NativeVoid(), name = 'o', is_optional=True))],
                            results   = [FunctionDefResult(Variable(dtype=NativeVoid(), name = 'v', memory_handling='alias', rank = 1))])
 
 # Basic Array Flags
diff --git a/pyccel/codegen/printing/ccode.py b/pyccel/codegen/printing/ccode.py
index acc894316a..d30e778650 100644
--- a/pyccel/codegen/printing/ccode.py
+++ b/pyccel/codegen/printing/ccode.py
@@ -447,7 +447,7 @@ def copy_NumpyArray_Data(self, expr):
                     operations += self._print(AugAssign(offset_var, '+', LiteralInteger(lenSubset)))
 
         if order == "F":
-            operations += f"array_copy_data({lhs_address}, {copy_to}, 0);\nfree_array({copy_to});\n"
+            operations += f"array_copy_data({lhs_address}, {self._print(copy_to)}, 0);\n" + self._print(Deallocate(copy_to))
         return operations
 
     def arrayFill(self, expr):
@@ -1163,7 +1163,9 @@ def _print_Declare(self, expr):
             preface = ''
             init    = ''
 
-        declaration = f'{declaration_type} {variable}{init};\n'
+        external = 'extern ' if expr.external else ''
+
+        declaration = f'{external}{declaration_type} {variable}{init};\n'
 
         return preface + declaration
 
@@ -1233,8 +1235,9 @@ def get_arg_declaration(var):
                 """ Get the code which declares the argument variable.
                 """
                 code = "const " * var.is_const
-                code += self.get_declare_type(var) + ' '
-                code += var.name * print_arg_names
+                code += self.get_declare_type(var)
+                if print_arg_names:
+                    code += ' ' + var.name
                 return code
 
             arg_code_list = [self.function_signature(var, False) if isinstance(var, FunctionAddress)
@@ -1419,9 +1422,10 @@ def _print_Allocate(self, expr):
     def _print_Deallocate(self, expr):
         if isinstance(expr.variable, InhomogeneousTupleVariable):
             return ''.join(self._print(Deallocate(v)) for v in expr.variable)
+        variable_address = self._print(ObjectAddress(expr.variable))
         if expr.variable.is_alias:
-            return 'free_pointer({});\n'.format(self._print(expr.variable))
-        return 'free_array({});\n'.format(self._print(expr.variable))
+            return f'free_pointer({variable_address});\n'
+        return f'free_array({variable_address});\n'
 
     def _print_Slice(self, expr):
         start = self._print(expr.start)
@@ -1678,8 +1682,12 @@ def _print_Interface(self, expr):
     def _print_FunctionDef(self, expr):
         if expr.is_inline:
             return ''
+
         self.set_scope(expr.scope)
 
+        # Reinitialise optional partners
+        self._optional_partners = {}
+
         arguments = [a.var for a in expr.arguments]
         results = [r.var for r in expr.results]
         if len(expr.results) > 1:
@@ -2044,6 +2052,29 @@ def _print_PythonConjugate(self, expr):
         return 'conj({})'.format(self._print(expr.internal_var))
 
     def _handle_is_operator(self, Op, expr):
+        """
+        Get the code to print an `is` or `is not` expression.
+
+        Get the code to print an `is` or `is not` expression. These two operators
+        function similarly so this helper function reduces code duplication.
+
+        Parameters
+        ----------
+        Op : str
+            The C operator representing "is" or "is not".
+
+        expr : PyccelIs/PyccelIsNot
+            The expression being printed.
+
+        Returns
+        -------
+        str
+            The code describing the expression.
+
+        Raises
+        ------
+        PyccelError : Raised if the comparison is poorly defined.
+        """
 
         lhs = self._print(expr.lhs)
         rhs = self._print(expr.rhs)
diff --git a/pyccel/codegen/printing/cwrappercode.py b/pyccel/codegen/printing/cwrappercode.py
index a62e9871b8..dcbbde0a3f 100644
--- a/pyccel/codegen/printing/cwrappercode.py
+++ b/pyccel/codegen/printing/cwrappercode.py
@@ -4,51 +4,22 @@
 # go to https://github.com/pyccel/pyccel/blob/master/LICENSE for full license details.     #
 #------------------------------------------------------------------------------------------#
 
-from collections import OrderedDict
-
 from pyccel.codegen.printing.ccode import CCodePrinter
 
 from pyccel.ast.bind_c   import BindCPointer
-from pyccel.ast.bind_c   import BindCModule, BindCFunctionDef, BindCFunctionDefArgument
-from pyccel.ast.bind_c   import BindCFunctionDefResult
-
-from pyccel.ast.builtins import PythonTuple, PythonType
-
-from pyccel.ast.core import Assign, AliasAssign, FunctionDef, FunctionAddress
-from pyccel.ast.core import If, IfSection, Return, FunctionCall, Deallocate
-from pyccel.ast.core import SeparatorComment, Allocate
-from pyccel.ast.core import Import, Module, Declare
-from pyccel.ast.core import AugAssign, CodeBlock
-from pyccel.ast.core import FunctionDefArgument, FunctionDefResult
-
-from pyccel.ast.cwrapper    import PyArg_ParseTupleNode, PyBuildValueNode
-from pyccel.ast.cwrapper    import PyArgKeywords
-from pyccel.ast.cwrapper    import Py_None, Py_DECREF
-from pyccel.ast.cwrapper    import generate_datatype_error, set_python_error_message
-from pyccel.ast.cwrapper    import scalar_object_check
-from pyccel.ast.cwrapper    import PyccelPyObject
-from pyccel.ast.cwrapper    import C_to_Python, Python_to_C
-from pyccel.ast.cwrapper    import PyModule_AddObject
-
-from pyccel.ast.datatypes import NativeInteger, NativeBool, NativeFloat
-from pyccel.ast.datatypes import datatype, NativeVoid
+from pyccel.ast.bind_c   import BindCModule, BindCFunctionDef
 
-from pyccel.ast.literals  import LiteralTrue, LiteralInteger, LiteralString
-from pyccel.ast.literals  import Nil
+from pyccel.ast.core import FunctionAddress, SeparatorComment
+from pyccel.ast.core import Import, Module
 
-from pyccel.ast.numpy_wrapper   import array_type_check
-from pyccel.ast.numpy_wrapper   import pyarray_to_ndarray
-from pyccel.ast.numpy_wrapper   import array_get_data, array_get_dim, array_get_c_step, array_get_f_step
-
-from pyccel.ast.operators import PyccelEq, PyccelNot, PyccelOr, PyccelAssociativeParenthesis
-from pyccel.ast.operators import PyccelIsNot, PyccelLt, PyccelUnarySub
+from pyccel.ast.cwrapper    import PyBuildValueNode
+from pyccel.ast.cwrapper    import Py_None
+from pyccel.ast.cwrapper    import PyccelPyObject
 
-from pyccel.ast.variable  import Variable, DottedVariable
+from pyccel.ast.literals  import LiteralString, Nil
 
 from pyccel.ast.c_concepts import ObjectAddress
 
-from pyccel.parser.scope  import Scope
-
 from pyccel.errors.errors   import Errors
 
 errors = Errors()
@@ -88,7 +59,6 @@ class CWrapperCodePrinter(CCodePrinter):
     def __init__(self, filename, target_language, **settings):
         CCodePrinter.__init__(self, filename, **settings)
         self._target_language = target_language
-        self._cast_functions_dict = OrderedDict()
         self._to_free_PyObject_list = []
         self._function_wrapper_names = dict()
         self._module_name = None
@@ -203,22 +173,6 @@ def get_declare_type(self, expr):
         else:
             return dtype
 
-    def get_new_PyObject(self, name):
-        """
-        Create new PyccelPyObject Variable with the desired name
-
-        Parameters
-        -----------
-        name       : String
-            The desired name
-
-        Returns: Variable
-        -------
-        """
-        return Variable(dtype=PyccelPyObject(),
-                        name=self.scope.get_new_name(name),
-                        memory_handling='alias')
-
     def find_in_dtype_registry(self, dtype, prec):
         """
         Find the corresponding C dtype in the dtype_registry
@@ -241,781 +195,45 @@ def find_in_dtype_registry(self, dtype, prec):
         except KeyError:
             return CCodePrinter.find_in_dtype_registry(self, dtype, prec)
 
-    def get_default_assign(self, arg, func_arg, value):
-        """
-        Provide the Assign which initialises an argument to its default value.
-
-        When a function def argument has a default value, this function
-        provides the code which initialises the argument. This value can
-        then either be used or overwritten with the provided argument.
-
-        Parameters
-        ----------
-        arg : Variable
-            The Variable where the default value should be saved.
-        func_arg : FunctionDefArgument
-            The argument object where the value may be provided.
-        value : PyccelAstNode
-            The default value which should be assigned.
-
-        Returns
-        -------
-        Assign
-            The code describing the assignement.
-
-        Raises
-        ------
-        NotImplementedError
-            If the type of the default value is not handled.
-        """
-        if func_arg.is_optional:
-            return AliasAssign(arg, Py_None)
-        elif isinstance(arg.dtype, (NativeFloat, NativeInteger, NativeBool)):
-            return Assign(arg, value)
-        elif isinstance(arg.dtype, PyccelPyObject):
-            return AliasAssign(arg, Py_None)
-        else:
-            raise NotImplementedError('Default values are not implemented for this datatype : {}'.format(func_arg.dtype))
-
-    def static_function_signature(self, expr):
-        """
-        Get the C representation of the function signature using only basic types.
-
-        Extract from the function definition `expr` all the
-        information (name, input, output) needed to create the
-        function signature used for C/Fortran binding and return
-        a string describing the function.
-
-        Parameters
-        ----------
-        expr : FunctionDef
-            The function definition for which a signature is needed.
-
-        Returns
-        -------
-        str
-            Signature of the function.
-        """
-        #if target_language is C no need for the binding
-        if self._target_language == 'c':
-            return self.function_signature(expr)
-
-        args = [a.var for a in expr.arguments]
-        results = [r.var for r in expr.results]
-        if len(results) == 1:
-            ret_type = self.get_declare_type(results[0])
-        elif len(results) > 1:
-            ret_type = self._print(datatype('int'))
-            args += [a.clone(name = a.name, memory_handling='alias') for a in results]
-        else:
-            ret_type = self._print(datatype('void'))
-        name = expr.name
-        if not args:
-            arg_code = 'void'
-        else:
-            arg_code = ', '.join(self.function_signature(i, False)
-                        if isinstance(i, FunctionAddress)
-                        else self.get_static_declare_type(i)
-                        for i in args)
-
-        return f'{ret_type} {name}({arg_code})'
-
-    def get_static_args(self, argument):
+    def _handle_is_operator(self, Op, expr):
         """
-        Get the value(s) which should be passed for the provided argument.
+        Get the code to print an `is` or `is not` expression.
 
-        Get the value(s) which should be passed to the function for the
-        specified argument. In the case of a BindCFunctionDef (when the
-        target language is Fortran) and an argument with rank > 0,
-        multiple arguments are needed:
-        - buffer holding data.
-        - shape of array in each dimension.
-        - stride for the array in each dimension.
+        Get the code to print an `is` or `is not` expression. These two operators
+        function similarly so this helper function reduces code duplication.
+        This function overrides CCodePrinter._handle_is_operator to add the
+        handling of `Py_None`.
 
         Parameters
         ----------
-        argument : Variable
-            Variable holding information needed (rank).
-
-        Returns
-        -------
-        List of arguments
-            List that can contain Variables and FunctionCalls.
+        Op : str
+            The C operator representing "is" or "is not".
 
-        Examples
-        --------
-        If target language is Fortran:
-        >>> x = Variable('int', 'x', rank=2, order='c')
-        >>> self.get_static_args(x)
-        [&nd_data(x), nd_ndim(x, 0), nd_ndim(x, 1), nd_nstep_C(x, 0), nd_nstep_C(x, 1)]
-
-        If target language is C:
-        >>> x = Variable('int', 'x', rank=2, order='c')
-        >>> self.get_static_args(x)
-        [x]
-        """
-
-        if self._target_language == 'fortran' and argument.rank > 0:
-            arg_address = ObjectAddress(argument)
-            static_args = [ObjectAddress(FunctionCall(array_get_data, [arg_address]))]
-            static_args+= [
-                FunctionCall(array_get_dim, [arg_address, i]) for i in range(argument.rank)
-            ]
-            if argument.order == 'C':
-                static_args+= [
-                    FunctionCall(array_get_c_step, [arg_address, i]) for i in range(argument.rank)
-                ]
-            else:
-                static_args+= [
-                    FunctionCall(array_get_f_step, [arg_address, i]) for i in range(argument.rank)
-                ]
-        else:
-            static_args = [argument]
-
-        return static_args
-
-    def get_static_declare_type(self, variable):
-        """
-        Get the declaration type of a variable which may be passed to a Fortran binding function.
-
-        Get the declaration type of a variable, this function is used for
-        C/Fortran binding using native C datatypes.
-
-        Parameters
-        ----------
-        variable : Variable
-            Variable holding information needed to choose the declaration type.
+        expr : PyccelIs/PyccelIsNot
+            The expression being printed.
 
         Returns
         -------
         str
-            The code describing the type.
-        """
-        dtype = self._print(variable.dtype)
-        prec  = variable.precision
-
-        dtype = self.find_in_dtype_registry(dtype, prec)
-
-        if self.is_c_pointer(variable):
-            return f'{dtype}*'
-
-        else:
-            return dtype
-
-    def get_static_results(self, result):
-        """
-        Get the value(s) which should be used to collect the provided result.
-
-        Get the value(s) which should be collected from the function for the
-        specified argument. In the case of a BindCFunctionDefResult (when the
-        target language is Fortran) and an argument with rank > 0,
-        multiple outputs are needed:
-        - buffer holding data.
-        - shape of array in each dimension.
-        These must then be used to create the expected result variable.
-
-        Parameters
-        ----------
-        result : FunctionDefResult
-            The result of the function which we want to collect.
-
-        Returns
-        -------
-        body : list
-            Additional instructions (allocations and pointer assignments) for function body.
-
-        static_results : list
-            Expanded list of function arguments corresponding to the given result.
-
-        Examples
-        --------
-        If target language is Fortran:
-        >>> x_res = BindCFunctionDefResult(Variable(BindCPointer(), 'x_ptr', memory_handling='alias'), Variable('int', 'x', rank=2, order='c'), scope)
-        >>> self.get_static_results(x)
-        [allocate(x, [x_shape_0, x_shape_1]), x.raw_data=>x_ptr], [&nd_data(x_ptr), x_shape_0, x_shape_1]
-
-        If target language is C:
-        >>> x_res = FunctionDefResult(Variable('int', 'x', rank=2, order='c'))
-        >>> self.get_static_args(x)
-        [], [Variable(x)]
-        """
-
-        body = []
-        var_name = self.scope.get_expected_name(result.var.name)
-
-        if isinstance(result, BindCFunctionDefResult) and result.shape:
-            shape = [self.scope.get_temporary_variable(s) for s in result.shape]
-            orig_name = result.original_function_result_variable.name
-            orig_var = self.scope.find(self.scope.get_expected_name(orig_name), category='variables')
-            nd_var = self.scope.find(var_name, category='variables')
-            body.append(Allocate(orig_var, shape = shape, order = orig_var.order,
-                status='unallocated'))
-            body.append(AliasAssign(DottedVariable(NativeVoid(), 'raw_data', memory_handling = 'alias',
-                lhs=orig_var), nd_var))
-
-            static_results = [ObjectAddress(nd_var), *shape]
-
-        else:
-            var = self.scope.find(var_name, category='variables')
-            static_results = [var]
-
-        return body, static_results
-
-    def _get_static_func_call_code(self, expr, static_func_args, results):
-        """
-        Get all code necessary to call the wrapped function.
-
-        Get the function call which calls the underlying translated function
-        being wrapped. This may involve creating new variables in order to
-        call the function in a compatible way.
+            The code describing the expression.
 
-        Parameters
-        ----------
-        expr : FunctionDef
-            The function being wrapped.
-
-        static_func_args : List of arguments
-            Arguments compatible with the static function.
-
-        results : List of results
-            Results of the wrapped function.
-
-        Returns
-        -------
-        list of pyccel.ast.basic.Basic
-            List of nodes describing the instructions which call the
-            wrapped function.
-        """
-        body = []
-        if len(results) == 0:
-            body.append(FunctionCall(expr, static_func_args))
+        Raises
+        ------
+        PyccelError : Raised if the comparison is poorly defined.
+        """
+        if expr.rhs is Py_None:
+            lhs = ObjectAddress(expr.lhs)
+            rhs = ObjectAddress(expr.rhs)
+            lhs = self._print(lhs)
+            rhs = self._print(rhs)
+            return f'{lhs} {Op} {rhs}'
         else:
-            static_func_results = []
-            for r in results:
-                b, s = self.get_static_results(r)
-                body.extend(b)
-                static_func_results.extend(s)
-
-            results   = static_func_results if len(static_func_results)>1 else static_func_results[0]
-            func_call = Assign(results,FunctionCall(expr, static_func_args))
-            body.insert(0, func_call)
-
-        return body
-
-    def _get_check_type_statement(self, variable, collect_var, compulsory):
-        """
-        Check if the provided variable has the expected type.
-
-        Get the code which checks if the variable collected from python
-        has the expected type.
-
-        Parameters
-        ----------
-        variable : Variable
-                      The variable containing the PythonObject.
-        collect_var : Variable
-                      The variable in which the result will be saved,
-                      used to provide information about the expected type.
-        compulsory : bool
-                      Indicates whether the argument is a compulsory argument
-                      to the function (if not then it must have a default or
-                      be optional).
-
-        Returns
-        -------
-        str
-                A string containing the code which determines whether 'variable'
-                contains an object which can be saved in 'collect_var'.
-        """
-
-        if variable.rank > 0 :
-            check = array_type_check(collect_var, variable, False)
-
-        else :
-            check = scalar_object_check(collect_var, variable)
-
-        if not compulsory:
-            default = PyccelEq(ObjectAddress(collect_var), ObjectAddress(Py_None))
-            check = PyccelAssociativeParenthesis(PyccelOr(default, check))
-
-        return check
-
-    def _get_wrapper_name(self, func):
-        """
-        create wrapper function name
-
-        Parameters
-        -----------
-        func      : FunctionDef or Interface
-
-        Returns
-        -------
-        wrapper_name : string
-        """
-        name         = func.name
-        wrapper_name = self.scope.get_new_name(name+"_wrapper")
-
-        self._function_wrapper_names[func.name] = wrapper_name
-
-        return wrapper_name
-
-    def get_wrapper_arguments(self):
-        """
-        Create wrapper arguments
-
-        Returns
-        -------
-        List of variables
-        """
-        python_func_args    = self.get_new_PyObject("args")
-        python_func_kwargs  = self.get_new_PyObject("kwargs")
-        python_func_selfarg = self.get_new_PyObject("self"  )
-        self.scope.insert_variable(python_func_args)
-        self.scope.insert_variable(python_func_kwargs)
-        self.scope.insert_variable(python_func_selfarg)
-
-        return [python_func_selfarg, python_func_args, python_func_kwargs]
-
+            return super()._handle_is_operator(Op, expr)
 
     # -------------------------------------------------------------------
     # Functions managing the creation of wrapper body
     # -------------------------------------------------------------------
 
-    def _valued_variable_management(self, variable, collect_var, tmp_variable, default_value):
-        """
-        Responsible for creating the body collecting the default value of a Variable
-        and the check needed.
-        If the Variable is optional create body to collect the new value
-
-        Parameters
-        ----------
-        variable      : Variable
-                        The optional variable
-        collect_var   : Variable
-                        variable which holds the value collected with PyArg_Parsetuple
-        tmp_variable  : Variable
-                        The temporary variable  to hold result
-        default_value : PyccelAstNode
-                        Object containing the default value of the variable
-
-        Returns
-        -------
-        section      :
-            IfSection
-        collect_body : List
-            list containing the lines necessary to collect the new optional variable value
-        """
-
-        valued_var_check  = PyccelEq(ObjectAddress(collect_var), ObjectAddress(Py_None))
-        collect_body      = []
-
-        if variable.is_optional:
-            collect_body  = [AliasAssign(variable, tmp_variable)]
-            section       = IfSection(valued_var_check, [AliasAssign(variable, Nil())])
-
-        else:
-            section       = IfSection(valued_var_check, [Assign(variable, default_value)])
-
-        return section, collect_body
-
-
-    def _body_scalar(self, variable, collect_var, default_value = None, error_check = False, tmp_variable = None):
-        """
-        Responsible for collecting value and managing error and create the body
-        of arguments in format:
-
-        Parameters
-        ----------
-        variable      : Variable
-                        the variable to be collected
-        collect_var   : Variable
-                        variable which holds the value collected with PyArg_Parsetuple
-        default_value : PyccelAstNode
-                        Object containing the default value of the variable
-                        Default: None
-        error_check   : boolean
-                        True if checking the data type and raising error is needed
-                        Default: False
-        tmp_variable  : Variable
-                        temporary variable to hold value
-                        Default: None
-
-        Returns
-        -------
-        body : If block
-        """
-
-        var      = tmp_variable if tmp_variable else variable
-        sections = []
-
-        collect_value = [Assign(var, FunctionCall(Python_to_C(var), [collect_var]))]
-
-        if default_value is not None:
-            section, optional_collect = self._valued_variable_management(variable, collect_var, tmp_variable, default_value)
-            sections.append(section)
-            collect_value += optional_collect
-
-        if error_check:
-            check_type = scalar_object_check(collect_var, var)
-            sections.append(IfSection(check_type, collect_value))
-            error = generate_datatype_error(var)
-            sections.append(IfSection(LiteralTrue(), [error, Return([Nil()])]))
-        else:
-            sections.append(IfSection(LiteralTrue(), collect_value))
-
-        if len(sections)==1 and sections[0].condition == LiteralTrue():
-            return sections[0].body
-        else:
-            return If(*sections)
-
-    def _body_array(self, variable, collect_var, check_type = False) :
-        """
-        Create the code to extract an array.
-
-        This function is responsible for collecting the value of the array from
-        a provided Python variable, and saving it into a C object.
-        It also manages potential errors such as if the wrong type is provided.
-        Finally it also handles the case of an optional array.
-
-        Parameters
-        ----------
-        variable : Variable
-            The C variable where the result will be stored.
-        collect_var : Variable
-            The Variable containing the Python object, of type PyObject.
-        check_type : bool
-            True if the type is needed.
-
-        Returns
-        -------
-        list
-            A list of code statements.
-        """
-        self.add_import(cwrapper_ndarray_import)
-        body = []
-
-        in_if = False
-
-        #check optional :
-        if variable.is_optional :
-            check = PyccelEq(ObjectAddress(collect_var), ObjectAddress(Py_None))
-            body += [IfSection(check, [AliasAssign(variable, Nil())])]
-            in_if = True
-
-        if check_type:
-            check = array_type_check(collect_var, variable, True)
-            body += [IfSection(PyccelNot(check), [Return([Nil()])])]
-            in_if = True
-
-        collect_func = FunctionCall(pyarray_to_ndarray, [collect_var])
-        if in_if:
-            # Use this if other array storage (e.g. cuda arrays) is available
-            #body += [IfSection(FunctionCall(PyArray_Check, [collect_var]), [Assign(variable,
-            #                    collect_func)])]
-            body += [IfSection(LiteralTrue(), [Assign(variable, collect_func)])]
-            body = [If(*body)]
-        else:
-            body = [Assign(variable, collect_func)]
-
-        return body
-
-    def _body_management(self, variable, collect_var, default_value = None, check_type = False):
-        """
-        Responsible for calling functions that take care of body creation
-
-        Parameters
-        ----------
-        variable      : Variable
-                        The Variable (which may be optional)
-        collect_var   : Variable
-                        the pyobject type variable  holder of value
-        default_value : PyccelAstNode
-                        Object containing the default value of the variable
-                        Default: None
-        check_type    : Boolean
-                        True if the type is needed
-                        Default: False
-
-        Returns
-        -------
-        body : list
-            A list of statements
-        tmp_variable : Variable
-            temporary variable to hold value default None
-        """
-        tmp_variable = None
-        body         = []
-
-        if variable.rank > 0:
-            body = self._body_array(variable, collect_var, check_type)
-
-        else:
-            if variable.is_optional:
-                tmp_variable = Variable(dtype=variable.dtype, precision = variable.precision,
-                                        name = self.scope.get_new_name(variable.name+"_tmp"))
-                self.scope.insert_variable(tmp_variable)
-
-            body = [self._body_scalar(variable, collect_var, default_value, check_type, tmp_variable)]
-
-        return body, tmp_variable
-
-    def untranslatable_function(self, wrapper_name, wrapper_args, wrapper_results, error_msg):
-        """
-        Create code for a function complaining about an object which cannot be wrapped.
-
-        Certain functions are not handled in the wrapper (e.g. private),
-        This creates a wrapper function which raises NotImplementedError
-        exception and returns NULL.
-
-        Parameters
-        ----------
-        wrapper_name : str
-            The name of the C wrapper function.
-
-        wrapper_args : list of Variables
-            List of variables with dtype PyObject which hold the arguments
-            passed to the function.
-
-        wrapper_results : Variable
-            List containing one variable with dtype PyObject which represents
-            the variable which will be returned by the function.
-
-        error_msg : str
-            The message to be raised in the NotImplementedError.
-
-        Returns
-        -------
-        str
-            Returns the string containing the printed FunctionDef.
-        """
-        current_scope = self.scope
-        wrapper_func = FunctionDef(
-                name      = wrapper_name,
-                arguments = [FunctionDefArgument(a) for a in wrapper_args],
-                results   = [FunctionDefResult(r) for r in wrapper_results],
-                body      = [
-                                set_python_error_message('PyExc_NotImplementedError',
-                                            f'"{error_msg}"'),
-                                AliasAssign(wrapper_results[0], Nil()),
-                                Return(wrapper_results)
-                            ],
-                scope     = Scope())
-
-        code = CCodePrinter._print_FunctionDef(self, wrapper_func)
-        self.set_scope(current_scope)
-        return code
-
-    # -------------------------------------------------------------------
-    # Parsing arguments and building values Types functions
-    # -------------------------------------------------------------------
-    def get_PyArgParseType(self, variable):
-        """
-        Get the variable which collects the result of PyArgParse.
-
-        This function is responsible for creating any necessary intermediate variables which are used
-        to collect the result of PyArgParse.
-
-        Parameters
-        ----------
-        variable : Variable
-            The variable which will be passed to the translated function.
-
-        Returns
-        -------
-        Variable
-            The variable which will be used to collect the argument.
-        """
-
-        collect_type = PyccelPyObject()
-        collect_var  = Variable(dtype = collect_type,
-                            memory_handling='alias',
-                            name=self.scope.get_new_name(variable.name+"_tmp"))
-        self.scope.insert_variable(collect_var)
-
-        return collect_var
-
-    def get_PyBuildValue(self, result):
-        """
-        Get the necessary objects for calling PyBuildValue.
-
-        Responsible for collecting the variable required to build the result
-        using the Python function PyBuildValue. Also responsible for creating
-        the necessary cast function which creates this variable.
-
-        Parameters
-        ----------
-        result : Variable
-            The variable returned by the translated function.
-
-        Returns
-        -------
-        Variable
-            The variable which will be provided to PyBuild.
-
-        FunctionCall
-            Call to cast function responsible for the conversion of one data type into another.
-        """
-        out_var = getattr(result, 'original_function_result_variable', result.var)
-        name = self.scope.get_expected_name(out_var.name)
-        variable = self.scope.find(name, category='variables')
-        if variable.rank != 0:
-            self.add_import(cwrapper_ndarray_import)
-
-        cast_function = FunctionCall(C_to_Python(variable), [ObjectAddress(variable)])
-
-        collect_type = PyccelPyObject()
-        collect_var = Variable(dtype = collect_type, memory_handling='alias',
-            name = self.scope.get_new_name(variable.name+"_tmp"))
-        self.scope.insert_variable(collect_var)
-        self._to_free_PyObject_list.append(collect_var) #TODO remove in next PR
-
-        return collect_var, cast_function
-
-    def insert_constant(self, mod_name, var_name, var, collect_var):
-        """
-        Insert a variable into the module
-
-        Parameters
-        ----------
-        mod_name    : str
-                      The name of the module variable
-        var_name    : str
-                      The name which will be used to identify the
-                      variable in python. (This is usually var.name,
-                      however it may differ due to the case-insensitivity
-                      of fortran)
-        var         : Variable
-                      The module variable
-        collect_var : Variable
-                      A PyObject* variable to store temporaries
-
-        Returns
-        -------
-        list : A list of PyccelAstNodes to be appended to the body of the
-                pymodule initialisation function
-        """
-        if var.rank != 0:
-            self.add_import(cwrapper_ndarray_import)
-
-        collect_value = AliasAssign(collect_var,
-                                FunctionCall(C_to_Python(var), [ObjectAddress(var)]))
-        add_expr = PyModule_AddObject(mod_name, var_name, collect_var)
-        if_expr = If(IfSection(PyccelLt(add_expr,LiteralInteger(0)),
-                        [FunctionCall(Py_DECREF, [collect_var]),
-                         Return([PyccelUnarySub(LiteralInteger(1))])]))
-        return [collect_value, if_expr]
-
-    def get_module_exec_function(self, expr, exec_func_name):
-        """
-        Create code which initialises a module.
-
-        Create the function which executes any statements which happen
-        when the module is loaded.
-
-        Parameters
-        ----------
-        expr : Module
-            The module being wrapped.
-
-        exec_func_name : str
-            The name of the function.
-
-        Returns
-        -------
-        str
-            The code for a function which initialises a module.
-        """
-        # Create scope for the module initialisation function
-        scope = self.scope.new_child_scope(exec_func_name)
-        self.set_scope(scope)
-
-        #Create module variable
-        mod_var_name = self.scope.get_new_name('m')
-        mod_var = Variable(dtype = PyccelPyObject(),
-                      name       = mod_var_name,
-                      memory_handling = 'alias')
-        scope.insert_variable(mod_var)
-
-        # Collect module variables from translated code
-        body = []
-        if isinstance(expr, BindCModule):
-            orig_vars_to_wrap = [v for v in expr.original_module.variables if not v.is_private]
-            wrapper_funcs = {f.original_function: f for f in expr.variable_wrappers}
-            # Collect python compatible module variables
-            vars_to_wrap = []
-            for v in orig_vars_to_wrap:
-                if v in wrapper_funcs:
-                    # Get pointer to store array data
-                    var = scope.get_temporary_variable(dtype_or_var = v,
-                            name = v.name,
-                            memory_handling = 'alias',
-                            rank = 0, shape = None, order = None)
-                    # Create variables to store the shape of the array
-                    shape = [scope.get_temporary_variable(NativeInteger(),
-                            v.name+'_size') for _ in range(v.rank)]
-                    # Get the bind_c function which wraps a fortran array and returns c objects
-                    var_wrapper = wrapper_funcs[v]
-                    # Call bind_c function
-                    call = Assign(PythonTuple(ObjectAddress(var), *shape), FunctionCall(var_wrapper, ()))
-                    body.append(call)
-
-                    # Create ndarray to store array data
-                    nd_var = self.scope.get_temporary_variable(dtype_or_var = v,
-                            name = v.name,
-                            memory_handling = 'alias'
-                            )
-                    alloc = Allocate(nd_var, shape=shape, order=nd_var.order, status='unallocated')
-                    body.append(alloc)
-                    # Save raw_data into ndarray to obtain useable pointer
-                    set_data = AliasAssign(DottedVariable(NativeVoid(), 'raw_data',
-                            memory_handling = 'alias', lhs=nd_var), var)
-                    body.append(set_data)
-                    # Save the ndarray to vars_to_wrap to be handled as if it came from C
-                    vars_to_wrap.append(nd_var)
-                else:
-                    # Ensure correct name
-                    w = v.clone(scope.get_expected_name(v.name.lower()))
-                    assign = v.get_user_nodes(Assign)[0]
-                    # assign.fst should always exist, but is not always set when the
-                    # Assign is created in the codegen stage
-                    if assign.fst:
-                        w.set_fst(assign.fst)
-                    vars_to_wrap.append(w)
-        else:
-            orig_vars_to_wrap = [v for v in expr.variables if not v.is_private]
-            vars_to_wrap = orig_vars_to_wrap
-        var_names = [str(self.get_python_name(expr.scope, v)) for v in orig_vars_to_wrap]
-
-        # If there are any variables in the module then add them to the module object
-        if vars_to_wrap:
-            # Create variable for temporary python objects
-            tmp_var_name = self.scope.get_new_name('tmp')
-            tmp_var = Variable(dtype = PyccelPyObject(),
-                          name       = tmp_var_name,
-                          memory_handling = 'alias')
-            scope.insert_variable(tmp_var)
-            # Add code to add variable to module
-            body.extend(l for n,v in zip(var_names,vars_to_wrap) for l in self.insert_constant(mod_var_name, n, v, tmp_var))
-
-        if expr.init_func:
-            # Call init function code
-            body.insert(0,FunctionCall(expr.init_func,[],[]))
-
-        body.append(Return([LiteralInteger(0)]))
-        self.exit_scope()
-
-        func = FunctionDef(name = exec_func_name,
-            arguments = (FunctionDefArgument(mod_var),),
-            results = (FunctionDefResult(scope.get_temporary_variable(NativeInteger(),
-                precision = 4)),),
-            body = CodeBlock(body),
-            scope = scope)
-        func_code = super()._print_FunctionDef(func).split('\n')
-        func_code[1] = "static "+func_code[1]
-
-
-        return '\n'.join(func_code)
-
     def _print_BindCPointer(self, expr):
         return 'bind_c_ptr'
 
@@ -1027,325 +245,9 @@ def _print_DottedName(self, expr):
         names = expr.name
         return '.'.join(self._print(n) for n in names)
 
-    def _print_Interface(self, expr):
-
-        # Find a name for the wrapper function
-        wrapper_name = self._get_wrapper_name(expr)
-
-        mod_scope = self.scope
-        scope = self.scope.new_child_scope(wrapper_name)
-        self.set_scope(scope)
-
-        # Collecting all functions
-        funcs = expr.functions
-
-        # Save all used names
-        for n in funcs:
-            self.scope.insert_symbol(n.name)
-
-        # Collect arguments and results
-        wrapper_args    = self.get_wrapper_arguments()
-        wrapper_results = [self.get_new_PyObject("result")]
-        self.scope.insert_variable(wrapper_results[0])
-
-        if isinstance(funcs[0], BindCFunctionDef):
-            example_args = funcs[0].bind_c_arguments
-            example_arg_vars = {(a.original_function_argument_variable if isinstance(a, BindCFunctionDefArgument) else a.var): a \
-                    for a in example_args}
-            arg_names = [a.var.name for a in funcs[0].original_function.arguments]
-        else:
-            example_args = funcs[0].arguments
-            example_arg_vars = {(a.original_function_argument_variable if isinstance(a, BindCFunctionDefArgument) else a.var): a \
-                    for a in example_args}
-            arg_names = [a.name for a in example_args]
-
-        # Collect argument names for PyArgParse
-        keyword_list_name = self.scope.get_new_name('kwlist')
-        keyword_list      = PyArgKeywords(keyword_list_name, arg_names)
-        wrapper_body      = [keyword_list]
-
-        wrapper_body_translations = []
-        body_tmp = []
-
-        # To store the mini function responsible for collecting value and calling interfaces functions and return the builded value
-        funcs_def = []
-        default_value = {} # dict to collect all initialisation needed in the wrapper
-        check_var = Variable(dtype = NativeInteger(), name = self.scope.get_new_name("check"))
-        scope.insert_variable(check_var, check_var.name)
-        types_dict = OrderedDict((a, set()) for a in example_arg_vars) #dict to collect each variable possible type and the corresponding flags
-        # collect parse arg
-        parse_args = [self.get_PyArgParseType(a) for a in example_arg_vars]
-
-        # Determine flags which indicate argument type
-        argument_type_flags = self._determine_interface_flags(funcs)
-
-        # Managing the body of wrapper
-        for func in funcs :
-            mini_wrapper_func_body = []
-            res_args = []
-            static_func_args  = []
-
-            mini_wrapper_func_name = self.scope.get_new_name(func.name + '_mini_wrapper')
-            mini_scope = mod_scope.new_child_scope(mini_wrapper_func_name)
-            self.set_scope(mini_scope)
-
-            # update ndarray local variables properties
-            if isinstance(func, BindCFunctionDef):
-                arg_vars = {(a.original_function_argument_variable if isinstance(a, BindCFunctionDefArgument) else a.var): a \
-                        for a in func.bind_c_arguments}
-            else:
-                arg_vars = {(a.original_function_argument_variable if isinstance(a, BindCFunctionDefArgument) else a.var): a \
-                        for a in func.arguments}
-            results = func.bind_c_results if isinstance(func, BindCFunctionDef) else func.results
-            result_vars = []
-            for r in results:
-                var = r.var
-                name = var.name
-                self.scope.insert_symbol(name)
-                if var.rank > 0:
-                    v = var.clone(self.scope.get_expected_name(name), memory_handling = 'alias')
-                else:
-                    v = var.clone(self.scope.get_expected_name(name))
-                result_vars.append(v)
-                self.scope.insert_variable(v)
-                if isinstance(r, BindCFunctionDefResult) and r.shape:
-                    original_var = r.original_function_result_variable
-                    original_name = original_var.name
-                    self.scope.insert_symbol(original_name)
-                    # Declare as pointer as the array should not free its data when it goes out of scope
-                    self.scope.insert_variable(original_var.clone(self.scope.get_expected_name(original_name), memory_handling = 'alias'))
-
-            local_arg_vars = {(v.clone(v.name, memory_handling='alias')
-                              if isinstance(v, Variable) and v.rank > 0 or v.is_optional \
-                              else v) : a for v,a in arg_vars.items()}
-
-            for a in local_arg_vars:
-                mini_scope.insert_variable(a)
-
-            # Loop for all args in every functions and create the corresponding condition and body
-            for idx, (p_arg, (f_var, f_arg)) in enumerate(zip(parse_args, local_arg_vars.items())):
-                collect_var  = self.get_PyArgParseType(f_var)
-                body, tmp_variable = self._body_management(f_var, p_arg, f_arg.value)
-
-                # get check type function
-                check = self._get_check_type_statement(f_var, p_arg, f_arg.value is None)
-
-                # Save the body
-                wrapper_body_translations.extend(body)
-
-                # Write default values
-                if f_arg.value is not None:
-                    wrapper_body.append(self.get_default_assign(parse_args[-1], f_var, f_arg.value))
-
-                # Get Bind/C arguments
-                static_func_args.extend(self.get_static_args(f_var))
-
-                # Save flag to types dict for interface recognition
-                flag_value = argument_type_flags[func][idx]
-                if flag_value >= len(types_dict[f_var]):
-                    types_dict[f_var].add((f_var, check, flag_value)) # collect variable type for each arguments
-
-                mini_wrapper_func_body += body
-
-            # create the corresponding function call
-            mini_wrapper_func_body.extend(self._get_static_func_call_code(func, static_func_args, results))
-
-
-            # Loop for all res in every functions and create the corresponding body and cast
-            for r in results :
-                collect_var, cast_func = self.get_PyBuildValue(r)
-                if cast_func is not None:
-                    mini_wrapper_func_body.append(AliasAssign(collect_var, cast_func))
-
-                res_args.append(ObjectAddress(collect_var) if collect_var.is_alias else collect_var)
-
-            # Building PybuildValue and freeing the allocated variable after.
-            mini_wrapper_func_body.append(AliasAssign(wrapper_results[0],PyBuildValueNode(res_args)))
-            mini_wrapper_func_body += [FunctionCall(Py_DECREF, [i]) for i in self._to_free_PyObject_list]
-
-            # Call free function for C type
-            mini_wrapper_func_body += [If(IfSection(PyccelIsNot(i, Nil()), [Deallocate(i)])) if self.is_c_pointer(i) \
-                                        else Deallocate(i) for i in local_arg_vars if i.rank > 0]
-            if self._target_language == 'fortran':
-                dealloc_results = [self.scope.find(self.scope.get_expected_name(r.original_function_result_variable.name), category='variables')
-                                    for r in results]
-            else:
-                dealloc_results = result_vars
-            mini_wrapper_func_body += [If(IfSection(PyccelIsNot(i, Nil()), [Deallocate(i)])) if self.is_c_pointer(i) \
-                                else Deallocate(i) for i in dealloc_results if i.rank > 0]
-            mini_wrapper_func_body.append(Return(wrapper_results))
-            self._to_free_PyObject_list.clear()
-
-            self.set_scope(scope)
-
-            # Building Mini wrapper function
-            mini_wrapper_func_def = FunctionDef(name = mini_wrapper_func_name,
-                arguments = [FunctionDefArgument(a) for a in parse_args],
-                results = [FunctionDefResult(r) for r in wrapper_results],
-                body = mini_wrapper_func_body,
-                scope = mini_scope)
-            funcs_def.append(mini_wrapper_func_def)
-
-            # append check condition to the functioncall
-            body_tmp.append(IfSection(PyccelEq(check_var, LiteralInteger(sum(argument_type_flags[func]))), [AliasAssign(wrapper_results[0],
-                    FunctionCall(mini_wrapper_func_def, parse_args))]))
-
-        # Errors / Types management
-        # Creating check_type function
-        check_func_def = self._create_wrapper_check(check_var, parse_args, types_dict, funcs[0].name)
-        funcs_def.append(check_func_def)
-
-        # Create the wrapper body with collected informations
-        body_tmp = [IfSection(PyccelEq(check_var, PyccelUnarySub(LiteralInteger(1))), [Return([Nil()])])] + body_tmp
-        body_tmp.append(IfSection(LiteralTrue(),
-            [set_python_error_message('PyExc_TypeError', '"This combination of arguments is not valid"'),
-            Return([Nil()])]))
-        wrapper_body_translations = [If(*body_tmp)]
-
-        # Parsing Arguments
-        parse_node = PyArg_ParseTupleNode(*wrapper_args[1:],
-                                          example_args,
-                                          parse_args, keyword_list)
-
-        wrapper_body += list(default_value.values())
-        wrapper_body.append(If(IfSection(PyccelNot(parse_node), [Return([Nil()])])))
-
-        #finishing the wrapper body
-        wrapper_body.append(Assign(check_var, FunctionCall(check_func_def, parse_args)))
-        wrapper_body.extend(wrapper_body_translations)
-        wrapper_body.append(Return(wrapper_results)) # Return
-
-        # Create FunctionDef
-        funcs_def.append(FunctionDef(name = wrapper_name,
-            arguments = [FunctionDefArgument(a) for a in wrapper_args],
-            results = [FunctionDefResult(r) for r in wrapper_results],
-            body = wrapper_body,
-            scope = scope))
-
-        sep = self._print(SeparatorComment(40))
-
-        self.exit_scope()
-
-        return sep + '\n'.join(CCodePrinter._print_FunctionDef(self, f) for f in funcs_def)
-
-    def _determine_interface_flags(self, funcs):
-        """
-        Determine the flags which allow correct function to be identified from the interface.
-
-        Each function must be identifiable by a different integer value. This value is known
-        as a flag. Different parts of the flag to indicate the types of different arguments.
-        Take for example the following function:
-        ```python
-        @types('int', 'int')
-        @types('float', 'float')
-        def f(a, b):
-            pass
-        ```
-        The values 0 (int) and 1 (float) would indicate the type of the argument a. In order
-        to preserve this information the values which indicate the type of the argument b
-        must only change the part of the flag which does not contain this information. In other
-        words `flag % n_types_a = flag_a`. Therefore the values 0 (int) and 2(float) indicate
-        the type of the argument b.
-        We then finally have the following four options:
-          1. 0 = 0 + 0 => (int,int)
-          2. 1 = 1 + 0 => (float,int)
-          3. 2 = 0 + 2 => (int, float)
-          4. 3 = 1 + 2 => (float, float)
-
-        of which only the first and last flags indicate acceptable arguments.
-
-        The function returns a dictionary whose keys are the functions and whose values are
-        a list of the flags which would indicate the correct types.
-        In the above example we would return `{func_0 : [0,0], func_1 : [1,2]}`.
-
-        Parameters
-        ----------
-        funcs : list of FunctionDefs
-            The functions in the Interface.
-
-        Returns
-        -------
-        dict
-            A dictionary whose keys are the functions and whose values are a list of integers
-            which should be used to increment the byte flag if the argument at the same index
-            has the type expected by the function.
-        """
-        orig_funcs = [func.original_function if isinstance(func, BindCFunctionDef) else func for func in funcs]
-        argument_type_flags = {func:[] for func in funcs}
-        nargs = len(orig_funcs[0].arguments)
-        step = 1
-        for i in range(nargs):
-            interface_args = [getattr(func.arguments[i], 'original_function_argument_variable', func.arguments[i].var) for func in orig_funcs]
-            interface_types = [(a.dtype, a.precision) for a in interface_args]
-            possible_types = list(dict.fromkeys(interface_types)) # Remove duplicates but preserve order
-            for func, t in zip(funcs, interface_types):
-                argument_type_flags[func].append(possible_types.index(t)*step)
-            step *= len(possible_types)
-        return argument_type_flags
-
-    def _create_wrapper_check(self, check_var, parse_args, types_dict, func_name):
-        """
-        Generate the function which determines the relevant interface.
-
-        Creates a FunctionDef which tests each of the arguments passed to the Interface.
-        For each of the arguments it checks if it has one of the expected types.
-        This function is necessary when wrapping an Interface in order to determine which
-        underlying function is being called. The created FunctionDef returns an integer
-        which acts as a flag to indicate which function has been chosed and raises an error
-        if the type received does not match any of the expected types. See _determine_interface_flags
-        for more detail about how the flag is defined.
-
-        Parameters
-        ----------
-        check_var : Variable
-            The integer variable which will be returned by the function.
-
-        parse_args : list of Variable
-            The arguments passed to the function in the Python code.
-
-        types_dict : dictionary
-            A dictionary linking the parsed arguments to the possible types.
-            The values are tuples containing the expected argument, the type check, and the flag.
-
-        func_name : str
-            The name of the interface.
-
-        Returns
-        -------
-        FunctionDef
-            A FunctionDef describing the function which allows the flag to be set.
-        """
-        check_func_body = []
-        for arg in types_dict:
-            var_name = ""
-            body = []
-            arg_type_check_list = list(types_dict[arg])
-            types = [elem[0] for elem in arg_type_check_list]
-            description = [PythonType(v).print_string for v in types]
-            error = ' or '.join([d.python_value for d in description])
-            if len(arg_type_check_list) == 1:
-                elem = arg_type_check_list[0]
-                var_name = elem[0].name
-                assert elem[2] == 0
-                body.append(IfSection(PyccelNot(elem[1]), [set_python_error_message('PyExc_TypeError', f'"{var_name} must be {error}"'), Return([PyccelUnarySub(LiteralInteger(1))])]))
-            else:
-                arg_type_check_list.sort(key= lambda x : x[2])
-                for elem in arg_type_check_list:
-                    var_name = elem[0].name
-                    body.append(IfSection(elem[1], [AugAssign(check_var, '+' ,elem[2])]))
-                body.append(IfSection(LiteralTrue(), [set_python_error_message('PyExc_TypeError', f'"{var_name} must be {error}"'), Return([PyccelUnarySub(LiteralInteger(1))])]))
-            check_func_body += [If(*body)]
-
-        check_func_body = [Assign(check_var, LiteralInteger(0))] + check_func_body
-        check_func_body.append(Return([check_var]))
-        # Creating check function definition
-        check_func_name = self.scope.parent_scope.get_new_name(f'type_check_{func_name}')
-        check_func_def = FunctionDef(name = check_func_name,
-            arguments = [FunctionDefArgument(a) for a in parse_args],
-            results = [FunctionDefResult(check_var)],
-            body = check_func_body,
-            scope = self.scope.new_child_scope(check_func_name))
-        return check_func_def
+    def _print_PyInterface(self, expr):
+        funcs_to_print = (*expr.functions, expr.type_check_func, expr.interface_func)
+        return '\n'.join(self._print(f) for f in funcs_to_print)
 
     def _print_PyccelPyObject(self, expr):
         return 'pyobject'
@@ -1356,29 +258,19 @@ def _print_PyArg_ParseTupleNode(self, expr):
         pykwarg = expr.pykwarg
         flags   = expr.flags
         # All args are modified so even pointers are passed by address
-        args    = ', '.join(['&{}'.format(a.name) for a in expr.args])
+        args    = ', '.join(f'&{a.name}' for a in expr.args)
 
         if expr.args:
-            code = '{name}({pyarg}, {pykwarg}, "{flags}", {kwlist}, {args})'.format(
-                            name=name,
-                            pyarg=pyarg,
-                            pykwarg=pykwarg,
-                            flags = flags,
-                            kwlist = expr.arg_names.name,
-                            args = args)
+            code = f'{name}({pyarg}, {pykwarg}, "{flags}", {expr.arg_names.name}, {args})'
         else :
-            code ='{name}({pyarg}, {pykwarg}, "", {kwlist})'.format(
-                    name=name,
-                    pyarg=pyarg,
-                    pykwarg=pykwarg,
-                    kwlist = expr.arg_names.name)
+            code =f'{name}({pyarg}, {pykwarg}, "", {expr.arg_names.name})'
 
         return code
 
     def _print_PyBuildValueNode(self, expr):
         name  = 'Py_BuildValue'
         flags = expr.flags
-        args  = ', '.join(['{}'.format(self._print(a)) for a in expr.args])
+        args  = ', '.join(self._print(a) for a in expr.args)
         #to change for args rank 1 +
         if expr.args:
             code = f'(*{name}("{flags}", {args}))'
@@ -1387,157 +279,18 @@ def _print_PyBuildValueNode(self, expr):
         return code
 
     def _print_PyArgKeywords(self, expr):
-        arg_names = ',\n'.join(['"{}"'.format(a) for a in expr.arg_names] + [self._print(Nil())])
-        return ('static char *{name}[] = {{\n'
-                        '{arg_names}\n'
-                        '}};\n'.format(name=expr.name, arg_names = arg_names))
+        arg_names = ',\n'.join([f'"{a}"' for a in expr.arg_names] + [self._print(Nil())])
+        return (f'static char *{expr.name}[] = {{\n'
+                        f'{arg_names}\n'
+                        '};\n')
 
     def _print_PyModule_AddObject(self, expr):
-        return 'PyModule_AddObject({}, {}, {})'.format(
-                expr.mod_name,
-                self._print(expr.name),
-                self._print(expr.variable))
-
-    def _print_FunctionDef(self, expr):
-        self.set_scope(self.scope.new_child_scope(expr.name))
-
-        local_arg_vars = {}
-        args = expr.bind_c_arguments if isinstance(expr, BindCFunctionDef) else expr.arguments
-        for a in args:
-            v = (a.original_function_argument_variable if isinstance(a, BindCFunctionDefArgument) else a.var)
-            if isinstance(v, Variable):
-                new_name = self.scope.get_new_name(v.name)
-                if isinstance(v, Variable) and (v.rank > 0 or v.is_optional):
-                    new_v = v.clone(new_name, memory_handling='alias')
-                else:
-                    new_v = v.clone(new_name)
-                local_arg_vars[new_v] = a
-                self.scope.insert_variable(new_v)
-            else:
-                self.scope.functions[v.name] = v
-                local_arg_vars[v] = a
-
-        results = expr.bind_c_results if isinstance(expr, BindCFunctionDef) else expr.results
-        result_vars = []
-        for r in results:
-            var = r.var
-            name = var.name
-            self.scope.insert_symbol(name)
-            if var.rank > 0:
-                v = var.clone(self.scope.get_expected_name(name), memory_handling = 'alias')
-            else:
-                v = var.clone(self.scope.get_expected_name(name))
-            result_vars.append(v)
-            self.scope.insert_variable(v)
-            if isinstance(r, BindCFunctionDefResult) and r.shape:
-                original_var = r.original_function_result_variable
-                original_name = original_var.name
-                self.scope.insert_symbol(original_name)
-                # Declare as pointer as the array should not free its data when it goes out of scope
-                new_var = original_var.clone(self.scope.get_expected_name(original_name), memory_handling='alias')
-                self.scope.insert_variable(new_var)
-        # update ndarray and optional local variables properties
-
-        # Find a name for the wrapper function
-        wrapper_name = self._get_wrapper_name(expr)
-
-        # Collect arguments and results
-        wrapper_args    = self.get_wrapper_arguments()
-        wrapper_results = [self.get_new_PyObject("result")]
-        self.scope.insert_variable(wrapper_results[0])
-
-        # Collect argument names for PyArgParse
-        arg_names         = [a.var.name for a in (expr.original_function.arguments if isinstance(expr, BindCFunctionDef) else expr.arguments)]
-        keyword_list_name = self.scope.get_new_name('kwlist')
-        keyword_list      = PyArgKeywords(keyword_list_name, arg_names)
-
-        if expr.is_private:
-            self.exit_scope()
-            return self.untranslatable_function(wrapper_name,
-                        wrapper_args, wrapper_results,
-                        "Private functions are not accessible from python")
-
-        if any(isinstance(arg, FunctionAddress) for arg in local_arg_vars):
-            self.exit_scope()
-            return self.untranslatable_function(wrapper_name,
-                        wrapper_args, wrapper_results,
-                        "Cannot pass a function as an argument")
-
-        wrapper_body              = [keyword_list]
-        wrapper_body_translations = []
-        static_func_args  = []
-
-        parse_args = []
-        for var, arg in local_arg_vars.items():
-            collect_var  = self.get_PyArgParseType(var)
-
-            body, tmp_variable = self._body_management(var, collect_var, arg.value, True)
-
-            # Save cast to argument variable
-            wrapper_body_translations.extend(body)
-
-            parse_args.append(collect_var)
-
-            # Get Bind/C arguments
-            static_func_args.extend(self.get_static_args(var))
-
-            # Write default values
-            if arg.value is not None:
-                wrapper_body.append(self.get_default_assign(parse_args[-1], var, arg.value))
-
-        # Parse arguments
-        parse_node = PyArg_ParseTupleNode(*wrapper_args[1:],
-                                          list(local_arg_vars.values()),
-                                          parse_args, keyword_list)
-
-        wrapper_body.append(If(IfSection(PyccelNot(parse_node), [Return([Nil()])])))
-        wrapper_body.extend(wrapper_body_translations)
-
-        wrapper_body.extend(self._get_static_func_call_code(expr, static_func_args, results))
-
-        # Loop over results to carry out necessary casts and collect Py_BuildValue type string
-        res_args = []
-        for a in results :
-            collect_var, cast_func = self.get_PyBuildValue(a)
-            if cast_func is not None:
-                wrapper_body.append(AliasAssign(collect_var, cast_func))
-
-            res_args.append(ObjectAddress(collect_var) if collect_var.is_alias else collect_var)
-
-        # Call PyBuildNode
-        wrapper_body.append(AliasAssign(wrapper_results[0],PyBuildValueNode(res_args)))
-
-        # Call free function for python type
-        wrapper_body += [FunctionCall(Py_DECREF, [i]) for i in self._to_free_PyObject_list]
-
-        # Call free function for C type
-        wrapper_body += [If(IfSection(PyccelIsNot(i, Nil()), [Deallocate(i)])) if self.is_c_pointer(i) \
-                            else Deallocate(i) for i in local_arg_vars if i.rank > 0]
-        if self._target_language == 'fortran':
-            dealloc_results = [self.scope.find(self.scope.get_expected_name(r.original_function_result_variable.name), category='variables')
-                                for r in results]
-        else:
-            dealloc_results = result_vars
-        wrapper_body += [If(IfSection(PyccelIsNot(i, Nil()), [Deallocate(i)])) if self.is_c_pointer(i) \
-                            else Deallocate(i) for i in dealloc_results if i.rank > 0]
-        self._to_free_PyObject_list.clear()
-
-        #Return
-        wrapper_body.append(Return(wrapper_results))
-
-        # Create FunctionDef and write using classic method
-        wrapper_func = FunctionDef(name = wrapper_name,
-            arguments = [FunctionDefArgument(a) for a in wrapper_args],
-            results = [FunctionDefResult(r) for r in wrapper_results],
-            body = wrapper_body,
-            scope = self.scope)
-
-        self.exit_scope()
+        name = self._print(expr.name)
+        var  = self._print(expr.variable)
+        return f'PyModule_AddObject({expr.mod_name}, {name}, {var})'
 
-        return CCodePrinter._print_FunctionDef(self, wrapper_func)
-
-    def _print_Module(self, expr):
-        scope = Scope(original_symbols = expr.scope.python_names.copy())
+    def _print_PyModule(self, expr):
+        scope = expr.scope
         self.set_scope(scope)
         # The initialisation and deallocation shouldn't be exposed to python
         funcs_to_wrap = [f for f in expr.funcs if f not in (expr.init_func, expr.free_func)]
@@ -1551,120 +304,79 @@ def _print_Module(self, expr):
                 scope.insert_symbol(v.name.lower())
 
         funcs = []
-        if self._target_language == 'fortran':
-            vars_to_wrap_decs = [Declare(v.dtype, v.clone(v.name.lower()), module_variable=True) \
-                                    for v in variables if not v.is_private and v.rank == 0]
-
-            for f in expr.original_module.funcs:
-                if f.is_private:
-                    funcs.append(f)
-        else:
-            vars_to_wrap_decs = [Declare(v.dtype, v, module_variable=True) \
-                                    for v in expr.variables if not v.is_private]
 
         self._module_name  = self.get_python_name(scope, expr)
         sep = self._print(SeparatorComment(40))
 
-        function_signatures = ''.join(f'{self.static_function_signature(f)};\n' for f in expr.funcs)
-        if self._target_language == 'fortran':
-            function_signatures += ''.join(f'{self.static_function_signature(f)};\n' for f in expr.variable_wrappers)
+        function_signatures = ''.join(self.function_signature(f, print_arg_names = False) + ';\n' for f in expr.external_funcs)
 
         interface_funcs = [f.name for i in expr.interfaces for f in i.functions]
         funcs += [*expr.interfaces, *(f for f in funcs_to_wrap if f.name not in interface_funcs)]
 
         self._in_header = True
-        decs = ''.join('extern '+self._print(d) for d in vars_to_wrap_decs)
+        decs = ''.join(self._print(d) for d in expr.declarations)
         self._in_header = False
 
         function_defs = '\n'.join(self._print(f) for f in funcs)
-        cast_functions = '\n'.join(CCodePrinter._print_FunctionDef(self, f)
-                                       for f in self._cast_functions_dict.values())
+
         method_def_func = ''.join(('{{\n'
                                      '"{name}",\n'
                                      '(PyCFunction){wrapper_name},\n'
                                      'METH_VARARGS | METH_KEYWORDS,\n'
                                      '{doc_string}\n'
                                      '}},\n').format(
-                                            name = self.get_python_name(expr.scope, f),
-                                            wrapper_name = self._function_wrapper_names[f.name],
+                                            name = self.get_python_name(expr.scope, f.original_function),
+                                            wrapper_name = f.name,
                                             doc_string = self._print(LiteralString('\n'.join(f.doc_string.comments))) \
                                                         if f.doc_string else '""')
-                                     for f in funcs if f is not expr.init_func)
+                                     for f in funcs if f is not expr.init_func and not getattr(f, 'is_header', False))
 
         slots_name = self.scope.get_new_name('{}_slots'.format(expr.name))
-        exec_func_name = self.scope.get_new_name('exec_func')
-        slots_def = ('static PyModuleDef_Slot {name}[] = {{\n'
-                     '{{Py_mod_exec, {exec_func}}},\n'
-                     '{{0, NULL}},\n'
-                     '}};\n').format(name = slots_name,
-                             exec_func = exec_func_name)
+        exec_func_name = expr.init_func.name
+        slots_def = (f'static PyModuleDef_Slot {slots_name}[] = {{\n'
+                     f'{{Py_mod_exec, {exec_func_name}}},\n'
+                     '{0, NULL},\n'
+                     '};\n')
 
         method_def_name = self.scope.get_new_name('{}_methods'.format(expr.name))
-        method_def = ('static PyMethodDef {method_def_name}[] = {{\n'
-                        '{method_def_func}'
-                        '{{ NULL, NULL, 0, NULL}}\n'
-                        '}};\n'.format(method_def_name = method_def_name,
-                            method_def_func = method_def_func))
+        method_def = (f'static PyMethodDef {method_def_name}[] = {{\n'
+                        f'{method_def_func}'
+                        '{ NULL, NULL, 0, NULL}\n'
+                        '};\n')
 
         module_def_name = self.scope.get_new_name('{}_module'.format(expr.name))
-        module_def = ('static struct PyModuleDef {module_def_name} = {{\n'
+        module_def = (f'static struct PyModuleDef {module_def_name} = {{\n'
                 'PyModuleDef_HEAD_INIT,\n'
                 '/* name of module */\n'
-                '\"{mod_name}\",\n'
+                f'"{self._module_name}",\n'
                 '/* module documentation, may be NULL */\n'
                 'NULL,\n' #TODO: Add documentation
                 '/* size of per-interpreter state of the module, or -1 if the module keeps state in global variables. */\n'
                 '0,\n'
-                '{method_def_name},\n'
-                '{slots_name}\n'
-                '}};\n'.format(module_def_name = module_def_name,
-                    mod_name = self._module_name,
-                    method_def_name = method_def_name,
-                    slots_name = slots_name))
+                f'{method_def_name},\n'
+                f'{slots_name}\n'
+                '};\n')
 
-        exec_func = self.get_module_exec_function(expr, exec_func_name)
+        exec_func = self._print(expr.init_func)
 
-        init_func = ('PyMODINIT_FUNC PyInit_{mod_name}(void)\n{{\n'
+        init_func = (f'PyMODINIT_FUNC PyInit_{self._module_name}(void)\n{{\n'
                 'import_array();\n'
-                'return PyModuleDef_Init(&{module_def_name});\n'
-                '}}\n'.format(mod_name=self._module_name,
-                    module_def_name = module_def_name))
+                f'return PyModuleDef_Init(&{module_def_name});\n'
+                '}\n')
 
         # Print imports last to be sure that all additional_imports have been collected
+        for i in expr.imports:
+            self.add_import(i)
         imports  = module_imports.copy()
         imports += self._additional_imports.values()
         imports  = ''.join(self._print(i) for i in imports)
 
         self.exit_scope()
 
-        return ('#define PY_ARRAY_UNIQUE_SYMBOL CWRAPPER_ARRAY_API\n'
-                '{imports}\n'
-                '{variable_declarations}\n'
-                '{function_signatures}\n'
-                '{sep}\n'
-                '{cast_functions}\n'
-                '{sep}\n'
-                '{function_defs}\n'
-                '{exec_func}\n'
-                '{sep}\n'
-                '{method_def}\n'
-                '{sep}\n'
-                '{slots_def}\n'
-                '{sep}\n'
-                '{module_def}\n'
-                '{sep}\n'
-                '{init_func}'.format(
-                    imports = imports,
-                    variable_declarations = decs,
-                    function_signatures = function_signatures,
-                    sep = sep,
-                    cast_functions = cast_functions,
-                    function_defs = function_defs,
-                    exec_func = exec_func,
-                    method_def = method_def,
-                    slots_def  = slots_def,
-                    module_def = module_def,
-                    init_func = init_func))
+        return '\n'.join(['#define PY_ARRAY_UNIQUE_SYMBOL CWRAPPER_ARRAY_API',
+                imports, decs, function_signatures, sep, sep, function_defs,
+                exec_func, sep, method_def, sep, slots_def, sep, module_def,
+                sep, init_func])
 
 def cwrappercode(expr, filename, target_language, assign_to=None, **settings):
     """Converts an expr to a string of c wrapper code
diff --git a/pyccel/codegen/printing/fcode.py b/pyccel/codegen/printing/fcode.py
index b585fc8a0a..e6935c0cff 100644
--- a/pyccel/codegen/printing/fcode.py
+++ b/pyccel/codegen/printing/fcode.py
@@ -3205,6 +3205,9 @@ def indent_code(self, code):
 
         return new_code
 
+    def _print_BindCArrayVariable(self, expr):
+        return self._print(expr.wrapper_function)
+
 
 def fcode(expr, filename, assign_to=None, **settings):
     """Converts an expr to a string of Fortran code
diff --git a/pyccel/codegen/python_wrapper.py b/pyccel/codegen/python_wrapper.py
index 88cb8abbd8..ee4d4ed990 100644
--- a/pyccel/codegen/python_wrapper.py
+++ b/pyccel/codegen/python_wrapper.py
@@ -10,6 +10,7 @@
 from pyccel.codegen.printing.fcode          import fcode
 from pyccel.codegen.printing.cwrappercode   import CWrapperCodePrinter
 from pyccel.codegen.wrapper.fortran_to_c_wrapper   import FortranToCWrapper
+from pyccel.codegen.wrapper.c_to_python_wrapper    import CToPythonWrapper
 from pyccel.codegen.utilities      import recompile_object
 from pyccel.codegen.utilities      import copy_internal_library
 from pyccel.codegen.utilities      import internal_libs
@@ -156,7 +157,10 @@ def create_shared_library(codegen,
     codegen.ast.set_name(sharedlib_modname)
     wrapper_codegen = CWrapperCodePrinter(codegen.parser.filename, language)
     Scope.name_clash_checker = name_clash_checkers['c']
-    wrapper_code = wrapper_codegen.doprint(c_ast)
+    wrapper = CToPythonWrapper()
+    cwrap_ast = wrapper.wrap(c_ast)
+    wrapper_code = wrapper_codegen.doprint(cwrap_ast)
+    #wrapper_code = wrapper_codegen.doprint(c_ast)
     if errors.has_errors():
         return
 
diff --git a/pyccel/codegen/wrapper/c_to_python_wrapper.py b/pyccel/codegen/wrapper/c_to_python_wrapper.py
new file mode 100644
index 0000000000..8c0437a34e
--- /dev/null
+++ b/pyccel/codegen/wrapper/c_to_python_wrapper.py
@@ -0,0 +1,1112 @@
+# coding: utf-8
+#------------------------------------------------------------------------------------------#
+# This file is part of Pyccel which is released under MIT License. See the LICENSE file or #
+# go to https://github.com/pyccel/pyccel/blob/master/LICENSE for full license details.     #
+#------------------------------------------------------------------------------------------#
+"""
+Module describing the code-wrapping class : CToPythonWrapper
+which creates an interface exposing C code to Python.
+"""
+import warnings
+from pyccel.ast.bind_c        import BindCFunctionDef, BindCPointer, BindCFunctionDefArgument
+from pyccel.ast.bind_c        import BindCModule, BindCVariable
+from pyccel.ast.builtins      import PythonTuple
+from pyccel.ast.core          import Interface, If, IfSection, Return, FunctionCall
+from pyccel.ast.core          import FunctionDef, FunctionDefArgument, FunctionDefResult
+from pyccel.ast.core          import Assign, AliasAssign, Deallocate, Allocate
+from pyccel.ast.core          import Import, Module, AugAssign, CommentBlock
+from pyccel.ast.core          import FunctionAddress, Declare
+from pyccel.ast.cwrapper      import PyModule, PyccelPyObject, PyArgKeywords
+from pyccel.ast.cwrapper      import PyArg_ParseTupleNode, Py_None
+from pyccel.ast.cwrapper      import py_to_c_registry, check_type_registry, PyBuildValueNode
+from pyccel.ast.cwrapper      import PyErr_SetString, PyTypeError, PyNotImplementedError
+from pyccel.ast.cwrapper      import C_to_Python, PyFunctionDef, PyInterface
+from pyccel.ast.cwrapper      import PyModule_AddObject, Py_DECREF
+from pyccel.ast.c_concepts    import ObjectAddress
+from pyccel.ast.datatypes     import NativeVoid, NativeInteger
+from pyccel.ast.internals     import get_final_precision
+from pyccel.ast.literals      import Nil, LiteralTrue, LiteralString, LiteralInteger
+from pyccel.ast.numpy_wrapper import pyarray_to_ndarray
+from pyccel.ast.numpy_wrapper import array_get_data, array_get_dim
+from pyccel.ast.numpy_wrapper import array_get_c_step, array_get_f_step
+from pyccel.ast.numpy_wrapper import numpy_dtype_registry, numpy_flag_f_contig, numpy_flag_c_contig
+from pyccel.ast.numpy_wrapper import pyarray_check, is_numpy_array, no_order_check
+from pyccel.ast.operators     import PyccelNot, PyccelIsNot, PyccelUnarySub, PyccelEq
+from pyccel.ast.operators     import PyccelLt
+from pyccel.ast.variable      import Variable, DottedVariable
+from pyccel.parser.scope      import Scope
+from pyccel.errors.errors     import Errors
+from pyccel.errors.messages   import PYCCEL_RESTRICTION_TODO
+from .wrapper                 import Wrapper
+
+errors = Errors()
+
+cwrapper_ndarray_import = Import('cwrapper_ndarrays', Module('cwrapper_ndarrays', (), ()))
+
+class CToPythonWrapper(Wrapper):
+    """
+    Class for creating a wrapper exposing C code to Python.
+
+    A class which provides all necessary functions for wrapping different AST
+    objects such that the resulting AST is Python-compatible.
+    """
+    def __init__(self):
+        # A map used to find the Python-compatible Variable equivalent to an object in the AST
+        self._python_object_map = {}
+        # Indicate if arrays were wrapped.
+        self._wrapping_arrays = False
+        super().__init__()
+
+    def get_new_PyObject(self, name, is_temp = False):
+        """
+        Create new `PyccelPyObject` `Variable` with the desired name.
+
+        Create a new `Variable` with the datatype `PyccelPyObject` and the desired name.
+        A `PyccelPyObject` datatype means that this variable can be accessed and
+        manipulated from Python.
+
+        Parameters
+        ----------
+        name : str
+            The desired name.
+
+        is_temp : bool, default=False
+            Indicates if the Variable is temporary. A temporary variable may be ignored
+            by the printer.
+
+        Returns
+        -------
+        Variable
+            The new variable.
+        """
+        var = Variable(dtype=PyccelPyObject(),
+                        name=self.scope.get_new_name(name),
+                        memory_handling='alias',
+                        is_temp=is_temp)
+        self.scope.insert_variable(var)
+        return var
+
+    def _get_python_argument_variables(self, args):
+        """
+        Get a new set of `PyccelPyObject` `Variable`s representing each of the arguments.
+
+        Create a new `PyccelPyObject` variable for each argument returned in Python.
+        The results are saved to the `self._python_object_map` dictionary so they can be
+        discovered later.
+
+        Parameters
+        ----------
+        args : iterable of FunctionDefArguments
+            The arguments of the function.
+
+        Returns
+        -------
+        list of Variable
+            Variables which will hold the arguments in Python.
+        """
+        collect_args = [self.get_new_PyObject(a.var.name+'_obj') for a in args]
+        self._python_object_map.update(dict(zip(args, collect_args)))
+        return collect_args
+
+    def _unpack_python_args(self, args):
+        """
+        Unpack the arguments received from Python into the expected Python variables.
+
+        Create the wrapper arguments of the current `FunctionDef` (`self`, `args`, `kwargs`).
+        Get a new set of `PyccelPyObject` `Variable`s representing each of the expected
+        arguments. Add the code which unpacks the `args` and `kwargs` into individual
+        `PyccelPyObject`s for each of the expected arguments.
+
+        Parameters
+        ----------
+        args : iterable of FunctionDefArguments
+            The expected arguments of the function.
+
+        Returns
+        -------
+        func_args : list of Variable
+            The arguments of the FunctionDef.
+
+        body : list of pyccel.ast.basic.Basic
+            The code which unpacks the arguments.
+
+        Examples
+        --------
+        >>> arg = Variable('int', 'x')
+        >>> func_args = (FunctionDefArgument(arg),)
+        >>> wrapper_args, body = self._unpack_python_args(func_args)
+        >>> wrapper_args
+        [Variable('self', dtype=PyccelPyObject()), Variable('args', dtype=PyccelPyObject()), Variable('kwargs', dtype=PyccelPyObject())]
+        >>> body
+        [<pyccel.ast.cwrapper.PyArgKeywords object at 0x7f99ec128cc0>, <pyccel.ast.core.If object at 0x7f99ed3a5b20>]
+        >>> CWrapperCodePrinter('wrapper_file.c').doprint(expr)
+        static char *kwlist[] = {
+            "x",
+            NULL
+        };
+        if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O", kwlist, &x_obj))
+        {
+            return NULL;
+        }
+        """
+        # Create necessary variables
+        func_args = [self.get_new_PyObject(n) for n in ("self", "args", "kwargs")]
+        arg_vars  = self._get_python_argument_variables(args)
+        keyword_list_name = self.scope.get_new_name('kwlist')
+
+        # Create the list of argument names
+        arg_names = [getattr(a, 'original_function_argument_variable', a.var).name for a in args]
+        keyword_list = PyArgKeywords(keyword_list_name, arg_names)
+
+        # Initialise optionals
+        body = [AliasAssign(py_arg, Py_None) for func_def_arg, py_arg in zip(args, arg_vars) if func_def_arg.has_default]
+
+        # Parse arguments
+        parse_node = PyArg_ParseTupleNode(*func_args[1:], args, arg_vars, keyword_list)
+
+        body.append(keyword_list)
+        body.append(If(IfSection(PyccelNot(parse_node), [Return([Nil()])])))
+
+        return func_args, body
+
+    def _get_python_result_variables(self, results):
+        """
+        Get a new set of `PyccelPyObject` `Variable`s representing each of the results.
+
+        Create a new `PyccelPyObject` variable for each result returned in Python.
+        The results are saved to the `self._python_object_map` dictionary so they can be
+        discovered later.
+
+        Parameters
+        ----------
+        results : iterable of FunctionDefResults
+            The results of the function.
+
+        Returns
+        -------
+        list of Variable
+            Variables which will hold the results in Python.
+        """
+        collect_results = [self.get_new_PyObject(r.var.name+'_obj') for r in results]
+        self._python_object_map.update(dict(zip(results, collect_results)))
+        return collect_results
+
+    def _get_check_function(self, py_obj, arg, raise_error):
+        """
+        Get the function which checks if an argument has the expected type.
+
+        Using the c-compatible description of a function argument, determine whether the Python
+        object (with datatype `PyccelPyObject`) holds data which is compatible with the expected
+        type. The check is returned along with any errors that may be raised depending upon the
+        result and the value of `raise_error`.
+
+        Parameters
+        ----------
+        py_obj : Variable
+            The variable with datatype `PyccelPyObject` where the arguments is stored in Python.
+
+        arg : Variable
+            The C-compatible variable which holds all the details about the expected type.
+
+        raise_error : bool
+            True if an error should be raised in case of an unexpected type, False otherwise.
+
+        Returns
+        -------
+        func_call : FunctionCall
+            The function call which checks if the argument has the expected type.
+
+        error_code : tuple of pyccel.ast.basic.Basic
+            The code which raises any necessary errors.
+        """
+        rank = arg.rank
+        error_code = ()
+        if rank == 0:
+            dtype = arg.dtype
+            prec  = arg.precision
+            try :
+                cast_function = check_type_registry[(dtype, prec)]
+            except KeyError:
+                errors.report(f"Can't check the type of {dtype}[kind = {prec}]\n"+PYCCEL_RESTRICTION_TODO,
+                        symbol=arg, severity='fatal')
+            func = FunctionDef(name = cast_function,
+                               body      = [],
+                               arguments = [FunctionDefArgument(Variable(dtype=PyccelPyObject(), name = 'o', memory_handling='alias'))],
+                               results   = [FunctionDefResult(Variable(dtype=dtype, name = 'v', precision = prec))])
+
+            if raise_error:
+                message = LiteralString(f"Expected an argument of type {dtype} for argument {arg.name}")
+                python_error = FunctionCall(PyErr_SetString, [PyTypeError, message])
+                error_code = (python_error,)
+
+            func_call = FunctionCall(func, [py_obj])
+        else:
+            dtype = str(arg.dtype)
+            prec  = get_final_precision(arg)
+            try :
+                type_ref = numpy_dtype_registry[(dtype, prec)]
+            except KeyError:
+                errors.report(f"Can't check the type of an array of {dtype}[kind = {prec}]\n"+PYCCEL_RESTRICTION_TODO,
+                        symbol=arg, severity='fatal')
+
+            # order flag
+            if rank == 1:
+                flag     = no_order_check
+            elif arg.order == 'F':
+                flag = numpy_flag_f_contig
+            else:
+                flag = numpy_flag_c_contig
+
+            check_func = pyarray_check if raise_error else is_numpy_array
+            # No error code required as the error is raised inside pyarray_check
+
+            func_call = FunctionCall(check_func, [py_obj, type_ref, LiteralInteger(rank), flag])
+
+        return func_call, error_code
+
+    def _get_type_check_function(self, name, args, funcs):
+        """
+        Determine the flags which allow correct function to be identified from the interface.
+
+        Each function must be identifiable by a different integer value. This value is known
+        as a flag. Different parts of the flag indicate the types of different arguments.
+        Take for example the following function:
+        ```python
+        @types('int', 'int')
+        @types('float', 'float')
+        def f(a, b):
+            pass
+        ```
+        The values 0 (int) and 1 (float) would indicate the type of the argument a. In order
+        to preserve this information the values which indicate the type of the argument b
+        must only change the part of the flag which does not contain this information. In other
+        words `flag % n_types_a = flag_a`. Therefore the values 0 (int) and 2(float) indicate
+        the type of the argument b.
+        We then finally have the following four options:
+          1. 0 = 0 + 0 => (int,int)
+          2. 1 = 1 + 0 => (float,int)
+          3. 2 = 0 + 2 => (int, float)
+          4. 3 = 1 + 2 => (float, float)
+
+        of which only the first and last flags indicate acceptable arguments.
+
+        The function returns a dictionary whose keys are the functions and whose values are
+        a list of the flags which would indicate the correct types.
+        In the above example we would return `{func_0 : [0,0], func_1 : [1,2]}`.
+        It also returns a FunctionDef which determines the index of the chosen function.
+
+        Parameters
+        ----------
+        name : str
+            The name of the function to be generated.
+
+        args : iterable of Variable
+            A list containing the variables of datatype `PyccelPyObject` describing the
+            arguments that were passed to the function from Python.
+
+        funcs : list of FunctionDefs
+            The functions in the Interface.
+
+        Returns
+        -------
+        func : FunctionDef
+            The function which determines the key identifying the relevant function.
+
+        argument_type_flags : dict
+            A dictionary whose keys are the functions and whose values are the integer keys
+            which indicate that the function should be chosen.
+        """
+        func_scope = self.scope.new_child_scope(name)
+        self.scope = func_scope
+        orig_funcs = [getattr(func, 'original_function', func) for func in funcs]
+        type_indicator = Variable('int', self.scope.get_new_name('type_indicator'))
+
+        # Initialise the argument_type_flags
+        argument_type_flags = {func : 0 for func in funcs}
+
+        # Initialise type_indicator
+        body = [Assign(type_indicator, LiteralInteger(0))]
+
+        step = 1
+        for i, py_arg in enumerate(args):
+            # Get the relevant typed arguments from the original functions
+            interface_args = [getattr(func.arguments[i], 'original_function_argument_variable', func.arguments[i].var) for func in orig_funcs]
+            # Get the type key
+            interface_types = [(a.dtype, a.precision, a.rank, a.order) for a in interface_args]
+            # Get a dictionary mapping each unique type key to an example argument
+            type_to_example_arg = dict(zip(interface_types, interface_args))
+            # Get a list of unique keys
+            possible_types = list(type_to_example_arg.keys())
+
+            n_possible_types = len(possible_types)
+            if n_possible_types != 1:
+                # Update argument_type_flags with the index of the type key
+                for func, t in zip(funcs, interface_types):
+                    index = possible_types.index(t)*step
+                    argument_type_flags[func] += index
+
+                # Create the type checks and incrementation of the type_indicator
+                if_blocks = []
+                for index, t in enumerate(possible_types):
+                    check_func_call, _ = self._get_check_function(py_arg, type_to_example_arg[t], False)
+                    if_blocks.append(IfSection(check_func_call, [AugAssign(type_indicator, '+', LiteralInteger(index*step))]))
+                body.append(If(*if_blocks, IfSection(LiteralTrue(),
+                            [FunctionCall(PyErr_SetString, [PyTypeError, f"Unexpected type for argument {interface_args[0].name}"]),
+                             Return([PyccelUnarySub(LiteralInteger(1))])])))
+
+            # Update the step to ensure unique indices for each argument
+            step *= n_possible_types
+
+        body.append(Return([type_indicator]))
+
+        self.exit_scope()
+
+        doc_string = CommentBlock("Assess the types. Raise an error for unexpected types and calculate an integer\n" +
+                        "which indicates which function should be called.")
+
+        # Build the function
+        func = FunctionDef(name, [FunctionDefArgument(a) for a in args], [FunctionDefResult(type_indicator)],
+                            body, doc_string=doc_string, scope=func_scope)
+
+        return func, argument_type_flags
+
+    def _get_untranslatable_function(self, name, scope, original_function, error_msg):
+        """
+        Create code for a function complaining about an object which cannot be wrapped.
+
+        Certain functions are not handled in the wrapper (e.g. private),
+        This creates a wrapper function which raises NotImplementedError
+        exception and returns NULL.
+
+        Parameters
+        ----------
+        name : str
+            The name of the generated function.
+
+        scope : Scope
+            The scope of the generated function.
+
+        original_function : FunctionDef
+           The function we were trying to wrap.
+
+        error_msg : str
+            The message to be raised in the NotImplementedError.
+
+        Returns
+        -------
+        PyFunctionDef
+            The new function which raises the error.
+        """
+        func_args = [FunctionDefArgument(self.get_new_PyObject(n)) for n in ("self", "args", "kwargs")]
+        func_results = [FunctionDefResult(self.get_new_PyObject("result", is_temp=True))]
+        function = PyFunctionDef(name = name, arguments = func_args, results = func_results,
+                body = [FunctionCall(PyErr_SetString, [PyNotImplementedError,
+                                        LiteralString(error_msg)]),
+                        Return([Nil()])],
+                scope = scope, original_function = original_function)
+
+        self.scope.functions[name] = function
+
+        return function
+
+    def _build_module_exec_function(self, expr):
+        """
+        Build the function that will be called when the module is first imported.
+
+        Build the function that will be called when the module is first imported.
+        This function must call any initialisation function of the underlying
+        module and must add any variables to the module variable.
+
+        Parameters
+        ----------
+        expr : Module
+            The module of interest.
+
+        Returns
+        -------
+        FunctionDef
+            The initialisation function.
+        """
+        # Initialise the scope
+        func_name = self.scope.get_new_name(expr.name+'_exec_func')
+        func_scope = self.scope.new_child_scope(func_name)
+        self.scope = func_scope
+
+        for v in expr.variables:
+            func_scope.insert_symbol(v.name)
+
+        # Create necessary variables
+        module_var = self.get_new_PyObject("mod")
+        result_var = self.scope.get_temporary_variable('int', precision=4)
+
+        # Call the initialisation function
+        if expr.init_func:
+            body = [FunctionCall(expr.init_func, [])]
+        else:
+            body = []
+
+        # Save module variables to the module variable
+        for v in expr.variables:
+            if v.is_private:
+                continue
+            body.extend(self._wrap(v))
+            wrapped_var = self._python_object_map[v]
+            name = getattr(v, 'indexed_name', v.name)
+            var_name = LiteralString(self.scope.get_python_name(name))
+            add_expr = PyModule_AddObject(module_var, var_name, wrapped_var)
+            if_expr = If(IfSection(PyccelLt(add_expr,LiteralInteger(0)),
+                            [Return([PyccelUnarySub(LiteralInteger(1))])]))
+            body.append(if_expr)
+
+        body.append(Return([LiteralInteger(0)]))
+
+        self.exit_scope()
+
+        return FunctionDef(func_name, [FunctionDefArgument(module_var)], [FunctionDefResult(result_var)], body,
+                scope = func_scope, is_static=True)
+    #--------------------------------------------------------------------------------------------------------------------------------------------
+
+    def _wrap_Module(self, expr):
+        """
+        Build a `PyModule` from a `Module`.
+
+        Create a `PyModule` which wraps a C-compatible `Module`.
+
+        Parameters
+        ----------
+        expr : Module
+            The module which can be called from C.
+
+        Returns
+        -------
+        PyModule
+            The module which can be called from Python.
+        """
+        # Define scope
+        scope = expr.scope
+        mod_scope = Scope(used_symbols = scope.local_used_symbols.copy(), original_symbols = scope.python_names.copy())
+        self.scope = mod_scope
+
+        # Wrap classes
+        classes = [self._wrap(i) for i in expr.classes]
+
+        # Wrap functions
+        funcs_to_wrap = [f for f in expr.funcs if f not in (expr.init_func, expr.free_func)]
+
+        # Add any functions removed by the Fortran printer
+        removed_functions = getattr(expr, 'removed_functions', None)
+        if removed_functions:
+            funcs_to_wrap.extend(removed_functions)
+
+        funcs = [self._wrap(f) for f in funcs_to_wrap]
+
+        # Wrap interfaces
+        interfaces = [self._wrap(i) for i in expr.interfaces]
+
+        exec_func = self._build_module_exec_function(expr)
+
+        self.exit_scope()
+
+        imports = [cwrapper_ndarray_import] if self._wrapping_arrays else []
+        if not isinstance(expr, BindCModule):
+            imports.append(Import(expr.name, expr))
+        original_mod = getattr(expr, 'original_module', expr)
+        return PyModule(original_mod.name, (), funcs, imports = imports,
+                        interfaces = interfaces, classes = classes, scope = mod_scope,
+                        init_func = exec_func)
+
+    def _wrap_BindCModule(self, expr):
+        """
+        Build a `PyModule` from a `BindCModule`.
+
+        Create a `PyModule` which wraps a C-compatible `BindCModule`. This function calls the
+        more general `_wrap_Module` however additional steps are required to ensure that the
+        Fortran functions and variables are declared in C.
+
+        Parameters
+        ----------
+        expr : Module
+            The module which can be called from C.
+
+        Returns
+        -------
+        PyModule
+            The module which can be called from Python.
+
+        """
+        pymod = self._wrap_Module(expr)
+
+        # Add declarations for C-compatible variables
+        decs = [Declare(v.dtype, v.clone(v.name.lower()), module_variable=True, external = True) \
+                                    for v in expr.variables if not v.is_private and isinstance(v, BindCVariable)]
+        pymod.declarations = decs
+
+        external_funcs = []
+        # Add external functions for functions wrapping array variables
+        for v in expr.variable_wrappers:
+            f = v.wrapper_function
+            external_funcs.append(FunctionDef(f.name, f.arguments, f.results, [], is_header = True, scope = Scope()))
+
+        # Add external functions for normal functions
+        for f in expr.funcs:
+            external_funcs.append(FunctionDef(f.name.lower(), f.arguments, f.results, [], is_header = True, scope = Scope()))
+        pymod.external_funcs = external_funcs
+
+        return pymod
+
+    def _wrap_Interface(self, expr):
+        """
+        Build a `PyInterface` from an `Interface`.
+
+        Create a `PyInterface` which wraps a C-compatible `Interface`. The `PyInterface`
+        should take three arguments (`self`, `args`, and `kwargs`) and return a
+        `PyccelPyObject`. The arguments are unpacked into multiple `PyccelPyObject`s
+        which are passed to `PyFunctionDef`s describing each of the internal
+        `FunctionDef` objects. The appropriate `PyFunctionDef` is chosen using an
+        additional function which calculates an integer type_indicator.
+
+        Parameters
+        ----------
+        expr : Interface
+            The interface which can be called from C.
+
+        Returns
+        -------
+        PyInterface
+            The interface which can be called from Python.
+
+        See Also
+        --------
+        CToPythonWrapper._get_type_check_function : The function which defines the calculation
+            of the type_indicator.
+        """
+        # Initialise the scope
+        func_name = self.scope.get_new_name(expr.name+'_wrapper')
+        func_scope = self.scope.new_child_scope(func_name)
+        self.scope = func_scope
+        original_funcs = expr.functions
+        example_func = original_funcs[0]
+
+        # Add the variables to the expected symbols in the scope
+        for a in getattr(example_func, 'bind_c_arguments', example_func.arguments):
+            func_scope.insert_symbol(a.var.name)
+
+        # Create necessary arguments
+        python_args = getattr(example_func, 'bind_c_arguments', example_func.arguments)
+        func_args, body = self._unpack_python_args(python_args)
+
+        # Get python arguments which will be passed to FunctionDefs
+        python_arg_objs = [self._python_object_map[a] for a in python_args]
+
+        type_indicator = Variable('int', self.scope.get_new_name('type_indicator'))
+        self.scope.insert_variable(type_indicator)
+
+        self.exit_scope()
+
+        # Determine flags which indicate argument type
+        type_check_name = self.scope.get_new_name(expr.name+'_type_check')
+        type_check_func, argument_type_flags = self._get_type_check_function(type_check_name, python_arg_objs, original_funcs)
+
+        self.scope = func_scope
+        # Build the body of the function
+        body.append(Assign(type_indicator, FunctionCall(type_check_func, python_arg_objs)))
+
+        functions = []
+        if_sections = []
+        for func, index in argument_type_flags.items():
+            # Add an IfSection calling the appropriate function if the type_indicator matches the index
+            wrapped_func = self._python_object_map[func]
+            if_sections.append(IfSection(PyccelEq(type_indicator, LiteralInteger(index)),
+                                [Return([FunctionCall(wrapped_func, python_arg_objs)])]))
+            functions.append(wrapped_func)
+        if_sections.append(IfSection(LiteralTrue(),
+                    [FunctionCall(PyErr_SetString, [PyTypeError, "Unexpected type combination"]),
+                     Return([Nil()])]))
+        body.append(If(*if_sections))
+        self.exit_scope()
+
+        interface_func = FunctionDef(func_name,
+                                     [FunctionDefArgument(a) for a in func_args],
+                                     [FunctionDefResult(self.get_new_PyObject("result", is_temp=True))],
+                                     body,
+                                     scope=func_scope)
+        for a in python_args:
+            self._python_object_map.pop(a)
+
+        return PyInterface(func_name, functions, interface_func, type_check_func, expr)
+
+    def _wrap_FunctionDef(self, expr):
+        """
+        Build a `PyFunctionDef` form a `FunctionDef`.
+
+        Create a `PyFunctionDef` which wraps a C-compatible `FunctionDef`.
+        The `PyFunctionDef` should take three arguments (`self`, `args`,
+        and `kwargs`) and return a `PyccelPyObject`. If the function is
+        called from an Interface then the arguments are `PyccelPyObject`s
+        describing each of the arguments of the C-compatible function.
+
+        Parameters
+        ----------
+        expr : FunctionDef
+            The function which can be called from C.
+
+        Returns
+        -------
+        PyFunctionDef
+            The function which can be called from Python.
+        """
+        original_func = getattr(expr, 'original_function', expr)
+        func_name = self.scope.get_new_name(original_func.name+'_wrapper')
+        func_scope = self.scope.new_child_scope(func_name)
+        self.scope = func_scope
+
+        is_bind_c_function_def = isinstance(expr, BindCFunctionDef)
+
+        if expr.is_private:
+            self.exit_scope()
+            return self._get_untranslatable_function(func_name,
+                         func_scope, expr,
+                         "Private functions are not accessible from python")
+
+        # Handle un-wrappable functions
+        if any(isinstance(getattr(a, 'original_function_argument_variable', a.var), FunctionAddress) for a in expr.arguments):
+            self.exit_scope()
+            warnings.warn("Functions with functions as arguments will not be callable from Python")
+            return self._get_untranslatable_function(func_name,
+                         func_scope, expr,
+                         "Cannot pass a function as an argument")
+
+        # Add the variables to the expected symbols in the scope
+        for a in expr.arguments:
+            func_scope.insert_symbol(a.var.name)
+        for a in getattr(expr, 'bind_c_arguments', ()):
+            func_scope.insert_symbol(a.original_function_argument_variable.name)
+        for r in expr.results:
+            func_scope.insert_symbol(r.var.name)
+
+        in_interface = len(expr.get_user_nodes(Interface)) > 0
+
+        # Get variables describing the arguments and results that are seen from Python
+        python_args = expr.bind_c_arguments if is_bind_c_function_def else expr.arguments
+        python_results = expr.bind_c_results if is_bind_c_function_def else expr.results
+
+        # Get variables describing the arguments and results that must be passed to the function
+        original_c_args = expr.arguments
+        original_c_results = expr.results
+
+        # Get the arguments of the PyFunctionDef
+        if in_interface:
+            func_args = [FunctionDefArgument(a) for a in self._get_python_argument_variables(python_args)]
+            body = []
+        else:
+            func_args, body = self._unpack_python_args(python_args)
+            func_args = [FunctionDefArgument(a) for a in func_args]
+
+        # Get the results of the PyFunctionDef
+        python_result_variables = self._get_python_result_variables(python_results)
+
+        # Get the code required to extract the C-compatible arguments from the Python arguments
+        body += [l for a in python_args for l in self._wrap(a)]
+
+        # Get the code required to wrap the C-compatible results into Python objects
+        # This function creates variables so it must be called before extracting them from the scope.
+        result_wrap = [l for r in python_results for l in self._wrap(r)]
+
+        # Get the names of the arguments which should be used to call the C-compatible function
+        func_call_arg_names = []
+        for a in original_c_args:
+            if isinstance(a, BindCFunctionDefArgument):
+                orig_var = a.original_function_argument_variable
+                if orig_var.is_optional and not orig_var.is_ndarray:
+                    func_call_arg_names.append(orig_var.name)
+                    continue
+            func_call_arg_names.append(a.var.name)
+
+        # Get the arguments and results which should be used to call the c-compatible function
+        func_call_args = [self.scope.find(self.scope.get_expected_name(n), category='variables') for n in func_call_arg_names]
+        c_results = [self.scope.find(self.scope.get_expected_name(r.var.name), category='variables') for r in original_c_results]
+        c_results = [ObjectAddress(r) if r.dtype is BindCPointer() else r for r in c_results]
+
+        # Call the C-compatible function
+        n_c_results = len(c_results)
+        if n_c_results == 0:
+            body.append(FunctionCall(expr, func_call_args))
+        elif n_c_results == 1:
+            body.append(Assign(c_results[0], FunctionCall(expr, func_call_args)))
+        else:
+            body.append(Assign(c_results, FunctionCall(expr, func_call_args)))
+
+        # Deallocate the C equivalent of any array arguments
+        # The C equivalent is the same variable that is passed to the function unless the target language is Fortran.
+        # In this case the function arguments are the data pointer and the shapes and strides, but the C equivalent
+        # is an ndarray.
+        for a in original_c_args:
+            orig_var = getattr(a, 'original_function_argument_variable', a.var)
+            v = self.scope.find(self.scope.get_expected_name(orig_var.name), category='variables')
+            if v.is_ndarray:
+                if v.is_optional:
+                    body.append(If( IfSection(PyccelIsNot(v, Nil()), [Deallocate(v)]) ))
+                else:
+                    body.append(Deallocate(v))
+        body.extend(result_wrap)
+
+        # Pack the Python compatible results of the function into one argument.
+        n_py_results = len(python_result_variables)
+        if n_py_results == 0:
+            res = Py_None
+            func_results = [FunctionDefResult(self.get_new_PyObject("result", is_temp=True))]
+        elif n_py_results == 1:
+            res = python_result_variables[0]
+            func_results = [FunctionDefResult(res)]
+        else:
+            res = self.get_new_PyObject("result")
+            body.append(AliasAssign(res, PyBuildValueNode([ObjectAddress(r) for r in python_result_variables])))
+            for r in python_result_variables:
+                body.append(FunctionCall(Py_DECREF, [r]))
+            func_results = [FunctionDefResult(res)]
+        body.append(Return([res]))
+
+        self.exit_scope()
+        for a in python_args:
+            self._python_object_map.pop(a)
+        for r in python_results:
+            self._python_object_map.pop(r)
+
+        function = PyFunctionDef(func_name, func_args, func_results, body, scope=func_scope,
+                doc_string = expr.doc_string, original_function = original_func)
+
+        self.scope.functions[func_name] = function
+        self._python_object_map[expr] = function
+
+        return function
+
+    def _wrap_FunctionDefArgument(self, expr):
+        """
+        Get the code which translates a Python `FunctionDefArgument` to a C-compatible `Variable`.
+
+        Get the code necessary to transform a Variable passed as an argument in Python, from an object with
+        datatype `PyccelPyObject` to a Variable that can be used in C code.
+
+        The relevant `PyccelPyObject` is collected from `self._python_object_map`.
+
+        The necessary steps are:
+        - Create a variable to store the C-compatible result.
+        - Initialise the variable to any provided default value.
+        - Cast the Python object to the C object using utility functions.
+        - Raise any useful errors (this is not necessary if the FunctionDef is in an interface as errors are
+            raised while determining which function to call).
+
+        Parameters
+        ----------
+        expr : FunctionDefArgument
+            The argument of the C function.
+
+        Returns
+        -------
+        list of pyccel.ast.basic.Basic
+            The code which translates the `PyccelPyObject` to a C-compatible variable.
+        """
+
+        collect_arg = self._python_object_map[expr]
+        in_interface = len(expr.get_user_nodes(Interface)) > 0
+
+        orig_var = getattr(expr, 'original_function_argument_variable', expr.var)
+
+        if orig_var.is_ndarray:
+            arg_var = orig_var.clone(self.scope.get_expected_name(orig_var.name), is_argument = False, memory_handling='alias')
+            self._wrapping_arrays = True
+        else:
+            arg_var = orig_var.clone(self.scope.get_expected_name(orig_var.name), is_argument = False)
+
+        self.scope.insert_variable(arg_var)
+
+        body = []
+
+        # Initialise to any default value
+        if expr.has_default:
+            default_val = expr.value
+            if isinstance(default_val, Nil):
+                body.append(AliasAssign(arg_var, default_val))
+            else:
+                body.append(Assign(arg_var, default_val))
+
+        # Collect the function which casts from a Python object to a C object
+        if arg_var.rank == 0:
+            dtype = arg_var.dtype
+            prec  = get_final_precision(arg_var)
+            try :
+                cast_function = py_to_c_registry[(dtype, prec)]
+            except KeyError:
+                errors.report(PYCCEL_RESTRICTION_TODO, symbol=dtype,severity='fatal')
+            cast_func = FunctionDef(name = cast_function,
+                               body      = [],
+                               arguments = [FunctionDefArgument(Variable(dtype=PyccelPyObject(), name = 'o', memory_handling='alias'))],
+                               results   = [FunctionDefResult(Variable(dtype=dtype, name = 'v', precision = prec))])
+        else:
+            cast_func = pyarray_to_ndarray
+
+        cast = Assign(arg_var, FunctionCall(cast_func, [collect_arg]))
+
+        # Create any necessary type checks and errors
+        if expr.has_default:
+            check_func, err = self._get_check_function(collect_arg, arg_var, False)
+            body.append(If( IfSection(check_func, [cast]),
+                        IfSection(PyccelIsNot(collect_arg, Py_None), [*err, Return([Nil()])])
+                        ))
+        elif not in_interface:
+            check_func, err = self._get_check_function(collect_arg, arg_var, True)
+            body.append(If( IfSection(check_func, [cast]),
+                        IfSection(LiteralTrue(), [*err, Return([Nil()])])
+                        ))
+        else:
+            body.append(cast)
+
+        return body
+
+    def _wrap_BindCFunctionDefArgument(self, expr):
+        """
+        Get the code which translates a Python `FunctionDefArgument` to a C-compatible `Variable`.
+
+        Get the code necessary to transform a Variable passed as an argument in Python, from an object with
+        datatype `PyccelPyObject` to a Variable that can be used in C code to call code written in Fortran.
+
+        This function calls the more general self._wrap_FunctionDefArgument, however some additional
+        steps are necessary to handle arrays. In this case the arguments passed to the Fortran function are
+        not the same as the C-compatible arguments so they must also be created and initialised.
+
+        Parameters
+        ----------
+        expr : FunctionDefArgument
+            The argument of the C function.
+
+        Returns
+        -------
+        list of pyccel.ast.basic.Basic
+            The code which translates the `PyccelPyObject` to a C-compatible variable.
+        """
+        body = self._wrap_FunctionDefArgument(expr)
+
+        orig_var = expr.original_function_argument_variable
+
+        if orig_var.rank:
+            # Create variable to hold raw data pointer
+            arg_var = expr.var.clone(self.scope.get_expected_name(expr.var.name), is_argument = False)
+            # Create variables for the shapes and strides
+            shape_vars = [s.clone(self.scope.get_expected_name(s.name), is_argument = False) for s in expr.shape]
+            stride_vars = [s.clone(self.scope.get_expected_name(s.name), is_argument = False) for s in expr.strides]
+
+            # Add variables to scope
+            self.scope.insert_variable(arg_var, expr.var.name)
+            for v,s in zip(shape_vars, expr.shape):
+                self.scope.insert_variable(v,s.name)
+            for v,s in zip(stride_vars, expr.strides):
+                self.scope.insert_variable(v,s.name)
+
+            # Get the C-compatible variable created in self._wrap_FunctionDefArgument
+            c_arg = self.scope.find(self.scope.get_expected_name(orig_var.name), category='variables')
+
+            # Unpack the C-compatible variable
+            body.append(AliasAssign(arg_var, FunctionCall(array_get_data, [c_arg])))
+            body.extend(Assign(s, FunctionCall(array_get_dim, [c_arg, i])) for i,s in enumerate(shape_vars))
+            if orig_var.order == 'C':
+                body.extend(Assign(s, FunctionCall(array_get_c_step, [c_arg, i])) for i,s in enumerate(stride_vars))
+            else:
+                body.extend(Assign(s, FunctionCall(array_get_f_step, [c_arg, i])) for i,s in enumerate(stride_vars))
+
+        return body
+
+    def _wrap_FunctionDefResult(self, expr):
+        """
+        Get the code which translates a C-compatible `Variable` to a Python `FunctionDefResult`.
+
+        Get the code necessary to transform a Variable returned from a C-compatible function to an object with
+        datatype `PyccelPyObject`.
+
+        The relevant `PyccelPyObject` is collected from `self._python_object_map`.
+
+        The necessary steps are:
+        - Create a variable to store the C-compatible result.
+        - Cast the C object to the Python object using utility functions.
+        - Deallocate any unused memory (e.g. shapes of a C array).
+
+        Parameters
+        ----------
+        expr : FunctionDefArgument
+            The argument of the C function.
+
+        Returns
+        -------
+        list of pyccel.ast.basic.Basic
+            The code which translates the variable to a `PyccelPyObject`.
+        """
+
+        orig_var = expr.var
+
+        # Create a variable to store the C-compatible result.
+        if orig_var.is_ndarray:
+            # An array is a pointer to ensure the shape is freed but the data is passed through to NumPy
+            c_res = orig_var.clone(self.scope.get_expected_name(orig_var.name), is_argument = False, memory_handling='alias')
+            self._wrapping_arrays = True
+        else:
+            c_res = orig_var.clone(self.scope.get_expected_name(orig_var.name), is_argument = False)
+        self.scope.insert_variable(c_res)
+
+        # Get the object with datatype PyccelPyObject
+        python_res = self._python_object_map[expr]
+
+        # Cast from C to Python
+        body = [AliasAssign(python_res, FunctionCall(C_to_Python(c_res), [c_res]))]
+
+        # Deallocate any unused memory
+        if orig_var.rank:
+            body.append(Deallocate(c_res))
+
+        return body
+
+    def _wrap_BindCFunctionDefResult(self, expr):
+        """
+        Get the code which translates a C-compatible `Variable` to a Python `FunctionDefResult`.
+
+        Get the code necessary to transform a Variable returned from a C-compatible function written in
+        Fortran to an object with datatype `PyccelPyObject`.
+
+        The relevant `PyccelPyObject` is collected from `self._python_object_map`.
+
+        The necessary steps are:
+        - Create a variable to store the C-compatible result.
+        - For arrays also create variables for the Fortran-compatible results.
+        - If necessary, pack the Fortran-compatible results into a C-compatible array.
+        - Cast the Python object to the C object using utility functions.
+        - Deallocate any unused memory (e.g. shapes of a C array).
+
+        Parameters
+        ----------
+        expr : FunctionDefArgument
+            The argument of the C function.
+
+        Returns
+        -------
+        list of pyccel.ast.basic.Basic
+            The code which translates the variable to a `PyccelPyObject`.
+        """
+
+        orig_var = expr.original_function_result_variable
+
+        body = []
+
+        if orig_var.rank:
+            # C-compatible result variable
+            c_res = orig_var.clone(self.scope.get_new_name(orig_var.name), is_argument = False, memory_handling='alias')
+            self._wrapping_arrays = True
+            # Result of calling the bind-c function
+            arg_var = expr.var.clone(self.scope.get_expected_name(expr.var.name), is_argument = False, memory_handling='alias')
+            shape_vars = [s.clone(self.scope.get_expected_name(s.name), is_argument = False) for s in expr.shape]
+            # Save so we can find by iterating over func.results
+            self.scope.insert_variable(arg_var, expr.var.name)
+            for v,s in zip(shape_vars, expr.shape):
+                self.scope.insert_variable(v,s.name)
+            # Save so we can find by iterating over func.bind_c_results
+            self.scope.insert_variable(c_res, orig_var.name)
+
+            body.append(Allocate(c_res, shape = shape_vars, order = orig_var.order, status='unallocated'))
+            body.append(AliasAssign(DottedVariable(NativeVoid(), 'raw_data', memory_handling = 'alias', lhs=c_res), arg_var))
+        else:
+            c_res = orig_var.clone(self.scope.get_expected_name(orig_var.name), is_argument = False)
+            self.scope.insert_variable(c_res)
+
+        python_res = self._python_object_map[expr]
+        body.append(AliasAssign(python_res, FunctionCall(C_to_Python(c_res), [c_res])))
+
+        if orig_var.rank:
+            body.append(Deallocate(c_res))
+
+        return body
+
+    def _wrap_Variable(self, expr):
+        """
+        Get the code which translates a C-compatible module variable to an object with datatype `PyccelPyObject`.
+
+        Get the code which translates a C-compatible module variable to an object with datatype `PyccelPyObject`.
+        This new object is saved into self._python_object_map. The translation is achieved using utility
+        functions.
+
+        Parameters
+        ----------
+        expr : Variable
+            The module variable.
+
+        Returns
+        -------
+        list of pyccel.ast.basic.Basic
+            The code which translates the Variable to a Python-compatible variable.
+        """
+
+        # Ensure that cwrapper_ndarrays is imported
+        if expr.rank > 0:
+            self._wrapping_arrays = True
+
+        # Create the resulting Variable with datatype `PyccelPyObject`
+        py_equiv = self.scope.get_temporary_variable(PyccelPyObject(), memory_handling='alias')
+        # Save the Variable so it can be located later
+        self._python_object_map[expr] = py_equiv
+
+        # Cast the C variable into a Python variable
+        wrapper_function = C_to_Python(expr)
+        return [AliasAssign(py_equiv, FunctionCall(wrapper_function, [expr]))]
+
+    def _wrap_BindCArrayVariable(self, expr):
+        """
+        Get the code which translates a Fortran array module variable to an object with datatype `PyccelPyObject`.
+
+        Get the code which translates a Fortran array module variable to an object with datatype `PyccelPyObject`
+        which can be used as a Python module variable. This new object is saved into self._python_object_map.
+        Fortran arrays are not compatible with C, but objects of type `BindCArrayVariable` contain wrapper
+        functions which can be used to retrieve C-compatible variables.
+
+        The necessary steps are:
+        - Create the variables necessary to retrieve array objects from Fortran.
+        - Call the bind c wrapper function to initialise these objects.
+        - Pack the results into a C-compatible `ndarray`.
+        - Use `self._wrap_Variable` to get the object with datatype `PyccelPyObject`.
+        - Correct the key in self._python_object_map initialised by `self._wrap_Variable`.
+
+        Parameters
+        ----------
+        expr : BindCArrayVariable
+            The array module variable.
+
+        Returns
+        -------
+        list of pyccel.ast.basic.Basic
+            The code which translates the Variable to a Python-compatible variable.
+        """
+        v = expr.original_variable
+
+        # Get pointer to store raw array data
+        var = self.scope.get_temporary_variable(dtype_or_var = NativeVoid(),
+                name = v.name + '_data', memory_handling = 'alias')
+        # Create variables to store the shape of the array
+        shape = [self.scope.get_temporary_variable(NativeInteger(),
+                v.name+'_size') for _ in range(v.rank)]
+        # Get the bind_c function which wraps a fortran array and returns c objects
+        var_wrapper = expr.wrapper_function
+        # Call bind_c function
+        call = Assign(PythonTuple(ObjectAddress(var), *shape), FunctionCall(var_wrapper, ()))
+
+        # Create ndarray to store array data
+        nd_var = self.scope.get_temporary_variable(dtype_or_var = v,
+                name = v.name, memory_handling = 'alias')
+        alloc = Allocate(nd_var, shape=shape, order=nd_var.order, status='unallocated')
+        # Save raw_data into ndarray to obtain useable pointer
+        set_data = AliasAssign(DottedVariable(NativeVoid(), 'raw_data',
+                memory_handling = 'alias', lhs=nd_var), var)
+
+        # Save the ndarray to vars_to_wrap to be handled as if it came from C
+        body = [call, alloc, set_data] + self._wrap_Variable(nd_var)
+
+        # Correct self._python_object_map key
+        py_equiv = self._python_object_map.pop(nd_var)
+        self._python_object_map[expr] = py_equiv
+
+        return body
+
diff --git a/pyccel/codegen/wrapper/fortran_to_c_wrapper.py b/pyccel/codegen/wrapper/fortran_to_c_wrapper.py
index e03764933e..0efa1dfdd4 100644
--- a/pyccel/codegen/wrapper/fortran_to_c_wrapper.py
+++ b/pyccel/codegen/wrapper/fortran_to_c_wrapper.py
@@ -10,7 +10,8 @@
 import warnings
 from pyccel.ast.bind_c import BindCFunctionDefArgument, BindCFunctionDefResult
 from pyccel.ast.bind_c import BindCPointer, BindCFunctionDef, C_F_Pointer
-from pyccel.ast.bind_c import CLocFunc, BindCModule
+from pyccel.ast.bind_c import CLocFunc, BindCModule, BindCVariable
+from pyccel.ast.bind_c import BindCArrayVariable
 from pyccel.ast.core import Assign, FunctionCall, FunctionCallArgument
 from pyccel.ast.core import Allocate, EmptyNode, FunctionAddress
 from pyccel.ast.core import If, IfSection, Import, Interface
@@ -164,7 +165,7 @@ def _wrap_Module(self, expr):
         self.scope = mod_scope
 
         # Wrap contents
-        funcs_to_wrap = [f for f in expr.funcs if not f.is_private]
+        funcs_to_wrap = expr.funcs
         funcs = [self._wrap(f) for f in funcs_to_wrap]
         if expr.init_func:
             init_func = funcs[next(i for i,f in enumerate(funcs_to_wrap) if f == expr.init_func)]
@@ -174,11 +175,12 @@ def _wrap_Module(self, expr):
             free_func = funcs[next(i for i,f in enumerate(funcs_to_wrap) if f == expr.free_func)]
         else:
             free_func = None
+        removed_functions = [f for f,w in zip(funcs_to_wrap, funcs) if isinstance(w, EmptyNode)]
         funcs = [f for f in funcs if not isinstance(f, EmptyNode)]
         interfaces = [self._wrap(f) for f in expr.interfaces]
         classes = [self._wrap(f) for f in expr.classes]
-        variable_getters = [self._wrap(v) for v in expr.variables if not v.is_private]
-        variable_getters = [v for v in variable_getters if not isinstance(v, EmptyNode)]
+        variables = [self._wrap(v) for v in expr.variables if not v.is_private]
+        variable_getters = [v for v in variables if isinstance(v, BindCArrayVariable)]
         imports = [Import(expr.name, target = expr, mod=expr)]
 
         name = mod_scope.get_new_name(f'bind_c_{expr.name.target}')
@@ -186,11 +188,11 @@ def _wrap_Module(self, expr):
 
         self.exit_scope()
 
-        return BindCModule(name, (), funcs, variable_wrappers = variable_getters,
+        return BindCModule(name, variables, funcs, variable_wrappers = variable_getters,
                 init_func = init_func, free_func = free_func,
                 interfaces = interfaces, classes = classes,
                 imports = imports, original_module = expr,
-                scope = mod_scope)
+                scope = mod_scope, removed_functions = removed_functions)
 
     def _wrap_FunctionDef(self, expr):
         """
@@ -215,6 +217,9 @@ def _wrap_FunctionDef(self, expr):
         BindCFunctionDef
             The C-compatible function.
         """
+        if expr.is_private:
+            return EmptyNode()
+
         name = self.scope.get_new_name(f'bind_c_{expr.name.lower()}')
         self._wrapper_names_dict[expr.name] = name
 
@@ -369,7 +374,7 @@ def _wrap_FunctionDefResult(self, expr):
             # Create the C-compatible data pointer
             bind_var = Variable(dtype=BindCPointer(),
                                 name=scope.get_new_name('bound_'+name),
-                                is_const=True, memory_handling='alias')
+                                is_const=False, memory_handling='alias')
             scope.insert_variable(bind_var)
 
             result = BindCFunctionDefResult(bind_var, var, scope)
@@ -417,7 +422,7 @@ def _wrap_Variable(self, expr):
             the wrapping module to expose the variable.
         """
         if expr.rank == 0 and expr.dtype in NativeNumeric:
-            return EmptyNode()
+            return expr.clone(expr.name, new_class = BindCVariable)
         else:
             scope = self.scope
             func_name = scope.get_new_name('bind_c_'+expr.name.lower())
@@ -448,4 +453,5 @@ def _wrap_Variable(self, expr):
                           imports   = [import_mod],
                           scope = func_scope,
                           original_function = expr)
-            return func
+            return expr.clone(expr.name, new_class = BindCArrayVariable, wrapper_function = func,
+                                original_variable = expr)
diff --git a/pyccel/stdlib/cwrapper_ndarrays/cwrapper_ndarrays.c b/pyccel/stdlib/cwrapper_ndarrays/cwrapper_ndarrays.c
index c6bd04846f..3b95c21ea8 100644
--- a/pyccel/stdlib/cwrapper_ndarrays/cwrapper_ndarrays.c
+++ b/pyccel/stdlib/cwrapper_ndarrays/cwrapper_ndarrays.c
@@ -203,9 +203,9 @@ static char* _check_pyarray_type(PyObject *a)
 	return NULL;
 }
 
-enum NPY_TYPES get_numpy_type(t_ndarray *o)
+enum NPY_TYPES get_numpy_type(t_ndarray o)
 {
-    enum e_types nd_type = o->type;
+    enum e_types nd_type = o.type;
     enum NPY_TYPES npy_type;
     switch (nd_type)
     {
@@ -304,16 +304,16 @@ t_ndarray	pyarray_to_ndarray(PyObject *o)
 	return array;
 }
 
-PyObject* ndarray_to_pyarray(t_ndarray *o)
+PyObject* ndarray_to_pyarray(t_ndarray o)
 {
     int FLAGS;
-    if (o->nd == 1) {
+    if (o.nd == 1) {
         FLAGS = NPY_ARRAY_F_CONTIGUOUS | NPY_ARRAY_C_CONTIGUOUS | NPY_ARRAY_WRITEABLE;
     }
-    else if (o->order == order_c) {
+    else if (o.order == order_c) {
         FLAGS = NPY_ARRAY_C_CONTIGUOUS | NPY_ARRAY_WRITEABLE;
     }
-    else if (o->order == order_f) {
+    else if (o.order == order_f) {
         FLAGS = NPY_ARRAY_C_CONTIGUOUS | NPY_ARRAY_WRITEABLE;
     }
     else {
@@ -323,9 +323,9 @@ PyObject* ndarray_to_pyarray(t_ndarray *o)
     enum NPY_TYPES npy_type = get_numpy_type(o);
 
     return PyArray_NewFromDescr(&PyArray_Type, PyArray_DescrFromType(npy_type),
-            o->nd, _ndarray_to_numpy_shape(o->shape, o->nd),
-            _ndarray_to_numpy_strides(o->strides, o->type_size, o->nd),
-            o->raw_data, FLAGS, NULL);
+            o.nd, _ndarray_to_numpy_shape(o.shape, o.nd),
+            _ndarray_to_numpy_strides(o.strides, o.type_size, o.nd),
+            o.raw_data, FLAGS, NULL);
 }
 
 /*
diff --git a/pyccel/stdlib/cwrapper_ndarrays/cwrapper_ndarrays.h b/pyccel/stdlib/cwrapper_ndarrays/cwrapper_ndarrays.h
index c92c005a0c..2e69532212 100644
--- a/pyccel/stdlib/cwrapper_ndarrays/cwrapper_ndarrays.h
+++ b/pyccel/stdlib/cwrapper_ndarrays/cwrapper_ndarrays.h
@@ -35,12 +35,12 @@
  * -------------------------------------------
  * https://numpy.org/doc/stable/reference/c-api/array.html
  */
-enum NPY_TYPES get_numpy_type(t_ndarray *o);
+enum NPY_TYPES get_numpy_type(t_ndarray o);
 enum e_types get_ndarray_type(PyArrayObject *a);
 t_ndarray	pyarray_to_ndarray(PyObject *o);
-PyObject* ndarray_to_pyarray(t_ndarray *o);
-PyObject* c_ndarray_to_pyarray(t_ndarray *o);
-PyObject* fortran_ndarray_to_pyarray(t_ndarray *o);
+PyObject* ndarray_to_pyarray(t_ndarray o);
+PyObject* c_ndarray_to_pyarray(t_ndarray o);
+PyObject* fortran_ndarray_to_pyarray(t_ndarray o);
 
 
 /* arrays checkers and helpers */
diff --git a/pyccel/stdlib/ndarrays/ndarrays.c b/pyccel/stdlib/ndarrays/ndarrays.c
index 833a804c12..02c940815a 100644
--- a/pyccel/stdlib/ndarrays/ndarrays.c
+++ b/pyccel/stdlib/ndarrays/ndarrays.c
@@ -270,28 +270,28 @@ void   _array_fill_cdouble(double complex c, t_ndarray arr)
 ** deallocation
 */
 
-int32_t free_array(t_ndarray arr)
+int32_t free_array(t_ndarray* arr)
 {
-    if (arr.shape == NULL)
+    if (arr->shape == NULL)
         return (0);
-    free(arr.raw_data);
-    arr.raw_data = NULL;
-    free(arr.shape);
-    arr.shape = NULL;
-    free(arr.strides);
-    arr.strides = NULL;
+    free(arr->raw_data);
+    arr->raw_data = NULL;
+    free(arr->shape);
+    arr->shape = NULL;
+    free(arr->strides);
+    arr->strides = NULL;
     return (1);
 }
 
 
-int32_t free_pointer(t_ndarray arr)
+int32_t free_pointer(t_ndarray* arr)
 {
-    if (arr.is_view == false || arr.shape == NULL)
+    if (arr->is_view == false || arr->shape == NULL)
         return (0);
-    free(arr.shape);
-    arr.shape = NULL;
-    free(arr.strides);
-    arr.strides = NULL;
+    free(arr->shape);
+    arr->shape = NULL;
+    free(arr->strides);
+    arr->strides = NULL;
     return (1);
 }
 
diff --git a/pyccel/stdlib/ndarrays/ndarrays.h b/pyccel/stdlib/ndarrays/ndarrays.h
index 0ed60a6c3e..37b890f126 100644
--- a/pyccel/stdlib/ndarrays/ndarrays.h
+++ b/pyccel/stdlib/ndarrays/ndarrays.h
@@ -141,8 +141,8 @@ void        alias_assign(t_ndarray *dest, t_ndarray src);
 void        transpose_alias_assign(t_ndarray *dest, t_ndarray src);
 
 /* free */
-int32_t         free_array(t_ndarray dump);
-int32_t         free_pointer(t_ndarray dump);
+int32_t         free_array(t_ndarray* dump);
+int32_t         free_pointer(t_ndarray* dump);
 
 /* indexing */
 int64_t         get_index(t_ndarray arr, ...);
diff --git a/pyccel/version.py b/pyccel/version.py
index 9efa22ff52..19c4be4e56 100644
--- a/pyccel/version.py
+++ b/pyccel/version.py
@@ -1,4 +1,4 @@
 """
 Module specifying the current version string for pyccel
 """
-__version__ = "1.8.1"
+__version__ = "1.9.0"


Test patch:
diff --git a/tests/ndarrays/test_ndarrays.c b/tests/ndarrays/test_ndarrays.c
index 59bce65ea3..e308aced9c 100644
--- a/tests/ndarrays/test_ndarrays.c
+++ b/tests/ndarrays/test_ndarrays.c
@@ -174,7 +174,7 @@ int32_t test_indexing_int64(void)
     value = x.nd_int64[index];
     c_value = 103;
     my_assert(value , c_value, "testing the value");
-    free_array(x);
+    free_array(&x);
     return (0);
 }
 
@@ -203,7 +203,7 @@ int32_t test_indexing_int64_order_f(void)
     value = x.nd_int64[index];
     c_value = 9;
     my_assert(value , c_value, "testing the value");
-    free_array(x);
+    free_array(&x);
     return (0);
 }
 
@@ -232,7 +232,7 @@ int32_t test_indexing_int32(void)
     value = x.nd_int32[index];
     c_value = 103;
     my_assert(value , c_value, "testing the value");
-    free_array(x);
+    free_array(&x);
     return (0);
 }
 
@@ -261,7 +261,7 @@ int32_t test_indexing_int32_order_f(void)
     value = x.nd_int32[index];
     c_value = 6;
     my_assert(value , c_value, "testing the value");
-    free_array(x);
+    free_array(&x);
     return (0);
 }
 
@@ -291,7 +291,7 @@ int32_t test_indexing_int16(void)
     value = x.nd_int16[index];
     c_value = 103;
     my_assert(value , c_value, "testing the value");
-    free_array(x);
+    free_array(&x);
     return (0);
 }
 
@@ -320,7 +320,7 @@ int32_t test_indexing_int16_order_f(void)
     value = x.nd_int16[index];
     c_value = 6;
     my_assert(value , c_value, "testing the value");
-    free_array(x);
+    free_array(&x);
     return (0);
 }
 
@@ -349,7 +349,7 @@ int32_t test_indexing_int8(void)
     value = x.nd_int8[index];
     c_value = 103;
     my_assert(value , c_value, "testing the value");
-    free_array(x);
+    free_array(&x);
     return (0);
 }
 
@@ -378,7 +378,7 @@ int32_t test_indexing_int8_order_f(void)
     value = x.nd_int8[index];
     c_value = 6;
     my_assert(value , c_value, "testing the value");
-    free_array(x);
+    free_array(&x);
     return (0);
 }
 
@@ -407,7 +407,7 @@ int32_t test_indexing_double(void)
     value = x.nd_double[index];
     c_value = 103.009;
     my_assert(value , c_value, "testing the value");
-    free_array(x);
+    free_array(&x);
     return (0);
 }
 
@@ -441,7 +441,7 @@ int32_t test_indexing_double_order_f(void)
     value = x.nd_double[index];
     c_value = 0.39310326602036905;
     my_assert(value , c_value, "testing the value");
-    free_array(x);
+    free_array(&x);
     return (0);
 }
 
@@ -470,7 +470,7 @@ int32_t test_indexing_cdouble(void)
     value = x.nd_cdouble[index];
     c_value = 0.58532094+0.67890618*I;
     my_assert(value , c_value, "testing the value");
-    free_array(x);
+    free_array(&x);
     return (0);
 }
 
@@ -499,7 +499,7 @@ int32_t test_indexing_cdouble_order_f(void)
     value = x.nd_cdouble[index];
     c_value = 0.02005023+0.59950758*I;
     my_assert(value , c_value, "testing the value");
-    free_array(x);
+    free_array(&x);
     return (0);
 }
 
@@ -528,7 +528,7 @@ int32_t test_indexing_cfloat(void)
     value = x.nd_cfloat[index];
     c_value = 0.58532094+0.67890618*I;
     my_assert(value , c_value, "testing the value");
-    free_array(x);
+    free_array(&x);
     return (0);
 }
 
@@ -557,7 +557,7 @@ int32_t test_indexing_cfloat_order_f(void)
     value = x.nd_cfloat[index];
     c_value = 0.8023905547293535+0.17882460280193202*I;
     my_assert(value , c_value, "testing the value");
-    free_array(x);
+    free_array(&x);
     return (0);
 }
 
@@ -601,8 +601,8 @@ int32_t test_slicing_int64(void)
     xview.nd_int64[get_index(xview, 0, 1)] = c_value;
     value = x.nd_int64[get_index(x, 1, 2)];
     my_assert(value , c_value, "testing xview assignment");
-    free_array(x);
-    free_pointer(xview);
+    free_array(&x);
+    free_pointer(&xview);
     return (0);
 }
 
@@ -639,8 +639,8 @@ int32_t test_slicing_int64_order_f(void)
     xview.nd_int64[get_index(xview, 0, 1)] = c_value;
     value = x.nd_int64[get_index(x, 1, 2)];
     my_assert(value , c_value, "testing xview assignment");
-    free_array(x);
-    free_pointer(xview);
+    free_array(&x);
+    free_pointer(&xview);
     return (0);
 }
 
@@ -680,8 +680,8 @@ int32_t test_slicing_int32(void)
     xview.nd_int32[get_index(xview, 0, 1)] = c_value;
     value = x.nd_int32[get_index(x, 1, 2)];
     my_assert(value , c_value, "testing xview assignment");
-    free_array(x);
-    free_pointer(xview);
+    free_array(&x);
+    free_pointer(&xview);
     return (0);
 }
 
@@ -715,8 +715,8 @@ int32_t test_slicing_int32_order_f(void)
     xview.nd_int32[get_index(xview, 1)] = c_value;
     value = x.nd_int32[get_index(x, 1, 2)];
     my_assert(value , c_value, "testing xview assignment");
-    free_array(x);
-    free_pointer(xview);
+    free_array(&x);
+    free_pointer(&xview);
     return (0);
 }
 
@@ -756,8 +756,8 @@ int32_t test_slicing_int16(void)
     xview.nd_int16[get_index(xview, 0, 1)] = c_value;
     value = x.nd_int16[get_index(x, 1, 2)];
     my_assert(value , c_value, "testing xview assignment");
-    free_array(x);
-    free_pointer(xview);
+    free_array(&x);
+    free_pointer(&xview);
     return (0);
 }
 
@@ -794,8 +794,8 @@ int32_t test_slicing_int16_order_f(void)
     xview.nd_int16[get_index(xview, 0, 1)] = c_value;
     value = x.nd_int16[get_index(x, 1, 2)];
     my_assert(value , c_value, "testing xview assignment");
-    free_array(x);
-    free_pointer(xview);
+    free_array(&x);
+    free_pointer(&xview);
     return (0);
 }
 
@@ -835,8 +835,8 @@ int32_t test_slicing_int8(void)
     xview.nd_int8[get_index(xview, 0, 1)] = c_value;
     value = x.nd_int8[get_index(x, 1, 2)];
     my_assert(value , c_value, "testing xview assignment");
-    free_array(x);
-    free_pointer(xview);
+    free_array(&x);
+    free_pointer(&xview);
     return (0);
 }
 
@@ -873,8 +873,8 @@ int32_t test_slicing_int8_order_f(void)
     xview.nd_int8[get_index(xview, 0, 1)] = c_value;
     value = x.nd_int8[get_index(x, 1, 2)];
     my_assert(value , c_value, "testing xview assignment");
-    free_array(x);
-    free_pointer(xview);
+    free_array(&x);
+    free_pointer(&xview);
     return (0);
 }
 
@@ -914,8 +914,8 @@ int32_t test_slicing_double(void)
     xview.nd_double[get_index(xview, 0, 1)] = c_value;
     value = x.nd_double[get_index(x, 1, 2)];
     my_assert(value, c_value, "testing xview assignment");
-    free_array(x);
-    free_pointer(xview);
+    free_array(&x);
+    free_pointer(&xview);
     return (0);
 }
 
@@ -952,8 +952,8 @@ int32_t test_slicing_double_order_f(void)
     xview.nd_double[get_index(xview, 0, 1)] = c_value;
     value = x.nd_double[get_index(x, 1, 2)];
     my_assert(value, c_value, "testing xview assignment");
-    free_array(x);
-    free_pointer(xview);
+    free_array(&x);
+    free_pointer(&xview);
     return (0);
 }
 
@@ -989,8 +989,8 @@ int32_t test_slicing_cdouble(void)
     xview.nd_cdouble[get_index(xview, 0, 1)] = c_value;
     value = x.nd_cdouble[get_index(x, 1, 2)];
     my_assert(value, c_value, "testing xview assignment");
-    free_array(x);
-    free_pointer(xview);
+    free_array(&x);
+    free_pointer(&xview);
     return (0);
 }
 
@@ -1028,8 +1028,8 @@ int32_t test_slicing_cdouble_order_f(void)
     xview.nd_cdouble[get_index(xview, 0, 1)] = c_value;
     value = x.nd_cdouble[get_index(x, 1, 2)];
     my_assert(value, c_value, "testing xview assignment");
-    free_array(x);
-    free_pointer(xview);
+    free_array(&x);
+    free_pointer(&xview);
     return (0);
 }
 
@@ -1055,7 +1055,7 @@ int32_t test_array_fill_int64(void)
     value = x.nd_int64[index];
     c_value = 32;
     my_assert(value , c_value, "testing the value");
-    free_array(x);
+    free_array(&x);
     return (0);
 }
 
@@ -1079,7 +1079,7 @@ int32_t test_array_fill_int64_order_f(void)
     value = x.nd_int64[index];
     c_value = 64;
     my_assert(value , c_value, "testing the value");
-    free_array(x);
+    free_array(&x);
     return (0);
 }
 
@@ -1104,7 +1104,7 @@ int32_t test_array_fill_int32(void)
     value = x.nd_int32[index];
     c_value = 32;
     my_assert(value , c_value, "testing the value");
-    free_array(x);
+    free_array(&x);
     return (0);
 }
 
@@ -1128,7 +1128,7 @@ int32_t test_array_fill_int32_order_f(void)
     value = x.nd_int32[index];
     c_value = 32;
     my_assert(value , c_value, "testing the value");
-    free_array(x);
+    free_array(&x);
     return (0);
 }
 
@@ -1153,7 +1153,7 @@ int32_t test_array_fill_int16(void)
     value = x.nd_int16[index];
     c_value = 32;
     my_assert(value , c_value, "testing the value");
-    free_array(x);
+    free_array(&x);
     return (0);
 }
 
@@ -1177,7 +1177,7 @@ int32_t test_array_fill_int16_order_f(void)
     value = x.nd_int16[index];
     c_value = 16;
     my_assert(value , c_value, "testing the value");
-    free_array(x);
+    free_array(&x);
     return (0);
 }
 
@@ -1201,7 +1201,7 @@ int32_t test_array_fill_int8(void)
     value = x.nd_int8[index];
     c_value = 32;
     my_assert(value , c_value, "testing the value");
-    free_array(x);
+    free_array(&x);
     return (0);
 }
 
@@ -1225,7 +1225,7 @@ int32_t test_array_fill_int8_order_f(void)
     value = x.nd_int8[index];
     c_value = 8;
     my_assert(value , c_value, "testing the value");
-    free_array(x);
+    free_array(&x);
     return (0);
 }
 
@@ -1249,7 +1249,7 @@ int32_t test_array_fill_double(void)
     value = x.nd_double[index];
     c_value = 2.;
     my_assert(value , c_value, "testing the value");
-    free_array(x);
+    free_array(&x);
     return (0);
 }
 
@@ -1273,7 +1273,7 @@ int32_t test_array_fill_double_order_f(void)
     value = x.nd_double[index];
     c_value = 2.;
     my_assert(value , c_value, "testing the value");
-    free_array(x);
+    free_array(&x);
     return (0);
 }
 
@@ -1297,7 +1297,7 @@ int32_t test_array_fill_cdouble(void)
     value = x.nd_cdouble[index];
     c_value = 0.3+0.54*I;
     my_assert(value , c_value, "testing the value");
-    free_array(x);
+    free_array(&x);
     return (0);
 }
 
@@ -1321,7 +1321,7 @@ int32_t test_array_fill_cdouble_order_f(void)
     value = x.nd_cdouble[index];
     c_value = 0.3+0.54*I;
     my_assert(value , c_value, "testing the value");
-    free_array(x);
+    free_array(&x);
     return (0);
 }
 
@@ -1347,7 +1347,7 @@ int32_t test_array_zeros_double(void)
     value = x.nd_double[index];
     c_value = 0.;
     my_assert(value , c_value, "testing the value");
-    free_array(x);
+    free_array(&x);
     return (0);
 }
 
@@ -1371,7 +1371,7 @@ int32_t test_array_zeros_int32(void)
     value = x.nd_int32[index];
     c_value = 0;
     my_assert(value , c_value, "testing the value");
-    free_array(x);
+    free_array(&x);
     return (0);
 }
 
@@ -1395,7 +1395,7 @@ int32_t test_array_zeros_cdouble(void)
     value = x.nd_cdouble[index];
     c_value = 0+0*I;
     my_assert(value , c_value, "testing the value");
-    free_array(x);
+    free_array(&x);
     return (0);
 }
 
diff --git a/tests/pyccel/test_pyccel.py b/tests/pyccel/test_pyccel.py
index 42a52568a7..0e13b60caf 100644
--- a/tests/pyccel/test_pyccel.py
+++ b/tests/pyccel/test_pyccel.py
@@ -59,7 +59,7 @@ def get_python_output(abs_path, cwd = None):
     return out
 
 #------------------------------------------------------------------------------
-def compile_pyccel(path_dir,test_file, options = ""):
+def compile_pyccel(path_dir, test_file, options = ""):
     if "python" in options and "--output" not in options:
         options += " --output=__pyccel__"
     cmd = [shutil.which("pyccel"), "%s" % test_file]
@@ -70,15 +70,101 @@ def compile_pyccel(path_dir,test_file, options = ""):
     assert(p.returncode==0)
 
 #------------------------------------------------------------------------------
-def compile_c(path_dir,test_file,dependencies,is_mod=False):
-    compile_fortran_or_c('gcc', '.c', path_dir,test_file,dependencies,is_mod)
+def compile_c(path_dir, test_file, dependencies, is_mod=False):
+    """
+    Compile C code manually.
+
+    Compile C code manually. This is a wrapper around compile_fortran_or_c.
+
+    Parameters
+    ----------
+    path_dir : str
+        The path to the directory where the compilation command should be run from.
+
+    test_file : str
+        The Python file which was translated.
+
+    dependencies : list of str
+        A list of any Python dependencies of the file.
+
+    is_mod : bool, default=False
+        True if translating a module, False if translating a program
+
+    See also
+    --------
+    compile_fortran_or_c : The function that is called.
+    """
+    gcc = shutil.which('gcc')
+    folder = os.path.join(os.path.dirname(test_file), '__pyccel__')
+    deps = []
+    subfolders = [ f.path for f in os.scandir(folder) if f.is_dir() ]
+    for f in subfolders:
+        for fi in os.listdir(f):
+            root, ext = os.path.splitext(fi)
+            if ext == '.c':
+                deps.append(os.path.join(f, root) +'.py')
+                with subprocess.Popen([gcc, '-c', fi, '-o', root+'.o'], text=True, cwd=f) as p:
+                    p.wait()
+    compile_fortran_or_c(gcc, '.c', path_dir, test_file, dependencies, deps, is_mod)
 
 #------------------------------------------------------------------------------
-def compile_fortran(path_dir,test_file,dependencies,is_mod=False):
-    compile_fortran_or_c('gfortran', '.f90', path_dir,test_file,dependencies,is_mod)
+def compile_fortran(path_dir, test_file, dependencies, is_mod=False):
+    """
+    Compile Fortran code manually.
+
+    Compile Fortran code manually. This is a wrapper around compile_fortran_or_c.
+
+    Parameters
+    ----------
+    path_dir : str
+        The path to the directory where the compilation command should be run from.
+
+    test_file : str
+        The Python file which was translated.
+
+    dependencies : list of str
+        A list of any Python dependencies of the file.
+
+    is_mod : bool, default=False
+        True if translating a module, False if translating a program
+
+    See also
+    --------
+    compile_fortran_or_c : The function that is called.
+    """
+    compile_fortran_or_c(shutil.which('gfortran'), '.f90', path_dir, test_file, dependencies, (), is_mod)
 
 #------------------------------------------------------------------------------
-def compile_fortran_or_c(compiler,extension,path_dir,test_file,dependencies,is_mod=False):
+def compile_fortran_or_c(compiler, extension, path_dir, test_file, dependencies, std_deps, is_mod=False):
+    """
+    Compile Fortran or C code manually.
+
+    Compile Fortran or C code manually. This is necessary when support is missing for the
+    wrapper or when dependencies also need to be translated and compiled.
+
+    Parameters
+    ----------
+    compiler : str
+        The compiler (gfortran/gcc).
+
+    extension : str
+        The extension of the generated file (.c/.f90).
+
+    path_dir : str
+        The path to the directory where the compilation command should be run from.
+
+    test_file : str
+        The Python file which was translated.
+
+    dependencies : list of str
+        A list of any Python dependencies of the file.
+
+    std_deps : list of str
+        A list of any language-specific dependencies of the file (e.g. ndarrays).
+
+    is_mod : bool, default=False
+        True if translating a module, False if translating a program
+    """
     root = insert_pyccel_folder(test_file)[:-3]
 
     assert(os.path.isfile(root+extension))
@@ -91,7 +177,8 @@ def compile_fortran_or_c(compiler,extension,path_dir,test_file,dependencies,is_m
         if os.path.isfile(prog_root+extension):
             compile_fortran_or_c(compiler, extension,
                                 path_dir, test_file,
-                                dependencies, is_mod = True)
+                                dependencies, std_deps,
+                                is_mod = True)
             root = prog_root
             deps.append(test_file)
 
@@ -100,12 +187,17 @@ def compile_fortran_or_c(compiler,extension,path_dir,test_file,dependencies,is_m
         for d in deps:
             d = insert_pyccel_folder(d)
             command.append("-I"+os.path.dirname(d))
+        for d in std_deps:
+            command.append("-I"+os.path.dirname(d))
     else:
-        command = [shutil.which(compiler), "-O3", root+extension]
+        command = [compiler, "-O3", root+extension]
         for d in deps:
             d = insert_pyccel_folder(d)
             command.append(d[:-3]+".o")
             command.append("-I"+os.path.dirname(d))
+        for d in std_deps:
+            command.append(d[:-3]+".o")
+            command.append("-I"+os.path.dirname(d))
     command.append("-I"+base_dir)
 
     command.append("-o")
@@ -113,8 +205,9 @@ def compile_fortran_or_c(compiler,extension,path_dir,test_file,dependencies,is_m
         command.append("%s.o" % root)
     else:
         command.append("%s" % test_file[:-3])
-    p = subprocess.Popen(command, universal_newlines=True, cwd=path_dir)
-    p.wait()
+
+    with subprocess.Popen(command, universal_newlines=True, cwd=path_dir) as p:
+        p.wait()
 
 #------------------------------------------------------------------------------
 def get_lang_output(abs_path, language):
@@ -161,7 +254,7 @@ def compare_pyth_fort_output_by_type( p_output, f_output, dtype=float, language=
             f, f_output  = get_value(f_output, rx, float)
             f2, f_output = get_value(f_output, rx, float)
             f = f+f2*1j
-        assert(np.isclose(p,f))
+        assert(np.isclose(p, f))
     elif dtype is bool:
         rx = re.compile('TRUE|True|true|1|T|t|FALSE|False|false|F|f|0')
         bool_conversion = lambda m: m.lower() in ['true', 't', '1']
@@ -173,7 +266,7 @@ def compare_pyth_fort_output_by_type( p_output, f_output, dtype=float, language=
         rx = re.compile('[-0-9.eE]+')
         p, p_output = get_value(p_output, rx, float)
         f, f_output = get_value(f_output, rx, float)
-        assert(np.isclose(p,f))
+        assert(np.isclose(p, f))
 
     elif dtype is int:
         rx = re.compile('[-0-9eE]+')
@@ -182,24 +275,24 @@ def compare_pyth_fort_output_by_type( p_output, f_output, dtype=float, language=
         assert(p==f)
     else:
         raise NotImplementedError("Type comparison not implemented")
-    return p_output,f_output
+    return p_output, f_output
 
 #------------------------------------------------------------------------------
 def compare_pyth_fort_output( p_output, f_output, dtype=float, language=None):
 
-    if isinstance(dtype,list):
+    if isinstance(dtype, list):
         for d in dtype:
-            p_output,f_output = compare_pyth_fort_output_by_type(p_output,f_output,d,language=language)
+            p_output, f_output = compare_pyth_fort_output_by_type(p_output, f_output, d, language=language)
     elif dtype is complex:
         while len(p_output)>0 and len(f_output)>0:
-            p_output,f_output = compare_pyth_fort_output_by_type(p_output,f_output,complex, language=language)
+            p_output, f_output = compare_pyth_fort_output_by_type(p_output, f_output, complex, language=language)
     elif dtype is str:
-        compare_pyth_fort_output_by_type(p_output,f_output,dtype)
+        compare_pyth_fort_output_by_type(p_output, f_output, dtype)
     else:
         p_output = p_output.strip().split()
         f_output = f_output.strip().split()
         for p, f in zip(p_output, f_output):
-            compare_pyth_fort_output_by_type(p,f,dtype)
+            compare_pyth_fort_output_by_type(p, f, dtype)
 
 #------------------------------------------------------------------------------
 def pyccel_test(test_file, dependencies = None, compile_with_pyccel = True,
@@ -265,7 +358,7 @@ def pyccel_test(test_file, dependencies = None, compile_with_pyccel = True,
 
     if output_dir is None:
         if language=="python":
-            output_dir = os.path.join(get_abs_path(rel_test_dir),'__pyccel__')
+            output_dir = os.path.join(get_abs_path(rel_test_dir), '__pyccel__')
 
     if dependencies:
         if isinstance(dependencies, str):
@@ -318,7 +411,7 @@ def test_relative_imports_in_project(language):
     dependencies = ['project_rel_imports/project/folder1/mod1.py',
                     'project_rel_imports/project/folder2/mod2.py',
                     'project_rel_imports/project/folder2/mod3.py']
-    pyccel_test("project_rel_imports/runtest.py",dependencies,
+    pyccel_test("project_rel_imports/runtest.py", dependencies,
             cwd = path_dir,
             language = language)
 
@@ -348,7 +441,7 @@ def test_rel_imports_python_accessible_folder(language):
 
     pyccel_opt = '--language={}'.format(language)
     if language == 'python':
-        pyccel_opt += ' --output={}'.format(os.path.join(tmp_dir,"folder2"))
+        pyccel_opt += ' --output={}'.format(os.path.join(tmp_dir, "folder2"))
     compile_pyccel(os.path.join(path_dir, "folder2"), get_abs_path("scripts/folder2/folder2_funcs.py"), pyccel_opt)
     compile_pyccel(path_dir, get_abs_path("scripts/folder2/runtest_rel_imports.py"), pyccel_opt)
     if language == 'python':
@@ -387,19 +480,19 @@ def test_multi_imports_project(language):
 #------------------------------------------------------------------------------
 @pytest.mark.xdist_incompatible
 def test_imports_compile(language):
-    pyccel_test("scripts/runtest_imports.py","scripts/funcs.py",
+    pyccel_test("scripts/runtest_imports.py", "scripts/funcs.py",
             compile_with_pyccel = False, language = language)
 
 #------------------------------------------------------------------------------
 @pytest.mark.xdist_incompatible
 def test_imports_in_folder(language):
-    pyccel_test("scripts/runtest_folder_imports.py","scripts/folder1/folder1_funcs.py",
+    pyccel_test("scripts/runtest_folder_imports.py", "scripts/folder1/folder1_funcs.py",
             compile_with_pyccel = False, language = language)
 
 #------------------------------------------------------------------------------
 @pytest.mark.xdist_incompatible
 def test_imports(language):
-    pyccel_test("scripts/runtest_imports.py","scripts/funcs.py",
+    pyccel_test("scripts/runtest_imports.py", "scripts/funcs.py",
             language = language)
 
 #------------------------------------------------------------------------------
@@ -418,12 +511,12 @@ def test_folder_imports(language):
     language_opt = '--language={}'.format(language)
     pyccel_opt = language_opt
     if language == 'python':
-        pyccel_opt = language_opt+' --output={}'.format(os.path.join(tmp_dir,"folder1"))
-    compile_pyccel(os.path.join(path_dir,"folder1"), get_abs_path("scripts/folder1/folder1_funcs.py"),
+        pyccel_opt = language_opt+' --output={}'.format(os.path.join(tmp_dir, "folder1"))
+    compile_pyccel(os.path.join(path_dir, "folder1"), get_abs_path("scripts/folder1/folder1_funcs.py"),
             pyccel_opt)
     if language == 'python':
-        pyccel_opt = language_opt+' --output={}'.format(os.path.join(tmp_dir,"folder2"))
-    compile_pyccel(os.path.join(path_dir,"folder2"), get_abs_path("scripts/folder2/runtest_imports2.py"),
+        pyccel_opt = language_opt+' --output={}'.format(os.path.join(tmp_dir, "folder2"))
+    compile_pyccel(os.path.join(path_dir, "folder2"), get_abs_path("scripts/folder2/runtest_imports2.py"),
             pyccel_opt)
 
     if language == 'python':
@@ -466,17 +559,17 @@ def test_generic_functions():
     pyccel_test("scripts/runtest_generic_functions.py",
             dependencies = "scripts/generic_functions.py",
             compile_with_pyccel = False,
-            output_dtype = [float,float,float,float,float,float,
-                    float,float,float,float,float,float,float,int,float,
-                    int,int])
+            output_dtype = [float, float, float, float, float, float,
+                    float, float, float, float, float, float, float, int, float,
+                    int, int])
 
 #------------------------------------------------------------------------------
 def test_default_arguments(language):
     pyccel_test("scripts/runtest_default_args.py",
             dependencies = "scripts/default_args_mod.py",
-            output_dtype = [int,int,float,float,float,
-                float,float,float,float,bool,bool,bool,
-                float,float,float,float],
+            output_dtype = [int, int, float, float, float,
+                float, float, float, float, bool, bool, bool,
+                float, float, float, float],
             language=language)
 
 #------------------------------------------------------------------------------
@@ -627,10 +720,10 @@ def test_numpy_kernels_compile(language):
 #------------------------------------------------------------------------------
 def test_multiple_results(language):
     pyccel_test("scripts/runtest_multiple_results.py",
-            output_dtype = [int,float,complex,bool,int,complex,
-                int,bool,float,float,float,float,float,float,
-                float,float,float,float,float,float
-                ,float,float,float,float,int,int], language=language)
+            output_dtype = [int, float, complex, bool, int, complex,
+                int, bool, float, float, float, float, float, float,
+                float, float, float, float, float, float
+                , float, float, float, float, int, int], language=language)
 
 #------------------------------------------------------------------------------
 def test_elemental(language):
@@ -773,7 +866,22 @@ def test_basic_header():
                                         "scripts/classes/classes_1.py",
                                         "scripts/classes/classes_5.py",
                                         "scripts/classes/generic_methods.py",
-                                        "scripts/classes/classes_2_C.py",
+                                        ] )
+@pytest.mark.parametrize( 'language', (
+        pytest.param("python", marks = pytest.mark.python),
+        pytest.param("fortran", marks = pytest.mark.fortran)
+    )
+)
+
+def test_classes_f_only( test_file , language):
+    if language == "python":
+        pyccel_test(test_file, language=language)
+    else:
+        pyccel_test(test_file, compile_with_pyccel = False, language=language)
+
+#------------------------------------------------------------------------------
+@pytest.mark.xdist_incompatible
+@pytest.mark.parametrize( "test_file", ["scripts/classes/classes_2_C.py",
                                         ] )
 @pytest.mark.parametrize( 'language', (
         pytest.param("python", marks = pytest.mark.python),
@@ -783,10 +891,10 @@ def test_basic_header():
 )
 
 def test_classes( test_file , language):
-    if language == "fortran":
-        pyccel_test(test_file, compile_with_pyccel = False, language=language)
-    elif language != "c" or test_file.endswith("_C.py"):
+    if language == "python":
         pyccel_test(test_file, language=language)
+    else:
+        pyccel_test(test_file, compile_with_pyccel = False, language=language)
 
 #------------------------------------------------------------------------------
 @pytest.mark.parametrize( "test_file", ["scripts/lapack_subroutine.py",
@@ -814,7 +922,7 @@ def test_lapack( test_file ):
             lang_out_vals.append(f)
         except AssertionError:
             lang_out = None
-    output_mat = np.array(lang_out_vals).reshape(4,4)
+    output_mat = np.array(lang_out_vals).reshape(4, 4)
     expected_output = np.eye(4)
 
     assert np.allclose(output_mat, expected_output, rtol=1e-14, atol=1e-15)
@@ -887,7 +995,7 @@ def test_assert(language, test_file):
     test_dir = os.path.dirname(test_file)
     test_file = get_abs_path(os.path.normpath(test_file))
 
-    output_dir   = os.path.join(get_abs_path(test_dir),'__pyccel__')
+    output_dir   = os.path.join(get_abs_path(test_dir), '__pyccel__')
     output_test_file = os.path.join(output_dir, os.path.basename(test_file))
 
     cwd = get_abs_path(test_dir)
@@ -916,7 +1024,7 @@ def test_exit(language, test_file):
     test_dir = os.path.dirname(test_file)
     test_file = get_abs_path(os.path.normpath(test_file))
 
-    output_dir   = os.path.join(get_abs_path(test_dir),'__pyccel__')
+    output_dir   = os.path.join(get_abs_path(test_dir), '__pyccel__')
     output_test_file = os.path.join(output_dir, os.path.basename(test_file))
 
     cwd = get_abs_path(test_dir)
@@ -1002,11 +1110,11 @@ def test_inline_import(language):
 def test_json():
     pyccel_test("scripts/runtest_funcs.py", language = 'fortran',
             pyccel_commands='--export-compile-info test.json')
-    with open(get_abs_path('scripts/test.json'),'r') as f:
+    with open(get_abs_path('scripts/test.json'), 'r') as f:
         dict_1 = json.load(f)
     pyccel_test("scripts/runtest_funcs.py", language = 'fortran',
         pyccel_commands='--compiler test.json --export-compile-info test2.json')
-    with open(get_abs_path('scripts/test2.json'),'r') as f:
+    with open(get_abs_path('scripts/test2.json'), 'r') as f:
         dict_2 = json.load(f)
 
     assert dict_1 == dict_2