Repository: pyccel/pyccel
Pull request #1607 (2023-11-06 16:12:22)

Allow calling class methods from Python
Adds all code necessary to wrap class functions. `__init__`, `__del__` and `__new__` are special cases of this with their own prototypes in the cwrapper. These are also handled and tests are added in `tests/epyccel` (inspired by the tests in `tests/pyccel/scripts/classes`).

pyccel-bot[bot]: Hello again! Thank you for this new pull request :star_struck:.

Please begin by requesting your checklist using the command `/bot checklist`

EmilyBourne: /bot checklist
EmilyBourne: /bot run docs coverage spelling pylint pyccel_lint
EmilyBourne: /bot run pylint pyccel_lint docs spelling coverage
EmilyBourne: /bot run pylint pyccel_lint docs spelling coverage
EmilyBourne: /bot run docs
EmilyBourne: /bot run docs
EmilyBourne: /bot run docs
EmilyBourne: /bot run pylint pyccel_lint docs spelling coverage
EmilyBourne: /bot run coverage
EmilyBourne: /bot run coverage
yguclu: /bot run pr_tests

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b1fa623a..559b5d0f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -17,6 +17,7 @@ All notable changes to this project will be documented in this file.
 -   #1495 : Add support for functions with class instance arguments.
 -   #1684 : Add support for classes without `__init__` functions.
 -   #1685 : Add support for `type()` function with class instance argument.
+-   #1605 : Add support for class methods and interfaces (including `__init__` and `__del__`).
 
 ### Fixed
 
diff --git a/pyccel/ast/bind_c.py b/pyccel/ast/bind_c.py
index 8b6bbd48..8ce10782 100644
--- a/pyccel/ast/bind_c.py
+++ b/pyccel/ast/bind_c.py
@@ -9,7 +9,7 @@ file.
 """
 
 from pyccel.ast.basic import PyccelAstNode
-from pyccel.ast.core import Module
+from pyccel.ast.core import Module, Deallocate
 from pyccel.ast.core import FunctionDef, ClassDef
 from pyccel.ast.core import FunctionDefArgument, FunctionDefResult
 from pyccel.ast.datatypes import DataType, NativeInteger
@@ -26,6 +26,7 @@ __all__ = (
     'BindCVariable',
     'CLocFunc',
     'C_F_Pointer',
+    'DeallocatePointer',
 )
 
 # =======================================================================================
@@ -136,6 +137,20 @@ class BindCFunctionDef(FunctionDef):
         """
         return [ai for a in self._arguments for ai in a.get_all_function_def_arguments()]
 
+    def rename(self, newname):
+        """
+        Rename the FunctionDef name->newname.
+
+        Rename the FunctionDef name->newname.
+
+        Parameters
+        ----------
+        newname : str
+            New name for the FunctionDef.
+        """
+        assert newname == newname.lower()
+        self._name = newname
+
 # =======================================================================================
 
 
@@ -163,6 +178,10 @@ class BindCFunctionDefArgument(FunctionDefArgument):
         in a C-Fortran interface. This variable may have a type which is not
         compatible with C.
 
+    wrapping_bound_argument : bool
+        Indicates if the argument being wrapped was a bound argument of a
+        class method.
+
     **kwargs : dict
         See FunctionDefArgument.
 
@@ -172,11 +191,11 @@ class BindCFunctionDefArgument(FunctionDefArgument):
         The class from which BindCFunctionDefArgument inherits which
         contains all details about the args and kwargs.
     """
-    __slots__ = ('_shape', '_strides', '_original_arg_var', '_rank')
+    __slots__ = ('_shape', '_strides', '_original_arg_var', '_rank', '_wrapping_bound_argument')
     _attribute_nodes = FunctionDefArgument._attribute_nodes + \
                         ('_shape', '_strides', '_original_arg_var')
 
-    def __init__(self, var, scope, original_arg_var, **kwargs):
+    def __init__(self, var, scope, original_arg_var, wrapping_bound_argument, **kwargs):
         name = var.name
         self._rank = original_arg_var.rank
         shape   = [scope.get_temporary_variable(NativeInteger(),
@@ -188,6 +207,7 @@ class BindCFunctionDefArgument(FunctionDefArgument):
         self._shape = shape
         self._strides = strides
         self._original_arg_var = original_arg_var
+        self._wrapping_bound_argument = wrapping_bound_argument
         super().__init__(var, **kwargs)
 
     @property
@@ -265,6 +285,16 @@ class BindCFunctionDefArgument(FunctionDefArgument):
         else:
             return super().inout
 
+    @property
+    def wrapping_bound_argument(self):
+        """
+        Indicates if the argument being wrapped was originally a bound argument.
+
+        Indicates if the argument being wrapped originally appeared in a class
+        method as a bound argument.
+        """
+        return self._wrapping_bound_argument
+
 # =======================================================================================
 
 
@@ -555,15 +585,28 @@ class BindCClassDef(ClassDef):
     original_class : ClassDef
         The class being wrapped.
 
+    new_func : BindCFunctionDef
+        The function which provides a new instance of the class.
+
     **kwargs : dict
         See ClassDef.
     """
-    __slots__ = ('_original_class',)
+    __slots__ = ('_original_class', '_new_func')
 
-    def __init__(self, original_class, **kwargs):
+    def __init__(self, original_class, new_func, **kwargs):
         self._original_class = original_class
+        self._new_func = new_func
         super().__init__(original_class.name, scope = original_class.scope, **kwargs)
 
+    @property
+    def new_func(self):
+        """
+        Get the wrapper for `__new__`.
+
+        Get the wrapper for `__new__` which allocates the memory for the class instance.
+        """
+        return self._new_func
+
 # =======================================================================================
 #                                   Utility functions
 # =======================================================================================
@@ -668,3 +711,18 @@ class C_F_Pointer(PyccelAstNode):
         determine the size of the array in each dimension.
         """
         return self._shape
+
+class DeallocatePointer(Deallocate):
+    """
+    Represents memory deallocation for memory only stored in a pointer.
+
+    Represents memory deallocation for memory only stored in a pointer. Usually
+    `deallocate` is not called on pointers so as not to delete the target values
+    however this capability is necessary in the wrapper.
+
+    Parameters
+    ----------
+    variable : pyccel.ast.core.Variable
+        The typed variable (usually an array) that needs memory deallocation.
+    """
+    __slots__ = ()
diff --git a/pyccel/ast/core.py b/pyccel/ast/core.py
index d37a4a46..17674f16 100644
--- a/pyccel/ast/core.py
+++ b/pyccel/ast/core.py
@@ -719,19 +719,27 @@ class CodeBlock(PyccelAstNode):
                 l.set_current_ast(ast_node)
 
 class AliasAssign(PyccelAstNode):
+    """
+    Representing assignment of an alias to its target.
 
-    """Represents aliasing for code generation. An alias is any statement of the
-    form `lhs := rhs` where
+    Represents aliasing for code generation. An alias is any statement of the
+    form `lhs := rhs` where lhs is a pointer and rhs is a target. In other words
+    the contents of `lhs` will change if the contents of `rhs` are modfied.
 
     Parameters
     ----------
-    lhs : PyccelSymbol
-        at this point we don't know yet all information about lhs, this is why a
-        PyccelSymbol is the appropriate type.
+    lhs : TypedAstNode
+        In the syntactic stage:
+           Object representing the lhs of the expression. These should be
+           singular objects, such as one would use in writing code. Notable types
+           include PyccelSymbol, and IndexedElement. Types that
+           subclass these types are also supported.
+        In the semantic stage:
+           Variable.
 
-    rhs : Variable, IndexedElement
-        an assignable variable can be of any rank and any datatype, however its
-        shape must be known (not None)
+    rhs : PyccelSymbol | Variable, IndexedElement
+        The target of the assignment. A PyccelSymbol in the syntactic stage,
+        a Variable or a Slice of an array in the semantic stage.
 
     Examples
     --------
@@ -742,7 +750,6 @@ class AliasAssign(PyccelAstNode):
     >>> x = Variable('int', 'x', rank=1, shape=[n])
     >>> y = PyccelSymbol('y')
     >>> AliasAssign(y, x)
-
     """
     __slots__ = ('_lhs','_rhs')
     _attribute_nodes = ('_lhs','_rhs')
@@ -752,7 +759,7 @@ class AliasAssign(PyccelAstNode):
             if not lhs.is_alias:
                 raise TypeError('lhs must be a pointer')
 
