Repository: pyccel/pyccel (language: python)

Issue:
Add C support for a class containing only functions
### Relevant Discussion

https://github.com/pyccel/pyccel/discussions/1119

### Describe the feature
Add the printing of an empty class and any functions that it uses.

### Test Code
Provide code which does not currently work but which should do when this issue is fixed:
```python
# pylint: disable=missing-class-docstring,  missing-function-docstring, missing-module-docstring
import numpy as np

#$ header class Point(public)
#$ header method __init__(Point)
#$ header method addition(Point, float, float)
#$ header method subtraction(Point, float[:], float[:])

class Point:
    def __init__(self):
        pass

    def addition(self, a, b):
        return a + b
        
    def subtraction(self, a, b):
        return a - b

if __name__ == '__main__':
    p = Point()

    x = np.ones(4)
    y = np.full(4, 3.0)
    
    a = p.addition(1.0, 2.0)
    b = p.subtraction(y, x)

    print(a)
    print(b)
    print(p.addition(10.0, 11.0))
    print(p.subtraction(x, y))
```

### Proposed Solution
The printing stage should generate the following header code:
```
#ifndef TEST_MOD_H
#define TEST_MOD_H

#include <stdlib.h>
#include "ndarrays.h"

struct Point {
};

void Point__init(struct Point self);
double Point__addition(struct Point self, double a, double b);
t_ndarray Point__subtraction(struct Point self, t_ndarray a, t_ndarray b);
#endif // TEST_MOD_H
```
and the appropriate associated source code.

### Language
C


Discussion / hints:

Fix patch:
diff --git a/.dict_custom.txt b/.dict_custom.txt
index 850f4aecf7..aedd790061 100644
--- a/.dict_custom.txt
+++ b/.dict_custom.txt
@@ -64,6 +64,7 @@ RedBaron
 intel
 nvidia
 boolean
+booleans
 quicksort
 iterm
 textx
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 899dc8a14f..0cbeaf77b6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,13 +5,18 @@ All notable changes to this project will be documented in this file.
 
 ### Added
 
--   Python support for a simple class
+-   Allow interfaces in classes.
+-   Python support for a simple class.
+-   #1430 : Added conjugate support to booleans.
 
 ### Fixed
 
+-   \[INTERNALS\] Fix string base class selection.
+
 ### Changed
 
 -   Updating `stdlib` files if they are modified not just accessed.
+-   \[INTERNALS\] Add class object to class function call arguments.
 
 ### Deprecated
 
diff --git a/pyccel/ast/class_defs.py b/pyccel/ast/class_defs.py
index 9b785f0abd..c2f2a9ccaf 100644
--- a/pyccel/ast/class_defs.py
+++ b/pyccel/ast/class_defs.py
@@ -6,9 +6,9 @@
 This module contains all types which define a python class which is automatically recognised by pyccel
 """
 from .builtins  import PythonImag, PythonReal, PythonConjugate
-from .core      import ClassDef, FunctionDef
+from .core      import ClassDef, PyccelFunctionDef
 from .datatypes import (NativeBool, NativeInteger, NativeFloat,
-                        NativeComplex, NativeString)
+                        NativeComplex, NativeString, NativeNumeric)
 from .numpyext  import (Shape, NumpySum, NumpyAmin, NumpyAmax,
                         NumpyImag, NumpyReal, NumpyTranspose,
                         NumpyConjugate, NumpyArraySize)
@@ -27,24 +27,24 @@
 
 ComplexClass = ClassDef('complex',
         methods=[
-            FunctionDef('imag',[],[],body=[],
-                decorators={'property':'property', 'numpy_wrapper':PythonImag}),
-            FunctionDef('real',[],[],body=[],
-                decorators={'property':'property', 'numpy_wrapper':PythonReal}),
-            FunctionDef('conjugate',[],[],body=[],
-                decorators={'numpy_wrapper':PythonConjugate}),
+            PyccelFunctionDef('imag', func_class = PythonImag,
+                decorators={'property':'property', 'numpy_wrapper':'numpy_wrapper'}),
+            PyccelFunctionDef('real', func_class = PythonReal,
+                decorators={'property':'property', 'numpy_wrapper': 'numpy_wrapper'}),
+            PyccelFunctionDef('conjugate', func_class = PythonConjugate,
+                decorators={'numpy_wrapper': 'numpy_wrapper'}),
             ])
 
 #=======================================================================================
 
 FloatClass = ClassDef('float',
         methods=[
-            FunctionDef('imag',[],[],body=[],
-                decorators={'property':'property', 'numpy_wrapper':PythonImag}),
-            FunctionDef('real',[],[],body=[],
-                decorators={'property':'property', 'numpy_wrapper':PythonReal}),
-            FunctionDef('conjugate',[],[],body=[],
-                decorators={'numpy_wrapper':PythonConjugate}),
+            PyccelFunctionDef('imag', func_class = PythonImag,
+                decorators={'property':'property', 'numpy_wrapper': 'numpy_wrapper'}),
+            PyccelFunctionDef('real', func_class = PythonReal,
+                decorators={'property':'property', 'numpy_wrapper': 'numpy_wrapper'}),
+            PyccelFunctionDef('conjugate', func_class = PythonConjugate,
+                decorators={'numpy_wrapper': 'numpy_wrapper'}),
             #as_integer_ratio
             #fromhex
             #hex
@@ -55,12 +55,12 @@
 
 IntegerClass = ClassDef('integer',
         methods=[
-            FunctionDef('imag',[],[],body=[],
-                decorators={'property':'property', 'numpy_wrapper':PythonImag}),
-            FunctionDef('real',[],[],body=[],
-                decorators={'property':'property', 'numpy_wrapper':PythonReal}),
-            FunctionDef('conjugate',[],[],body=[],
-                decorators={'numpy_wrapper':PythonConjugate}),
+            PyccelFunctionDef('imag', func_class = PythonImag,
+                decorators={'property':'property', 'numpy_wrapper': 'numpy_wrapper'}),
+            PyccelFunctionDef('real', func_class = PythonReal,
+                decorators={'property':'property', 'numpy_wrapper': 'numpy_wrapper'}),
+            PyccelFunctionDef('conjugate', func_class = PythonConjugate,
+                decorators={'numpy_wrapper': 'numpy_wrapper'}),
             #as_integer_ratio
             #bit_length
             #denominator
@@ -72,7 +72,7 @@
 #=======================================================================================
 
 BooleanClass = ClassDef('boolean',
-        superclass=(IntegerClass,))
+        superclasses=(IntegerClass,))
 
 #=======================================================================================
 
@@ -137,28 +137,28 @@
 
 NumpyArrayClass = ClassDef('numpy.ndarray',
         methods=[
-            FunctionDef('shape',[],[],body=[],
-                decorators={'property':'property', 'numpy_wrapper':Shape}),
-            FunctionDef('size',[],[],body=[],
-                decorators={'property':'property', 'numpy_wrapper':NumpyArraySize}),
-            FunctionDef('T',[],[],body=[],
-                decorators={'property':'property', 'numpy_wrapper':NumpyTranspose}),
-            FunctionDef('transpose',[],[],body=[],
-                decorators={'numpy_wrapper':NumpyTranspose}),
-            FunctionDef('sum',[],[],body=[],
-                decorators={'numpy_wrapper':NumpySum}),
-            FunctionDef('min',[],[],body=[],
-                decorators={'numpy_wrapper':NumpyAmin}),
-            FunctionDef('max',[],[],body=[],
-                decorators={'numpy_wrapper':NumpyAmax}),
-            FunctionDef('imag',[],[],body=[],
-                decorators={'property':'property', 'numpy_wrapper':NumpyImag}),
-            FunctionDef('real',[],[],body=[],
-                decorators={'property':'property', 'numpy_wrapper':NumpyReal}),
-            FunctionDef('conj',[],[],body=[],
-                decorators={'numpy_wrapper':NumpyConjugate}),
-            FunctionDef('conjugate',[],[],body=[],
-                decorators={'numpy_wrapper':NumpyConjugate})])
+            PyccelFunctionDef('shape', func_class = Shape,
+                decorators={'property':'property', 'numpy_wrapper':'numpy_wrapper'}),
+            PyccelFunctionDef('size', func_class = NumpyArraySize,
+                decorators={'property':'property', 'numpy_wrapper': 'numpy_wrapper'}),
+            PyccelFunctionDef('T', func_class = NumpyTranspose,
+                decorators={'property':'property', 'numpy_wrapper': 'numpy_wrapper'}),
+            PyccelFunctionDef('transpose', func_class = NumpyTranspose,
+                decorators={'numpy_wrapper': 'numpy_wrapper'}),
+            PyccelFunctionDef('sum', func_class = NumpySum,
+                decorators={'numpy_wrapper': 'numpy_wrapper'}),
+            PyccelFunctionDef('min', func_class = NumpyAmin,
+                decorators={'numpy_wrapper': 'numpy_wrapper'}),
+            PyccelFunctionDef('max', func_class = NumpyAmax,
+                decorators={'numpy_wrapper': 'numpy_wrapper'}),
+            PyccelFunctionDef('imag', func_class = NumpyImag,
+                decorators={'property':'property', 'numpy_wrapper': 'numpy_wrapper'}),
+            PyccelFunctionDef('real', func_class = NumpyReal,
+                decorators={'property':'property', 'numpy_wrapper': 'numpy_wrapper'}),
+            PyccelFunctionDef('conj', func_class = NumpyConjugate,
+                decorators={'numpy_wrapper': 'numpy_wrapper'}),
+            PyccelFunctionDef('conjugate', func_class = NumpyConjugate,
+                decorators={'numpy_wrapper': 'numpy_wrapper'})])
 
 #=======================================================================================
 
@@ -174,10 +174,39 @@
 
 def get_cls_base(dtype, precision, rank):
     """
-    From the dtype and rank, determine the base class of an object
+    Determine the base class of an object.
+
+    From the dtype and rank, determine the base class of an object.
+
+    Parameters
+    ----------
+    dtype : DataType
+        The data type of the object.
+
+    precision : int
+        The precision of the object.
+
+    rank : int
+        The rank of the object.
+
+    Returns
+    -------
+    ClassDef
+        A class definition describing the base class of an object.
+
+    Raises
+    ------
+    NotImplementedError
+        Raised if the base class cannot be found.
     """
-    if precision == -1 and rank == 0:
+    if precision in (-1, 0, None) and rank == 0:
         return literal_classes[dtype]
-    else:
+    elif dtype in NativeNumeric:
         return NumpyArrayClass
+    else:
+        type_name = f"{dtype}({precision})"
+        if rank:
+            dims = ','.join(':' for _ in range(rank))
+            type_name += f"[{dims}]"
+        raise NotImplementedError(f"No class definition found for type {type_name}")
 
diff --git a/pyccel/ast/core.py b/pyccel/ast/core.py
index 04fb09c901..5465df749d 100644
--- a/pyccel/ast/core.py
+++ b/pyccel/ast/core.py
@@ -4,6 +4,7 @@
 # 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.     #
 #------------------------------------------------------------------------------------------#
+from itertools import chain
 
 from sympy.logic.boolalg      import And as sp_And
 
@@ -2153,8 +2154,6 @@ def __init__(self, func, args, current_function=None):
 
         # add the missing argument in the case of optional arguments
         f_args = func.arguments
-        if func.cls_name:
-            f_args = f_args[1:]
         if not len(args) == len(f_args):
             # Collect dict of keywords and values (initialised as default)
             f_args_dict = {a.name: (a.name, a.value) if a.has_default \
@@ -2352,8 +2351,9 @@ class FunctionDef(ScopedNode):
     imports : list, tuple
         A list of needed imports.
 
-    decorators : list, tuple
-        A list of properties.
+    decorators : dict
+        A dictionary whose keys are the names of decorators and whose values
+        contain their implementation.
 
     headers : list,tuple
         A list of headers describing the function.
@@ -2905,26 +2905,35 @@ def global_funcs(self):
         return self._global_funcs
 
 class PyccelFunctionDef(FunctionDef):
-    """ Class inheriting from FunctionDef which can store a pointer
+    """
+    Class used for storing `PyccelInternalFunction` objects in a FunctionDef.
+
+    Class inheriting from `FunctionDef` which can store a pointer
     to a class type defined by pyccel for treating internal functions.
-    This is useful for importing builtin functions
+    This is useful for importing builtin functions and for defining
+    classes which have `PyccelInternalFunction`s as attributes or methods.
 
     Parameters
     ----------
     name : str
-           The name of the function
+           The name of the function.
+
     func_class : type inheriting from PyccelInternalFunction / PyccelAstNode
-                 The class which should be instantiated upon a FunctionCall
-                 to this FunctionDef object
+         The class which should be instantiated upon a FunctionCall
+         to this FunctionDef object.
+
+    decorators : dictionary
+        A dictionary whose keys are the names of decorators and whose values
+        contain their implementation.
     """
     __slots__ = ()