-            if isinstance(rhs, FunctionCall) and not rhs.funcdef.results[0].is_alias:
+            if isinstance(rhs, FunctionCall) and not rhs.funcdef.results[0].var.is_alias:
                 raise TypeError("A pointer cannot point to the address of a temporary variable")
 
         self._lhs = lhs
diff --git a/pyccel/ast/cwrapper.py b/pyccel/ast/cwrapper.py
index cfed10f5..cfaf9ab2 100644
--- a/pyccel/ast/cwrapper.py
+++ b/pyccel/ast/cwrapper.py
@@ -8,6 +8,8 @@ Module representing objects (functions/variables etc) required for the interface
 between Python code and C code (using Python/C Api and cwrapper.c).
 """
 
+from pyccel.utilities.metaclasses import Singleton
+
 from ..errors.errors import Errors
 from ..errors.messages import PYCCEL_RESTRICTION_TODO
 
@@ -40,8 +42,9 @@ __all__ = (
     'PyInterface',
     'PyClassDef',
     'PyModule',
-    'PyccelPyObject',
     'PyccelPyClassType',
+    'PyccelPyObject',
+    'PyccelPyTypeObject',
     'WrapperCustomDataType',
     'PyArgKeywords',
     'PyArg_ParseTupleNode',
@@ -61,7 +64,7 @@ __all__ = (
 #-------------------------------------------------------------------
 #                        Python DataTypes
 #-------------------------------------------------------------------
-class PyccelPyObject(DataType):
+class PyccelPyObject(DataType, metaclass=Singleton):
     """
     Datatype representing a `PyObject`.
 
@@ -71,7 +74,7 @@ class PyccelPyObject(DataType):
     __slots__ = ()
     _name = 'pyobject'
 
-class PyccelPyClassType(DataType):
+class PyccelPyClassType(DataType, metaclass=Singleton):
     """
     Datatype representing a subclass of `PyObject`.
 
@@ -81,6 +84,16 @@ class PyccelPyClassType(DataType):
     __slots__ = ()
     _name = 'pyclasstype'
 
+class PyccelPyTypeObject(DataType, metaclass=Singleton):
+    """
+    Datatype representing a `PyTypeObject`.
+
+    Datatype representing a `PyTypeObject` which is the
+    class used to hold Python class objects in `Python.h`.
+    """
+    __slots__ = ()
+    _name = 'pytypeobject'
+
 class WrapperCustomDataType(CustomDataType):
     """
     Datatype representing a subclass of `PyObject`.
@@ -549,13 +562,15 @@ class PyClassDef(ClassDef):
         The class from which PyClassDef inherits. This is also the object being
         wrapped.
     """
-    __slots__ = ('_original_class', '_struct_name', '_type_name', '_type_object')
+    __slots__ = ('_original_class', '_struct_name', '_type_name', '_type_object',
+                 '_new_func')
 
     def __init__(self, original_class, struct_name, type_name, scope, **kwargs):
         self._original_class = original_class
         self._struct_name = struct_name
         self._type_name = type_name
         self._type_object = Variable(PyccelPyClassType(), type_name)
+        self._new_func = None
         variables = [Variable(NativeVoid(), 'instance', memory_handling='alias')]
         scope.insert_variable(variables[0])
         super().__init__(original_class.name, variables, scope=scope, **kwargs)
@@ -599,6 +614,28 @@ class PyClassDef(ClassDef):
         """
         return self._original_class
 
+    def add_alloc_method(self, f):
+        """
+        Add the wrapper for `__new__` to the class definition.
+
+        Add the wrapper for `__new__` which allocates the memory for the class instance.
+
+        Parameters
+        ----------
+        f : PyFunctionDef
+            The wrapper for the `__new__` function.
+        """
+        self._new_func = f
+
+    @property
+    def new_func(self):
+        """
+        Get the wrapper for `__new__`.
+
+        Get the wrapper for `__new__` which allocates the memory for the class instance.
+        """
+        return self._new_func
+
 #-------------------------------------------------------------------
 #                      Python.h Constants
 #-------------------------------------------------------------------
diff --git a/pyccel/codegen/printing/ccode.py b/pyccel/codegen/printing/ccode.py
index 731f07fb..d3e042a4 100644
--- a/pyccel/codegen/printing/ccode.py
+++ b/pyccel/codegen/printing/ccode.py
@@ -260,6 +260,7 @@ class CCodePrinter(CodePrinter):
                       (NativeInteger(),8)     : 'int64_t',
                       (NativeInteger(),2)     : 'int16_t',
                       (NativeInteger(),1)     : 'int8_t',
+                      (NativeInteger(),-2)    : 'int',
                       (NativeBool(),-1) : 'bool',
                       (NativeVoid(), 0) : 'void',
                       }
@@ -1517,9 +1518,13 @@ class CCodePrinter(CodePrinter):
         if isinstance(expr.variable.dtype, CustomDataType):
             Pyccel__del = expr.variable.cls_base.scope.find('__del__').name
             return f"{Pyccel__del}({variable_address});\n"
-        if expr.variable.is_alias:
-            return f'free_pointer({variable_address});\n'
-        return f'free_array({variable_address});\n'
+        elif expr.variable.is_ndarray:
+            if expr.variable.is_alias:
+                return f'free_pointer({variable_address});\n'
+            else:
+                return f'free_array({variable_address});\n'
+        else:
+            return f'free({variable_address});\n'
 
     def _print_Slice(self, expr):
         start = self._print(expr.start)
diff --git a/pyccel/codegen/printing/cwrappercode.py b/pyccel/codegen/printing/cwrappercode.py
index 9bff8f21..e750f7dd 100644
--- a/pyccel/codegen/printing/cwrappercode.py
+++ b/pyccel/codegen/printing/cwrappercode.py
@@ -12,7 +12,7 @@ from pyccel.ast.core       import FunctionAddress, SeparatorComment
 from pyccel.ast.core       import Import, Module, Declare
 from pyccel.ast.cwrapper   import PyBuildValueNode
 from pyccel.ast.cwrapper   import Py_None, WrapperCustomDataType
-from pyccel.ast.cwrapper   import PyccelPyObject
+from pyccel.ast.cwrapper   import PyccelPyObject, PyccelPyTypeObject
 from pyccel.ast.literals   import LiteralString, Nil
 from pyccel.ast.c_concepts import ObjectAddress
 
@@ -50,6 +50,7 @@ class CWrapperCodePrinter(CCodePrinter):
     """
     dtype_registry = {**CCodePrinter.dtype_registry,
                       (PyccelPyObject() , 0) : 'PyObject',
+                      (PyccelPyTypeObject() , 0) : 'PyTypeObject',
                       (BindCPointer()   , 0) : 'void'}
 
     def __init__(self, filename, target_language, **settings):
@@ -84,7 +85,7 @@ class CWrapperCodePrinter(CCodePrinter):
         --------
         CCodePrinter.is_c_pointer : The extended function.
         """
-        if isinstance(a.dtype, PyccelPyObject):
+        if isinstance(a.dtype, (PyccelPyObject, PyccelPyTypeObject, WrapperCustomDataType)):
             return True
         elif isinstance(a, PyBuildValueNode):
             return True
@@ -347,17 +348,59 @@ class CWrapperCodePrinter(CCodePrinter):
                 + attributes +
                 "};\n")
 
+        original_scope = expr.original_class.scope
+        print_methods = expr.methods + (expr.new_func,) + expr.interfaces
+        functions = '\n'.join(self._print(f) for f in print_methods)
+        sig_methods = expr.methods + (expr.new_func,) + tuple(f for i in expr.interfaces for f in i.functions) + \
+                      tuple(i.interface_func for i in expr.interfaces)
+        function_sigs = ''.join(self.function_signature(f)+';\n' for f in sig_methods)
+        init_string = ''
+        del_string = ''
+        funcs = {}
+        for f in expr.methods:
+            py_name = self.get_python_name(original_scope, f.original_function)
+            if py_name == '__init__':
+                init_string = f"    .tp_init = (initproc) {f.name},\n"
+            elif py_name == '__del__':
+                del_string = f"    .tp_dealloc = (destructor) {f.name},\n"
+            else:
+                docstring = self._print(LiteralString('\n'.join(f.docstring.comments))) \
+                                                        if f.docstring else '""'
+                funcs[py_name] = (f.name, docstring)
+
+        for f in expr.interfaces:
+            py_name = self.get_python_name(original_scope, f.original_function)
+            docstring = self._print(LiteralString('\n'.join(f.docstring.comments))) \
+                                                    if f.docstring else '""'
+            funcs[py_name] = (f.name, docstring)
+
+        method_def_funcs = ''.join(('{\n'
+                                     f'"{name}",\n'
+                                     f'(PyCFunction){wrapper_name},\n'
+                                      'METH_VARARGS | METH_KEYWORDS,\n'
+                                     f'{doc_string}\n'
+                                     '},\n')
+                                     for name, (wrapper_name, doc_string) in funcs.items())
+
+        method_def_name = self.scope.get_new_name(f'{expr.name}_methods')
+        method_def = (f'static PyMethodDef {method_def_name}[] = {{\n'
+                        f'{method_def_funcs}'
+                        '{ NULL, NULL, 0, NULL}\n'
+                        '};\n')
+
         type_code = (f"static PyTypeObject {type_name} = {{\n"
                 "    PyVarObject_HEAD_INIT(NULL, 0)\n"
                 f"    .tp_name = \"{self._module_name}.{name}\",\n"
                 f"    .tp_doc = PyDoc_STR({docstring}),\n"
                 f"    .tp_basicsize = sizeof(struct {struct_name}),\n"
-                "    .tp_itemsize = 0,\n"
-                "    .tp_flags = Py_TPFLAGS_DEFAULT,\n"
-                "    .tp_new = PyType_GenericNew,\n"
+                 "    .tp_itemsize = 0,\n"
+                 "    .tp_flags = Py_TPFLAGS_DEFAULT,\n"
+                f"    .tp_new = {expr.new_func.name},\n"
+                f"{init_string}{del_string}"
+                f"    .tp_methods = {method_def_name},\n"
                 "};\n")
 
-        return class_code + '\n' + type_code
+        return class_code + '\n' + function_sigs + '\n' + method_def + '\n' + type_code + '\n' + functions
 
     def _print_Allocate(self, expr):
         variable = expr.variable
@@ -371,6 +414,17 @@ class CWrapperCodePrinter(CCodePrinter):
         else:
             return CCodePrinter._print_Allocate(self, expr)
 
+    def _print_Deallocate(self, expr):
+        variable = expr.variable
+        if isinstance(variable.dtype, WrapperCustomDataType):
+            class_def = self.scope.find(variable.cls_base.original_class.name, 'classes')
+
+            type_name = class_def.type_name
+            var_code = self._print(ObjectAddress(variable))
+            return f'{type_name}.tp_free({var_code});\n'
+        else:
+            return CCodePrinter._print_Deallocate(self, expr)
+
 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 b6f6b01f..3bcb330d 100644
--- a/pyccel/codegen/printing/fcode.py
+++ b/pyccel/codegen/printing/fcode.py
@@ -16,7 +16,7 @@ import functools
 
 from pyccel.ast.basic import TypedAstNode
 
-from pyccel.ast.bind_c import BindCPointer, BindCFunctionDef, BindCFunctionDefArgument, BindCModule
+from pyccel.ast.bind_c import BindCPointer, BindCFunctionDef, BindCFunctionDefArgument, BindCModule, BindCClassDef
 
 from pyccel.ast.builtins import PythonInt, PythonType, PythonPrint, PythonRange
 from pyccel.ast.builtins import PythonTuple
@@ -167,6 +167,7 @@ math_function_to_fortran = {
 INF = math_constants['inf']
 
 _default_methods = {
+    '__new__' : 'alloc',
     '__init__': 'create',
     '__del__' : 'free',
 }
@@ -450,7 +451,7 @@ class FCodePrinter(CodePrinter):
             The class whose functions should be renamed.
         """
         scope = expr.scope
-        name = expr.name
+        name = expr.name.lower()
         for method in expr.methods:
             if not method.is_inline:
                 m_name = method.name
@@ -487,11 +488,11 @@ class FCodePrinter(CodePrinter):
         decs = ''
         # ...
         for c in expr.classes:
-            self._calculate_class_names(c)
+            if not isinstance(c, BindCClassDef):
+                self._calculate_class_names(c)
 
         class_decs_and_methods = [self._print(i) for i in expr.classes]
-        if not isinstance(expr, BindCModule):
-            decs += '\n'.join(c[0] for c in class_decs_and_methods)
+        decs += '\n'.join(c[0] for c in class_decs_and_methods)
         # ...
 
         decs += ''.join(self._print(i) for i in expr.declarations)
@@ -517,8 +518,7 @@ class FCodePrinter(CodePrinter):
 
         func_strings = []
         # Get class functions
-        if not isinstance(expr, BindCModule):
-            func_strings += [c[1] for c in class_decs_and_methods]
+        func_strings += [c[1] for c in class_decs_and_methods]
         if expr.funcs:
             func_strings += [''.join([sep, self._print(i), sep]) for i in expr.funcs]
         if isinstance(expr, BindCModule):
@@ -1769,6 +1769,11 @@ class FCodePrinter(CodePrinter):
             code += '  deallocate({})\n'     .format(var_code)
             code += 'end if\n'
             return code
+
+    def _print_DeallocatePointer(self, expr):
+        var_code = self._print(expr.variable)
+        return f'deallocate({var_code})'
+
 #------------------------------------------------------------------------------
 
     def _print_NativeBool(self, expr):
@@ -3227,7 +3232,9 @@ class FCodePrinter(CodePrinter):
         return self._print(expr.wrapper_function)
 
     def _print_BindCClassDef(self, expr):
-        return ''
+        funcs = [expr.new_func, *expr.methods, *[f for i in expr.interfaces for f in i.functions]]
+        sep = f'\n{self._print(SeparatorComment(40))}\n'
+        return '', sep.join(self._print(f) for f in funcs)
 
 
 def fcode(expr, filename, assign_to=None, **settings):
diff --git a/pyccel/codegen/wrapper/c_to_python_wrapper.py b/pyccel/codegen/wrapper/c_to_python_wrapper.py
index 0a9bacfe..c8fbfdee 100644
--- a/pyccel/codegen/wrapper/c_to_python_wrapper.py
+++ b/pyccel/codegen/wrapper/c_to_python_wrapper.py
@@ -10,12 +10,13 @@ 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, BindCFunctionDefResult
+from pyccel.ast.bind_c        import BindCClassDef
 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.core          import FunctionAddress, Declare, ClassDef
 from pyccel.ast.cwrapper      import PyModule, PyccelPyObject, PyArgKeywords
 from pyccel.ast.cwrapper      import PyArg_ParseTupleNode, Py_None, PyClassDef
 from pyccel.ast.cwrapper      import py_to_c_registry, check_type_registry, PyBuildValueNode
@@ -23,6 +24,7 @@ from pyccel.ast.cwrapper      import PyErr_SetString, PyTypeError, PyNotImplemen
 from pyccel.ast.cwrapper      import C_to_Python, PyFunctionDef, PyInterface
 from pyccel.ast.cwrapper      import PyModule_AddObject, Py_DECREF, PyObject_TypeCheck
 from pyccel.ast.cwrapper      import Py_INCREF, PyType_Ready, WrapperCustomDataType
+from pyccel.ast.cwrapper      import PyccelPyTypeObject
 from pyccel.ast.c_concepts    import ObjectAddress, PointerCast
 from pyccel.ast.datatypes     import NativeVoid, NativeInteger, CustomDataType, DataTypeFactory
 from pyccel.ast.datatypes     import NativeNumeric
@@ -57,6 +59,8 @@ class CToPythonWrapper(Wrapper):
         self._python_object_map = {}
         # Indicate if arrays were wrapped.
         self._wrapping_arrays = False
+        # The object that should be returned to indicate an error
+        self._error_exit_code = Nil()
         super().__init__()
 
     def get_new_PyObject(self, name, dtype = None, is_temp = False):
@@ -121,11 +125,15 @@ class CToPythonWrapper(Wrapper):
         list of Variable
             Variables which will hold the arguments in Python.
         """
-        collect_args = [self.get_new_PyObject(getattr(a, 'original_function_argument_variable', a.var).name+'_obj') for a in args]
+        orig_args = [getattr(a, 'original_function_argument_variable', a.var) for a in args]
+        is_bound = [getattr(a, 'wrapping_bound_argument', a.bound_argument) for a in args]
+        collect_args = [self.get_new_PyObject(o_a.name+'_obj',
+                                              dtype = o_a.dtype if b else None)
+                        for a, b, o_a in zip(args, is_bound, orig_args)]
         self._python_object_map.update(dict(zip(args, collect_args)))
         return collect_args
 
-    def _unpack_python_args(self, args):
+    def _unpack_python_args(self, args, class_base = None):
         """
         Unpack the arguments received from Python into the expected Python variables.
 
@@ -139,6 +147,10 @@ class CToPythonWrapper(Wrapper):
         args : iterable of FunctionDefArguments
             The expected arguments of the function.
 
+        class_base : DataType, optional
+            The DataType of the class which the method belongs to. In the case of a method
+            defined in a module this value is None.
+
         Returns
         -------
         func_args : list of Variable
@@ -166,23 +178,29 @@ class CToPythonWrapper(Wrapper):
             return NULL;
         }
         """
+        has_bound_arg = class_base is not None
+        bound_arg = args[0] if has_bound_arg else None
+        args = args[int(has_bound_arg):]
         # Create necessary variables
-        func_args = [self.get_new_PyObject(n) for n in ("self", "args", "kwargs")]
+        func_args = [self.get_new_PyObject("self", class_base)] + [self.get_new_PyObject(n) for n in ("args", "kwargs")]
         arg_vars  = self._get_python_argument_variables(args)
         keyword_list_name = self.scope.get_new_name('kwlist')
 
+        if has_bound_arg:
+            self._python_object_map[bound_arg] = func_args[0]
+
         # 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)
 
+        # 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]
+
         body.append(keyword_list)
-        body.append(If(IfSection(PyccelNot(parse_node), [Return([Nil()])])))
+        body.append(If(IfSection(PyccelNot(parse_node), [Return([self._error_exit_code])])))
 
         return func_args, body
 
@@ -254,11 +272,6 @@ class CToPythonWrapper(Wrapper):
                                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(dtype)
@@ -282,6 +295,11 @@ class CToPythonWrapper(Wrapper):
 
             func_call = FunctionCall(check_func, [py_obj, type_ref, LiteralInteger(rank), flag])
 
+        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,)
+
         return func_call, error_code
 
     def _get_type_check_function(self, name, args, funcs):