-    def __init__(self, name, func_class):
+    def __init__(self, name, func_class, *, decorators = {}):
         assert isinstance(func_class, type) and \
                 issubclass(func_class, (PyccelInternalFunction, PyccelAstNode))
         arguments = ()
         results = ()
         body = ()
-        super().__init__(name, arguments, results, body)
+        super().__init__(name, arguments, results, body, decorators=decorators)
         self._cls_name = func_class
 
 class Interface(Basic):
@@ -3132,28 +3141,38 @@ class SympyFunction(FunctionDef):
 
 
 class ClassDef(ScopedNode):
+    """
+    Represents a class definition.
 
-    """Represents a class definition.
+    Class representing a class definition in the code. It holds all objects
+    which may be defined in a class including methods, interfaces, attributes,
+    etc. It also handles inheritance.
 
     Parameters
     ----------
     name : str
         The name of the class.
 
-    attributes: iterable
+    attributes : iterable
         The attributes to the class.
 
-    methods: iterable
-        Class methods
+    methods : iterable
+        Class methods.
 
-    options: list, tuple
-        list of options ('public', 'private', 'abstract')
+    options : list, tuple
+        A list of options ('public', 'private', 'abstract').
 
-    imports: list, tuple
-        list of needed imports
+    imports : list, tuple
+        A list of required imports.
+
+    superclasses : iterable
+        The definition of all classes from which this class inherits.
+
+    interfaces : iterable
+        The interface methods.
 
-    superclass : str
-        superclass's class name
+    scope : Scope
+        The scope for the class contents.
 
     Examples
     --------
@@ -3173,7 +3192,7 @@ class ClassDef(ScopedNode):
     ClassDef(Point, (x, y), (FunctionDef(translate, (x, y, a, b), (z, t), [y := a + x], [], [], None, False, function),), [public])
     """
     __slots__ = ('_name','_attributes','_methods','_options',
-                 '_imports','_superclass','_interfaces')
+                 '_imports','_superclasses','_interfaces')
     _attribute_nodes = ('_attributes', '_methods', '_imports', '_interfaces')
 
     def __init__(
@@ -3183,7 +3202,7 @@ def __init__(
         methods=(),
         options=('public',),
         imports=(),
-        superclass=(),
+        superclasses=(),
         interfaces=(),
         scope = None
         ):
@@ -3216,8 +3235,12 @@ def __init__(
         if not iterable(imports):
             raise TypeError('imports must be an iterable')
 
-        if not iterable(superclass):
-            raise TypeError('superclass must be iterable')
+        if not iterable(superclasses):
+            raise TypeError('superclasses must be iterable')
+        if pyccel_stage != 'syntactic':
+            for s in superclasses:
+                if not isinstance(s, ClassDef):
+                    raise TypeError('superclass item must be a ClassDef')
 
         if not iterable(interfaces):
             raise TypeError('interfaces must be iterable')
@@ -3263,7 +3286,7 @@ def __init__(
         self._methods = methods
         self._options = options
         self._imports = imports
-        self._superclass  = superclass
+        self._superclasses  = superclasses
         self._interfaces = interfaces
 
         super().__init__(scope = scope)
@@ -3289,8 +3312,14 @@ def imports(self):
         return self._imports
 
     @property
-    def superclass(self):
-        return self._superclass
+    def superclasses(self):
+        """
+        Get the superclasses.
+
+        Get the class definitions for the classes from which this class
+        inherits.
+        """
+        return self._superclasses
 
     @property
     def interfaces(self):
@@ -3349,6 +3378,48 @@ def get_attribute(self, O, attr):
             cls_base=var.cls_base,
             )
 
+    def get_method(self, name):
+        """
+        Get the method `name` of the current class.
+
+        Look through all methods and interfaces of the current class to
+        find a method called `name`. If this class inherits from another
+        class, that class is also searched to ensure that the inherited
+        methods are available.
+
+        Parameters
+        ----------
+        name : str
+            The name of the attribute we are looking for.
+
+        Returns
+        -------
+        FunctionDef
+            The definition of the method.
+
+        Raises
+        ------
+        ValueError
+            Raised if the method cannot be found.
+        """
+        try:
+            method = next(i for i in chain(self.methods, self.interfaces) if i.name == name)
+        except StopIteration:
+            method = None
+            i = 0
+            n_classes = len(self.superclasses)
+            while method is None and i<n_classes:
+                try:
+                    method = self.superclasses[i].get_method(name)
+                except StopIteration:
+                    method = None
+
+        if method is None:
+            errors.report(f"Can't find method {name} in class {self.name}",
+                    severity='fatal', symbol=self)
+
+        return method
+
     @property
     def is_iterable(self):
         """Returns True if the class has an iterator."""
diff --git a/pyccel/ast/datatypes.py b/pyccel/ast/datatypes.py
index b25086f384..e5282ddf55 100644
--- a/pyccel/ast/datatypes.py
+++ b/pyccel/ast/datatypes.py
@@ -182,13 +182,14 @@ class NativeSymbol(DataType):
     __slots__ = ()
     _name = 'Symbol'
 
-
-# TODO to be removed
 class CustomDataType(DataType):
-    __slots__ = ('_name',)
+    """
+    Class from which user-defined types inherit.
 
-    def __init__(self, name='__UNDEFINED__'):
-        self._name = name
+    A general class for custom data types which is used as a
+    base class when a user defines their own type using classes.
+    """
+    __slots__ = ()
 
 class NativeGeneric(DataType):
     __slots__ = ()
@@ -239,11 +240,32 @@ def args(self):
 
 def DataTypeFactory(name, argnames=["_name"],
                     BaseClass=CustomDataType,
-                    prefix=None,
-                    alias=None,
-                    is_iterable=False,
-                    is_with_construct=False,
-                    is_polymorphic=False):
+                    prefix=None):
+    """
+    Create a new data class.
+
+    Create a new data class which sub-classes a DataType. This provides
+    a new data type which can be used, for example, for class types.
+
+    Parameters
+    ----------
+    name : str
+        The name of the new class.
+
+    argnames : list of str
+        A list of all the arguments for the new class.
+
+    BaseClass : type inheriting from DataType
+        The class from which the new type will be sub-classed.
+
+    prefix : str
+        A prefix which will be added to the class name.
+
+    Returns
+    -------
+    type
+        A new DataType class.
+    """
     def __init__(self, **kwargs):
         for key, value in list(kwargs.items()):
             # here, the argnames variable is the one passed to the
@@ -252,7 +274,7 @@ def __init__(self, **kwargs):
                 raise TypeError("Argument %s not valid for %s"
                     % (key, self.__class__.__name__))
             setattr(self, key, value)
-        BaseClass.__init__(self, name=name[:-len("Class")])
+        BaseClass.__init__(self)
 
     if prefix is None:
         prefix = 'Pyccel'
@@ -260,13 +282,10 @@ def __init__(self, **kwargs):
         prefix = 'Pyccel{0}'.format(prefix)
 
     newclass = type(prefix + name, (BaseClass,),
-                    {"__init__":          __init__,
-                     "_name":             name,
-                     "prefix":            prefix,
-                     "alias":             alias,
-                     "is_iterable":       is_iterable,
-                     "is_with_construct": is_with_construct,
-                     "is_polymorphic":    is_polymorphic})
+                    {"__init__": __init__,
+                     "_name": name,
+                     "prefix": prefix,
+                     "alias": name})
     return newclass
 
 def is_pyccel_datatype(expr):
@@ -318,15 +337,26 @@ def datatype(arg):
 def str_dtype(dtype):
 
     """