@@ -422,7 +440,7 @@ class CToPythonWrapper(Wrapper):
         function = PyFunctionDef(name = name, arguments = func_args, results = func_results,
                 body = [FunctionCall(PyErr_SetString, [PyNotImplementedError,
                                         LiteralString(error_msg)]),
-                        Return([Nil()])],
+                        Return([self._error_exit_code])],
                 scope = scope, original_function = original_function)
 
         self.scope.functions[name] = function
@@ -500,6 +518,221 @@ class CToPythonWrapper(Wrapper):
 
         return FunctionDef(func_name, [FunctionDefArgument(module_var)], [FunctionDefResult(result_var)], body,
                 scope = func_scope, is_static=True)
+
+    def _get_class_allocator(self, class_dtype, func = None):
+        """
+        Create the allocator for the class.
+
+        Create a function which will allocate the memory for the class instance. This
+        is equivalent to the `__new__` function.
+
+        Parameters
+        ----------
+        class_dtype : DataType
+            The datatype of the class being translated.
+
+        func : FunctionDef, optional
+            The function which provides a new instance of the class.
+
+        Returns
+        -------
+        PyFunctionDef
+            A function that can be called to create the class instance.
+        """
+        if func:
+            func_name = self.scope.get_new_name(f'{func.name}___wrapper')
+        else:
+            func_name = self.scope.get_new_name(f'{class_dtype.name}__new___wrapper')
+        func_scope = self.scope.new_child_scope(func_name)
+        self.scope = func_scope
+
+        self_var = Variable(dtype=PyccelPyTypeObject(), name=self.scope.get_new_name('self'),
+                              memory_handling='alias')
+        self.scope.insert_variable(self_var)
+        func_args = [self_var] + [self.get_new_PyObject(n) for n in ("args", "kwargs")]
+        func_args = [FunctionDefArgument(a) for a in func_args]
+
+        func_results = [FunctionDefResult(self.get_new_PyObject("result", is_temp=True))]
+
+        # Get the results of the PyFunctionDef
+        python_result_var = self.get_new_PyObject('result_obj', class_dtype)
+        scope = python_result_var.cls_base.scope
+        attribute = scope.find('instance', 'variables', raise_if_missing = True)
+        c_res = attribute.clone(attribute.name, new_class = DottedVariable, lhs = python_result_var)
+
+        body = [Allocate(python_result_var, shape=(), order=None, status='unallocated', like = self_var)]
+
+        if func:
+            body.append(AliasAssign(c_res, FunctionCall(func, ())))
+        else:
+            result_name = self.scope.get_new_name('result')
+            result = Variable(class_dtype, result_name)
+            body.append(Allocate(c_res, shape=(), order=None, status='unallocated',
+                         like = result))
+
+        body.append(Return([ObjectAddress(PointerCast(python_result_var, func_results[0].var))]))
+
+        self.exit_scope()
+
+        return PyFunctionDef(func_name, func_args, func_results,
+                             body, scope=func_scope, original_function = None)
+
+    def _get_class_initialiser(self, init_function, cls_dtype):
+        """
+        Create the constructor for the class.
+
+        Create a function which will initialise the class. This function creates
+        the `__new__` function to allocate the memory which stores the class
+        instance and calls the `__init__` function.
+
+        Parameters
+        ----------
+        init_function : FunctionDef
+            The `__init__` function in the translated class.
+
+        cls_dtype : DataType
+            The datatype of the class being translated.
+
+        Returns
+        -------
+        new_function : PyFunctionDef
+            A function that can be called to create the class instance.
+
+        init_function : PyFunctionDef
+            A function that can be called to create the class instance.
+        """
+        original_func = getattr(init_function, 'original_function', init_function)
+        func_name = self.scope.get_new_name(original_func.name+'_wrapper')
+        func_scope = self.scope.new_child_scope(func_name)
+        self.scope = func_scope
+        self._error_exit_code = PyccelUnarySub(LiteralInteger(1, precision=-2))
+        class_dtype = init_function.arguments[0].var.dtype
+
+        is_bind_c_function_def = isinstance(init_function, BindCFunctionDef)
+
+        # Handle un-wrappable functions
+        if any(isinstance(getattr(a, 'original_function_argument_variable', a.var), FunctionAddress) for a in init_function.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, init_function,
+                         "Cannot pass a function as an argument")
+
+        # Add the variables to the expected symbols in the scope
+        for a in init_function.arguments:
+            func_scope.insert_symbol(a.var.name)
+        for a in getattr(init_function, 'bind_c_arguments', ()):
+            func_scope.insert_symbol(a.original_function_argument_variable.name)
+
+        # Get variables describing the arguments and results that are seen from Python
+        python_args = init_function.bind_c_arguments if is_bind_c_function_def else init_function.arguments
+
+        # Get variables describing the arguments and results that must be passed to the function
+        original_c_args = init_function.arguments
+
+        # Get the arguments of the PyFunctionDef
+        func_args, body = self._unpack_python_args(python_args, class_dtype)
+        func_args = [FunctionDefArgument(a) for a in func_args]
+
+        # Get the results of the PyFunctionDef
+        python_result_variable = Variable(NativeInteger(), self.scope.get_new_name(),
+                                          precision = -2, is_temp = True)
+
+        # 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 arguments and results which should be used to call the c-compatible function
+        func_call_args = [self.scope.find(n.var.name, category='variables', raise_if_missing = True) for n in original_c_args]
+
+        # Call the C-compatible function
+        body.append(FunctionCall(init_function, 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)
+            if orig_var.is_ndarray:
+                v = self.scope.find(orig_var.name, category='variables', raise_if_missing = True)
+                if v.is_optional:
+                    body.append(If( IfSection(PyccelIsNot(v, Nil()), [Deallocate(v)]) ))
+                else:
+                    body.append(Deallocate(v))
+
+        # Pack the Python compatible results of the function into one argument.
+        func_results = [FunctionDefResult(python_result_variable)]
+        body.append(Return([LiteralInteger(0, precision=-2)]))
+
+        self.exit_scope()
+        for a in python_args:
+            if not a.bound_argument:
+                self._python_object_map.pop(a)
+
+        function = PyFunctionDef(func_name, func_args, func_results, body, scope=func_scope,
+                docstring = init_function.docstring, original_function = original_func)
+
+        self.scope.functions[func_name] = function
+        self._python_object_map[init_function] = function
+        self._error_exit_code = Nil()
+
+        return function
+
+    def _get_class_destructor(self, del_function, cls_dtype, wrapper_scope):
+        """
+        Create the destructor for the class.
+
+        Create a function which will act as a destructor for the class. This
+        function calls the `__del__` function and frees the memory allocated
+        to store the class instance.
+
+        Parameters
+        ----------
+        del_function : FunctionDef
+            The `__del__` function in the translated class.
+
+        cls_dtype : DataType
+            The datatype of the class being translated.
+
+        wrapper_scope : Scope
+            The scope for the wrapped version of the class.
+
+        Returns
+        -------
+        PyFunctionDef
+            A function that can be called to destroy the class instance.
+        """
+        original_func = getattr(del_function, 'original_function', del_function)
+        func_name = self.scope.get_new_name(original_func.name+'_wrapper')
+        func_scope = self.scope.new_child_scope(func_name)
+        self.scope = func_scope
+
+        # Add the variables to the expected symbols in the scope
+        for a in del_function.arguments:
+            func_scope.insert_symbol(a.var.name)
+        func_arg = self.get_new_PyObject('self', cls_dtype)
+
+        attribute = wrapper_scope.find('instance', 'variables')
+        c_obj = attribute.clone(attribute.name, new_class = DottedVariable, lhs = func_arg)
+
+        if isinstance(del_function, BindCFunctionDef):
+            body = [FunctionCall(del_function, [c_obj]),
+                    Deallocate(func_arg)]
+        else:
+            body = [FunctionCall(del_function, [c_obj]),
+                    Deallocate(c_obj),
+                    Deallocate(func_arg)]
+
+        self.exit_scope()
+
+        function = PyFunctionDef(func_name, [FunctionDefArgument(func_arg)], [], body, scope=func_scope,
+                original_function = original_func)
+
+        self.scope.functions[func_name] = function
+        self._python_object_map[del_function] = function
+
+        return function
+
     #--------------------------------------------------------------------------------------------------------------------------------------------
 
     def _wrap_Module(self, expr):
@@ -586,6 +819,15 @@ class CToPythonWrapper(Wrapper):
         # 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()))
+
+        for c in expr.classes:
+            m = c.new_func
+            external_funcs.append(FunctionDef(m.name, m.arguments, m.results, [], is_header = True, scope = Scope()))
+            for m in c.methods:
+                external_funcs.append(FunctionDef(m.name, m.arguments, m.results, [], is_header = True, scope = Scope()))
+            for i in c.interfaces:
+                for f in i.functions:
+                    external_funcs.append(FunctionDef(f.name, f.arguments, f.results, [], is_header = True, scope = Scope()))
         pymod.external_funcs = external_funcs
 
         return pymod
@@ -622,6 +864,11 @@ class CToPythonWrapper(Wrapper):
         self.scope = func_scope
         original_funcs = expr.functions
         example_func = original_funcs[0]
+        possible_class_base = expr.get_user_nodes((ClassDef,))
+        if possible_class_base:
+            class_dtype = possible_class_base[0].class_type
+        else:
+            class_dtype = None
 
         # Add the variables to the expected symbols in the scope
         for a in getattr(example_func, 'bind_c_arguments', example_func.arguments):
@@ -629,7 +876,7 @@ class CToPythonWrapper(Wrapper):
 
         # Create necessary arguments
         python_args = getattr(example_func, 'bind_c_arguments', example_func.arguments)
-        func_args, body = self._unpack_python_args(python_args)
+        func_args, body = self._unpack_python_args(python_args, class_dtype)
 
         # Get python arguments which will be passed to FunctionDefs
         python_arg_objs = [self._python_object_map[a] for a in python_args]
@@ -657,7 +904,7 @@ class CToPythonWrapper(Wrapper):
             functions.append(wrapped_func)
         if_sections.append(IfSection(LiteralTrue(),
                     [FunctionCall(PyErr_SetString, [PyTypeError, "Unexpected type combination"]),
-                     Return([Nil()])]))
+                     Return([self._error_exit_code])]))
         body.append(If(*if_sections))
         self.exit_scope()
 