-    This function takes a datatype and returns a pyccel datatype as a string
+    Get a string describing a datatype.
 
-    Example
+    This function takes a pyccel datatype and returns a string which describes it.
+
+    Parameters
+    ----------
+    dtype : DataType
+        The datatype.
+
+    Returns
     -------
+    str
+        A description of the data type.
+
+    Examples
+    --------
     >>> str_dtype('int')
     'integer'
     >>> str_dtype(NativeInteger())
     'integer'
-
     """
     if isinstance(dtype, str):
         if dtype == 'int':
@@ -343,5 +373,7 @@ def str_dtype(dtype):
         return 'complex'
     elif isinstance(dtype, NativeBool):
         return 'bool'
+    elif isinstance(dtype, CustomDataType):
+        return dtype.name
     else:
         raise TypeError('Unknown datatype {0}'.format(str(dtype)))
diff --git a/pyccel/ast/variable.py b/pyccel/ast/variable.py
index 3c84a7cf2d..33b263676e 100644
--- a/pyccel/ast/variable.py
+++ b/pyccel/ast/variable.py
@@ -543,6 +543,14 @@ class DottedName(Basic):
     """
     Represents a dotted object.
 
+    Represents an object accessed via a dot. This usually means that
+    the object belongs to a class or module.
+
+    Parameters
+    ----------
+    *args : tuple of PyccelSymbol
+        The different symbols making up the dotted name.
+
     Examples
     --------
     >>> from pyccel.ast.core import DottedName
@@ -569,6 +577,9 @@ def name(self):
     def __str__(self):
         return """.""".join(str(n) for n in self.name)
 
+    def __repr__(self):
+        return """.""".join(repr(n) for n in self.name)
+
     def __eq__(self, other):
         return str(self) == str(other)
 
diff --git a/pyccel/codegen/printing/fcode.py b/pyccel/codegen/printing/fcode.py
index ffc1cea134..2aef2bc956 100644
--- a/pyccel/codegen/printing/fcode.py
+++ b/pyccel/codegen/printing/fcode.py
@@ -51,7 +51,7 @@
 from pyccel.ast.datatypes import NativeInteger, NativeBool, NativeFloat, NativeComplex
 from pyccel.ast.datatypes import iso_c_binding
 from pyccel.ast.datatypes import iso_c_binding_shortcut_mapping
-from pyccel.ast.datatypes import NativeRange
+from pyccel.ast.datatypes import NativeRange, NativeNumeric
 from pyccel.ast.datatypes import CustomDataType
 
 from pyccel.ast.internals import Slice, PrecomputedCode
@@ -434,16 +434,19 @@ def _print_Module(self, expr):
             name = '{prefix}_{name}'.format(prefix=self.prefix_module,
                                             name=name)
 
-        # ARA : issue-999
-        #       we look for external functions and declare their result type
-        external_decs = self._get_external_declarations()
-
         imports = ''.join(self._print(i) for i in expr.imports)
 
-        decs    = ''.join(self._print(i) for i in expr.declarations)
-        # ARA : issue-999
-        decs   += ''.join(self._print(i) for i in external_decs.values())
-        body    = ''
+        # Define declarations
+        decs = ''
+        # ...
+        class_decs_and_methods = [self._print(i) for i in expr.classes]
+        decs += '\n'.join(c[0] for c in class_decs_and_methods)
+        # ...
+
+        decs += ''.join(self._print(i) for i in expr.declarations)
+        # look for external functions and declare their result type
+        external_decs = self._get_external_declarations()
+        decs += ''.join(self._print(i) for i in external_decs.values())
 
         # ... TODO add other elements
         private_funcs = [f.name for f in expr.funcs if f.is_private]
@@ -461,20 +464,13 @@ def _print_Module(self, expr):
         if expr.interfaces and not isinstance(expr, BindCModule):
             interfaces = '\n'.join(self._print(i) for i in expr.interfaces)
 
-        func_strings = []
+        # Get class functions
+        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]
+            func_strings += [''.join([sep, self._print(i), sep]) for i in expr.funcs]
         if isinstance(expr, BindCModule):
             func_strings += [''.join([sep, self._print(i), sep]) for i in expr.variable_wrappers]
-        body += '\n'.join(func_strings)
-        # ...
-
-        # ...
-        for i in expr.classes:
-            # update decs with declarations from ClassDef
-            c_decs, c_funcs = self._print(i)
-            decs = '{0}\n{1}'.format(decs, c_decs)
-            body = '{0}\n{1}\n'.format(body, c_funcs)
+        body = '\n'.join(func_strings)
         # ...
 
         contains = 'contains\n' if (expr.funcs or expr.classes or expr.interfaces) else ''
@@ -1328,13 +1324,6 @@ def _print_MacroCount(self, expr):
 
     def _print_Declare(self, expr):
         # ... ignored declarations
-        # we don't print the declaration if iterable object
-        if is_iterable_datatype(expr.dtype):
-            return ''
-
-        if is_with_construct_datatype(expr.dtype):
-            return ''
-
         if isinstance(expr.dtype, NativeSymbol):
             return ''
 
@@ -1378,7 +1367,7 @@ def _print_Declare(self, expr):
             prefix = expr_dtype.prefix
             alias  = expr_dtype.alias
 
-            if expr_dtype.is_polymorphic or expr.passed_from_dotted:
+            if var.is_argument:
                 sig = 'class'
             else:
                 sig = 'type'
@@ -1482,7 +1471,7 @@ def _print_Declare(self, expr):
 #                severity='fatal')
 
         mod_str = ''
-        if expr.module_variable and not is_private and (rank == 0):
+        if expr.module_variable and not is_private and (rank == 0) and expr_dtype in NativeNumeric:
             mod_str = ', bind(c)'
 
         # Construct declaration
@@ -2037,7 +2026,7 @@ def _print_ClassDef(self, expr):
                     'contains\n'
                     '{1}').format(code, methods)
         decs = ('{0}\n'
-                'end type {1}').format(code, name)
+                'end type {1}\n').format(code, name)
 
         sep = self._print(SeparatorComment(40))
         # we rename all methods because of the aliasing
@@ -2957,6 +2946,9 @@ def _print_FunctionCall(self, expr):
         parent_assign = expr.get_direct_user_nodes(lambda x: isinstance(x, Assign))
         is_function =  len(func_results) == 1 and func_results[0].rank == 0
 
+        if isinstance(expr, DottedFunctionCall):
+            args = args[1:]
+
         if (not self._additional_code):
             self._additional_code = ''
         if parent_assign:
@@ -2965,8 +2957,8 @@ def _print_FunctionCall(self, expr):
                 lhs_vars = {func_results[0]:lhs}
             else:
                 lhs_vars = dict(zip(func_results,lhs))
-            args = []
-            for a in expr.args:
+            assign_args = []
+            for a in args:
                 key = a.keyword
                 arg = a.value
                 if arg in lhs_vars.values():
@@ -2976,7 +2968,8 @@ def _print_FunctionCall(self, expr):
                     newarg = var
                 else:
                     newarg = arg
-                args.append(FunctionCallArgument(newarg, key))
+                assign_args.append(FunctionCallArgument(newarg, key))
+            args = assign_args
             results = list(lhs_vars.values())
             if is_function:
                 results_strs = []
diff --git a/pyccel/codegen/printing/pycode.py b/pyccel/codegen/printing/pycode.py
index 773e9d97bd..b0414e9a69 100644
--- a/pyccel/codegen/printing/pycode.py
+++ b/pyccel/codegen/printing/pycode.py
@@ -8,7 +8,7 @@
 
 from pyccel.ast.builtins   import PythonMin, PythonMax
 from pyccel.ast.core       import CodeBlock, Import, Assign, FunctionCall, For, AsName, FunctionAddress
-from pyccel.ast.core       import IfSection, FunctionDef, Module
+from pyccel.ast.core       import IfSection, FunctionDef, Module, DottedFunctionCall
 from pyccel.ast.datatypes  import default_precision
 from pyccel.ast.functionalexpr import FunctionalFor
 from pyccel.ast.literals   import LiteralTrue, LiteralString
@@ -180,11 +180,6 @@ def _print_DottedVariable(self, expr):
         lhs_code = self._print(expr.lhs)
         return f"{lhs_code}.{rhs_code}"
 
-    def _print_DottedFunctionCall(self, expr):
-        func = self._print(expr.func_name)
-        args = ', '.join(self._print(arg) for arg in expr.args)
-        return (f"{func}({args})\n")
-
     def _print_FunctionDefArgument(self, expr):
         name = self._print(expr.name)
         type_annotation = ''
@@ -468,9 +463,12 @@ def _print_FunctionCall(self, expr):
         if expr.interface:
             func_name = expr.interface_name
         else:
-            func_name = expr.funcdef.name
-        args = ', '.join(self._print(i) for i in expr.args)
-        code = '{func}({args})'.format(func=func_name, args=args)
+            func_name = expr.func_name
+        args = expr.args
+        if isinstance(expr, DottedFunctionCall):
+            args = args[1:]
+        args_str = ', '.join(self._print(i) for i in args)
+        code = f'{func_name}({args_str})'
         if expr.funcdef.results:
             return code
         else:
@@ -1010,10 +1008,12 @@ def _print_PythonType(self, expr):
     #-----------------Class Printer---------------------------------
 
     def _print_ClassDef(self, expr):
-        classDefName = 'class {}({}):'.format(expr.name,', '.join(self._print(arg) for arg in  expr.superclass))
+        classDefName = 'class {}({}):'.format(expr.name,', '.join(self._print(arg) for arg in  expr.superclasses))
         methods = ''.join(self._print(method) for method in expr.methods)
         methods = self._indent_codestring(methods)
-        classDef = '\n'.join([classDefName, methods]) + '\n'
+        interfaces = ''.join(self._print(method) for method in expr.interfaces)
+        interfaces = self._indent_codestring(interfaces)
+        classDef = '\n'.join([classDefName, methods, interfaces]) + '\n'
         return classDef
 
     def _print_ConstructorCall(self, expr):