@@ -692,10 +939,16 @@ class CToPythonWrapper(Wrapper):
             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_name = self.scope.get_new_name(expr.name+'_wrapper')
         func_scope = self.scope.new_child_scope(func_name)
         self.scope = func_scope
 
+        possible_class_base = expr.get_user_nodes((ClassDef,))
+        if possible_class_base:
+            class_dtype = possible_class_base[0].class_type
+        else:
+            class_dtype = None
+
         is_bind_c_function_def = isinstance(expr, BindCFunctionDef)
 
         if expr.is_private:
@@ -735,7 +988,7 @@ class CToPythonWrapper(Wrapper):
             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, body = self._unpack_python_args(python_args, class_dtype)
             func_args = [FunctionDefArgument(a) for a in func_args]
 
         # Get the results of the PyFunctionDef
@@ -812,7 +1065,8 @@ class CToPythonWrapper(Wrapper):
 
         self.exit_scope()
         for a in python_args:
-            self._python_object_map.pop(a)
+            if not a.bound_argument:
+                self._python_object_map.pop(a)
         for r in python_results:
             self._python_object_map.pop(r)
 
@@ -855,6 +1109,7 @@ class CToPythonWrapper(Wrapper):
         in_interface = len(expr.get_user_nodes(Interface)) > 0
 
         orig_var = getattr(expr, 'original_function_argument_variable', expr.var)
+        bound_argument = getattr(expr, 'wrapping_bound_argument', expr.bound_argument)
 
         if orig_var.is_ndarray:
             arg_var = orig_var.clone(self.scope.get_expected_name(orig_var.name), is_argument = False, memory_handling='alias')
@@ -886,14 +1141,19 @@ class CToPythonWrapper(Wrapper):
             python_cls_base = self.scope.find(dtype.name, 'classes', raise_if_missing = True)
             scope = python_cls_base.scope
             attribute = scope.find('instance', 'variables', raise_if_missing = True)
-            cast_type = Variable(dtype=self._python_object_map[dtype],
-                                name=self.scope.get_new_name(collect_arg.name),
-                                memory_handling='alias',
-                                cls_base = self.scope.find(dtype.name, 'classes', raise_if_missing = True))
-            self.scope.insert_variable(cast_type)
+            if expr.bound_argument:
+                cast_type = collect_arg
+                cast = []
+            else:
+                cast_type = Variable(dtype=self._python_object_map[dtype],
+                                    name=self.scope.get_new_name(collect_arg.name),
+                                    memory_handling='alias',
+                                    cls_base = self.scope.find(dtype.name, 'classes', raise_if_missing = True))
+                self.scope.insert_variable(cast_type)
+                cast = [AliasAssign(cast_type, PointerCast(collect_arg, cast_type))]
             c_res = attribute.clone(attribute.name, new_class = DottedVariable, lhs = cast_type)
-            cast = [AliasAssign(cast_type, PointerCast(collect_arg, cast_type)),
-                    AliasAssign(arg_var, PointerCast(c_res, arg_var))]
+            cast_c_res = PointerCast(c_res, orig_var)
+            cast.append(AliasAssign(arg_var, cast_c_res))
         elif arg_var.rank == 0:
             prec  = get_final_precision(orig_var)
             try :
@@ -916,12 +1176,12 @@ class CToPythonWrapper(Wrapper):
         if expr.has_default:
             check_func, err = self._get_check_function(collect_arg, orig_var, False)
             body.append(If( IfSection(check_func, cast),
-                        IfSection(PyccelIsNot(collect_arg, Py_None), [*err, Return([Nil()])])
+                        IfSection(PyccelIsNot(collect_arg, Py_None), [*err, Return([self._error_exit_code])])
                         ))
-        elif not in_interface:
+        elif not (in_interface or bound_argument):
             check_func, err = self._get_check_function(collect_arg, orig_var, True)
             body.append(If( IfSection(check_func, cast),
-                        IfSection(LiteralTrue(), [*err, Return([Nil()])])
+                        IfSection(LiteralTrue(), [*err, Return([self._error_exit_code])])
                         ))
         else:
             body.extend(cast)
@@ -1219,11 +1479,36 @@ class CToPythonWrapper(Wrapper):
 
         type_name = self.scope.get_new_name(f'Py{name}Type')
         docstring = expr.docstring
-        wrapped_class = PyClassDef(expr, struct_name, type_name, Scope(), docstring = docstring, class_type = dtype)
+        wrapped_class = PyClassDef(expr, struct_name, type_name, self.scope.new_child_scope(expr.name),
+                                   docstring = docstring, class_type = dtype)
+
+        orig_cls_dtype = expr.scope.parent_scope.cls_constructs[name]
 
         self._python_object_map[expr] = wrapped_class