diff --git a/pyccel/parser/scope.py b/pyccel/parser/scope.py
index 74d79df50d..25a485fe06 100644
--- a/pyccel/parser/scope.py
+++ b/pyccel/parser/scope.py
@@ -86,7 +86,6 @@ def __init__(self, *, decorators = (), is_loop = False,
         self._parent_scope       = parent_scope
         self._sons_scopes        = {}
 
-
         self._is_loop = is_loop
         # scoping for loops
         self._loops = []
@@ -325,12 +324,16 @@ def remove_variable(self, var, name = None):
             raise RuntimeError("Variable not found in scope")
 
     def insert_class(self, cls):
-        """ Add a class to the current scope
+        """
+        Add a class to the current scope.
+
+        Add the definition of a class to the current scope to
+        make it discoverable when used.
 
         Parameters
         ----------
-        cls  : ClassDef
-                The class to be inserted into the current scope
+        cls : ClassDef
+            The class to be inserted into the current scope.
         """
         if not isinstance(cls, ClassDef):
             raise TypeError('class must be of type ClassDef')
@@ -340,8 +343,35 @@ def insert_class(self, cls):
         if self.is_loop:
             self.parent_scope.insert_class(cls)
         else:
-            #if name in self._locals['classes']:
-            #    raise RuntimeError('New class already exists in scope')
+            if name in self._locals['classes']:
+                raise RuntimeError(f"A class with name '{name}' already exists in the scope")
+            self._locals['classes'][name] = cls
+
+    def update_class(self, cls):
+        """
+        Update a class which is in scope.
+
+        Search for a class in the current scope and its parents. Once it
+        has been found, replace it with the updated ClassDef passed as
+        argument.
+
+        Parameters
+        ----------
+        cls : ClassDef
+            The class to be inserted into the current scope.
+        """
+        if not isinstance(cls, ClassDef):
+            raise TypeError('class must be of type ClassDef')
+
+        name = cls.name
+
+        name_found = name in self._locals['classes']
+
+        if not name_found and self.parent_scope:
+            self.parent_scope.update_class(cls)
+        else:
+            if not name_found:
+                raise RuntimeError('Class not found in scope')
             self._locals['classes'][name] = cls
 
     def insert_macro(self, macro):
@@ -362,7 +392,21 @@ def insert_template(self, expr):
         self._locals['templates'][expr.name] = expr
 
     def insert_header(self, expr):
-        """ Add a header to the current scope
+        """
+        Add a header to the current scope.
+
+        Add a header describing a function, method or class to
+        the current scope.
+
+        Parameters
+        ----------
+        expr : pyccel.ast.Header
+            The header description.
+
+        Raises
+        ------
+        TypeError
+            Raised if the header type is unknown.
         """
         if isinstance(expr, (FunctionHeader, MethodHeader)):
             if expr.name in self.headers:
@@ -374,11 +418,7 @@ def insert_header(self, expr):
 
             #  create a new Datatype for the current class
 
-            iterable = 'iterable' in expr.options
-            with_construct = 'with' in expr.options
-            dtype = DataTypeFactory(expr.name, '_name',
-                                    is_iterable=iterable,
-                                    is_with_construct=with_construct)
+            dtype = DataTypeFactory(expr.name, '_name')
             self.cls_constructs[expr.name] = dtype
         else:
             msg = 'header of type{0} is not supported'
diff --git a/pyccel/parser/semantic.py b/pyccel/parser/semantic.py
index 32466b7889..e3c1c9cb0f 100644
--- a/pyccel/parser/semantic.py
+++ b/pyccel/parser/semantic.py
@@ -526,7 +526,7 @@ def _infer_type(self, expr):
             d_var['memory_handling'] = expr.memory_handling
             d_var['shape'          ] = expr.shape
             d_var['rank'           ] = expr.rank
-            d_var['cls_base'       ] = expr.cls_base
+            d_var['cls_base'       ] = expr.cls_base or self.scope.find(expr.dtype, 'classes')
             d_var['is_target'      ] = expr.is_target
             d_var['order'          ] = expr.order
             d_var['precision'      ] = expr.precision
@@ -881,7 +881,7 @@ def incompatible(i_arg, f_arg):
                         symbol = expr,
                         severity='error')
 