-        self._python_object_map[expr.scope.parent_scope.cls_constructs[name]] = dtype
+        self._python_object_map[orig_cls_dtype] = dtype
 
         self.scope.insert_class(wrapped_class, name)
+        orig_scope = expr.scope
+
+        for f in expr.methods:
+            orig_f = getattr(f, 'original_function', f)
+            name = orig_f.name
+            python_name = orig_scope.get_python_name(name)
+            if python_name == '__del__':
+                wrapped_class.add_new_method(self._get_class_destructor(f, orig_cls_dtype, wrapped_class.scope))
+            elif python_name == '__init__':
+                wrapped_class.add_new_method(self._get_class_initialiser(f, orig_cls_dtype))
+            else:
+                wrapped_class.add_new_method(self._wrap(f))
+
+        for i in expr.interfaces:
+            for f in i.functions:
+                self._wrap(f)
+            wrapped_class.add_new_interface(self._wrap(i))
+
+        if isinstance(expr, BindCClassDef):
+            wrapped_class.add_alloc_method(self._get_class_allocator(orig_cls_dtype, expr.new_func))
+        else:
+            wrapped_class.add_alloc_method(self._get_class_allocator(orig_cls_dtype))
 
         return wrapped_class
diff --git a/pyccel/codegen/wrapper/fortran_to_c_wrapper.py b/pyccel/codegen/wrapper/fortran_to_c_wrapper.py
index 30ab84c8..721c5037 100644
--- a/pyccel/codegen/wrapper/fortran_to_c_wrapper.py
+++ b/pyccel/codegen/wrapper/fortran_to_c_wrapper.py
@@ -11,7 +11,7 @@ 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, BindCVariable
-from pyccel.ast.bind_c import BindCArrayVariable, BindCClassDef
+from pyccel.ast.bind_c import BindCArrayVariable, BindCClassDef, DeallocatePointer
 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
@@ -102,7 +102,7 @@ class FortranToCWrapper(Wrapper):
 
             # If the function is inlined and takes an array argument create a pointer to ensure that the bounds
             # are respected
-            if func.is_inline and any(isinstance(a.value, IndexedElement) for a in args):
+            if getattr(func, 'is_inline', False) and any(isinstance(a.value, IndexedElement) for a in args):
                 array_args = {a: self.scope.get_temporary_variable(a.value.base, a.keyword, memory_handling = 'alias') for a in args if isinstance(a.value, IndexedElement)}
                 body += [AliasAssign(v, k.value) for k,v in array_args.items()]
                 args = [FunctionCallArgument(array_args[a], keyword=a.keyword) if a in array_args else a for a in args]
@@ -222,8 +222,10 @@ class FortranToCWrapper(Wrapper):
         if expr.is_private:
             return EmptyNode()
 
-        name = self.scope.get_new_name(f'bind_c_{expr.name.lower()}')
+        orig_name = expr.cls_name or expr.name
+        name = self.scope.get_new_name(f'bind_c_{orig_name.lower()}')
         self._wrapper_names_dict[expr.name] = name
+        in_cls = expr.arguments and expr.arguments[0].bound_argument
 
         # Create the scope
         func_scope = self.scope.new_child_scope(name)
@@ -244,11 +246,19 @@ class FortranToCWrapper(Wrapper):
 
         func_call_results = [r.var.clone(self.scope.get_expected_name(r.var.name)) for r in expr.results]
 
-        body = self._get_function_def_body(expr, func_arguments, func_to_call, func_call_results)
+        interface = expr.get_direct_user_nodes(lambda u: isinstance(u, Interface))
+
+        if in_cls and interface:
+            body = self._get_function_def_body(interface[0], func_arguments, func_to_call, func_call_results)
+        else:
+            body = self._get_function_def_body(expr, func_arguments, func_to_call, func_call_results)
 
         body.extend(self._additional_exprs)
         self._additional_exprs.clear()
 
+        if expr.scope.get_python_name(expr.name) == '__del__':
+            body.append(DeallocatePointer(call_arguments[0]))
+
         self.exit_scope()
 
         func = BindCFunctionDef(name, func_arguments, func_results, body, scope=func_scope, original_function = expr,
@@ -328,7 +338,8 @@ class FortranToCWrapper(Wrapper):
         self.scope.insert_variable(new_var)
 
         return BindCFunctionDefArgument(new_var, value = expr.value, original_arg_var = expr.var,
-                kwonly = expr.is_kwonly, annotation = expr.annotation, scope=self.scope)
+                kwonly = expr.is_kwonly, annotation = expr.annotation, scope=self.scope,
+                wrapping_bound_argument = expr.bound_argument)
 
     def _wrap_FunctionDefResult(self, expr):
         """
@@ -474,4 +485,37 @@ class FortranToCWrapper(Wrapper):
         BindCClassDef
             The wrapped class.
         """
-        return BindCClassDef(expr, docstring = expr.docstring, class_type = expr.class_type)
+        name = expr.name
+        func_name = self.scope.get_new_name(f'{name}_bind_c_alloc'.lower())
+        func_scope = self.scope.new_child_scope(func_name)
+
+        local_var = Variable(expr.class_type, func_scope.get_new_name(f'{name}_obj'),
+                             cls_base = expr, memory_handling='alias')
+
+        # Allocatable is not returned so it must appear in local scope
+        func_scope.insert_variable(local_var)
+
+        # Create the C-compatible data pointer
+        bind_var = Variable(dtype=BindCPointer(),
+                            name=func_scope.get_new_name('bound_'+name),
+                            is_const=False, memory_handling='alias')
+        func_scope.insert_variable(bind_var)
+
+        result = BindCFunctionDefResult(bind_var, local_var, func_scope)
+
+        # Define the additional steps necessary to define and fill ptr_var
+        alloc = Allocate(local_var, shape=(), order=None, status='unallocated')
+        c_loc = CLocFunc(local_var, bind_var)
+        body = [alloc, c_loc]
+
+        new_method = BindCFunctionDef(func_name, [], [result], body, original_function = None, scope = func_scope)
+
+        methods = [self._wrap(m) for m in expr.methods]
+        methods = [m for m in methods if not isinstance(m, EmptyNode)]
+        for i in expr.interfaces:
+            for f in i.functions:
+                self._wrap(f)
+        interfaces = [self._wrap(i) for i in expr.interfaces]
+        return BindCClassDef(expr, new_func = new_method, methods = methods,
+                             interfaces = interfaces,
+                             docstring = expr.docstring, class_type = expr.class_type)


Test patch:
diff --git a/tests/epyccel/classes/class_headers.py b/tests/epyccel/classes/class_headers.py
new file mode 100644
index 00000000..ca9235df
--- /dev/null
+++ b/tests/epyccel/classes/class_headers.py
@@ -0,0 +1,16 @@
+# pylint: disable=missing-function-docstring, missing-module-docstring, missing-class-docstring
+
+#$ header method get_4(A) results(int)
+
+class A:
+    def __init__(self : 'A'):
+        self.x = 3
+
+    def get_4(self):
+        return 4
+
+if __name__ == '__main__':
+    #$ header variable myA A
+    myA = A()
+
+    print(myA.x)
diff --git a/tests/epyccel/classes/class_variables.py b/tests/epyccel/classes/class_variables.py
new file mode 100644
index 00000000..d6a10fa5
--- /dev/null
+++ b/tests/epyccel/classes/class_variables.py
@@ -0,0 +1,15 @@
+# pylint: disable=missing-function-docstring, missing-module-docstring, missing-class-docstring
+
+class A:
+    x : int
+    def __init__(self : 'A'):
+        self.x = 3
+        self.z : float = 10.0
+
+    def get_4(self : 'A'):
+        return 4
+
+if __name__ == '__main__':
+    myA : 'A' = A()
+
+    print(myA.x)
diff --git a/tests/epyccel/classes/classes.py b/tests/epyccel/classes/classes.py
new file mode 100644
index 00000000..7ee5bf5e
--- /dev/null
+++ b/tests/epyccel/classes/classes.py
@@ -0,0 +1,32 @@
+# pylint: disable=missing-class-docstring,  missing-function-docstring, missing-module-docstring
+
+class Point(object):
+    def __init__(self, x : float, y : float):
+        self.x = x
+        self.y = y
+
+    def __del__(self : 'Point'):
+        pass
+
+    def translate(self : 'Point', a : float, b : float):
+        self.x = self.x + a
+        self.y = self.y + b
+
+if __name__ == '__main__':
+    p = Point(0.0, 0.0)
+    x=p.x
+    p.x=x
+    a = p.x
+    a = p.x - 2
+    a = 2 * p.x - 2
+    a = 2 * (p.x + 6) - 2
+
+    p.y = a + 5
+    p.y = p.x + 5
+
+    p.translate(1.0, 2.0)
+
+    print(p.x, p.y)
+    print(a)
+
+    del p
diff --git a/tests/epyccel/classes/classes_1.py b/tests/epyccel/classes/classes_1.py
new file mode 100644
index 00000000..3554a6a5
--- /dev/null
+++ b/tests/epyccel/classes/classes_1.py
@@ -0,0 +1,29 @@
+# pylint: disable=missing-class-docstring, missing-function-docstring, missing-module-docstring
+# coding: utf-8
+
+class Point(object):
+    def __init__(self : 'Point', x : 'float[:]'):
+        self._X = 10
+        self._x = x
+
+    def __del__(self : 'Point'):
+        pass
+
+    def translate(self : 'Point', a : 'float[:]'):
+        self._x[:]   =  self._x + a
+
+    def get_x(self : 'Point'):
+        return self._x[0]
+
+    def get_X(self : 'Point'):
+        return self._X
+
+class Line(object):
+    def __init__(self : 'Line', l : Point):
+        self.l = l
+        self.l._X = 11
+
+        self._x = self.l.get_x()
+
+    def get_x(self):
+        return self._x
diff --git a/tests/epyccel/classes/classes_2.py b/tests/epyccel/classes/classes_2.py
new file mode 100644
index 00000000..0854e340
--- /dev/null
+++ b/tests/epyccel/classes/classes_2.py
@@ -0,0 +1,11 @@
+# pylint: disable=missing-class-docstring,  missing-function-docstring, missing-module-docstring
+
+class Point:
+    def __init__(self : 'Point'):
+        pass
+
+    def addition(self : 'Point', a : float, b : float):
+        return a + b
+
+    def subtraction(self : 'Point', a : 'float[:]', b : 'float[:]'):
+        return a - b
diff --git a/tests/epyccel/classes/classes_3.py b/tests/epyccel/classes/classes_3.py
new file mode 100644
index 00000000..b24d2a18
--- /dev/null
+++ b/tests/epyccel/classes/classes_3.py
@@ -0,0 +1,26 @@
+# pylint: disable=missing-class-docstring, missing-function-docstring, missing-module-docstring
+# coding: utf-8
+
+class Point:
+    def __init__(self, x : float, y : float):
+        self.x = x
+        self.X = y
+
+    def set_coordinates(self, x : float, y : float):
+        self.x = x
+        self.X = y
+
+    def get_coordinates(self):
+        return self.x, self.X
+
+class Point1:
+    def __init__(self, x : float):
+        self.x = x
+
+class Point2:
+    def __init__(self, y : float):
+        self.y = y
+
+    def test_func(self):
+        p = Point1(self.y)
+        return p.x
diff --git a/tests/epyccel/classes/classes_4.py b/tests/epyccel/classes/classes_4.py
new file mode 100644
index 00000000..bb4e8366
--- /dev/null
+++ b/tests/epyccel/classes/classes_4.py
@@ -0,0 +1,14 @@
+# pylint: disable=missing-class-docstring, missing-function-docstring, missing-module-docstring
+# coding: utf-8
+
+class Point(object):
+    def __init__(self : 'Point', x : 'float[:]' = None):
+        if x is not None:
+            self._x = x
+
+    def translate(self : 'Point', a : 'float[:]'):
+        self._x[:]   =  self._x + a
+
+    def get_x(self : 'Point'):
+        return self._x[0]
+
diff --git a/tests/epyccel/classes/classes_6.py b/tests/epyccel/classes/classes_6.py
new file mode 100644
index 00000000..79355484
--- /dev/null
+++ b/tests/epyccel/classes/classes_6.py
@@ -0,0 +1,19 @@
+# pylint: disable=missing-function-docstring, missing-module-docstring, missing-class-docstring
+
+from pyccel.decorators import inline
+
+class Point(object):
+    def __init__(self : 'Point', x : float, y : float):
+        self.x = x
+        self.y = y
+
+    def __del__(self : 'Point'):
+        pass
+
+    def translate(self : 'Point', a : float, b : float):
+        self.x = self.x + a
+        self.y = self.y + b
+
+    @inline
+    def get_attributes(self : 'Point', a : 'int | float'):
+        return self.x, self.y, a
diff --git a/tests/epyccel/classes/classes_7.py b/tests/epyccel/classes/classes_7.py
new file mode 100644
index 00000000..9a5bc255
--- /dev/null
+++ b/tests/epyccel/classes/classes_7.py
@@ -0,0 +1,29 @@
+# pylint: disable=missing-function-docstring, missing-module-docstring, missing-class-docstring
+
+class A:
+    def __init__(self : 'A', x : int):
+        self.x = x
+
+    def update(self : 'A', x : int):
+        self.x = x
+
+def get_A():
+    return A(4)
+
+def get_A_int():
+    return A(2), 2
+
+def get_x_from_A(a : 'A' = None):
+    if a is not None:
+        return a.x
+    else:
+        return 5
+
+if __name__ == '__main__':
+    print(get_A().x)
+    print(get_x_from_A())
+    print(get_x_from_A(get_A()))
+    x = get_A()
+    x.update(10)
+    print(get_x_from_A(x))
+    x, p = get_A_int()
diff --git a/tests/epyccel/classes/generic_methods.py b/tests/epyccel/classes/generic_methods.py
new file mode 100644
index 00000000..5ae65e45
--- /dev/null
+++ b/tests/epyccel/classes/generic_methods.py
@@ -0,0 +1,21 @@
+# pylint: disable=missing-function-docstring, missing-module-docstring, missing-class-docstring
+from pyccel.decorators import template
+
+class Point(object):
+    def __init__(self : 'Point', x : float, y : float):
+        self.x = x
+        self.y = y
+
+    def __del__(self : 'Point'):
+        pass
+
+    @template('T', [int, float])
+    def translate(self : 'Point', a : 'T', b : 'T'):
+        self.x = self.x + a
+        self.y = self.y + b
+
+    def get_x(self):
+        return self.x
+
+    def get_y(self):
+        return self.y
diff --git a/tests/epyccel/classes/pep526.py b/tests/epyccel/classes/pep526.py
new file mode 100644
index 00000000..66c8bc4e
--- /dev/null
+++ b/tests/epyccel/classes/pep526.py
@@ -0,0 +1,14 @@
+# pylint: disable=missing-function-docstring, missing-module-docstring, missing-class-docstring
+
+class A:
+    x : int
+    def __init__(self : 'A'):
+        self.x = 3
+
+    def get_4(self : 'A'):
+        return 4
+
+if __name__ == '__main__':
+    myA : 'A' = A()
+
+    print(myA.x)
diff --git a/tests/epyccel/modules/expose_classes.py b/tests/epyccel/modules/expose_classes.py
index 2dfff70b..19e5534b 100644
--- a/tests/epyccel/modules/expose_classes.py
+++ b/tests/epyccel/modules/expose_classes.py
@@ -8,6 +8,13 @@ class B:
     def __init__(self : 'B', x : float):
         self.x = x
 
+class C:
+    def __init__(self):
+        pass
+
+    def get_3(self):
+        return 3
+
 def get_A():
     return A()
 
diff --git a/tests/epyccel/test_epyccel_classes.py b/tests/epyccel/test_epyccel_classes.py
index bf3694df..62442b72 100644
--- a/tests/epyccel/test_epyccel_classes.py
+++ b/tests/epyccel/test_epyccel_classes.py
@@ -1,8 +1,12 @@
 # pylint: disable=missing-function-docstring, missing-module-docstring, missing-class-docstring
 import pytest
+import numpy as np
 import modules.expose_classes as mod
 from pyccel.epyccel import epyccel
 
+RTOL = 2e-14
+ATOL = 1e-15
+
 @pytest.fixture(scope="module")
 def modnew(language):
     return epyccel(mod, language = language)
@@ -40,3 +44,167 @@ def test_class_argument(modnew):
     assert x == 3.0
     x = modnew.get_an_x_from_B()
     assert x == -2.0