-    def _handle_function(self, expr, func, args):
+    def _handle_function(self, expr, func, args, is_method = False):
         """
         Create the node representing the function call.
 
@@ -899,6 +899,9 @@ def _handle_function(self, expr, func, args):
         args : tuple
                The arguments passed to the function.
 
+        is_method : bool
+                Indicates if the function is a method (and should return a DottedFunctionCall).
+
         Returns
         -------
         FunctionCall/PyccelInternalFunction
@@ -952,7 +955,11 @@ def _handle_function(self, expr, func, args):
 
             args = input_args
 
-            new_expr = FunctionCall(func, args, self._current_function)
+            if is_method:
+                new_expr = DottedFunctionCall(func, args, current_function = self._current_function, prefix = args[0].value)
+            else:
+                new_expr = FunctionCall(func, args, self._current_function)
+
             if None in new_expr.args:
                 errors.report("Too few arguments passed in function call",
                         symbol = expr,
@@ -1239,7 +1246,7 @@ def _assign_lhs_variable(self, lhs, d_var, rhs, new_expressions, is_augassign,ar
                 cls      = self.scope.find(cls_name, 'classes')
 
                 attributes = cls.attributes
-                parent     = cls.superclass
+                parent     = cls.superclasses
                 attributes = list(attributes)
                 n_name     = str(lhs.name[-1])
 
@@ -1258,8 +1265,8 @@ def _assign_lhs_variable(self, lhs, d_var, rhs, new_expressions, is_augassign,ar
 
                 # update the attributes of the class and push it to the scope
                 attributes += [member]
-                new_cls = ClassDef(cls_name, attributes, [], superclass=parent)
-                self.scope.parent_scope.insert_class(new_cls)
+                new_cls = ClassDef(cls_name, attributes, [], superclasses=parent)
+                self.scope.parent_scope.update_class(new_cls)
             else:
                 lhs = self._visit(lhs)
         else:
@@ -1559,6 +1566,41 @@ def _assign_GeneratorComprehension(self, lhs_name, expr):
         expr_new.set_fst(expr.fst)
         return expr_new
 
+    def _find_superclasses(self, expr):
+        """
+        Find all the superclasses in the scope.
+
+        From a syntactic ClassDef, extract the names of the superclasses and
+        search through the scope to find their definitions. If there is no
+        definition then an error is raised.
+
+        Parameters
+        ----------
+        expr : ClassDef
+            The class whose superclasses we wish to find.
+
+        Returns
+        -------
+        list
+            An iterable containing the definitions of all the superclasses.
+
+        Raises
+        ------
+        PyccelSemanticError
+            A `PyccelSemanticError` is reported and will be raised after the
+            semantic stage is complete.
+        """
+        parent = {s: self.scope.find(s, 'classes') for s in expr.superclasses}
+        if any(c is None for c in parent.values()):
+            for s,c in parent.items():
+                if c is None:
+                    errors.report(f"Couldn't find class {s} in scope", symbol=expr,
+                            severity='error')
+            parent = {s:c for s,c in parent.items() if c is not None}
+
+        return list(parent.values())
+
+
     #====================================================
     #                 _visit functions
     #====================================================
@@ -1954,9 +1996,7 @@ def _visit_PyccelSymbol(self, expr):
         var = self.check_for_variable(name)
 
         if var is None:
-            var = self.scope.find(name, 'functions')
-        if var is None:
-            var = self.scope.find(name, 'symbolic_functions')
+            var = self.scope.find(name)
         if var is None:
             var = python_builtin_datatype(name)
 
@@ -2038,6 +2078,9 @@ def _visit_DottedName(self, expr):
                         symbol=expr,
                         bounding_box=(self._current_fst_node.lineno, self._current_fst_node.col_offset),
                         severity='fatal')
+        if isinstance(first, ClassDef):
+            errors.report("Static class methods are not yet supported", symbol=expr,
+                    severity='fatal')
 
         d_var = self._infer_type(first)
         if d_var.get('cls_base', None) is None:
@@ -2052,14 +2095,6 @@ def _visit_DottedName(self, expr):
 
         # look for a class method
         if isinstance(rhs, FunctionCall):
-            methods = list(cls_base.methods) + list(cls_base.interfaces)
-            for method in methods:
-                if isinstance(method, Interface):
-                    errors.report('Generic methods are not supported yet',
-                        symbol=method.name,
-                        bounding_box=(self._current_fst_node.lineno,
-                            self._current_fst_node.col_offset),
-                        severity='fatal')
             macro = self.scope.find(rhs_name, 'macros')
             if macro is not None:
                 master = macro.master
@@ -2069,28 +2104,15 @@ def _visit_DottedName(self, expr):
                 args = macro.apply(args)
                 return FunctionCall(master, args, self._current_function)
 
-            args = [self._visit(arg) for arg in
-                    rhs.args]
-            for i in methods:
-                if str(i.name) == rhs_name:
-                    if 'numpy_wrapper' in i.decorators.keys():
-                        func = i.decorators['numpy_wrapper']
-                        self.insert_import('numpy', AsName(func, rhs_name))
-                        return func(visited_lhs, *args)
-                    else:
-                        return DottedFunctionCall(i, args, prefix = visited_lhs,
-                                    current_function = self._current_function)
+            args = [FunctionCallArgument(visited_lhs), *self._handle_function_args(rhs.args)]
+            method = cls_base.get_method(rhs_name)
+            if cls_base.name == 'numpy.ndarray':
+                numpy_class = method.cls_name
+                self.insert_import('numpy', AsName(numpy_class, numpy_class.name))
+            return self._handle_function(expr, method, args, is_method = True)
 
         # look for a class attribute / property
         elif isinstance(rhs, PyccelSymbol) and cls_base:
-            methods = list(cls_base.methods) + list(cls_base.interfaces)
-            for method in methods:
-                if isinstance(method, Interface):
-                    errors.report('Generic methods are not supported yet',
-                        symbol=method.name,
-                        bounding_box=(self._current_fst_node.lineno,
-                            self._current_fst_node.col_offset),
-                        severity='fatal')
             # standard class attribute
             if rhs in attr_name:
                 self._current_class = cls_base
@@ -2100,16 +2122,12 @@ def _visit_DottedName(self, expr):
 
             # class property?
             else:
-                for i in methods:
-                    if i.name == rhs and \
-                            'property' in i.decorators.keys():
-                        if 'numpy_wrapper' in i.decorators.keys():
-                            func = i.decorators['numpy_wrapper']
-                            self.insert_import('numpy', AsName(func, rhs))
-                            return func(visited_lhs)
-                        else:
-                            return DottedFunctionCall(i, [], prefix = visited_lhs,
-                                    current_function = self._current_function)
+                method = cls_base.get_method(rhs_name)
+                assert 'property' in method.decorators
+                if cls_base.name == 'numpy.ndarray':
+                    numpy_class = method.cls_name
+                    self.insert_import('numpy', AsName(numpy_class, numpy_class.name))
+                return self._handle_function(expr, method, [FunctionCallArgument(visited_lhs)], is_method = True)
 
         # look for a macro
         else:
@@ -3260,7 +3278,10 @@ def _visit_FunctionDef(self, expr):
                         d_var['is_const'] = ahv.is_const
                         dtype = d_var.pop('datatype')
                         if not d_var['cls_base']:
-                            d_var['cls_base'] = get_cls_base( dtype, d_var['precision'], d_var['rank'] )
+                            try:
+                                d_var['cls_base'] = get_cls_base( dtype, d_var['precision'], d_var['rank'] )
+                            except KeyError:
+                                d_var['cls_base'] = self.scope.find( dtype, 'classes' )
 
                         if 'allow_negative_index' in self.scope.decorators:
                             if a.name in decorators['allow_negative_index']:
@@ -3422,9 +3443,9 @@ def _visit_FunctionDef(self, expr):
                 methods = list(cls.methods) + [func]
 
                 # update the class methods
-
-                self.scope.insert_class(ClassDef(cls_name, cls.attributes,
-                        methods, superclass=cls.superclass))
+                superclasses = self._find_superclasses(cls)
+                self.scope.update_class(ClassDef(cls_name, cls.attributes,
+                        methods, superclasses=superclasses))
 
             funcs += [func]
 
@@ -3498,16 +3519,18 @@ def _visit_ClassDef(self, expr):
         #      - wouldn't be better if it is done inside ClassDef?
 
         name = expr.name
+        # remove quotes for str representation
         name = name.replace("'", '')
+
+        parent = self._find_superclasses(expr)
+
+        cls = ClassDef(name, [], [], superclasses=parent)
+        self.scope.insert_class(cls)
+
         scope = self.create_new_class_scope(name, used_symbols=expr.scope.local_used_symbols,
                     original_symbols = expr.scope.python_names.copy())
         methods = list(expr.methods)
-        parent = expr.superclass
         interfaces = []
-
-        # remove quotes for str representation
-        cls = ClassDef(name, [], [], superclass=parent)
-        self.scope.insert_class(cls)
         const = None
 
         for (i, method) in enumerate(methods):
@@ -3551,8 +3574,8 @@ def _visit_ClassDef(self, expr):
         self.exit_class_scope()
 
         cls = ClassDef(name, attributes, methods,
-              interfaces=interfaces, superclass=parent, scope=scope)
-        self.scope.insert_class(cls)
+              interfaces=interfaces, superclasses=parent, scope=scope)
+        self.scope.update_class(cls)
 
         return EmptyNode()
 
diff --git a/pyccel/parser/syntactic.py b/pyccel/parser/syntactic.py
index d24fcfa667..3c57dd1cb0 100644
--- a/pyccel/parser/syntactic.py
+++ b/pyccel/parser/syntactic.py
@@ -850,10 +850,10 @@ def _visit_ClassDef(self, stmt):
         for i in methods:
             i.cls_name = name
         attributes = [a.var for a in methods[0].arguments]
-        parent = [self._visit(i) for i in stmt.bases]
+        parent = [p for p in (self._visit(i) for i in stmt.bases) if p != 'object']
         self.exit_class_scope()
         expr = ClassDef(name=name, attributes=attributes,
-                        methods=methods, superclass=parent, scope=scope)
+                        methods=methods, superclasses=parent, scope=scope)
 
         # we set the fst to keep track of needed information for errors
 


Test patch:
diff --git a/tests/codegen/fcode/scripts/concatenation.py b/tests/codegen/fcode/scripts/concatenation.py
deleted file mode 100644
index 9c8e1f9997..0000000000
--- a/tests/codegen/fcode/scripts/concatenation.py
+++ /dev/null
@@ -1,7 +0,0 @@
-# pylint: disable=missing-function-docstring, missing-module-docstring
-x = [1, 2, 3]
-l = x + [4, 5, 6]
-s = [1, 2, 3] + [1, 2, 3]
-
-st = "Hello" + "World"
-
diff --git a/tests/epyccel/test_class_expressions.py b/tests/epyccel/test_class_expressions.py
index 03670ece8e..d3ad842f18 100644
--- a/tests/epyccel/test_class_expressions.py
+++ b/tests/epyccel/test_class_expressions.py
@@ -150,7 +150,6 @@ def f(a : 'int32', b : 'int32'):
     assert r == epyc_r
     assert isinstance(r, type(epyc_r))
 
-@pytest.mark.xfail(reason="Class inheritance not fully implemented")
 def test_bool_conjugate(language):
     def f(a : 'bool', b : 'bool'):
         return (a or b).conjugate()
diff --git a/tests/codegen/fcode/scripts/context.py b/tests/errors/known_bugs/context.py
similarity index 100%
rename from tests/codegen/fcode/scripts/context.py
rename to tests/errors/known_bugs/context.py
diff --git a/tests/errors/semantic/blocking/unknown_method.py b/tests/errors/semantic/blocking/unknown_method.py
new file mode 100644
index 0000000000..a8a182ad5b
--- /dev/null
+++ b/tests/errors/semantic/blocking/unknown_method.py
@@ -0,0 +1,36 @@
+# pylint: disable=missing-class-docstring,  missing-function-docstring, missing-module-docstring
+#$ header class Point(public)
+#$ header method __init__(Point, double, double)
+#$ header method __del__(Point)
+#$ header method translate(Point, double, double)
+
+class Point(object):
+    def __init__(self, x, y):
+        self.x = x
+        self.y = y
+
+    def __del__(self):
+        pass
+
+    def translate(self, a, b):
+        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.translated(1.0, 2.0)
+
+    print(p.x, p.y)
+    print(a)
+
+    del p
diff --git a/tests/errors/semantic/non_blocking/class_inheritance.py b/tests/errors/semantic/non_blocking/class_inheritance.py
new file mode 100644
index 0000000000..40f2b19cd7
--- /dev/null
+++ b/tests/errors/semantic/non_blocking/class_inheritance.py
@@ -0,0 +1,7 @@
+# pylint: disable=missing-function-docstring, missing-module-docstring, missing-class-docstring
+#$ header class Point(public)
+#$ header method __init__(Point)
+
+class Point(NonExistantSuperClass): # pylint: disable=undefined-variable
+    def __init__(self):
+        pass
diff --git a/tests/errors/test_errors.py b/tests/errors/test_errors.py
index 0a7e912564..f6d3e5f25f 100644
--- a/tests/errors/test_errors.py
+++ b/tests/errors/test_errors.py
@@ -12,6 +12,7 @@
 
 from pyccel.parser.parser   import Parser
 from pyccel.codegen.codegen import Codegen
+from pyccel.codegen.pipeline import execute_pyccel
 from pyccel.errors.errors   import Errors, PyccelSyntaxError, PyccelSemanticError, PyccelCodegenError, PyccelError
 
 
@@ -110,18 +111,8 @@ def test_neat_errors_for_known_bugs(f):
     errors = Errors()
     errors.reset()
 
-    pyccel = Parser(f)
     with pytest.raises(PyccelError):
-        ast = pyccel.parse()
-
-        settings = {}
-        ast = pyccel.annotate(**settings)
-
-        name = os.path.basename(f)
-        name = os.path.splitext(name)[0]
-
-        codegen = Codegen(ast, name)
-        codegen.doprint()
+        execute_pyccel(f)
 
     assert(errors.has_errors())
 
diff --git a/tests/pyccel/scripts/classes/classes_5.py b/tests/pyccel/scripts/classes/classes_5.py
index afe664d198..343d31756c 100644
--- a/tests/pyccel/scripts/classes/classes_5.py
+++ b/tests/pyccel/scripts/classes/classes_5.py
@@ -20,7 +20,7 @@ def get_A():
 
 if __name__ == '__main__':
     b = get_A().x
-    c = get_A().f()
+    c = get_A().f()+3
 
     print(b)
     print(c)
diff --git a/tests/errors/known_bugs/generic_methods.py b/tests/pyccel/scripts/classes/generic_methods.py
similarity index 66%
rename from tests/errors/known_bugs/generic_methods.py
rename to tests/pyccel/scripts/classes/generic_methods.py
index 2bb77534b3..c3840406d4 100644
--- a/tests/errors/known_bugs/generic_methods.py
+++ b/tests/pyccel/scripts/classes/generic_methods.py
@@ -16,20 +16,21 @@ def translate(self, a, b):
         self.x = self.x + a
         self.y = self.y + b
 
-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
+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.y = a + 5
+    p.y = p.x + 5
 
-p.translate(1.0, 2.0)
+    p.translate(1.0, 2.0)
 
-print(p.x, p.y)
-print(a)
+    print(p.x, p.y)
+    print(a)
 
-del p
+    del p
diff --git a/tests/pyccel/scripts/concatenation.py b/tests/pyccel/scripts/concatenation.py
new file mode 100644
index 0000000000..9e9e60c30c
--- /dev/null
+++ b/tests/pyccel/scripts/concatenation.py
@@ -0,0 +1,13 @@
+# pylint: disable=missing-function-docstring, missing-module-docstring
+
+if __name__ == '__main__':
+    x = [1, 2, 3]
+    l = x + [4, 5, 6]
+    s = [1, 2, 3] + [1, 2, 3]
+
+    st = "Hello" + "World"
+
+    print(x)
+    print(l)
+    print(s)
+    print(st)
diff --git a/tests/pyccel/test_pyccel.py b/tests/pyccel/test_pyccel.py
index df2032daa9..73c2ce7930 100644
--- a/tests/pyccel/test_pyccel.py
+++ b/tests/pyccel/test_pyccel.py
@@ -772,6 +772,7 @@ def test_basic_header():
 @pytest.mark.parametrize( "test_file", ["scripts/classes/classes.py",
                                         "scripts/classes/classes_1.py",
                                         "scripts/classes/classes_5.py",
+                                        "scripts/classes/generic_methods.py",
                                         ] )
 @pytest.mark.parametrize( 'language', (
         pytest.param("python", marks = pytest.mark.python),
@@ -1014,3 +1015,8 @@ def test_reserved_file_name():
         libname = str(random.choice(tuple(python_builtin_libs))) + ".py" # nosec B311
         execute_pyccel(fname=libname)
     assert str(exc_info.value) == f"File called {libname} has the same name as a Python built-in package and can't be imported from Python. See #1402"
+
+def test_concatentation():
+    pyccel_test("scripts/concatenation.py",
+                language = 'fortran',
+                output_dtype=[int]*15+[str])