+
+def test_class_function(modnew):
+    c = modnew.C()
+    assert c.get_3() == 3
+
+@pytest.mark.parametrize( 'language', (
+        pytest.param("fortran", marks = [
+            pytest.mark.skip(reason="Argument reference count not increased. See #1586"),
+            pytest.mark.fortran]
+        ),
+        pytest.param("c", marks = [
+            pytest.mark.skip(reason="Argument reference count not increased. See #1586"),
+            pytest.mark.c]
+        ),
+        pytest.param("python", marks = pytest.mark.python)
+    )
+)
+def test_classes_1(language):
+    import classes.classes_1 as mod
+    modnew = epyccel(mod, language = language)
+
+    x1 = np.array([0.,0.,0.])
+    x2 = np.array([0.,0.,0.])
+    a = np.array([1.,1.,1.])
+
+    p1_py = mod.Point(x1)
+    p1_l  = modnew.Point(x2)
+
+    assert np.isclose(p1_py.get_x(), p1_l.get_x(), rtol=RTOL, atol=ATOL)
+    assert p1_py.get_X() == p1_l.get_X()
+
+    p1_py.translate(a)
+    p1_l.translate(a)
+
+    assert np.isclose(p1_py.get_x(), p1_l.get_x(), rtol=RTOL, atol=ATOL)
+    assert p1_py.get_X() == p1_l.get_X()
+    assert np.allclose(x1, x2, rtol=RTOL, atol=ATOL)
+
+    p2_py = mod.Point(np.array([6.,6.,6.]))
+    p2_l  = modnew.Point(np.array([6.,6.,6.]))
+
+    print(p2_py.get_x(), p2_l.get_x())
+
+    assert np.allclose(p2_py.get_x(), p2_l.get_x())
+    print(p2_py.get_x(), p2_l.get_x())
+    assert p2_py.get_X() == p2_l.get_X()
+
+    print(p2_py.get_x(), p2_l.get_x())
+    p2_py.translate(a)
+    print(p2_py.get_x(), p2_l.get_x())
+    p2_l.translate(a)
+    print(p2_py.get_x(), p2_l.get_x())
+
+    assert np.isclose(p2_py.get_x(), p2_l.get_x(), rtol=RTOL, atol=ATOL)
+    assert p2_py.get_X() == p2_l.get_X()
+    assert np.allclose(x1, x2, rtol=RTOL, atol=ATOL)
+
+    l_py = mod.Line(p1_py)
+    l_l  = mod.Line(p1_l)
+
+    assert p1_py.get_X() == p1_l.get_X()
+    assert l_py.get_x() == l_l.get_x()
+
+def test_classes_2(language):
+    import classes.classes_2 as mod
+    modnew = epyccel(mod, language = language)
+
+    p_py = mod.Point()
+    p_l  = modnew.Point()
+
+    x = np.ones(4)
+    y = np.full(4, 3.0)
+
+    a_py = p_py.addition(1.1, 2.0)
+    b_py = p_py.subtraction(y, x)
+
+    a_l  = p_l.addition(1.1, 2.0)
+    b_l  = p_l.subtraction(y, x)
+
+    assert a_py == a_l
+    assert isinstance(a_py, type(a_l))
+
+    assert np.allclose(b_py, b_l, rtol=RTOL, atol=ATOL)
+
+def test_classes_3(language):
+    import classes.classes_3 as mod
+    modnew = epyccel(mod, language = language)
+
+    p2_py = mod.Point2(2.2)
+    p2_l  = modnew.Point2(2.2)
+    assert p2_py.test_func() == p2_l.test_func()
+
+    p2_py = mod.Point2(6.5)
+    p2_l  = modnew.Point2(6.5)
+    assert p2_py.test_func() == p2_l.test_func()
+
+    p_py = mod.Point(3.5, 0.1)
+    p_l  = modnew.Point(3.5, 0.1)
+
+    assert p_py.get_coordinates() == p_l.get_coordinates()
+
+    p_py.set_coordinates(2.3, 5.1)
+    p_l.set_coordinates(2.3, 5.1)
+
+    assert p_py.get_coordinates() == p_l.get_coordinates()
+
+def test_classes_4(language):
+    import classes.classes_4 as mod
+    modnew = epyccel(mod, language = language)
+
+    x1 = np.array([0.,0.,0.])
+    x2 = np.array([0.,0.,0.])
+    a = np.array([1.,1.,1.])
+
+    p1_py = mod.Point(x1)
+    p1_l  = modnew.Point(x2)
+
+    assert np.isclose(p1_py.get_x(), p1_l.get_x(), rtol=RTOL, atol=ATOL)
+
+    p1_py.translate(a)
+    p1_l.translate(a)
+
+    assert np.isclose(p1_py.get_x(), p1_l.get_x(), rtol=RTOL, atol=ATOL)
+    assert np.allclose(x1, x2, rtol=RTOL, atol=ATOL)
+
+    p1_py = mod.Point()
+
+def test_classes_6(language):
+    import classes.classes_6 as mod
+    modnew = epyccel(mod, language = language)
+
+    p_py = mod.Point(0.0, 0.0)
+    p_l  = modnew.Point(0.0, 0.0)
+
+    assert p_py.get_attributes(3) == p_l.get_attributes(3)
+    assert p_py.get_attributes(4.5) == p_l.get_attributes(4.5)
+
+    p_py.translate(1.0, 2.0)
+    p_l.translate(1.0, 2.0)
+
+    assert p_py.get_attributes(3) == p_l.get_attributes(3)
+    assert p_py.get_attributes(4.5) == p_l.get_attributes(4.5)
+
+def test_generic_methods(language):
+    import classes.generic_methods as mod
+    modnew = epyccel(mod, language = language)
+
+    p_py = mod.Point(0.0, 0.0)
+    p_l  = modnew.Point(0.0, 0.0)
+
+    assert p_py.get_x() == p_l.get_x()
+    assert p_py.get_y() == p_l.get_y()
+
+    p_py.translate(1.0, 2.0)
+    p_l.translate(1.0, 2.0)
+
+    assert p_py.get_x() == p_l.get_x()
+    assert p_py.get_y() == p_l.get_y()
+
+    p_py.translate(1, 2)
+    p_l.translate(1, 2)
+
+    assert p_py.get_x() == p_l.get_x()
+    assert p_py.get_y() == p_l.get_y()
diff --git a/tests/warnings/test_warnings.py b/tests/warnings/test_warnings.py
index c2a5f79b..29548433 100644
--- a/tests/warnings/test_warnings.py
+++ b/tests/warnings/test_warnings.py
@@ -9,9 +9,12 @@
 import os
 import pytest
 
+from wrapper import HIGH_ORDER_FUNCTIONS_IN_CLASS_FUNCS
+
 from pyccel.parser.parser   import Parser
 from pyccel.codegen.codegen import Codegen
 from pyccel.errors.errors   import Errors
+from pyccel.epyccel         import epyccel
 
 def get_files_from_folder(foldername):
     base_dir = os.path.dirname(os.path.realpath(__file__))
@@ -72,6 +75,17 @@ def test_semantic_warnings(f):
 #    assert(errors.has_warnings())
 #    assert(not errors.has_errors())
 
+
+@pytest.mark.parametrize("f", [HIGH_ORDER_FUNCTIONS_IN_CLASS_FUNCS])
+def test_cwrapper_warnings(f, language):
+    with pytest.warns(UserWarning):
+        epyccel(f, language='c')
+
+@pytest.mark.parametrize("f", [HIGH_ORDER_FUNCTIONS_IN_CLASS_FUNCS])
+def test_bind_c_warnings(f, language):
+    with pytest.warns(UserWarning):
+        epyccel(f, language='fortran')
+
 ######################
 if __name__ == '__main__':
 #    print('*********************************')
diff --git a/tests/warnings/wrapper/HIGH_ORDER_FUNCTIONS_IN_CLASS_FUNCS.py b/tests/warnings/wrapper/HIGH_ORDER_FUNCTIONS_IN_CLASS_FUNCS.py
new file mode 100644
index 00000000..bf87f31e
--- /dev/null
+++ b/tests/warnings/wrapper/HIGH_ORDER_FUNCTIONS_IN_CLASS_FUNCS.py
@@ -0,0 +1,5 @@
+# pylint: disable=missing-function-docstring, missing-module-docstring, missing-class-docstring
+
+class A:
+    def __init__(self, f : '(int)(int)'):
+        self._x = f(3)