Repository: pyccel/pyccel (language: python)

Issue:
[INTERNALS] Change default type handling behaviour
### Relevant Discussion

#1487

### Describe the feature
Currently the default behaviour of type specification is based on headers. In order to improve type specification without immediately breaking backwards compatibility it would be good to change the default behaviour to type annotations.

### Proposed Solution
The header should be parsed in the syntactic stage and used to fill the type annotations. The `@types` decorator should also be used to fill the type annotations. This will make annotations the default and facilitate future developments.
Use template as partial type
As a USER, I would like to reuse a template to specify both scalar and vector quantities, e.g.
```python
@template("T", types=["int", "float", "complex"])
def func(a: "T", b: "T[:]"):
    b += a
```
This has many applications in the user code. See [this comment](https://github.com/pyccel/pyccel/pull/1278#issuecomment-1453650214) in issue #1278.



Discussion / hints:
Related to #387 


Fix patch:
diff --git a/.dict_custom.txt b/.dict_custom.txt
index df6ff79bf7..78947e2fda 100644
--- a/.dict_custom.txt
+++ b/.dict_custom.txt
@@ -97,4 +97,5 @@ Hadjout
 Ratnani
 unhandled
 unallocated
+mypy
 destructor
diff --git a/.pylintrc b/.pylintrc
index a4900f9ead..637e2c0466 100644
--- a/.pylintrc
+++ b/.pylintrc
@@ -48,7 +48,7 @@ unsafe-load-any-extension=no
 
 # Minimum Python version to use for version dependent checks. Will default to
 # the version used to run pylint.
-py-version=3.7 # Set to last python which has not reached end-of-life
+py-version=3.8 # Set to last python which has not reached end-of-life
 
 [MESSAGES CONTROL]
 
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0bcb09c2d0..3aea96216d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,8 +11,12 @@ All notable changes to this project will be documented in this file.
 
 ### Added
 
+-   #633 & #1518 : Allow non-trivial types to be specified with mypy-compatible annotations.
+-   #1336 : Use template as a partial type.
+
 ### Fixed
 
+-   #387 : Raise a clear error when an unrecognised type is used in a type annotation.
 -   #1556 : Fixed print format string for Intel compatibility.
 -   #1557 : Fix return a new instance of a class.
 -   #1557 : Fix save multiple class instances to the same variable.
@@ -48,10 +52,15 @@ All notable changes to this project will be documented in this file.
 
 -   #1484 : Improve handling of `DottedName` in `_assign_lhs_variable`.
 -   \[INTERNALS\] Move handling of variable headers to semantic stage.
+-   \[INTERNALS\] Moved handling of type annotations to the semantic stage.
+-   \[INTERNALS\] Remove unnecessary body argument from `FunctionAddress`.
 
 ### Deprecated
 
 -   #1513 : Stop printing `@types` decorators in generated Python code.
+-   Remove support for undocumented type syntax specifying precision (e.g. `int*8`).
+-   No longer possible to combine header annotations and argument type annotations.
+-   Remove support for specifying header annotations in a separate file.
 -   \[INTERNALS\] Remove `dtype_registry` in favour of `dtype_and_precision_registry`.
 -   \[INTERNALS\] Prefer `DataType` keys over string keys which describe data types.
 
diff --git a/developer_docs/type_inference.md b/developer_docs/type_inference.md
index aa95550349..47ea511ce1 100644
--- a/developer_docs/type_inference.md
+++ b/developer_docs/type_inference.md
@@ -1,7 +1,5 @@
 # Type inference
 
-:warning: Some of this documentation describes developments that are being added to Pyccel at the moment. For information about the previous state see the description in issue #1487 or contact Emily on the discord.
-
 Type inference, in other words deducing the type of an object is one of the keystones of Pyccel. It is what allows us to correctly declare our variables in the generated code and thereby obtain the expected results. The majority of the type inference is found in the semantic stage, however there are a few objects whose type cannot be deduced. The type of these objects is inferred from type annotations. As such there are a few steps of the type inference which occur in the syntactic stage.
 
 ## Syntactic Stage
diff --git a/docs/quickstart.md b/docs/quickstart.md
index d6472f2046..dcff0794fb 100644
--- a/docs/quickstart.md
+++ b/docs/quickstart.md
@@ -111,6 +111,14 @@ For scalar variables and arrays Pyccel supports the following data types:
 -   NumPy real types: `float32`, `float64`, `double`
 -   NumPy complex types: `complex64`, `complex128`
 
+Scalar variables can also be specified without using string type annotations:
+```python
+def fun(arg1: type1, arg2: type2, ..., argN: typeN) -> return_type:
+```
+Such type annotations are compatible with mypy and as such are recommended for use where this is possible. NumPy does not yet provide mypy compatible type annotations which provide sufficient information for Pyccel so string annotations are still necessary for arrays.
+
+:warning: When running Pyccel in interactive mode with `epyccel` anything imported before the function being translated is not copied into the file which will be translated. Using non-string types here is therefore likely to generate errors as the necessary imports for these objects will be missing.
+
 ## How to use Pyccel
 
 ### Installation
diff --git a/pyccel/ast/basic.py b/pyccel/ast/basic.py
index 4d2fc1a22e..f43cba187b 100644
--- a/pyccel/ast/basic.py
+++ b/pyccel/ast/basic.py
@@ -450,39 +450,131 @@ def update_pyccel_staging(self):
         self._pyccel_staging = pyccel_stage.current_stage
 
 class TypedAstNode(PyccelAstNode):
-    """Class from which all nodes containing objects inherit
+    """
+    Class from which all typed objects inherit.
+
+    The class from which all objects which can be described with type information
+    must inherit. Objects with type information are objects which take up memory
+    in a running program (e.g. a variable or the result of a function call).
+    Each typed object is described by an underlying datatype, a precision, a rank,
+    a shape, and a data layout ordering.
     """
     __slots__  = ()
 
     @property
     def shape(self):
-        """ Tuple containing the length of each dimension
-        of the object """
+        """
+        Tuple containing the length of each dimension of the object or None.
+
+        A tuple containing the length of each dimension of the object if the object
+        is an array (with rank>0). Otherwise None.
+        """
         return self._shape # pylint: disable=no-member
 
     @property
     def rank(self):
-        """ Number of dimensions of the object
+        """
+        Number of dimensions of the object.
+
+        Number of dimensions of the object. If the object is a scalar then
+        this is equal to 0.
         """
         return self._rank # pylint: disable=no-member
 
     @property
     def dtype(self):
-        """ Datatype of the object """
+        """
+        Datatype of the object.
+
+        The underlying datatype of the object. In the case of scalars this is
+        equivalent to the type of the object in Python. For objects in (homogeneous)
+        containers (e.g. list/ndarray/tuple), this is the type of an arbitrary element
+        of the container.
+        """
         return self._dtype # pylint: disable=no-member
 
     @property
     def precision(self):
-        """ Precision of the datatype of the object """
+        """
+        Precision of the datatype of the object.
+
+        The precision of the datatype of the object. This number is related to the
+        number of bytes that the datatype takes up in memory (e.g. `float64` has
+        precision = 8 as it takes up 8 bytes, `complex128` has precision = 8 as
+        it is comprised of two `float64` objects. The precision is equivalent to
+        the `kind` parameter in Fortran.
+        """
         return self._precision # pylint: disable=no-member
 
     @property
     def order(self):
-        """ Indicates whether the data is stored in
-        row-major ('C') or column-major ('F') format.
-        This is only relevant if rank > 1 """
+        """
+        The data layout ordering in memory.
+
+        Indicates whether the data is stored in row-major ('C') or column-major
+        ('F') format. This is only relevant if rank > 1. When it is not relevant
+        this function returns None.
+        """
         return self._order # pylint: disable=no-member
 
+    @classmethod
+    def static_rank(cls):
+        """
+        Number of dimensions of the object.
+
+        Number of dimensions of the object. If the object is a scalar then
+        this is equal to 0.
+
+        This function is static and will return an AttributeError if the
+        class does not have a predetermined rank.
+        """
+        return cls._rank # pylint: disable=no-member
+
+    @classmethod
+    def static_dtype(cls):
+        """
+        Datatype of the object.
+
+        The underlying datatype of the object. In the case of scalars this is
+        equivalent to the type of the object in Python. For objects in (homogeneous)
+        containers (e.g. list/ndarray/tuple), this is the type of an arbitrary element
+        of the container.
+
+        This function is static and will return an AttributeError if the
+        class does not have a predetermined datatype.
+        """
+        return cls._dtype # pylint: disable=no-member
+
+    @classmethod
+    def static_precision(cls):
+        """
+        Precision of the datatype of the object.
+
+        The precision of the datatype of the object. This number is related to the
+        number of bytes that the datatype takes up in memory (e.g. `float64` has
+        precision = 8 as it takes up 8 bytes, `complex128` has precision = 8 as
+        it is comprised of two `float64` objects. The precision is equivalent to
+        the `kind` parameter in Fortran.
+
+        This function is static and will return an AttributeError if the
+        class does not have a predetermined precision.
+        """
+        return cls._precision # pylint: disable=no-member
+
+    @classmethod
+    def static_order(cls):
+        """
+        The data layout ordering in memory.
+
+        Indicates whether the data is stored in row-major ('C') or column-major
+        ('F') format. This is only relevant if rank > 1. When it is not relevant
+        this function returns None.
+
+        This function is static and will return an AttributeError if the
+        class does not have a predetermined order.
+        """
+        return cls._order # pylint: disable=no-member
+
     def copy_attributes(self, x):
         """
         Copy the attributes describing a TypedAstNode into this node.
diff --git a/pyccel/ast/builtins.py b/pyccel/ast/builtins.py
index debfb55dcb..3c244477ef 100644
--- a/pyccel/ast/builtins.py
+++ b/pyccel/ast/builtins.py
@@ -20,7 +20,7 @@
                         NativeGeneric)
 from .internals import PyccelInternalFunction, max_precision, Slice
 from .literals  import LiteralInteger, LiteralFloat, LiteralComplex, Nil
-from .literals  import Literal, LiteralImaginaryUnit, get_default_literal_value
+from .literals  import Literal, LiteralImaginaryUnit, convert_to_literal
 from .literals  import LiteralString
 from .operators import PyccelAdd, PyccelAnd, PyccelMul, PyccelIsNot
 from .operators import PyccelMinus, PyccelUnarySub, PyccelNot
@@ -106,20 +106,25 @@ def __str__(self):
 
 #==============================================================================
 class PythonImag(PythonComplexProperty):
-    """Represents a call to the .imag property
+    """
+    Represents a call to the .imag property.
 
+    Represents a call to the .imag property of an object with a complex type.
     e.g:
-    > a = 1+2j
-    > a.imag
+    >>> a = 1+2j
+    >>> a.imag
     1.0
 
+    Parameters
+    ----------
     arg : Variable, Literal
+        The object on which the property is called.
     """
     __slots__ = ()
     name = 'imag'
     def __new__(cls, arg):
         if arg.dtype is not NativeComplex():
-            return get_default_literal_value(arg.dtype)
+            return convert_to_literal(0, dtype = arg.dtype)
         else:
             return super().__new__(cls)
 
@@ -960,7 +965,8 @@ def print_string(self):
     'bool'   : PythonBool,
     'float'  : PythonFloat,
     'int'    : PythonInt,
-    'complex': PythonComplex
+    'complex': PythonComplex,
+    'str'    : LiteralString
 }
 
 def python_builtin_datatype(name):
diff --git a/pyccel/ast/core.py b/pyccel/ast/core.py
index 7cf0b0bcdd..44bde0f222 100644
--- a/pyccel/ast/core.py
+++ b/pyccel/ast/core.py
@@ -22,6 +22,7 @@
                         NativeBool, NativeRange,
                         NativeTuple, str_dtype)
 from .internals import Slice, PyccelSymbol, PyccelInternalFunction, get_final_precision
+from .internals import AnnotatedPyccelSymbol
 
 from .literals  import LiteralInteger, Nil, LiteralFalse
 from .literals  import NilArgument, LiteralTrue
@@ -350,8 +351,11 @@ def args(self):
 
 
 class Assign(PyccelAstNode):
+    """
+    Represents variable assignment for code generation.
 
-    """Represents variable assignment for code generation.
+    Class representing an assignment node, where the result of an expression
+    (rhs: right hand side) is saved into a variable (lhs: left hand side).
 
     Parameters
     ----------
@@ -362,20 +366,23 @@ class Assign(PyccelAstNode):
            include PyccelSymbol, and IndexedElement. Types that
            subclass these types are also supported.
         In the semantic stage:
-           Variable or IndexedElement
+           Variable or IndexedElement.
 
     rhs : TypedAstNode
         In the syntactic stage:
-          Object representing the rhs of the expression
+          Object representing the rhs of the expression.
         In the semantic stage :
-          TypedAstNode with the same shape as the lhs
+          TypedAstNode with the same shape as the lhs.
 
-    status: None, str
-        if lhs is not allocatable, then status is None.
-        otherwise, status is {'allocated', 'unallocated'}
+    status : str, optional
+        If lhs is not allocatable, then status is None.
+        otherwise, status is {'allocated', 'unallocated'}.
 
-    like: None, Variable
-        contains the name of the variable from which the lhs will be cloned.
+    like : Variable, optional
+        Contains the name of the variable from which the lhs will be cloned.
+
+    fst : ast.Ast
+        The ast object parsed by Python's ast module.
 
     Examples
     --------
@@ -1769,22 +1776,29 @@ def ranges(self):
 
 class FunctionCallArgument(PyccelAstNode):
     """
-    An argument passed in a function call
+    An argument passed in a function call.
+
+    Class describing an argument passed to a function in a
+    function call.
 
     Parameters
-    ---------
-    value   : TypedAstNode
-              The expression passed as an argument
-    keyword : str
-              If the argument is passed by keyword then this
-              is that keyword
+    ----------
+    value : TypedAstNode
+        The expression passed as an argument.
+    keyword : str, optional
+        If the argument is passed by keyword then this
+        is that keyword.
+    fst : ast.Ast
+        The ast object parsed by Python's ast module.
     """
     __slots__ = ('_value', '_keyword')
     _attribute_nodes = ('_value',)
-    def __init__(self, value, keyword = None):
+    def __init__(self, value, keyword = None, *, fst = None):
         self._value = value
         self._keyword = keyword
         super().__init__()
+        if fst:
+            self.set_fst(fst)
 
     @property
     def value(self):
@@ -1859,6 +1873,9 @@ def __init__(self, name, *, value = None, kwonly=False, annotation=None):
         elif isinstance(name, PyccelSymbol):
             self._var  = name
             self._name = name
+        elif isinstance(name, AnnotatedPyccelSymbol):
+            self._var  = name
+            self._name = name.name
         else:
             raise TypeError("Name must be a PyccelSymbol, Variable or FunctionAddress")
         self._value      = value
@@ -1895,7 +1912,10 @@ def is_kwonly(self):
 
     @property
     def annotation(self):
-        """ The argument annotation providing dtype information
+        """
+        The argument annotation providing dtype information.
+
+        The argument annotation providing dtype information.
         """
         return self._annotation
 
@@ -1990,10 +2010,10 @@ def __init__(self, var, *, annotation=None):
         self._annotation = annotation
 
         if pyccel_stage == 'syntactic':
-            if not isinstance(var, PyccelSymbol):
-                raise TypeError("Var must be a PyccelSymbol")
+            if not isinstance(var, (PyccelSymbol, AnnotatedPyccelSymbol)):
+                raise TypeError(f"Var must be a PyccelSymbol or an AnnotatedPyccelSymbol, not a {type(var)}")
         elif not isinstance(var, Variable):
-            raise TypeError("Var must be a Variable")
+            raise TypeError(f"Var must be a Variable not a {type(var)}")
         else:
             self._is_argument = var.is_argument
 
@@ -2117,7 +2137,7 @@ def __init__(self, func, args, current_function=None):
 
         # Handle function as argument
         arg_vals = [None if a is None else a.value for a in args]
-        args = [FunctionCallArgument(FunctionAddress(av.name, av.arguments, av.results, []), keyword=a.keyword)
+        args = [FunctionCallArgument(FunctionAddress(av.name, av.arguments, av.results), keyword=a.keyword)
                 if isinstance(av, FunctionDef) else a for a, av in zip(args, arg_vals)]
 
         if current_function == func.name:
@@ -3073,8 +3093,10 @@ def point(self, args, use_final_precision = False):
                         severity='fatal')
 
 class FunctionAddress(FunctionDef):
+    """
+    Represents a function address.
 
-    """Represents a function address.
+    A function definition can have a FunctionAddress as an argument.
 
     Parameters
     ----------
@@ -3087,31 +3109,35 @@ class FunctionAddress(FunctionDef):
     results : iterable
         The direct outputs of the function address.
 
-    is_argument: bool
-        if object is the argument of a function [Default value: False]
+    is_optional : bool
+        If object is an optional argument of a function [Default value: False].
+
+    is_kwonly : bool
+        If object is an argument which can only be specified using its keyword.
 
-    is_kwonly: bool
-        if object is an argument which can only be specified using its keyword
+    is_argument : bool
+        If object is the argument of a function [Default value: False].
 
-    is_optional: bool
-        if object is an optional argument of a function [Default value: False]
+    memory_handling : str
+        Must be 'heap', 'stack' or 'alias' [Default value: 'stack'].
 
-    memory_handling: str
-        must be 'heap', 'stack' or 'alias' [Default value: 'stack']
+    **kwargs : dict
+        Any keyword arguments which should be passed to the super class FunctionDef.
+
+    See Also
+    --------
+    FunctionDef
+        The super class from which this object derives.
 
     Examples
     --------
     >>> from pyccel.ast.core import Variable, FunctionAddress, FuncAddressDeclare, FunctionDef
     >>> x = Variable('float', 'x')
     >>> y = Variable('float', 'y')
-
-    a function definition can have a FunctionAddress as an argument
-
-    >>> FunctionDef('g', [FunctionAddress('f', [x], [y], [])], [], [])
-
-    we can also Declare a FunctionAddress
-
-    >>> FuncAddressDeclare(FunctionAddress('f', [x], [y], []))
+    >>> # a function definition can have a FunctionAddress as an argument
+    >>> FunctionDef('g', [FunctionAddress('f', [x], [y])], [], [])
+    >>> # we can also Declare a FunctionAddress
+    >>> FuncAddressDeclare(FunctionAddress('f', [x], [y]))
     """
     __slots__ = ('_is_optional','_is_kwonly','_is_argument', '_memory_handling')
 
@@ -3120,14 +3146,13 @@ def __init__(
         name,
         arguments,
         results,
-        body,
         is_optional=False,
         is_kwonly=False,
         is_argument=False,
         memory_handling='stack',
         **kwargs
         ):
-        super().__init__(name, arguments, results, body, scope=1,**kwargs)
+        super().__init__(name, arguments, results, body=[], scope=None, **kwargs)
         if not isinstance(is_argument, bool):
             raise TypeError('Expecting a boolean for is_argument')
 
@@ -3173,6 +3198,28 @@ def is_kwonly(self):
     def is_optional(self):
         return self._is_optional
 
+    def __getnewargs__(self):
+        """
+        Function called during unpickling.
+
+        For more details see : https://docs.python.org/3/library/pickle.html#object.__getnewargs__.
+
+        Returns
+        -------
+        args
+            A tuple containing any arguments to be passed to the constructor.
+        kwargs
+            A dict containing any keyword arguments to be passed to the constructor.
+        """
+        args, kwargs = super().__getnewargs__()
+        args = args[:-1] # Remove body argument
+        kwargs.pop('scope')
+        kwargs['is_argument'] = self.is_argument
+        kwargs['is_kwonly'] = self.is_kwonly
+        kwargs['is_optional'] = self.is_optional
+        kwargs['memory_handling'] = self.memory_handling
+        return args, kwargs
+
 class SympyFunction(FunctionDef):
 
     """Represents a function definition."""
@@ -3691,18 +3738,20 @@ def source_module(self):
 # TODO: Should Declare have an optional init value for each var?
 
 class FuncAddressDeclare(PyccelAstNode):
+    """
+    Represents a FunctionAddress declaration in the code.
 
-    """Represents a FunctionAddress declaration in the code.
+    Represents a FunctionAddress declaration in the code.
 
     Parameters
     ----------
-    variable:
+    variable : FunctionAddress
         An instance of FunctionAddress.
-    intent: None, str
-        one among {'in', 'out', 'inout'}
-    value: TypedAstNode
-        variable value
-    static: bool
+    intent : str, optional
+        One among {'in', 'out', 'inout'}.
+    value : TypedAstNode
+        Variable value.
+    static : bool
         True for a static declaration of an array.
 
     Examples
@@ -3710,7 +3759,7 @@ class FuncAddressDeclare(PyccelAstNode):
     >>> from pyccel.ast.core import Variable, FunctionAddress, FuncAddressDeclare
     >>> x = Variable('float', 'x')
     >>> y = Variable('float', 'y')
-    >>> FuncAddressDeclare(FunctionAddress('f', [x], [y], []))
+    >>> FuncAddressDeclare(FunctionAddress('f', [x], [y]))
     """
     __slots__ = ('_variable','_intent','_value','_static')
     _attribute_nodes = ('_variable', '_value')
diff --git a/pyccel/ast/headers.py b/pyccel/ast/headers.py
index c5a9a9ebcb..2451b84b60 100644
--- a/pyccel/ast/headers.py
+++ b/pyccel/ast/headers.py
@@ -14,6 +14,7 @@
 from .datatypes         import datatype, DataTypeFactory, UnionType, default_precision
 from .internals         import PyccelSymbol, Slice
 from .macros            import Macro, MacroShape, construct_macro
+from .type_annotations  import SyntacticTypeAnnotation
 from .variable          import DottedName, DottedVariable
 from .variable          import Variable
 
@@ -81,15 +82,18 @@ def __reduce_ex__(self, i):
 
 #==============================================================================
 class Template(Header):
-    """Represents a template.
+    """
+    Represents a template.
+
+    Represents a call to the template decorator.
 
     Parameters
     ----------
-    name: str
+    name : str
         The name of the template.
 
-    dtypes: iterable
-        The types the template represents
+    dtypes : iterable
+        The types the template represents.
 
     Examples
     --------
@@ -117,6 +121,9 @@ def dtypes(self):
         "Types the template represents."
         return self._dtypes
 
+    def __iter__(self):
+        return self._dtypes.__iter__()
+
     def __reduce_ex__(self, i):
 
         """ Used by pickle to create an object of this class.
@@ -174,7 +181,6 @@ class FunctionHeader(Header):
     """
     __slots__ = ('_name','_dtypes','_results','_is_static')
 
-    # TODO dtypes should be a dictionary (useful in syntax)
     def __init__(self, name, dtypes,
                  results=None,
                  is_static=False):
@@ -211,180 +217,6 @@ def results(self):
     def is_static(self):
         return self._is_static
 
-    def create_definition(self, templates = (), is_external=False):
-        """
-        Create a FunctionDef with the described types.
-
-        Create a FunctionDef with arguments and results whose types
-        are indicated by the information stored in the header. The
-        body of the function is left empty.
-
-        Parameters
-        ----------
-        templates : list/tuple, default: ()
-            A list of all templates defined in the context which can be used
-            to define argument or result types.
-
-        is_external : bool, default: False
-            Indicates whether the function is an external function which
-            is defined elsewhere in the linked files.
-
-        Returns
-        -------
-        list of FunctionDef
-            A list of all functions described by the header.
-        """
-        # TODO factorize what can be factorized
-        from itertools import product
-
-        name = self.name
-
-        body      = []
-        cls_name  = None
-        is_static = self.is_static
-        used_names = set(name)
-        imports   = []
-        funcs = []
-        dtypes = []
-
-        def build_argument(var_name, dc):
-            """
-            Construct an argument variable from a dictionary describing its properties.
-
-            Use a dictionary describing the properties of a variable which is either
-            an argument (in/inout) or a result (out) to create a variable and an
-            annotation string for the variable.
-
-            Parameters
-            ----------
-            var_name : srt
-                The name of the variable.
-
-            dc : dict
-                The properties of the variable.
-
-            Returns
-            -------
-            Variable
-                The newly created variable.
-
-            str
-                The annotation string.
-            """
-            dtype    = dc['datatype']
-            memory_handling = dc['memory_handling']
-            precision = dc['precision']
-            rank = dc['rank']
-            is_const = dc['is_const']
-
-            order = None
-            shape = None
-            annotation = None
-
-            if isinstance(dtype, str):
-                annotation = dtype
-                try:
-                    dtype = datatype(dtype)
-                except ValueError:
-                    dtype = DataTypeFactory(str(dtype), ("_name"))()
-
-            if rank and precision == -1:
-                precision = default_precision[dtype]
-
-            if rank >1:
-                order = dc['order']
-
-            var = Variable(dtype, var_name,
-                           memory_handling=memory_handling, is_const=is_const,
-                           rank=rank, shape=shape ,order=order, precision=precision,
-                           is_temp=True)
-
-            return var, annotation
-
-        def process_template(signature, Tname, d_type):
-            #Replaces templates named Tname inside signature, with the given type.
-            new_sig = tuple(d_type if 'datatype' in t and t['datatype'] == Tname\
-                            else t for t in signature)
-            return new_sig
-
-        def find_templates(signature, templates):
-            #Creates a dictionary of only used templates in signature.
-            new_templates = {d_type['datatype']:templates[d_type['datatype']]\
-                             for d_type in signature\
-                             if 'datatype' in d_type and d_type['datatype'] in templates}
-            return new_templates
-
-        for i in self.dtypes:
-            if isinstance(i, UnionType):
-                for d_type in i.args:
-                    if d_type['datatype'] in templates:
-                        errors.report(TEMPLATE_IN_UNIONTYPE,
-                                      symbol=self.name,
-                                      severity='error')
-                dtypes += [i.args]
-            elif isinstance(i, dict):
-                dtypes += [[i]]
-            else:
-                raise TypeError('element must be of type UnionType or dict')
-
-        #TODO: handle the case of functions arguments
-
-        signatures = list(product(*dtypes))
-        new_templates = find_templates(signatures[0], templates)
-
-        for tmplt in new_templates:
-            signatures = tuple(process_template(s, tmplt, d_type)\
-                               for s in signatures for d_type in new_templates[tmplt].dtypes)
-
-        for args_ in signatures:
-            args = []
-            for i, d in enumerate(args_):
-                # TODO  handle function as argument, which itself has a function argument
-                if (d['is_func']):
-                    decs = []
-                    results = []
-                    _count = 0
-                    for dc in d['decs']:
-                        _name, _count = create_incremented_string(used_names, 'in', _count)
-                        var, annotation = build_argument(_name, dc)
-                        decs.append(FunctionDefArgument(var, annotation=annotation))
-                    _count = 0
-                    for dc in d['results']:
-                        _name, _count = create_incremented_string(used_names, 'out', _count)
-                        var, annotation = build_argument(_name, dc)
-                        results.append(FunctionDefResult(var, annotation=annotation))
-                    arg_name = 'arg_{0}'.format(str(i))
-                    arg = FunctionDefArgument(FunctionAddress(arg_name, decs, results, []))
-
-                else:
-                    arg_name = 'arg_{0}'.format(str(i))
-                    var, annotation = build_argument(arg_name, d)
-                    arg = FunctionDefArgument(var, annotation=annotation)
-                args.append(arg)
-
-            # ... factorize the following 2 blocks
-            results = []
-            for i,d_var in enumerate(self.results):
-                is_func = d_var.pop('is_func')
-                dtype = d_var.pop('datatype')
-                var = Variable(dtype, 'res_{}'.format(i), **d_var, is_temp = True)
-                results.append(FunctionDefResult(var, annotation = str(dtype)))
-                # we put back dtype otherwise macro will crash when it tries to
-                # call create_definition
-                d_var['datatype'] = dtype
-                d_var['is_func'] = is_func
-
-            func= FunctionDef(name, args, results, body,
-                              global_vars=[],
-                              cls_name=cls_name,
-                              is_static=is_static,
-                              imports=imports,
-                              is_header=True,
-                              is_external=is_external)
-            funcs += [func]
-
-        return funcs
-
     def to_static(self):
         """returns a static function header. needed for bind(c)"""
         return FunctionHeader(self.name,
@@ -431,28 +263,33 @@ def __reduce_ex__(self, i):
 
 
 #==============================================================================
-# TODO to be improved => use FunctionHeader
 class MethodHeader(FunctionHeader):
-    """Represents method header in the code.
+    """
+    Represents a method header in the code.
+
+    Represents a method header in the code.
+    To be removed when header support is deprecated.
 
-    name: iterable
-        method name as a list/tuple
+    Parameters
+    ----------
+    name : iterable
+        Method name as a list/tuple.
 
-    dtypes: tuple/list
-        a list of datatypes. an element of this list can be str/DataType of a
-        tuple (str/DataType, attr)
+    dtypes : tuple/list
+        A list of datatypes. an element of this list can be str/DataType of a
+        tuple (str/DataType, attr).
 
-    results: tuple/list
-        a list of datatypes. an element of this list can be str/DataType of a
-        tuple (str/DataType, attr)
+    results : tuple/list
+        A list of datatypes. an element of this list can be str/DataType of a
+        tuple (str/DataType, attr).
 
-    is_static: bool
+    is_static : bool
         True if we want to pass arrays in bind(c) mode. every argument of type
         array will be preceeded by its shape, the later will appear in the
-        argument declaration. default value: False
+        argument declaration. default value: False.
 
     Examples
-
+    --------
     >>> from pyccel.ast.headers import MethodHeader
     >>> m = MethodHeader(('point', 'rotate'), ['double'])
     >>> m
@@ -463,19 +300,18 @@ class MethodHeader(FunctionHeader):
     __slots__ = ()
 
     def __init__(self, name, dtypes, results=None, is_static=False):
-        if not isinstance(name, (list, tuple)):
-            raise TypeError("Expecting a list/tuple of strings.")
-        name      = '.'.join(str(n) for n in name)
+        if not isinstance(name, str):
+            raise TypeError("Expecting a string.")
 
         if not(iterable(dtypes)):
             raise TypeError("Expecting dtypes to be iterable.")
 
         for d in dtypes:
-            if not isinstance(d, UnionType) and not isinstance(d, dict):
+            if not isinstance(d, (UnionType, SyntacticTypeAnnotation)):
                 raise TypeError("Wrong element in dtypes.")
 
         for d in results:
-            if not isinstance(d, UnionType):
+            if not isinstance(d, (UnionType, SyntacticTypeAnnotation)):
                 raise TypeError("Wrong element in dtypes.")
 
 
diff --git a/pyccel/ast/internals.py b/pyccel/ast/internals.py
index 10af322a85..d7a9ad9817 100644
--- a/pyccel/ast/internals.py
+++ b/pyccel/ast/internals.py
@@ -349,7 +349,12 @@ class AnnotatedPyccelSymbol(PyccelAstNode):
     _attribute_nodes = ()
 
     def __init__(self, name, annotation, is_temp = False):
-        self._name = PyccelSymbol(name, is_temp)
+        if isinstance(name, PyccelSymbol):
+            self._name = name
+        elif isinstance(name, str):
+            self._name = PyccelSymbol(name, is_temp)
+        else:
+            raise TypeError(f"Name should be a string or a PyccelSymbol not a {type(name)}")
         self._annotation = annotation
         super().__init__()
 
diff --git a/pyccel/ast/literals.py b/pyccel/ast/literals.py
index e874fa59a2..a3e556595d 100644
--- a/pyccel/ast/literals.py
+++ b/pyccel/ast/literals.py
@@ -11,45 +11,44 @@
                                   NativeComplex, NativeString)
 
 __all__ = (
+    'convert_to_literal',
     'Literal',
-    'LiteralTrue',
+    'LiteralComplex',
     'LiteralFalse',
-    'LiteralInteger',
     'LiteralFloat',
-    'LiteralComplex',
     'LiteralImaginaryUnit',
+    'LiteralInteger',
+    'LiteralNumeric',
     'LiteralString',
+    'LiteralTrue',
     'Nil',
     'NilArgument',
-    'get_default_literal_value'
 )
 
 #------------------------------------------------------------------------------
 class Literal(TypedAstNode):
     """
-    Represents a python literal
+    Class representing a literal value.
+
+    Class representing a literal value. A literal is a value that is expressed
+    as itself rather than as a variable or an expression, e.g. the number 3
+    or the string "Hello".
+
     This class is abstract and should be implemented for each dtype
     """
-    __slots__ = ('_precision',)
+    __slots__ = ()
     _attribute_nodes  = ()
     _rank      = 0
     _shape     = None
     _order     = None
 
-    def __init__(self, precision):
-        if not isinstance(precision, int):
-            raise TypeError("precision must be an integer")
-        self._precision = precision
-        super().__init__()
-
-    @TypedAstNode.precision.setter
-    def precision(self, precision):
-        """ Set precision for a literal class"""
-        self._precision = precision
-
     @property
     def python_value(self):
-        """ Get python literal represented by this instance """
+        """
+        Get the Python literal represented by this instance.
+
+        Get the Python literal represented by this instance.
+        """
 
     def __repr__(self):
         return "Literal({})".format(repr(self.python_value))
@@ -67,8 +66,38 @@ def __hash__(self):
         return hash(self.python_value)
 
 #------------------------------------------------------------------------------
-class LiteralTrue(Literal):
-    """Represents the python value True"""
+class LiteralNumeric(Literal):
+    """
+    Class representing a literal numeric type.
+
+    Class representing a literal numeric type. A numeric type is
+    a type representing a number (boolean/integer/float/complex).
+
+    Parameters
+    ----------
+    precision : int
+        The precision of the data type.
+    """
+    __slots__ = ('_precision',)
+
+    def __init__(self, precision):
+        if not isinstance(precision, int):
+            raise TypeError("precision must be an integer")
+        self._precision = precision
+        super().__init__()
+
+#------------------------------------------------------------------------------
+class LiteralTrue(LiteralNumeric):
+    """
+    Class representing the Python value True.
+
+    Class representing the Python value True.
+
+    Parameters
+    ----------
+    precision : int
+        The precision of the data type.
+    """
     __slots__ = ()
     _dtype     = NativeBool()
 
@@ -77,11 +106,25 @@ def __init__(self, precision = -1):
 
     @property
     def python_value(self):
+        """
+        Get the Python literal represented by this instance.
+
+        Get the Python literal represented by this instance.
+        """
         return True
 
 #------------------------------------------------------------------------------
-class LiteralFalse(Literal):
-    """Represents the python value False"""
+class LiteralFalse(LiteralNumeric):
+    """
+    Class representing the Python value False.
+
+    Class representing the Python value False.
+
+    Parameters
+    ----------
+    precision : int
+        The precision of the data type.
+    """
     __slots__ = ()
     _dtype     = NativeBool()
 
@@ -90,11 +133,28 @@ def __init__(self, precision = -1):
 
     @property
     def python_value(self):
+        """
+        Get the Python literal represented by this instance.
+
+        Get the Python literal represented by this instance.
+        """
         return False
 
 #------------------------------------------------------------------------------
-class LiteralInteger(Literal):
-    """Represents an integer literal in python"""
+class LiteralInteger(LiteralNumeric):
+    """
+    Class representing an integer literal in Python.
+
+    Class representing an integer literal, such as 3, in Python.
+
+    Parameters
+    ----------
+    value : int
+        The Python literal.
+
+    precision : int
+        The precision of the integer.
+    """
     __slots__ = ('_value',)
     _dtype     = NativeInteger()
 
@@ -107,21 +167,38 @@ def __init__(self, value, precision = -1):
 
     @property
     def python_value(self):
+        """
+        Get the Python literal represented by this instance.
+
+        Get the Python literal represented by this instance.
+        """
         return self._value
 
     def __index__(self):
         return self.python_value
 
 #------------------------------------------------------------------------------
-class LiteralFloat(Literal):
-    """Represents a float literal in python"""
+class LiteralFloat(LiteralNumeric):
+    """
+    Class representing a float literal in Python.
+
+    Class representing a float literal, such as 3.5, in Python.
+
+    Parameters
+    ----------
+    value : float
+        The Python literal.
+
+    precision : int
+        The precision of the float.
+    """
     __slots__ = ('_value',)
     _dtype     = NativeFloat()
 
     def __init__(self, value, *, precision = -1):
         if not isinstance(value, (int, float, LiteralFloat)):
             raise TypeError("A LiteralFloat can only be created with an integer or a float")
-        Literal.__init__(self, precision)
+        super().__init__(precision)
         if isinstance(value, LiteralFloat):
             self._value = value.python_value
         else:
@@ -129,12 +206,32 @@ def __init__(self, value, *, precision = -1):
 
     @property
     def python_value(self):
+        """
+        Get the Python literal represented by this instance.
+
+        Get the Python literal represented by this instance.
+        """
         return self._value
 
 
 #------------------------------------------------------------------------------
-class LiteralComplex(Literal):
-    """Represents a complex literal in python"""
+class LiteralComplex(LiteralNumeric):
+    """
+    Class representing a complex literal in Python.
+
+    Class representing a complex literal, such as 3+2j, in Python.
+
+    Parameters
+    ----------
+    real : float
+        The real part of the Python literal.
+
+    imag : float
+        The imaginary part of the Python literal.
+
+    precision : int
+        The precision of the complex number.
+    """
     __slots__ = ('_real_part','_imag_part')
     _dtype     = NativeComplex()
 
@@ -164,50 +261,76 @@ def _collect_python_val(arg):
 
     @property
     def real(self):
-        """ Return the real part of the complex literal """
+        """
+        Return the real part of the complex literal.
+
+        Return the real part of the complex literal.
+        """
         return self._real_part
 
     @property
     def imag(self):
-        """ Return the imaginary part of the complex literal """
+        """
+        Return the imaginary part of the complex literal.
+
+        Return the imaginary part of the complex literal.
+        """
         return self._imag_part
 
     @property
     def python_value(self):
+        """
+        Get the Python literal represented by this instance.
+
+        Get the Python literal represented by this instance.
+        """
         return self.real.python_value + self.imag.python_value*1j
 
 #------------------------------------------------------------------------------
 class LiteralImaginaryUnit(LiteralComplex):
-    """Represents the python value j"""
+    """
+    Class representing the Python value j.
+
+    Class representing the imaginary unit j in Python.
+    """
     __slots__ = ()
     def __new__(cls):
         return super().__new__(cls, 0, 1)
 
-    def __init__(self, real=0, imag=1, precision = -1):
+    def __init__(self, real = 0, imag = 1, precision = -1):
         super().__init__(0, 1)
 
     @property
     def python_value(self):
+        """
+        Get the Python literal represented by this instance.
+
+        Get the Python literal represented by this instance.
+        """
         return 1j
 
 #------------------------------------------------------------------------------
 class LiteralString(Literal):
-    """Represents a string literal in python"""
+    """
+    Class representing a string literal in Python.
+
+    Class representing a string literal, such as 'hello' in Python.
+
+    Parameters
+    ----------
+    arg : str
+        The Python literal.
+    """
     __slots__ = ('_string',)
     _dtype     = NativeString()
+    _precision = 0
 
     def __init__(self, arg):
-        self._precision = 0
-        super().__init__(self._precision)
+        super().__init__()
         if not isinstance(arg, str):
             raise TypeError('arg must be of type str')
         self._string = arg
 
-    @property
-    def arg(self):
-        """ Return the python string literal """
-        return self._string
-
     def __repr__(self):
         return "'{}'".format(str(self.python_value))
 
@@ -221,22 +344,25 @@ def __add__(self, o):
 
     @property
     def python_value(self):
-        return self.arg
+        """
+        Get the Python literal represented by this instance.
 
-#------------------------------------------------------------------------------
+        Get the Python literal represented by this instance.
+        """
+        return self._string
 
-class Nil(TypedAstNode, metaclass=Singleton):
+#------------------------------------------------------------------------------
 
+class Nil(Literal, metaclass=Singleton):
     """
-    class for None object in the code.
+    Class representing a None object in the code.
+
+    Class representing the Python value None in the code.
     """
     __slots__ = ()
     _attribute_nodes = ()
     _dtype = NativeGeneric
     _precision = 0
-    _rank = 0
-    _shape = None
-    _order = None
 
     def __str__(self):
         return 'None'
@@ -253,9 +379,13 @@ def __hash__(self):
 #------------------------------------------------------------------------------
 
 class NilArgument(PyccelAstNode):
-    """Represents the python value None when passed as an argument
+    """
+    Represents None when passed as an argument to an inline function.
+
+    Represents the Python value None when passed as an argument
     to an inline function. This class is necessary as to avoid
-    accidental substitution due to Singletons"""
+    accidental substitution due to Singletons.
+    """
     __slots__ = ()
     _attribute_nodes = ()
 
@@ -267,43 +397,28 @@ def __bool__(self):
 
 #------------------------------------------------------------------------------
 
-def get_default_literal_value(dtype):
-    """Returns the default value of a native datatype."""
-    if isinstance(dtype, NativeInteger):
-        value = LiteralInteger(0)
-    elif isinstance(dtype, NativeFloat):
-        value = LiteralFloat(0.0)
-    elif isinstance(dtype, NativeComplex):
-        value = LiteralComplex(0.0, 0.0)
-    elif isinstance(dtype, NativeBool):
-        value = LiteralFalse()
-    elif isinstance(dtype, NativeString):
-        value = LiteralString('')
-    else:
-        raise TypeError('Unknown type')
-    return value
-
-#------------------------------------------------------------------------------
-
 def convert_to_literal(value, dtype = None, precision = None):
-    """ Convert a python value to a pyccel Literal
+    """
+    Convert a Python value to a pyccel Literal.
+
+    Convert a Python value to a pyccel Literal.
 
     Parameters
     ----------
-    value     : int/float/complex/bool/str
-                The python value
-    dtype     : DataType
-                The datatype of the python value
-                Default : Matches type of 'value'
+    value : int/float/complex/bool/str
+        The Python value.
+    dtype : DataType
+        The datatype of the Python value.
+        Default : Matches type of 'value'.
     precision : int
-                The precision of the value in the generated code
-                Default : python precision (see default_precision)
+        The precision of the value in the generated code.
+        Default : Python precision (see default_precision).
 
     Returns
     -------
-    literal_val : Literal
-                  The python value 'value' expressed as a literal
-                  with the specified dtype and precision
+    Literal
+        The Python value 'value' expressed as a literal
+        with the specified dtype and precision.
     """
     from .operators import PyccelUnarySub # Imported here to avoid circular import
 
diff --git a/pyccel/ast/type_annotations.py b/pyccel/ast/type_annotations.py
index b99a5abbbd..2272a734f6 100644
--- a/pyccel/ast/type_annotations.py
+++ b/pyccel/ast/type_annotations.py
@@ -7,17 +7,28 @@
 """ Module containing all classes useful for type annotation.
 """
 
+from pyccel.utilities.stage import PyccelStage
+
 from .basic import PyccelAstNode
 
+from .core import FunctionDefArgument
+
+from .internals import AnnotatedPyccelSymbol
+
+from .variable import DottedName
+
 __all__ = (
+        'FunctionTypeAnnotation',
         'SyntacticTypeAnnotation',
         'VariableTypeAnnotation',
         'UnionTypeAnnotation',
         )
 
+pyccel_stage = PyccelStage()
+
 class VariableTypeAnnotation(PyccelAstNode):
     """
-    A class which describes a type annotation.
+    A class which describes a type annotation on a variable.
 
     A class which stores all information which may be provided in a type annotation
     in order to declare a variable.
@@ -119,9 +130,80 @@ def is_const(self):
     def __hash__(self):
         return hash((self.datatype, self.cls_base, self.precision, self.rank, self.order))
 
+    def __eq__(self, other):
+        # Needed for set
+        if isinstance(other, VariableTypeAnnotation):
+            return self.datatype == other.datatype and \
+                   self.cls_base == other.cls_base and \
+                   self.precision == other.precision and \
+                   self.rank == other.rank and \
+                   self.order == other.order
+        else:
+            return False
+
     def __repr__(self):
         return f"{self._datatype}{self._precision}[{self._rank}]({self._order})"
 
+class FunctionTypeAnnotation(PyccelAstNode):
+    """
+    A class which describes a type annotation on a function address.
+
+    A class which stores all information necessary to describe the prototype
+    of the function being referenced. This includes the type annotations for
+    the arguments and the results.
+
+    Parameters
+    ----------
+    args : list of SyntacticTypeAnnotation | UnionTypeAnnotation
+        The type annotations describing the arguments of the function address.
+        In the syntactic stage these objects are of type SyntacticTypeAnnotation.
+        In the semantic stage these objects are of type UnionTypeAnnotation.
+
+    results : list of SyntacticTypeAnnotation | UnionTypeAnnotation
+        The type annotations describing the results of the function address.
+        In the syntactic stage these objects are of type SyntacticTypeAnnotation.
+        In the semantic stage these objects are of type UnionTypeAnnotation.
+    """
+    __slots__ = ('_args', '_results',)
+    _attribute_nodes = ('_args', '_results')
+
+    def __init__(self, args, results):
+        if pyccel_stage == 'syntactic':
+            self._args = [FunctionDefArgument(AnnotatedPyccelSymbol('_', a), annotation = a) \
+                            for i, a in enumerate(args)]
+            self._results = [FunctionDefArgument(AnnotatedPyccelSymbol('_', r), annotation = r) \
+                            for i, r in enumerate(results)]
+        else:
+            self._args = args
+            self._results = results
+
+        super().__init__()
+
+    @property
+    def args(self):
+        """
+        Get the type annotations describing the arguments of the function address.
+
+        Get the type annotations describing the arguments of the function address.
+        In the syntactic stage these objects are of type SyntacticTypeAnnotation.
+        In the semantic stage these objects are of type UnionTypeAnnotation.
+        """
+        return self._args
+
+    @property
+    def results(self):
+        """
+        Get the type annotations describing the results of the function address.
+
+        Get the type annotations describing the results of the function address.
+        In the syntactic stage these objects are of type SyntacticTypeAnnotation.
+        In the semantic stage these objects are of type UnionTypeAnnotation.
+        """
+        return self._results
+
+    def __repr__(self):
+        return f'func({repr(self.args)}) -> {repr(self.results)}'
+
 class UnionTypeAnnotation(PyccelAstNode):
     """
     A class which holds multiple possible type annotations.
@@ -130,8 +212,8 @@ class UnionTypeAnnotation(PyccelAstNode):
 
     Parameters
     ----------
-    *type_annotations : tuple of VariableTypeAnnotation
-        The VariableTypeAnnotation objects describing the possible type annotations.
+    *type_annotations : tuple of VariableTypeAnnotation | FunctionTypeAnnotation
+        The objects describing the possible type annotations.
     """
     __slots__ = ('_type_annotations',)
     _attribute_nodes = ('_type_annotations',)
@@ -151,6 +233,27 @@ def type_list(self):
         """
         return self._type_annotations
 
+    def add_type(self, annot):
+        """
+        Add an additional type to the type annotations.
+
+        Add an additional type to the type annotations stored in this UnionTypeAnnotation.
+
+        Parameters
+        ----------
+        annot : VariableTypeAnnotation | FunctionTypeAnnotation
+            The object describing the additional type annotation.
+        """
+        if annot not in self._type_annotations:
+            self._type_annotations += (annot,)
+            annot.set_current_user_node(self)
+
+    def __len__(self):
+        return len(self._type_annotations)
+
+    def __iter__(self):
+        return self._type_annotations.__iter__()
+
 class SyntacticTypeAnnotation(PyccelAstNode):
     """
     A class describing the type annotation parsed in the syntactic stage.
@@ -179,7 +282,7 @@ class SyntacticTypeAnnotation(PyccelAstNode):
     __slots__ = ('_dtypes', '_ranks', '_orders', '_is_const')
     _attribute_nodes = ()
     def __init__(self, dtypes, ranks, orders, is_const = None):
-        if any(not isinstance(d, str) for d in dtypes):
+        if any(not isinstance(d, (str, DottedName)) for d in dtypes):
             raise ValueError("Syntactic datatypes should be strings")
         if any(not isinstance(r, int) for r in ranks):
             raise ValueError("Ranks should have integer values")
@@ -187,9 +290,9 @@ def __init__(self, dtypes, ranks, orders, is_const = None):
             raise ValueError("Orders should be strings")
         if not (isinstance(is_const, bool) or is_const is None):
             raise ValueError("Is const should be a boolean")
-        self._dtypes = dtypes
-        self._ranks = ranks
-        self._orders = [o if o != '' else None for o in orders]
+        self._dtypes = tuple(dtypes)
+        self._ranks = tuple(ranks)
+        self._orders = tuple(o if o != '' else None for o in orders)
         self._is_const = is_const
         super().__init__()
 
@@ -233,13 +336,26 @@ def is_const(self):
         """
         return self._is_const
 
+    def __hash__(self):
+        return hash((self._dtypes, self._ranks, self._orders))
+
+    def __eq__(self, o):
+        if isinstance(o, SyntacticTypeAnnotation):
+            return self.dtypes == o.dtypes and \
+                    self.ranks == o.ranks and \
+                    self.orders == o.orders
+        else:
+            return False
+
     @staticmethod
     def build_from_textx(annotation):
         """
         Build a SyntacticTypeAnnotation from a textx annotation.
 
         Build a SyntacticTypeAnnotation from a textx annotation. This function should
-        be moved to the SyntacticParser once headers are deprecated.
+        be moved to the SyntacticParser once headers are deprecated. When that is
+        done there should only be 1 textx object handling types so the if conditions
+        can be changed to use isinstance.
 
         Parameters
         ----------
@@ -257,12 +373,33 @@ def build_from_textx(annotation):
         TypeError
             Raised if the type of the argument is not handled.
         """
-        if hasattr(annotation, 'dtype'):
+        if isinstance(annotation, (list, tuple)):
+            return tuple(SyntacticTypeAnnotation.build_from_textx(a) for a in annotation)
+        elif hasattr(annotation, 'const'):
+            # Handle UnionTypeStmt
+            is_const = annotation.const
+            dtypes = [SyntacticTypeAnnotation.build_from_textx(a) for a in annotation.dtypes]
+            if any(isinstance(d, FunctionTypeAnnotation) for d in dtypes):
+                if any(not isinstance(d, FunctionTypeAnnotation) for d in dtypes):
+                    raise TypeError("Can't mix function address with basic types")
+                return UnionTypeAnnotation(*dtypes)
+            else:
+                dtype_names = [n for d in dtypes for n in d.dtypes]
+                ranks = [r for d in dtypes for r in d.ranks]
+                orders = [o for d in dtypes for o in d.orders]
+                return SyntacticTypeAnnotation(dtype_names, ranks, orders, is_const)
+        elif hasattr(annotation, 'dtype'):
+            # Handle VariableType
             is_const = None
             dtype_names = [annotation.dtype]
             ranks = [len(getattr(annotation.trailer, 'args', ()))]
             orders = [getattr(annotation.trailer, 'order', None)]
             return SyntacticTypeAnnotation(dtype_names, ranks, orders, is_const)
+        elif hasattr(annotation, 'results'):
+            # Handle FuncType
+            args = [SyntacticTypeAnnotation.build_from_textx(a) for a in annotation.decs]
+            results = [SyntacticTypeAnnotation.build_from_textx(r) for r in annotation.results]
+            return FunctionTypeAnnotation(args, results)
         else:
             raise TypeError("Unexpected type")
 
diff --git a/pyccel/ast/variable.py b/pyccel/ast/variable.py
index 7602fb3778..7f4da2110a 100644
--- a/pyccel/ast/variable.py
+++ b/pyccel/ast/variable.py
@@ -612,6 +612,12 @@ class DottedName(PyccelAstNode):
     __slots__ = ('_name',)
     _attribute_nodes = ()
 
+    def __new__(cls, *args):
+        if len(args) == 1:
+            return args[0]
+        else:
+            return super().__new__(cls)
+
     def __init__(self, *args):
 
         self._name = args
diff --git a/pyccel/codegen/printing/ccode.py b/pyccel/codegen/printing/ccode.py
index 60d9907d0e..507a09f266 100644
--- a/pyccel/codegen/printing/ccode.py
+++ b/pyccel/codegen/printing/ccode.py
@@ -949,7 +949,7 @@ def _print_Import(self, expr):
             return '#include "{0}.h"\n'.format(source)
 
     def _print_LiteralString(self, expr):
-        format_str = format(expr.arg)
+        format_str = format(expr.python_value)
         format_str = format_str.replace("\\", "\\\\")\
                                .replace('\a', '\\a')\
                                .replace('\b', '\\b')\
diff --git a/pyccel/codegen/printing/fcode.py b/pyccel/codegen/printing/fcode.py
index 256c01697f..08c9b85860 100644
--- a/pyccel/codegen/printing/fcode.py
+++ b/pyccel/codegen/printing/fcode.py
@@ -1720,7 +1720,7 @@ def _print_LiteralString(self, expr):
         sp_chars = ['\a', '\b', '\f', '\r', '\t', '\v', "'", '\n']
         sub_str = ''
         formatted_str = []
-        for c in expr.arg:
+        for c in expr.python_value:
             if c in sp_chars:
                 if sub_str != '':
                     formatted_str.append("'{}'".format(sub_str))
diff --git a/pyccel/codegen/printing/pycode.py b/pyccel/codegen/printing/pycode.py
index 1164b81e17..ddbe16dc37 100644
--- a/pyccel/codegen/printing/pycode.py
+++ b/pyccel/codegen/printing/pycode.py
@@ -250,7 +250,7 @@ def _print_FunctionDefArgument(self, expr):
         default = ''
 
         if expr.annotation:
-            type_annotation = "'" + self._print(expr.annotation) + "'"
+            type_annotation = self._print(expr.annotation)
         else:
             var = expr.var
             def get_type_annotation(var):
@@ -1112,6 +1112,25 @@ def _print_Omp_End_Clause(self, expr):
         omp_expr = '#$omp {}\n'.format(omp_expr)
         return omp_expr
 
+    #------------------Annotation Printer------------------
+
+    def _print_UnionTypeAnnotation(self, expr):
+        types = [self._print(t)[1:-1] for t in expr.type_list]
+        return "'" + ' | '.join(types) + "'"
+
+    def _print_SyntacticTypeAnnotation(self, expr):
+        dtypes = expr.dtypes
+        ranks = [f"[{','.join(':'*r)}]" if r>0 else '' for r in expr.ranks]
+        order = [f"(order={o})" if o else '' for o in expr.orders]
+        annot = [f'{d}{r}{o}' for d,r,o in zip(dtypes, ranks, order)]
+        const = 'const ' if expr.is_const else ''
+        return "'" + const + ' | '.join(annot) + "'"
+
+    def _print_FunctionTypeAnnotation(self, expr):
+        args = ', '.join(self._print(a.annotation)[1:-1] for a in expr.args)
+        results = ', '.join(self._print(r.annotation)[1:-1] for r in expr.results)
+        return "'" + f"({results})({args})" + "'"
+
 #==============================================================================
 def pycode(expr, assign_to=None, **settings):
     """ Converts an expr to a string of Python code
diff --git a/pyccel/errors/errors.py b/pyccel/errors/errors.py
index eaf1d644a1..cd9cab68aa 100644
--- a/pyccel/errors/errors.py
+++ b/pyccel/errors/errors.py
@@ -319,8 +319,10 @@ def report(self,
                 fst = symbol.fst
 
         if fst:
-            line   = getattr(fst, 'lineno', None)
-            column = getattr(fst, 'col_offset', None)
+            if line is None:
+                line   = getattr(fst, 'lineno', None)
+            if column is None:
+                column = getattr(fst, 'col_offset', None)
 
         if self.mode == 'developer':
             if traceback:
diff --git a/pyccel/errors/messages.py b/pyccel/errors/messages.py
index e888a185b5..e241a87c11 100644
--- a/pyccel/errors/messages.py
+++ b/pyccel/errors/messages.py
@@ -104,7 +104,7 @@
 PYCCEL_RESTRICTION_IMPORT_STAR = 'import * not allowed'
 PYCCEL_RESTRICTION_OPTIONAL_NONE = 'Variables cannot be equal to None unless they are optional arguments and None is the default value'
 PYCCEL_RESTRICTION_UNSUPPORTED_SYNTAX = 'Pyccel has encountered syntax that it does not recognise'
-PYCCEL_RESTRICTION_TODO = "Pyccel has encountered syntax that has not been implemented yet. Please create an issue at https://github.com/pyccel/pyccel/issues and provide a small example of your problem. Do not forget to specify your target language"
+PYCCEL_RESTRICTION_TODO = "Pyccel has encountered syntax that has not been implemented yet. Please create an issue at https://github.com/pyccel/pyccel/issues and provide a small example of your problem. Do not forget to specify your target language."
 PYCCEL_RESTRICTION_MULTIPLE_COMPARISONS = 'Uncovered multi operator comparison statement'
 PYCCEL_RESTRICTION_LIST_COMPREHENSION_ASSIGN = "The result of a list comprehension expression must be saved in a variable"
 PYCCEL_RESTRICTION_LIST_COMPREHENSION_SIZE = 'Could not deduce the size of this list comprehension. If you believe this expression is simple then please create an issue at https://github.com/pyccel/pyccel/issues and provide a small example of your problem.'
diff --git a/pyccel/parser/grammar/headers.tx b/pyccel/parser/grammar/headers.tx
index 77a6f2dd5e..6260ea1218 100644
--- a/pyccel/parser/grammar/headers.tx
+++ b/pyccel/parser/grammar/headers.tx
@@ -17,19 +17,18 @@ TrailerSubscriptList: '[' args*=':' [','] ']'  ( '(' 'order' '=' order = ID ')'
 
 DataType: ID|STAR;
 
-Type: dtype=DataType ('*' prec=INT)? (trailer=TrailerSubscriptList)?;
-ListType : '[' dtype+=Type[','] ']';
+VariableType: dtype=DataType (trailer=TrailerSubscriptList)?;
 
 FuncType: '('(results*=TypeHeader[','])?')' '('decs*=UnionTypeStmt[',']')';
 
-TypeHeader: FuncType|Type|ListType;
-UnionTypeStmt: (const=Const)? dtypes+=TypeHeader['|'] ;
+TypeHeader: FuncType|VariableType;
+UnionTypeStmt: (const?=Const) dtypes+=TypeHeader['|'] ;
 Const: 'const';
 
-VariableHeaderStmt: 'variable'  name=ID ('::')? dec=TypeHeader;
+VariableHeaderStmt: 'variable'  name=ID ('::')? dec=UnionTypeStmt;
 
 FunctionHeaderStmt: 
-  ((name=ID '(') | ((kind=FunctionKind)? (static=Static)? name=ID '(')) decs*=UnionTypeStmt[','] ')' (results=HeaderResults)?
+  ((name=ID '(') | ((kind=FunctionKind)? (static?=Static) name=ID '(')) decs*=UnionTypeStmt[','] ')' (results=HeaderResults)?
 ;
 
 TemplateStmt: 'template' name = ID '(' dtypes+=TypeHeader['|'] ')';
@@ -81,5 +80,4 @@ FunctionMacroStmt:
 // ****************
 
 STAR: '*';
-OPTIONAL: '!';
 
diff --git a/pyccel/parser/grammar/types.tx b/pyccel/parser/grammar/types.tx
new file mode 100644
index 0000000000..93f1f6a302
--- /dev/null
+++ b/pyccel/parser/grammar/types.tx
@@ -0,0 +1,13 @@
+UnionTypeStmt: (const ?= Const) dtypes += TypeHeader['|'];
+
+Const: 'const';
+TypeHeader: FuncType|Type;
+
+Type: dtype=DataType (trailer=TrailerSubscriptList)?;
+TrailerSubscriptList: '[' args*=':' [','] ']'  ( '(' 'order' '=' order = ID ')' )?;
+
+FuncType: '('(results*=TypeHeader[','])?')' '('decs*=UnionTypeStmt[',']')';
+
+DataType: ID|STAR;
+STAR: '*';
+
diff --git a/pyccel/parser/scope.py b/pyccel/parser/scope.py
index 0630c064bb..47558e394b 100644
--- a/pyccel/parser/scope.py
+++ b/pyccel/parser/scope.py
@@ -9,9 +9,11 @@
 from pyccel.ast.core      import ClassDef
 from pyccel.ast.headers   import MacroFunction, MacroVariable
 from pyccel.ast.headers   import FunctionHeader, MethodHeader
-from pyccel.ast.internals import PyccelSymbol
+from pyccel.ast.internals import PyccelSymbol, AnnotatedPyccelSymbol
 from pyccel.ast.variable  import Variable, DottedName
 
+from pyccel.parser.syntax.headers import FunctionHeaderStmt
+
 from pyccel.errors.errors import Errors
 
 from pyccel.naming.pythonnameclashchecker import PythonNameClashChecker
@@ -53,10 +55,10 @@ class Scope(object):
     name_clash_checker = PythonNameClashChecker()
     __slots__ = ('_imports','_locals','_parent_scope','_sons_scopes',
             '_is_loop','_loops','_temporary_variables', '_used_symbols',
-            '_dummy_counter','_original_symbol')
+            '_dummy_counter','_original_symbol', '_dotted_symbols')
 
     categories = ('functions','variables','classes',
-            'imports','symbolic_functions',
+            'imports','symbolic_functions', 'symbolic_alias',
             'macros','templates','headers','decorators',
             'cls_constructs')
 
@@ -89,6 +91,8 @@ def __init__(self, *, decorators = (), is_loop = False,
         # scoping for loops
         self._loops = []
 
+        self._dotted_symbols = []
+
     def __setstate__(self, state):
         state = state[1] # Retrieve __dict__ ignoring None
         if any(s not in state for s in self.__slots__):
@@ -191,6 +195,16 @@ def sons_scopes(self):
         """
         return self._sons_scopes
 
+    @property
+    def symbolic_alias(self):
+        """
+        A dictionary of symbolic alias defined in this scope.
+
+        A symbolic alias is a symbol declared in the scope which is mapped
+        to a constant object. E.g. a symbol which represents a type.
+        """
+        return self._locals['symbolic_alias']
+
     @property
     def symbolic_functions(self):
         """ A dictionary of symbolic functions defined in this scope
@@ -421,7 +435,7 @@ def insert_header(self, expr):
         TypeError
             Raised if the header type is unknown.
         """
-        if isinstance(expr, (FunctionHeader, MethodHeader)):
+        if isinstance(expr, (FunctionHeader, MethodHeader, FunctionHeaderStmt)):
             if expr.name in self.headers:
                 self.headers[expr.name].append(expr)
             else:
@@ -432,18 +446,57 @@ def insert_header(self, expr):
             raise TypeError(msg)
 
     def insert_symbol(self, symbol):
-        """ Add a new symbol to the scope
         """
-        if not self.allow_loop_scoping and self.is_loop:
-            self.parent_scope.insert_symbol(symbol)
-        elif symbol not in self._used_symbols:
-            collisionless_symbol = self.name_clash_checker.get_collisionless_name(symbol,
-                    self._used_symbols.values())
-            collisionless_symbol = PyccelSymbol(collisionless_symbol,
-                    is_temp = getattr(symbol, 'is_temp', False))
-            self._used_symbols[symbol] = collisionless_symbol
-            self._original_symbol[collisionless_symbol] = symbol
+        Add a new symbol to the scope.
+
+        Add a new symbol to the scope in the syntactic stage. This should be used to
+        declare symbols defined by the user. Once the symbol is declared the Scope
+        generates a collisionless name if necessary which can be used in the target
+        language without causing problems by being a keyword or being confused with
+        other symbols (e.g. in Fortran which is not case-sensitive). This new name
+        can be retrieved later using `Scope.get_expected_name`.
 
+        Parameters
+        ----------
+        symbol : PyccelSymbol | AnnotatedPyccelSymbol | DottedName
+            The symbol to be added to the scope.
+        """
+        if isinstance(symbol, AnnotatedPyccelSymbol):
+            symbol = symbol.name
+
+        if isinstance(symbol, DottedName):
+            self._dotted_symbols.append(symbol)
+        else:
+            if not self.allow_loop_scoping and self.is_loop:
+                self.parent_scope.insert_symbol(symbol)
+            elif symbol not in self._used_symbols:
+                collisionless_symbol = self.name_clash_checker.get_collisionless_name(symbol,
+                        self._used_symbols.values())
+                collisionless_symbol = PyccelSymbol(collisionless_symbol,
+                        is_temp = getattr(symbol, 'is_temp', False))
+                self._used_symbols[symbol] = collisionless_symbol
+                self._original_symbol[collisionless_symbol] = symbol
+
+    def insert_symbolic_alias(self, symbol, alias):
+        """
+        Add a new symbolic alias to the scope.
+
+        A symbolic alias is a symbol declared in the scope which is mapped
+        to a constant object. E.g. a symbol which represents a type.
+
+        Parameters
+        ----------
+        symbol : PyccelSymbol
+            The symbol which will represent the object in the code.
+        alias : pyccel.ast.basic.Basic
+            The object which will be represented by the symbol.
+        """
+        symbolic_aliases = self._locals['symbolic_alias']
+        if symbol in symbolic_aliases:
+            errors.report(f"{symbol} cannot represent multiple static concepts",
+                    symbol=symbol, severity='error')
+
+        symbolic_aliases[symbol] = alias
 
     def insert_symbols(self, symbols):
         """ Add multiple new symbols to the scope
@@ -451,6 +504,17 @@ def insert_symbols(self, symbols):
         for s in symbols:
             self.insert_symbol(s)
 
+    @property
+    def dotted_symbols(self):
+        """
+        Return all dotted symbols that were inserted into the scope.
+
+        Return all dotted symbols that were inserted into the scope.
+        This is useful to ensure that class variable names are
+        in the class scope.
+        """
+        return self._dotted_symbols
+
     @property
     def all_used_symbols(self):
         """ Get all symbols which already exist in this scope
diff --git a/pyccel/parser/semantic.py b/pyccel/parser/semantic.py
index 47d6de4b8b..6dfe4f225f 100644
--- a/pyccel/parser/semantic.py
+++ b/pyccel/parser/semantic.py
@@ -7,16 +7,17 @@
 See the developer docs for more details
 """
 
-from itertools import chain
+from itertools import chain, product
 import warnings
 
+from math import prod
+
 from sympy.utilities.iterables import iterable as sympy_iterable
 
 from sympy import Sum as Summation
 from sympy import Symbol as sp_Symbol
 from sympy import Integer as sp_Integer
 from sympy import ceiling
-from sympy.core import cache
 
 #==============================================================================
 
@@ -74,6 +75,7 @@
 from pyccel.ast.headers import MacroFunction, MacroVariable
 
 from pyccel.ast.internals import PyccelInternalFunction, Slice, PyccelSymbol, get_final_precision
+from pyccel.ast.internals import AnnotatedPyccelSymbol
 from pyccel.ast.itertoolsext import Product
 
 from pyccel.ast.literals import LiteralTrue, LiteralFalse
@@ -103,7 +105,8 @@
 
 from pyccel.ast.sympy_helper import sympy_to_pyccel, pyccel_to_sympy
 
-from pyccel.ast.type_annotations import VariableTypeAnnotation, UnionTypeAnnotation
+from pyccel.ast.type_annotations import VariableTypeAnnotation, UnionTypeAnnotation, SyntacticTypeAnnotation
+from pyccel.ast.type_annotations import FunctionTypeAnnotation
 
 from pyccel.ast.utilities import builtin_function as pyccel_builtin_function
 from pyccel.ast.utilities import builtin_import as pyccel_builtin_import
@@ -132,8 +135,7 @@
         UNDEFINED_LAMBDA_FUNCTION, UNDEFINED_INIT_METHOD, UNDEFINED_FUNCTION,
         INVALID_MACRO_COMPOSITION, WRONG_NUMBER_OUTPUT_ARGS, INVALID_FOR_ITERABLE,
         PYCCEL_RESTRICTION_LIST_COMPREHENSION_LIMITS, PYCCEL_RESTRICTION_LIST_COMPREHENSION_SIZE,
-        UNUSED_DECORATORS, DUPLICATED_SIGNATURE, FUNCTION_TYPE_EXPECTED,
-        UNSUPPORTED_POINTER_RETURN_VALUE, PYCCEL_RESTRICTION_OPTIONAL_NONE,
+        UNUSED_DECORATORS, UNSUPPORTED_POINTER_RETURN_VALUE, PYCCEL_RESTRICTION_OPTIONAL_NONE,
         PYCCEL_RESTRICTION_PRIMITIVE_IMMUTABLE, PYCCEL_RESTRICTION_IS_ISNOT,
         FOUND_DUPLICATED_IMPORT, UNDEFINED_WITH_ACCESS, MACRO_MISSING_HEADER_OR_FUNC,)
 
@@ -1629,6 +1631,45 @@ def _find_superclasses(self, expr):
 
         return list(parent.values())
 
+    def _PyccelAstNode_to_TypeAnnotation(self, expr, input_rank = 0, input_order = None):
+        """
+        Convert a PyccelAstNode class type to a TypeAnnotation.
+
+        Convert a PyccelAstNode class type to a TypeAnnotation. This is done using
+        the static dtype, precision and rank stored in the class.
+
+        Parameters
+        ----------
+        expr : type deriving from PyccelAstNode
+            The object which was passed to the type annotation.
+
+        input_rank : int, default=0
+            The rank that was deduced from the annotation.
+
+        input_order : str, optional
+            The order that was deduced from the annotation.
+
+        Returns
+        -------
+        VariableTypeAnnotation
+            Object describing the type that should be created.
+        """
+        dtype = expr.static_dtype()
+        prec = expr.static_precision()
+        if input_rank != 0:
+            rank = input_rank
+            order = input_order
+        else:
+            rank = expr.static_rank()
+            if not isinstance(rank, int):
+                rank = 0
+            order = expr.static_order()
+            if order is not None and not isinstance(order, str):
+                order = None
+        if rank > 1 and order is None:
+            order = 'C'
+        cls_base = get_cls_base(dtype, prec, rank)
+        return VariableTypeAnnotation(dtype, cls_base, prec, rank, order)
 
     #====================================================
     #                 _visit functions
@@ -1808,7 +1849,24 @@ def _visit_Module(self, expr):
                         not isinstance(v, MethodHeader) for v in headers):
                     F = self.scope.find(name, 'functions')
                     if F is None:
-                        func_defs = [vi for v in headers for vi in v.create_definition(is_external=is_external)]
+                        func_defs = []
+                        for v in headers:
+                            types = [self._visit(d).type_list[0] for d in v.dtypes]
+                            args = [Variable(t.datatype, PyccelSymbol(f'anon_{i}'), precision = t.precision,
+                                shape = None, rank = t.rank, order = t.order, cls_base = t.cls_base,
+                                is_const = t.is_const, is_optional = False,
+                                memory_handling = 'heap' if t.rank > 0 else 'stack') for i,t in enumerate(types)]
+
+                            types = [self._visit(d).type_list[0] for d in v.results]
+                            results = [Variable(t.datatype, PyccelSymbol(f'result_{i}'), precision = t.precision,
+                                shape = None, rank = t.rank, order = t.order, cls_base = t.cls_base,
+                                is_const = t.is_const, is_optional = False,
+                                memory_handling = 'heap' if t.rank > 0 else 'stack') for i,t in enumerate(types)]
+
+                            args = [FunctionDefArgument(a) for a in args]
+                            results = [FunctionDefResult(r) for r in results]
+                            func_defs.append(FunctionDef(v.name, args, results, [], is_external = is_external, is_header = True))
+
                         if len(func_defs) == 1:
                             F = func_defs[0]
                             funcs.append(F)
@@ -1879,6 +1937,40 @@ def _visit_FunctionCallArgument(self, expr):
             a = FunctionCallArgument(self._visit(tmp_var))
         return a
 
+    def _visit_UnionTypeAnnotation(self, expr):
+        annotations = [self._visit(syntax_type_annot) for syntax_type_annot in expr.type_list]
+        types = [t for a in annotations for t in (a.type_list if isinstance(a, UnionTypeAnnotation) else [a])]
+        return UnionTypeAnnotation(*types)
+
+    def _visit_FunctionTypeAnnotation(self, expr):
+        arg_types = [self._visit(a)[0] for a in expr.args]
+        res_types = [self._visit(r)[0] for r in expr.results]
+        return FunctionTypeAnnotation(arg_types, res_types)
+
+    def _visit_FunctionDefArgument(self, expr):
+        arg = self._visit(expr.var)
+        value = None if expr.value is None else self._visit(expr.value)
+        kwonly = expr.is_kwonly
+        is_optional = isinstance(value, Nil)
+
+        args = []
+        for v in arg:
+            if isinstance(v, Variable):
+                dtype = v.dtype
+                prec = v.precision
+                if isinstance(value, Literal) and \
+                        value.dtype is dtype and \
+                        value.precision != prec:
+                    value = convert_to_literal(value.python_value, dtype, prec)
+                clone_var = v.clone(v.name, is_optional = is_optional, is_argument = True)
+                args.append(FunctionDefArgument(clone_var,
+                                        value = value, kwonly = kwonly, annotation = expr.annotation))
+            else:
+                args.append(FunctionDefArgument(v.clone(v.name, is_optional = is_optional,
+                                is_kwonly = kwonly, is_argument = True),
+                                value = value, kwonly = kwonly, annotation = expr.annotation))
+        return args
+
     def _visit_CodeBlock(self, expr):
         ls = []
         self._additional_exprs.append([])
@@ -2057,6 +2149,10 @@ def _visit_AnnotatedPyccelSymbol(self, expr):
         # Get the semantic type annotation (should be UnionTypeAnnotation)
         types = self._visit(expr.annotation)
 
+        if len(types.type_list) == 0:
+            errors.report(f'Missing type annotation for argument {expr}',
+                    severity='fatal', symbol=expr)
+
         # Get the collisionless name from the scope
         name = self.scope.get_expected_name(expr.name)
 
@@ -2075,7 +2171,12 @@ def _visit_AnnotatedPyccelSymbol(self, expr):
         # For each possible data type create the necessary variables
         possible_args = []
         for t in types.type_list:
-            if isinstance(t, VariableTypeAnnotation):
+            if isinstance(t, FunctionTypeAnnotation):
+                args = t.args
+                results = [FunctionDefResult(r.var.clone(r.var.name, is_argument = False), annotation=r.annotation) for r in t.results]
+                address = FunctionAddress(name, args, results)
+                possible_args.append(address)
+            elif isinstance(t, VariableTypeAnnotation):
                 dtype = t.datatype
                 prec  = t.precision
                 rank  = t.rank
@@ -2099,32 +2200,59 @@ def _visit_SyntacticTypeAnnotation(self, expr):
         types = []
 
         for dtype_name, rank, order in zip(expr.dtypes, expr.ranks, expr.orders):
-            # Find the DataType instance and the associated precision
-            prec = -1
-            try:
-                dtype, prec = dtype_and_precision_registry[dtype_name]
-            except KeyError:
-                errors.report(f'Could not identify type : {dtype_name}',
-                        severity='fatal', symbol=expr)
-
-            try:
-                cls_base = get_cls_base(dtype, prec, rank)
-            except KeyError:
-                cls_base = self.scope.find(dtype_name, 'classes')
-
             if rank > 1 and order is None:
                 order = 'C'
-            elif order is not None and rank < 2:
-                errors.report(f"Ordering is not applicable to objects with rank {rank}",
-                        symbol=expr.fst, severity='warning')
+
+            if isinstance(dtype_name, (PyccelSymbol, DottedName)):
+                dtype_from_scope = self._visit(dtype_name)
+            else:
+                dtype_from_scope = self.scope.find(dtype_name)
+
+            if isinstance(dtype_from_scope, type) and PyccelAstNode in dtype_from_scope.__mro__:
+                types.append(self._PyccelAstNode_to_TypeAnnotation(dtype_from_scope, rank, order))
+            elif isinstance(dtype_from_scope, PyccelFunctionDef):
+                types.append(self._PyccelAstNode_to_TypeAnnotation(dtype_from_scope.cls_name, rank, order))
+            elif isinstance(dtype_from_scope, VariableTypeAnnotation):
+                if rank == 0:
+                    types.append(dtype_from_scope)
+                else:
+                    types.append(VariableTypeAnnotation(dtype_from_scope.datatype,
+                        dtype_from_scope.cls_base, dtype_from_scope.precision,
+                        rank, order, dtype_from_scope.is_const))
+            elif isinstance(dtype_from_scope, ClassDef):
+                dtype = self.get_class_construct(dtype_name)
+                prec = 0
+                rank = 0
                 order = None
+                cls_base = dtype_from_scope
+                types.append(VariableTypeAnnotation(dtype, cls_base, prec, rank, order))
+            elif dtype_from_scope is not None:
+                errors.report(PYCCEL_RESTRICTION_TODO + f' Could not deduce type information from {type(dtype_from_scope)} object',
+                        severity='fatal', symbol=expr)
+            else:
+                # Find the DataType instance and the associated precision
+                try:
+                    dtype, prec = dtype_and_precision_registry[dtype_name]
+                except KeyError:
+                    errors.report(f'Could not identify type : {dtype_name}',
+                            severity='fatal', symbol=expr)
+
+                try:
+                    cls_base = get_cls_base(dtype, prec, rank)
+                except KeyError:
+                    cls_base = self.scope.find(dtype_name, 'classes')
+
+                if order is not None and rank < 2:
+                    errors.report(f"Ordering is not applicable to objects with rank {rank}",
+                            symbol=expr.fst, severity='warning')
+                    order = None
 
-            # NumPy objects cannot have default precision
-            if prec == -1 and cls_base is NumpyArrayClass:
-                prec = default_precision[dtype]
+                # NumPy objects cannot have default precision
+                if prec == -1 and cls_base is NumpyArrayClass:
+                    prec = default_precision[dtype]
 
-            # Save the potential type
-            types.append(VariableTypeAnnotation(dtype, cls_base, prec, rank, order, is_const))
+                # Save the potential type
+                types.append(VariableTypeAnnotation(dtype, cls_base, prec, rank, order, is_const))
 
         # Collect all possible types into a UnionTypeAnnotation
         return UnionTypeAnnotation(*types)
@@ -2494,7 +2622,7 @@ def _visit_FunctionCall(self, expr):
                 errors.report(UNDEFINED_INIT_METHOD, symbol=name,
                 bounding_box=(self._current_fst_node.lineno, self._current_fst_node.col_offset),
                 severity='error')
-            d_var = {'datatype': self.get_class_construct(method.cls_name)(),
+            d_var = {'datatype': self.get_class_construct(method.cls_name),
                     'memory_handling':'stack',
                     'shape' : None,
                     'rank' : 0,
@@ -3307,306 +3435,200 @@ def _visit_FunctionDef(self, expr):
         is_private      = expr.is_private
         is_inline       = expr.is_inline
         doc_string      = self._visit(expr.doc_string) if expr.doc_string else expr.doc_string
-        headers = []
 
         not_used = [d for d in decorators if d not in def_decorators.__all__]
         if len(not_used) >= 1:
             errors.report(UNUSED_DECORATORS, symbol=', '.join(not_used), severity='warning')
 
-        args_number = len(expr.arguments)
         templates = self.scope.find_all('templates')
         if decorators['template']:
             # Load templates dict from decorators dict
             templates.update(decorators['template']['template_dict'])
 
-        tmp_headers = expr.headers
-        python_name = expr.scope.get_python_name(name)
-        if cls_name:
-            tmp_headers += self.get_headers(cls_name + '.' + python_name)
-            args_number -= 1
-        else:
-            tmp_headers += self.get_headers(python_name)
-        for header in tmp_headers:
-            if all(header.dtypes != hd.dtypes for hd in headers):
-                headers.append(header)
-            else:
-                errors.report(DUPLICATED_SIGNATURE, symbol=header,
-                        severity='warning')
-        for hd in headers:
-            if (args_number != len(hd.dtypes)):
-                n_types = len(hd.dtypes)
-                msg = f"""The number of arguments in the function {name} ({args_number}) does not match the number
-                        of types in decorator/header ({n_types})."""
-                if (args_number < len(hd.dtypes)):
-                    errors.report(msg, symbol=expr.arguments, severity='warning')
-                else:
-                    errors.report(msg, symbol=expr.arguments, severity='fatal')
-
-        interfaces = []
-        if len(headers) == 0:
-            # check if a header is imported from a header file
-            # TODO improve in the case of multiple headers ( interface )
-            func       = self.scope.find(name, 'functions')
-            if func and func.is_header:
-                interfaces = [func]
-
-        if expr.arguments and not headers and not interfaces:
-
-            # TODO ERROR wrong position
-
-            errors.report(FUNCTION_TYPE_EXPECTED, symbol=name,
-                   bounding_box=(self._current_fst_node.lineno, self._current_fst_node.col_offset),
-                   severity='error')
+        for t,v in templates.items():
+            templates[t] = UnionTypeAnnotation(*[self._visit(vi) for vi in v])
 
-        # We construct a FunctionDef from each function header
-        for hd in headers:
-            interfaces += hd.create_definition(templates)
+        # Filter out unused templates
+        templatable_args = [a.annotation for a in expr.arguments if isinstance(a.annotation, (SyntacticTypeAnnotation, UnionTypeAnnotation))]
+        arg_annotations = [annot for a in templatable_args for annot in (a.type_list \
+                                        if isinstance(a, UnionTypeAnnotation) else [a]) \
+                                        if isinstance(annot, SyntacticTypeAnnotation)]
+        used_type_names = set(type_names for a in arg_annotations for type_names in a.dtypes)
+        templates = {t: v for t,v in templates.items() if t in used_type_names}
 
-        if not interfaces:
-            # this for the case of a function without arguments => no headers
-            interfaces = [FunctionDef(name, [], [], [])]
-
-#        TODO move this to codegen
-#        vec_func = None
-#        if 'vectorize' in decorators:
-#            #TODO move to another place
-#            vec_name  = 'vec_' + name
-#            arg       = decorators['vectorize'][0]
-#            arg       = str(arg.name)
-#            args      = [str(i.name) for i in expr.arguments]
-#            index_arg = args.index(arg)
-#            arg       = Symbol(arg)
-#            vec_arg   = arg
-#            index     = self.namespace.get_new_name()
-#            range_    = Function('range')(Function('len')(arg))
-#            args      = symbols(args)
-#            args[index_arg] = vec_arg[index]
-#            body_vec        = Assign(args[index_arg], Function(name)(*args))
-#            body_vec.set_fst(expr.fst)
-#            body_vec   = [For(index, range_, [body_vec])]
-#            header_vec = header.vectorize(index_arg)
-#            vec_func   = expr.vectorize(body_vec, header_vec)
+        template_combinations = list(product(*[v.type_list for v in templates.values()]))
+        template_names = list(templates.keys())
+        n_templates = len(template_combinations)
 
+        # this for the case of a function without arguments => no headers
         interface_name = name
 
-        for i, m in enumerate(interfaces):
-            args           = []
-            arg_vars       = []
-            results        = []
-            global_vars    = []
-            imports        = []
-            arg            = None
-            arguments      = expr.arguments
-            header_results = m.results
-
-            if len(interfaces) > 1:
-                name = interface_name + '_' + str(i).zfill(2)
+        for tmpl_idx in range(n_templates):
+            # Change to syntactic FunctionDef scope to ensure get_expected_name is available
             scope = self.create_new_function_scope(name, decorators = decorators,
                     used_symbols = expr.scope.local_used_symbols.copy(),
                     original_symbols = expr.scope.python_names.copy())
+            for n, v in zip(template_names, template_combinations[tmpl_idx]):
+                self.scope.insert_symbolic_alias(n, v)
+            self.scope.decorators.update(decorators)
+            arguments = [self._visit(a) for a in expr.arguments]
+            for n in template_names:
+                self.scope.symbolic_alias.pop(n)
+            self.exit_function_scope()
 
-            if cls_name and str(arguments[0].name) == 'self':
-                arg       = arguments[0]
-                arguments = arguments[1:]
-                dt        = self.get_class_construct(cls_name)()
-                cls_base  = self.scope.find(cls_name, 'classes')
-                cls_base.scope.insert_symbols(expr.scope.local_used_symbols.copy())
-                var       = Variable(dt, 'self', cls_base=cls_base, is_argument=True)
-                self.scope.insert_variable(var)
-
-            if arguments:
-                for (a, ah) in zip(arguments, m.arguments):
-                    ahv = ah.var
-                    if isinstance(ahv, FunctionAddress):
-                        d_var = {}
-                        d_var['is_argument'] = True
-                        d_var['memory_handling'] = 'alias'
-                        if a.has_default:
-                            # optional argument only if the value is None
-                            if isinstance(a.value, Nil):
-                                d_var['is_optional'] = True
-                        a_new = FunctionAddress(self.scope.get_expected_name(a.name),
-                                        ahv.arguments, ahv.results, [], **d_var)
+            n_interface_funcs = prod(len(a) for a in arguments)
+            argument_vars = list(product(*arguments))
+
+            is_interface = n_templates > 1 or n_interface_funcs > 1
+
+            for i in range(n_interface_funcs):
+                arguments      = argument_vars[i]
+                arg_dict = {a.name:a.var for a in arguments}
+
+                if is_interface:
+                    name = interface_name + '_' + str(tmpl_idx*n_interface_funcs + i).zfill(2)
+                scope = self.create_new_function_scope(name, decorators = decorators,
+                        used_symbols = expr.scope.local_used_symbols.copy(),
+                        original_symbols = expr.scope.python_names.copy())
+
+                if cls_name:
+                    if arguments[0].var.cls_base.name != cls_name:
+                        errors.report('Class method self argument does not have the expected type',
+                                severity='error', symbol=arguments[0])
+                    for s in expr.scope.dotted_symbols:
+                        base = s.name[0]
+                        if base in arg_dict:
+                            cls_base = arg_dict[base].cls_base
+                            cls_base.scope.insert_symbol(DottedName(*s.name[1:]))
+
+                for a in arguments:
+                    a_var = a.var
+                    if isinstance(a_var, FunctionAddress):
+                        self.insert_function(a_var)
                     else:
-                        d_var = self._infer_type(ahv)
-                        d_var['shape'] = ahv.alloc_shape
-                        d_var['is_argument'] = True
-                        d_var['is_const'] = ahv.is_const
-                        dtype = d_var.pop('datatype')
-                        if not d_var['cls_base']:
-                            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']:
-                                d_var.update(allows_negative_indexes=True)
-                        if a.has_default:
-                            # optional argument only if the value is None
-                            if isinstance(a.value, Nil):
-                                d_var['is_optional'] = True
-                        a_new = Variable(dtype, self.scope.get_expected_name(a.name), **d_var)
-
-
-                    value = None if a.value is None else self._visit(a.value)
-                    if isinstance(value, Literal) and \
-                            value.dtype is a_new.dtype and \
-                            value.precision != a_new.precision:
-                        value = convert_to_literal(value.python_value, a_new.dtype, a_new.precision)
-
-                    arg_new = FunctionDefArgument(a_new,
-                                value=value,
-                                kwonly=a.is_kwonly,
-                                annotation=ah.annotation)
-
-                    args.append(arg_new)
-                    arg_vars.append(a_new)
-                    if isinstance(a_new, FunctionAddress):
-                        self.insert_function(a_new)
-                    else:
-                        self.scope.insert_variable(a_new, a.name)
-            results = expr.results
-            if header_results:
-                new_results = []
-
-                for a, ah in zip(results, header_results):
-                    av = a.var
-                    d_var = self._infer_type(ah.var)
-                    dtype = d_var.pop('datatype')
-                    a_new = Variable(dtype, self.scope.get_expected_name(av),
-                            **d_var, is_temp = av.is_temp)
-                    self.scope.insert_variable(a_new, av)
-                    new_results.append(FunctionDefResult(a_new, annotation = ah.annotation))
+                        self.scope.insert_variable(a_var, expr.scope.get_python_name(a.name))
 
-                results = new_results
+                results = expr.results
+                if results and results[0].annotation:
+                    results = [self._visit(r) for r in expr.results]
 
-            # insert the FunctionDef into the scope
-            # to handle the case of a recursive function
-            # TODO improve in the case of an interface
-            recursive_func_obj = FunctionDef(name, args, results, [])
-            self.insert_function(recursive_func_obj)
+                # insert the FunctionDef into the scope
+                # to handle the case of a recursive function
+                # TODO improve in the case of an interface
+                recursive_func_obj = FunctionDef(name, arguments, results, [])
+                self.insert_function(recursive_func_obj)
 
-            # Create a new list that store local variables for each FunctionDef to handle nested functions
-            self._allocs.append([])
+                # Create a new list that store local variables for each FunctionDef to handle nested functions
+                self._allocs.append([])
 
-            # we annotate the body
-            body = self._visit(expr.body)
+                # we annotate the body
+                body = self._visit(expr.body)
 
-            # Calling the Garbage collecting,
-            # it will add the necessary Deallocate nodes
-            # to the body of the function
-            body.insert2body(*self._garbage_collector(body))
+                # Calling the Garbage collecting,
+                # it will add the necessary Deallocate nodes
+                # to the body of the function
+                body.insert2body(*self._garbage_collector(body))
 
-            results = [self._visit(a) for a in results]
+                results = [self._visit(a) for a in results]
 
-            if arg and cls_name:
-                dt       = self.get_class_construct(cls_name)()
-                cls_base = self.scope.find(cls_name, 'classes')
-                var      = Variable(dt, 'self', cls_base=cls_base)
-                args     = [FunctionDefArgument(var)] + args
+                # Determine local and global variables
+                global_vars = list(self.get_variables(self.scope.parent_scope))
+                global_vars = [g for g in global_vars if body.is_user_of(g)]
 
-            # Determine local and global variables
-            global_vars = list(self.get_variables(self.scope.parent_scope))
-            global_vars = [g for g in global_vars if body.is_user_of(g)]
+                # get the imports
+                imports   = self.scope.imports['imports'].values()
+                # Prefer dict to set to preserve order
+                imports   = list({imp:None for imp in imports}.keys())
 
-            # get the imports
-            imports   = self.scope.imports['imports'].values()
-            # Prefer dict to set to preserve order
-            imports   = list({imp:None for imp in imports}.keys())
+                # remove the FunctionDef from the function scope
+                # TODO improve func_ is None in the case of an interface
+                func_     = self.scope.functions.pop(name, None)
+                is_recursive = False
+                # check if the function is recursive if it was called on the same scope
+                if func_ and func_.is_recursive:
+                    is_recursive = True
 
-            # remove the FunctionDef from the function scope
-            # TODO improve func_ is None in the case of an interface
-            func_     = self.scope.functions.pop(name, None)
-            is_recursive = False
-            # check if the function is recursive if it was called on the same scope
-            if func_ and func_.is_recursive:
-                is_recursive = True
+                sub_funcs = [i for i in self.scope.functions.values() if not i.is_header and not isinstance(i, FunctionAddress)]
 
-            sub_funcs = [i for i in self.scope.functions.values() if not i.is_header and not isinstance(i, FunctionAddress)]
+                func_args = [i for i in self.scope.functions.values() if isinstance(i, FunctionAddress)]
+                if func_args:
+                    func_interfaces.append(Interface('', func_args, is_argument = True))
 
-            func_args = [i for i in self.scope.functions.values() if isinstance(i, FunctionAddress)]
-            if func_args:
-                func_interfaces.append(Interface('', func_args, is_argument = True))
-
-            namespace_imports = self.scope.imports
-            self.exit_function_scope()
-
-            results_names = [i.var.name for i in results]
+                namespace_imports = self.scope.imports
+                self.exit_function_scope()
 
-            # Find all nodes which can modify variables
-            assigns = body.get_attribute_nodes(Assign, excluded_nodes = (FunctionCall,))
-            calls   = body.get_attribute_nodes(FunctionCall)
+                results_names = [i.var.name for i in results]
+
+                # Find all nodes which can modify variables
+                assigns = body.get_attribute_nodes(Assign, excluded_nodes = (FunctionCall,))
+                calls   = body.get_attribute_nodes(FunctionCall)
+
+                # Collect the modified objects
+                lhs_assigns   = [a.lhs for a in assigns]
+                modified_args = [call_arg.value for f in calls
+                                    for call_arg, func_arg in zip(f.args, f.funcdef.arguments) if func_arg.inout]
+                # Collect modified variables
+                all_assigned = [v for a in (lhs_assigns + modified_args) for v in
+                                (a.get_attribute_nodes(Variable) if not isinstance(a, Variable) else [a])]
+
+                # ... computing inout arguments
+                for a in arguments:
+                    if a.name in chain(results_names, ['self']) or a.var in all_assigned:
+                        v = a.var
+                        if isinstance(v, Variable) and v.is_const:
+                            msg = f"Cannot modify 'const' argument ({v})"
+                            errors.report(msg, bounding_box=(self._current_fst_node.lineno,
+                                self._current_fst_node.col_offset),
+                                severity='fatal')
+                    else:
+                        a.make_const()
+                # ...
 
-            # Collect the modified objects
-            lhs_assigns   = [a.lhs for a in assigns]
-            modified_args = [call_arg.value for f in calls
-                                for call_arg, func_arg in zip(f.args, f.funcdef.arguments) if func_arg.inout]
-            # Collect modified variables
-            all_assigned = [v for a in (lhs_assigns + modified_args) for v in
-                            (a.get_attribute_nodes(Variable) if not isinstance(a, Variable) else [a])]
+                # Raise an error if one of the return arguments is an alias.
+                for r in results:
+                    if r.var.is_alias:
+                        errors.report(UNSUPPORTED_POINTER_RETURN_VALUE,
+                        symbol=r,bounding_box=(self._current_fst_node.lineno, self._current_fst_node.col_offset),
+                        severity='error')
 
-            # ... computing inout arguments
-            for a in args:
-                if a.name in chain(results_names, ['self']) or a.var in all_assigned:
-                    v = a.var
-                    if isinstance(v, Variable) and v.is_const:
-                        msg = f"Cannot modify 'const' argument ({v})"
-                        errors.report(msg, bounding_box=(self._current_fst_node.lineno,
-                            self._current_fst_node.col_offset),
-                            severity='fatal')
+                func_kwargs = {
+                        'global_vars':global_vars,
+                        'cls_name':cls_name,
+                        'is_pure':is_pure,
+                        'is_elemental':is_elemental,
+                        'is_private':is_private,
+                        'imports':imports,
+                        'decorators':decorators,
+                        'is_recursive':is_recursive,
+                        'functions': sub_funcs,
+                        'interfaces': func_interfaces,
+                        'doc_string': doc_string,
+                        'scope': scope
+                        }
+                if is_inline:
+                    func_kwargs['namespace_imports'] = namespace_imports
+                    global_funcs = [f for f in body.get_attribute_nodes(FunctionDef) if self.scope.find(f.name, 'functions')]
+                    func_kwargs['global_funcs'] = global_funcs
+                    cls = InlineFunctionDef
                 else:
-                    a.make_const()
-            # ...
-
-            # Raise an error if one of the return arguments is an alias.
-            for r in results:
-                if r.var.is_alias:
-                    errors.report(UNSUPPORTED_POINTER_RETURN_VALUE,
-                    symbol=r,bounding_box=(self._current_fst_node.lineno, self._current_fst_node.col_offset),
-                    severity='error')
+                    cls = FunctionDef
+                func = cls(name,
+                        arguments,
+                        results,
+                        body,
+                        **func_kwargs)
+                if not is_recursive:
+                    recursive_func_obj.invalidate_node()
 
-            func_kwargs = {
-                    'global_vars':global_vars,
-                    'cls_name':cls_name,
-                    'is_pure':is_pure,
-                    'is_elemental':is_elemental,
-                    'is_private':is_private,
-                    'imports':imports,
-                    'decorators':decorators,
-                    'is_recursive':is_recursive,
-                    'functions': sub_funcs,
-                    'interfaces': func_interfaces,
-                    'doc_string': doc_string,
-                    'scope': scope
-                    }
-            if is_inline:
-                func_kwargs['namespace_imports'] = namespace_imports
-                global_funcs = [f for f in body.get_attribute_nodes(FunctionDef) if self.scope.find(f.name, 'functions')]
-                func_kwargs['global_funcs'] = global_funcs
-                cls = InlineFunctionDef
-            else:
-                cls = FunctionDef
-            func = cls(name,
-                    args,
-                    results,
-                    body,
-                    **func_kwargs)
-            if not is_recursive:
-                recursive_func_obj.invalidate_node()
+                if cls_name:
+                    cls = self.scope.find(cls_name, 'classes')
 
-            if cls_name:
-                cls = self.scope.find(cls_name, 'classes')
+                    # update the class methods
+                    if expr.name == func.name:
+                        cls.add_new_method(func)
 
-                # update the class methods
-                if expr.name == func.name:
-                    cls.add_new_method(func)
+                funcs += [func]
 
-            funcs += [func]
-
-            #clear the sympy cache
-            #TODO clear all variable except the global ones
-            cache.clear_cache()
         if len(funcs) == 1:
             funcs = funcs[0]
             self.insert_function(funcs)
@@ -3680,7 +3702,7 @@ def _visit_ClassDef(self, expr):
 
         #  create a new Datatype for the current class
         dtype = DataTypeFactory(name, '_name')
-        self.scope.cls_constructs[name] = dtype
+        self.scope.cls_constructs[name] = dtype()
 
         parent = self._find_superclasses(expr)
 
@@ -3983,7 +4005,13 @@ def _visit_MacroFunction(self, expr):
         else:
             interfaces = []
             for hd in header:
-                interfaces += hd.create_definition()
+                for i,_ in enumerate(hd.dtypes):
+                    self.scope.insert_symbol(f'arg_{i}')
+                arguments = [FunctionDefArgument(self._visit(AnnotatedPyccelSymbol(f'arg_{i}', annotation = arg))[0]) \
+                        for i, arg in enumerate(hd.dtypes)]
+                results = [FunctionDefResult(self._visit(AnnotatedPyccelSymbol(f'out_{i}', annotation = arg))[0]) \
+                        for i, arg in enumerate(hd.results)]
+                interfaces.append(FunctionDef(f_name, arguments, results, []))
 
             # TODO -> Said: must handle interface
 
@@ -4023,7 +4051,7 @@ def _visit_MacroVariable(self, expr):
         if header is None:
             var = self.get_variable(master)
         else:
-            var = Variable(header.dtype, header.name)
+            var = self.get_variable(master)
 
                 # TODO -> Said: must handle interface
 
@@ -4074,4 +4102,14 @@ def _visit_NumpyNonZero(self, func_call):
 
     def _visit_FunctionDefResult(self, expr):
         var = self._visit(expr.var)
+        if isinstance(var, list):
+            n_types = len(var)
+            if n_types == 0:
+                errors.report("Can't deduce type for function definition result.",
+                        severity = 'fatal', symbol = expr)
+            elif n_types != 1:
+                errors.report("The type of the result of a function definition cannot be a union of multiple types.",
+                        severity = 'error', symbol = expr)
+            var = var[0]
+            self.scope.insert_variable(var)
         return FunctionDefResult(var, annotation = expr.annotation)
diff --git a/pyccel/parser/syntactic.py b/pyccel/parser/syntactic.py
index 94dd3519c7..9c152805f3 100644
--- a/pyccel/parser/syntactic.py
+++ b/pyccel/parser/syntactic.py
@@ -14,10 +14,6 @@
 
 #==============================================================================
 
-from sympy.core import cache
-
-#==============================================================================
-
 from pyccel.ast.basic import PyccelAstNode
 
 from pyccel.ast.core import FunctionCall, FunctionCallArgument
@@ -57,7 +53,7 @@
 
 from pyccel.ast.builtins import PythonTuple, PythonList
 from pyccel.ast.builtins import PythonPrint, Lambda
-from pyccel.ast.headers  import MetaVariable
+from pyccel.ast.headers  import MetaVariable, FunctionHeader, MethodHeader
 from pyccel.ast.literals import LiteralInteger, LiteralFloat, LiteralComplex
 from pyccel.ast.literals import LiteralFalse, LiteralTrue, LiteralString
 from pyccel.ast.literals import Nil
@@ -66,12 +62,14 @@
 
 from pyccel.ast.internals import Slice, PyccelSymbol, PyccelInternalFunction, AnnotatedPyccelSymbol
 
+from pyccel.ast.type_annotations import SyntacticTypeAnnotation, UnionTypeAnnotation
+
 from pyccel.parser.base        import BasicParser
 from pyccel.parser.extend_tree import extend_tree
 from pyccel.parser.utilities   import read_file
 from pyccel.parser.utilities   import get_default_path
 
-from pyccel.parser.syntax.headers import parse as hdr_parse
+from pyccel.parser.syntax.headers import parse as hdr_parse, types_meta
 from pyccel.parser.syntax.openmp  import parse as omp_parse
 from pyccel.parser.syntax.openacc import parse as acc_parse
 
@@ -226,10 +224,12 @@ def _treat_comment_line(self, line, stmt):
                     errors.report(f"Invalid header. {e.message}",
                             symbol = stmt, column = e.col,
                               severity='fatal')
-                if isinstance(expr, AnnotatedPyccelSymbol):
+                if isinstance(expr, (MethodHeader, FunctionHeader)):
+                    self.scope.insert_header(expr)
+                    expr = EmptyNode()
+                elif isinstance(expr, AnnotatedPyccelSymbol):
                     self.scope.insert_symbol(expr.name)
-                if isinstance(expr, MetaVariable):
-
+                elif isinstance(expr, MetaVariable):
                     # a metavar will not appear in the semantic stage.
                     # but can be used to modify the ast
 
@@ -247,6 +247,56 @@ def _treat_comment_line(self, line, stmt):
         expr.set_fst(stmt)
         return expr
 
+    def _treat_type_annotation(self, stmt, annotation):
+        """
+        Treat an object passed as a type annotation.
+
+        Ensure that an object that was passed as a type annotation can be
+        recognised in the semantic stage by packing it into a SyntacticTypeAnnotation
+        in the correct way. Also check the syntax of any string type
+        annotations.
+
+        Parameters
+        ----------
+        stmt : ast.Ast
+            The ast node about which any errors should be raised.
+
+        annotation : pyccel.ast.basic.PyccelAstNode
+            A visited object which is describing a type annotation.
+
+        Returns
+        -------
+        SyntacticTypeAnnotation | UnionTypeAnnotation
+            The type annotation.
+        """
+        if isinstance(annotation, (tuple, list)):
+            return UnionTypeAnnotation(*[self._treat_type_annotation(stmt, a) for a in annotation])
+        if isinstance(annotation, (PyccelSymbol, DottedName)):
+            return SyntacticTypeAnnotation(dtypes=[annotation], ranks=[0], orders=[None], is_const=False)
+        elif isinstance(annotation, IndexedElement):
+            return SyntacticTypeAnnotation(dtypes=[annotation], ranks=[len(annotation.indices)], orders=[None], is_const=False)
+        elif isinstance(annotation, LiteralString):
+            try:
+                annotation = types_meta.model_from_str(annotation.python_value)
+            except TextXSyntaxError as e:
+                errors.report(f"Invalid header. {e.message}",
+                        symbol = stmt, column = e.col,
+                        severity='fatal')
+            annot = SyntacticTypeAnnotation.build_from_textx(annotation)
+            if isinstance(stmt, PyccelAstNode):
+                annot.set_fst(stmt.fst)
+            else:
+                annot.set_fst(stmt)
+            return annot
+        elif annotation is Nil():
+            return None
+        elif isinstance(annotation, PyccelBitOr):
+            return UnionTypeAnnotation(*[self._treat_type_annotation(stmt, a) for a in annotation.args])
+        else:
+            errors.report('Invalid type annotation',
+                        symbol = stmt, severity='error')
+            return EmptyNode()
+
     #====================================================
     #                 _visit functions
     #====================================================
@@ -391,26 +441,35 @@ def _visit_arguments(self, stmt):
         arguments       = []
         if stmt.args:
             n_expl = len(stmt.args)-len(stmt.defaults)
-            positional_args        = [FunctionDefArgument(PyccelSymbol(a.arg),
-                                            annotation=self._visit(a.annotation))
-                                        for a in stmt.args[:n_expl]]
-            valued_arguments       = [FunctionDefArgument(PyccelSymbol(a.arg),
-                                            annotation=self._visit(a.annotation),
+
+            arguments = []
+            for a in stmt.args[:n_expl]:
+                annotation=self._treat_type_annotation(a, self._visit(a.annotation))
+                new_arg = FunctionDefArgument(AnnotatedPyccelSymbol(a.arg, annotation),
+                                            annotation=annotation)
+                new_arg.set_fst(a)
+                arguments.append(new_arg)
+
+            for a,d in zip(stmt.args[n_expl:], stmt.defaults):
+                annotation=self._treat_type_annotation(a, self._visit(a.annotation))
+                new_arg = FunctionDefArgument(AnnotatedPyccelSymbol(a.arg, annotation),
+                                            annotation=annotation,
                                             value = self._visit(d))
-                                        for a,d in zip(stmt.args[n_expl:],stmt.defaults)]
-            arguments              = positional_args + valued_arguments
-            self.scope.insert_symbols(PyccelSymbol(a.arg) for a in stmt.args)
+                new_arg.set_fst(a)
+                arguments.append(new_arg)
 
         if stmt.kwonlyargs:
             for a,d in zip(stmt.kwonlyargs,stmt.kw_defaults):
-                annotation = self._visit(a.annotation)
+                annotation=self._treat_type_annotation(a, self._visit(a.annotation))
                 val = self._visit(d) if d is not None else d
-                arg = FunctionDefArgument(PyccelSymbol(a.arg),
+                arg = FunctionDefArgument(AnnotatedPyccelSymbol(a.arg, annotation),
                             annotation=annotation,
                             value=val, kwonly=True)
+                arg.set_fst(a)
 
                 arguments.append(arg)
-                self.scope.insert_symbol(a.arg)
+
+        self.scope.insert_symbols(a.var for a in arguments)
 
         return arguments
 
@@ -644,14 +703,13 @@ def _visit_FunctionDef(self, stmt):
 
         name = PyccelSymbol(self._visit(stmt.name))
         self.scope.insert_symbol(name)
-        name = name.replace("'", '')
+
+        headers = self.scope.find(name, 'headers')
 
         scope = self.create_new_function_scope(name)
 
         arguments    = self._visit(stmt.args)
 
-        global_vars  = []
-        headers      = []
         template    = {}
         is_pure      = False
         is_elemental = False
@@ -660,32 +718,8 @@ def _visit_FunctionDef(self, stmt):
         imports      = []
         doc_string   = None
 
-        def fill_types(ls):
-            container = []
-            for arg in ls:
-                if isinstance(arg, FunctionCallArgument):
-                    arg = arg.value
-
-                if isinstance(arg, PyccelSymbol):
-                    container.append(arg)
-                elif isinstance(arg, LiteralString):
-                    arg = str(arg)
-                    arg = arg.strip("'").strip('"')
-                    container.append(arg)
-                else:
-                    msg = 'Invalid argument of type {} passed to types decorator'.format(type(arg))
-                    errors.report(msg,
-                                bounding_box = (stmt.lineno, stmt.col_offset),
-                                severity='error')
-            return container
-
         decorators = {}
 
-        # add the decorator @types if the arguments are annotated
-        annotated_args = []
-        for a in arguments:
-            annotated_args.append(a.annotation)
-
         for d in self._visit(stmt.decorator_list):
             tmp_var = d if isinstance(d, PyccelSymbol) else d.funcdef
             if tmp_var in decorators:
@@ -696,109 +730,175 @@ def fill_types(ls):
         if 'types' in decorators:
             warnings.warn("The @types decorator will be removed in a future version of Pyccel. Please use type hints. The @template decorator can be used to specify multiple types", FutureWarning)
 
-        if all(not isinstance(a, Nil) for a in annotated_args):
-            if stmt.returns:
-                returns = FunctionCallArgument(self._visit(stmt.returns), keyword='results')
-                annotated_args.append(returns)
-            decorators.setdefault('types', []).append(FunctionCall('types', annotated_args))
-
-        if 'bypass' in decorators:
-            return EmptyNode()
-
         if 'stack_array' in decorators:
             decorators['stack_array'] = tuple(str(b.value) for a in decorators['stack_array']
                 for b in a.args)
 
         if 'allow_negative_index' in decorators:
             decorators['allow_negative_index'] = tuple(str(b.value) for a in decorators['allow_negative_index'] for b in a.args)
+
+        if 'pure' in decorators:
+            is_pure = True
+
+        if 'elemental' in decorators:
+            is_elemental = True
+            if len(arguments) > 1:
+                errors.report(FORTRAN_ELEMENTAL_SINGLE_ARGUMENT,
+                              symbol=decorators['elemental'],
+                              bounding_box=(stmt.lineno, stmt.col_offset),
+                              severity='error')
+
+        if 'private' in decorators:
+            is_private = True
+
+        if 'inline' in decorators:
+            is_inline = True
+
         template['template_dict'] = {}
         # extract the templates
         if 'template' in decorators:
-            for comb_types in decorators['template']:
-                cache.clear_cache()
-                types = []
-                if len(comb_types.args) != 2:
+            for template_decorator in decorators['template']:
+                dec_args = template_decorator.args
+                if len(dec_args) != 2:
                     msg = 'Number of Arguments provided to the template decorator is not valid'
-                    errors.report(msg,
-                                    symbol = comb_types,
-                                    bounding_box = (stmt.lineno, stmt.col_offset),
+                    errors.report(msg, symbol = template_decorator,
                                     severity='error')
 
-                for i in comb_types.args:
-                    if i.has_keyword and i.keyword not in ('name', 'types'):
-                        msg = 'Argument provided to the template decorator is not valid'
-                        errors.report(msg,
-                                        symbol = comb_types,
-                                        bounding_box = (stmt.lineno, stmt.col_offset),
-                                        severity='error')
-                if all(i.has_keyword for i in comb_types.args):
-                    tp_name, ls = (comb_types.args[0].value, comb_types.args[1].value) if\
-                            comb_types.args[0].keyword == 'name' else\
-                            (comb_types.args[1].value, comb_types.args[0].value)
+                if any(i.keyword not in (None, 'name', 'types') for i in dec_args):
+                    errors.report('Argument provided to the template decorator is not valid',
+                                    symbol = template_decorator, severity='error')
+
+                if dec_args[0].has_keyword and dec_args[0].keyword != 'name':
+                    type_name = dec_args[1].value.python_value
+                    type_descriptors = dec_args[0].value
                 else:
-                    tp_name = comb_types.args[0].value
-                    ls = comb_types.args[1].value
-                try:
-                    tp_name = str(tp_name)
-                    ls = ls if isinstance(ls, PythonTuple) else list(ls)
-                except TypeError:
-                    msg = 'Argument provided to the template decorator is not valid'
-                    errors.report(msg,
-                                    symbol = comb_types,
-                                    bounding_box = (stmt.lineno, stmt.col_offset),
-                                    severity='fatal')
+                    type_name = dec_args[0].value.python_value
+                    type_descriptors = dec_args[1].value
 
-                types = fill_types(ls)
+                if not isinstance(type_descriptors, (PythonTuple, PythonList)):
+                    type_descriptors = PythonTuple(type_descriptors)
+
+                if type_name in template['template_dict']:
+                    errors.report(f'The template "{type_name}" is duplicated',
+                                symbol = template_decorator, severity='warning')
+
+                possible_types = self._treat_type_annotation(template_decorator, type_descriptors.args)
 
-                txt  = '#$ header template ' + tp_name
-                txt += '(' + '|'.join(types) + ')'
-                if tp_name in template['template_dict']:
-                    msg = 'The template "{}" is duplicated'.format(tp_name)
-                    errors.report(msg,
-                                bounding_box = (stmt.lineno, stmt.col_offset),
-                                severity='warning')
                 # Make templates decorator dict accessible from decorators dict
-                template['template_dict'][tp_name] = hdr_parse(stmts=txt)
+                template['template_dict'][type_name] = possible_types
+
             # Make template decorator list accessible from decorators dict
             template['decorator_list'] = decorators['template']
             decorators['template'] = template
 
         if not template['template_dict']:
             decorators['template'] = None
+
+        argument_annotations = [a.annotation for a in arguments]
+        result_annotation = self._treat_type_annotation(stmt, self._visit(stmt.returns))
+
+        #---------------------------------------------------------------------------------------------------------
+        #                   To remove when headers are deprecated
+        #---------------------------------------------------------------------------------------------------------
+        if headers:
+            warnings.warn("Support for specifying types via headers will be removed in a " +
+                          "future version of Pyccel. Please use type hints. The @template " +
+                          "decorator can be used to specify multiple types. See the " +
+                          "documentation at " +
+                          "https://github.com/pyccel/pyccel/blob/devel/docs/quickstart.md#type-annotations " +
+                          "for examples.", FutureWarning)
+            if any(a is not None for a in argument_annotations):
+                errors.report("Type annotations and type specification via headers should not be mixed",
+                        symbol=stmt, severity='error')
+
+            for i, _ in enumerate(argument_annotations):
+                argument_annotations[i] = UnionTypeAnnotation()
+            if result_annotation is not None:
+                errors.report("Type annotations and type specification via headers should not be mixed",
+                            symbol=stmt, severity='error')
+
+            n_results = 0
+
+            for head in headers:
+                if len(head.dtypes) != len(argument_annotations):
+                    errors.report(f"Wrong number of types in header for function {name}",
+                            severity='error', symbol=stmt)
+                else:
+                    for i,arg in enumerate(head.dtypes):
+                        argument_annotations[i].add_type(arg)
+                if head.results:
+                    if result_annotation is None:
+                        result_annotation = head.results
+                    else:
+                        if len(result_annotation) != len(head.results):
+                            errors.report("Different length results in headers.",
+                                    severity='error', symbol=stmt)
+                        else:
+                            result_annotation = tuple(UnionTypeAnnotation(r, *getattr(t, 'type_list', [t])) \
+                                                        for r,t in zip(head.results, result_annotation))
+                    n_results += 1
+
+            if n_results and n_results != len(headers):
+                errors.report("Results have only been provided for some of the headers. The types of the results will all be chosen from the provided types. This may result in unexpected result types.",
+                        severity='warning', symbol=stmt)
+
         # extract the types to construct a header
         if 'types' in decorators:
-            for comb_types in decorators['types']:
-
-                cache.clear_cache()
-                results = []
-                ls = comb_types.args
-
-                if len(ls) > 0 and ls[-1].has_keyword:
-                    arg_name = ls[-1].keyword
-                    if not arg_name == 'results':
-                        msg = 'Argument "{}" provided to the types decorator is not valid'.format(arg_name)
-                        errors.report(msg,
-                                    symbol = comb_types,
+            if any(a is not None for a in argument_annotations):
+                errors.report("Type annotations and type specification via headers should not be mixed",
+                        symbol=stmt, severity='error')
+
+            for i, _ in enumerate(argument_annotations):
+                argument_annotations[i] = UnionTypeAnnotation()
+            n_results = 0
+
+            for types_decorator in decorators['types']:
+                type_args = types_decorator.args
+
+                args = [a for a in type_args if not a.has_keyword]
+                kwargs = [a for a in type_args if a.has_keyword]
+
+                if len(kwargs) > 1:
+                    errors.report('Too many keyword arguments passed to @types decorator',
+                                symbol = types_decorator,
+                                bounding_box = (stmt.lineno, stmt.col_offset),
+                                severity='error')
+                elif kwargs:
+                    if kwargs[0].keyword != 'results':
+                        errors.report('Wrong keyword argument passed to @types decorator',
+                                    symbol = types_decorator,
                                     bounding_box = (stmt.lineno, stmt.col_offset),
                                     severity='error')
+                    annots = self._treat_type_annotation(kwargs[0], kwargs[0].value.args)
+                    if result_annotation is None:
+                        result_annotation = annots
                     else:
-                        container = ls[-1].value
-                        container = container if isinstance(container, PythonTuple) else [container]
-                        results = fill_types(container)
-                    types = fill_types(ls[:-1])
+                        if len(result_annotation) != len(annots):
+                            errors.report("Different length results in headers.",
+                                    severity='error', symbol=stmt)
+                        else:
+                            result_annotation = tuple(UnionTypeAnnotation(r, *getattr(t, 'type_list', [t])) \
+                                                        for r,t in zip(annots, result_annotation))
+                    n_results += 1
+
+                if len(args) != len(argument_annotations):
+                    errors.report(f"Wrong number of types in header for function {name}",
+                            severity='error', symbol=stmt)
                 else:
-                    types = fill_types(ls)
+                    for i,arg in enumerate(args):
+                        argument_annotations[i].add_type(self._treat_type_annotation(arg, arg.value))
 
-                txt  = '#$ header '
-                if len(self._context) > 1 and isinstance(self._context[-2], ast.ClassDef):
-                    txt += 'method '
-                txt += name + '(' + ','.join(types) + ')'
+            if n_results and n_results != len(decorators['types']):
+                errors.report("Results have only been provided for some of the types decorators. The types of the results will all be chosen from the provided types. This may result in unexpected result types.",
+                        severity='warning', symbol=stmt)
 
-                if results:
-                    txt += ' results(' + ','.join(results) + ')'
+        #---------------------------------------------------------------------------------------------------------
+        #                   End of : To remove when headers are deprecated
+        #---------------------------------------------------------------------------------------------------------
 
-                header = hdr_parse(stmts=txt)
-                headers += [header]
+        # Repack AnnotatedPyccelSymbols to insert argument_annotations from headers or types decorators
+        arguments = [FunctionDefArgument(AnnotatedPyccelSymbol(a.var.name, annot), annotation=annot, value=a.value, kwonly=a.is_kwonly)
+                           for a, annot in zip(arguments, argument_annotations)]
 
         body = stmt.body
 
@@ -812,28 +912,13 @@ def fill_types(ls):
 
         else:
             body = self._visit(body)
+
+        # Collect docstring
         if len(body) > 0 and isinstance(body[0], CommentBlock):
             doc_string = body[0]
             doc_string.header = ''
             body = body[1:]
 
-        if 'pure' in decorators:
-            is_pure = True
-
-        if 'elemental' in decorators:
-            is_elemental = True
-            if len(arguments) > 1:
-                errors.report(FORTRAN_ELEMENTAL_SINGLE_ARGUMENT,
-                              symbol=decorators['elemental'],
-                              bounding_box=(stmt.lineno, stmt.col_offset),
-                              severity='error')
-
-        if 'private' in decorators:
-            is_private = True
-
-        if 'inline' in decorators:
-            is_inline = True
-
         body = CodeBlock(body)
 
         returns = [i.expr for i in body.get_attribute_nodes(Return,
@@ -848,7 +933,10 @@ def fill_types(ls):
 
         local_symbols = self.scope.local_used_symbols
 
-        for r in zip(*returns):
+        if result_annotation and not isinstance(result_annotation, tuple):
+            result_annotation = [result_annotation]
+
+        for i,r in enumerate(zip(*returns)):
             r0 = r[0]
 
             pyccel_symbol  = isinstance(r0, PyccelSymbol)
@@ -860,7 +948,11 @@ def fill_types(ls):
             else:
                 result_name, result_counter = self.scope.get_new_incremented_symbol('Out', result_counter)
 
-            results.append(FunctionDefResult(result_name))
+            if result_annotation:
+                result_name = AnnotatedPyccelSymbol(result_name, annotation = result_annotation[i])
+
+            results.append(FunctionDefResult(result_name, annotation = result_annotation))
+            results[-1].set_fst(stmt)
 
         self.exit_function_scope()
 
@@ -870,13 +962,11 @@ def fill_types(ls):
                arguments,
                results,
                body,
-               global_vars=global_vars,
                is_pure=is_pure,
                is_elemental=is_elemental,
                is_private=is_private,
                imports=imports,
                decorators=decorators,
-               headers=headers,
                doc_string=doc_string,
                scope=scope)
 
@@ -936,19 +1026,24 @@ def _visit_Index(self, stmt):
 
     def _visit_Attribute(self, stmt):
         val  = self._visit(stmt.value)
-        if self._in_lhs_assign:
-            self.scope.insert_symbol(stmt.attr)
         attr = PyccelSymbol(stmt.attr)
-        return DottedName(val, attr)
+        dotted = DottedName(val, attr)
+        if self._in_lhs_assign:
+            self.scope.insert_symbol(dotted)
+        return dotted
 
 
     def _visit_Call(self, stmt):
 
         args = []
         if stmt.args:
-            args += [FunctionCallArgument(self._visit(a)) for a in stmt.args]
+            args += [FunctionCallArgument(self._visit(a), fst=a) for a in stmt.args]
         if stmt.keywords:
-            args += self._visit(stmt.keywords)
+            kwargs = self._visit(stmt.keywords)
+            for k, a in zip(kwargs, stmt.keywords):
+                k.set_fst(a)
+
+            args += kwargs
 
         if len(args) == 0:
             args = ()
diff --git a/pyccel/parser/syntax/headers.py b/pyccel/parser/syntax/headers.py
index 8eb327733d..b848411a1c 100644
--- a/pyccel/parser/syntax/headers.py
+++ b/pyccel/parser/syntax/headers.py
@@ -64,58 +64,43 @@ def expr(self):
         return d_var
 
 class TemplateStmt(BasicStmt):
-    """Base class representing a  template in the grammar."""
-    def __init__(self, **kwargs):
-        self.dtypes  = kwargs.pop('dtypes')
-        self.name   = kwargs.pop('name')
-        BasicStmt.__init__(self)
+    """
+    Base class representing a template in the grammar.
+
+    Base class representing a template in the grammar.
+    To be removed when header support is deprecated.
+
+    Parameters
+    ----------
+    name : str
+        The name of the template type symbol.
+    dtypes : list of str
+        A list of the types that the template describes.
+    **kwargs : dict
+        The textx arguments.
+    """
+    def __init__(self, *, name, dtypes, **kwargs):
+        self.dtypes = dtypes
+        self.name   = name
+        super().__init__(**kwargs)
 
     @property
     def expr(self):
+        """
+        Get the Pyccel equivalent of this object.
+
+        Get the Pyccel equivalent of this object.
+        """
         if any(isinstance(d_type, FuncType) for d_type in self.dtypes):
             msg = 'Functions in a template are not supported yet'
             errors.report(msg,
                         severity='error')
             return EmptyNode()
 
-        possible_dtypes = {tuple(t.expr.items())  for t in self.dtypes}
-        dtypes = tuple(dict(d_type) for d_type in possible_dtypes)
+        dtypes = {SyntacticTypeAnnotation.build_from_textx(t)  for t in self.dtypes}
         return Template(self.name, dtypes)
 
-class ListType(BasicStmt):
-    """Base class representing a  ListType in the grammar."""
-
-    def __init__(self, **kwargs):
-        """
-        Constructor for a TypeHeader.
-
-        dtype: list of str
-        """
-        self.dtype = kwargs.pop('dtype')
-
-        super(ListType, self).__init__(**kwargs)
-
-    @property
-    def expr(self):
-        dtypes = [str(i.expr['datatype']) for i in self.dtype]
-        precisions = [i.expr['precision'] for i in self.dtype]
-        if not (all(dtypes[0]==i for i in dtypes)):
-            raise TypeError('all element of the TypeList must have the same type')
-
-        d_var = {}
-        d_var['datatype'] = str(dtypes[0])
-        d_var['rank'] = len(dtypes)
-        d_var['memory_handling'] = 'alias' if len(dtypes) > 0 else 'stack'
-        d_var['precision'] = max(precisions)
-        d_var['order'] = 'C'
-        d_var['is_func'] = False
-        d_var['is_const'] = False
-        if not(d_var['precision']):
-            if d_var['datatype'] in ['double','float','complex','int']:
-                d_var['precision'] = default_precision[d_var['datatype']]
-        return d_var
-
-class Type(BasicStmt):
+class VariableType(BasicStmt):
     """
     Base class representing a header type in the grammar.
 
@@ -126,7 +111,7 @@ class Type(BasicStmt):
     dtype : str
         The variable type.
 
-    prec : int
+    prec : int, optional
         The precision of the object.
 
     trailer : iterable, TrailerSubscriptsList
@@ -138,47 +123,15 @@ class Type(BasicStmt):
         The textx arguments.
     """
 
-    def __init__(self, dtype, prec, trailer = (), **kwargs):
+    def __init__(self, dtype, prec = None, trailer = (), **kwargs):
         self.dtype   = dtype
         self.prec    = prec
-        self.trailer = trailer
-
-        super(Type, self).__init__(**kwargs)
-
-    @property
-    def expr(self):
-        """
-        Get the dictionary describing the type.
-
-        Get the dictionary describing the type.
-        """
-        dtype = self.dtype
-        precision = self.prec
-        if dtype in dtype_registry.keys():
-            dtype,precision = dtype_registry[dtype]
-        trailer = self.trailer
-        order = 'C'
-
         if trailer:
-            if trailer.order:
-                order = str(trailer.order)
-            trailer = [str(i) for i in trailer.args]
+            self.trailer = trailer
         else:
-            trailer = []
-        d_var={}
-        d_var['datatype']=dtype
-        d_var['rank'] = len(trailer)
-        d_var['memory_handling'] = 'heap' if len(trailer) > 0 else 'stack'
-        d_var['precision']  = precision
-        d_var['is_func'] = False
-        d_var['is_const'] = False
-        if not(precision):
-            if dtype in NativeNumeric:
-                d_var['precision'] = default_precision[dtype]
-
-        if d_var['rank']>1:
-            d_var['order'] = order
-        return d_var
+            self.trailer = []
+
+        super().__init__(**kwargs)
 
 class ShapedID(BasicStmt):
     """class representing a ShapedID in the grammar.
@@ -207,30 +160,58 @@ def expr(self):
 
         return d_var
 
-class TypeHeader(BasicStmt):
-    pass
-
 class StringStmt(BasicStmt):
-    def __init__(self, **kwargs):
-        self.arg = kwargs.pop('arg')
+    """
+    Class describing a string in a macro.
+
+    Class describing a string in a macro.
+    To be removed when macro support is deprecated.
+
+    Parameters
+    ----------
+    arg : str
+        The string.
+    **kwargs : dict
+        TextX keyword arguments.
+    """
+    def __init__(self, arg, **kwargs):
+        self.arg = arg
+        super().__init__(**kwargs)
     @property
     def expr(self):
         return LiteralString(str(self.arg))
 
 class UnionTypeStmt(BasicStmt):
-    def __init__(self, **kwargs):
-        """
-        Constructor for a TypeHeader.
+    """
+    Class describing a union of possible types.
 
-        dtype: list fo str
-        """
-        self.dtypes = kwargs.pop('dtypes')
-        self.const = kwargs.pop('const')
+    A class object describing a union of possible types described in a type descriptor.
+    These types are either VariableTypes or FuncTypes.
 
-        super(UnionTypeStmt, self).__init__(**kwargs)
+    Parameters
+    ----------
+    dtypes : list of VariableHeader | FuncHeader
+        A list of the possible types described.
+    const : bool
+        A boolean indicating if the generated object will be constant or
+        modifiable.
+    **kwargs : dict
+        TextX keyword arguments.
+    """
+    def __init__(self, dtypes, const = None, **kwargs):
+        self.dtypes = list(dtypes)
+        self.const = const
+
+        super().__init__(**kwargs)
 
     @property
     def expr(self):
+        """
+        Get the Pyccel equivalent of this object.
+
+        Get the Pyccel equivalent of this object.
+        To be removed when header support is deprecated.
+        """
         dtypes = [i.expr for i in self.dtypes]
         if self.const:
             for d_type in dtypes:
@@ -248,21 +229,33 @@ def expr(self):
         return UnionType(dtypes)
 
 class HeaderResults(BasicStmt):
-    """Base class representing a HeaderResults in the grammar."""
+    """
+    Base class representing a HeaderResults in the grammar.
 
-    def __init__(self, **kwargs):
-        """
-        Constructor for a HeaderResults.
+    Base class representing a HeaderResults in the grammar.
+    To be removed when header support is deprecated.
 
-        decs: list of TypeHeader
-        """
-        self.decs = kwargs.pop('decs')
+    Parameters
+    ----------
+    decs : list of VariableHeader | FuncHeader
+        List of TypeHeaders defining the return type(s).
+    **kwargs : dict
+        TextX keyword arguments.
+    """
 
-        super(HeaderResults, self).__init__(**kwargs)
+    def __init__(self, decs, **kwargs):
+        self.decs = decs
+
+        super().__init__(**kwargs)
 
     @property
     def expr(self):
-        decs = [i.expr for i in self.decs]
+        """
+        Get the Pyccel equivalent of this object.
+
+        Get the Pyccel equivalent of this object.
+        """
+        decs = SyntacticTypeAnnotation.build_from_textx(self.decs)
         return decs
 
 
@@ -306,58 +299,61 @@ def expr(self):
         return AnnotatedPyccelSymbol(self.name, annotation=dtype)
 
 class FunctionHeaderStmt(BasicStmt):
-    """Base class representing a function header statement in the grammar."""
+    """
+    Base class representing a function header statement in the grammar.
 
-    def __init__(self, **kwargs):
-        """
-        Constructor for a FunctionHeader statement
+    Base class representing a function header statement in the grammar.
+    To be removed when header support is deprecated.
 
-        name: str
-            function name
-        kind: str
-            one among {function, method}
-        decs: list, tuple
-            list of argument types
-        results: list, tuple
-            list of output types
-        """
-        self.name = kwargs.pop('name')
-        self.kind = kwargs.pop('kind', None)
-        self.static = kwargs.pop('static', None)
-        self.decs = kwargs.pop('decs')
-        self.results = kwargs.pop('results', None)
+    Parameters
+    ----------
+    name : str
+        Function name.
+    decs : list, tuple
+        List of argument types.
+    kind : str
+        One among {function, method}.
+    static : bool
+        Indicates if the function is a static function.
+    results : list, tuple
+        List of output types.
+    **kwargs : dict
+        Arguments defined by textx.
+    """
 
-        super(FunctionHeaderStmt, self).__init__(**kwargs)
+    def __init__(self, name, decs, kind=None, static = None, results=None, **kwargs):
+        self.name = name
+        self.kind = kind
+        self.static = static
+        self.decs = decs
+        self.results = results
+
+        super().__init__(**kwargs)
 
     @property
     def expr(self):
-        # TODO: do we need dtypes and results to be attributs of the class?
-        dtypes = []
-        for dec in self.decs:
-            if isinstance(dec,UnionTypeStmt):
-                dtypes += [dec.expr]
+        """
+        Get the Pyccel equivalent of this object.
+
+        Get the Pyccel equivalent of this object.
+        """
+        dtypes = SyntacticTypeAnnotation.build_from_textx(self.decs)
 
         if self.kind is None:
             kind = 'function'
         else:
             kind = str(self.kind)
 
-        is_static = False
-        if self.static == 'static':
-            is_static = True
+        is_static = self.static
 
         results = []
         if self.results:
             results = self.results.expr
 
         if kind == 'method':
-            dtype = dtypes[0]
-            if isinstance(dtype, UnionType):
-                cls_instance = dtype.args[0]['datatype']
-            else:
-                cls_instance = dtype['datatype']
-            dtypes = dtypes[1:] # remove the attribut
-            return MethodHeader((cls_instance, self.name), dtypes, [] )
+            return MethodHeader(self.name, dtypes,
+                                  results=results,
+                                  is_static=is_static)
         else:
             return FunctionHeader(self.name,
                                   dtypes,
@@ -556,8 +552,7 @@ def expr(self):
 #################################################
 # whenever a new rule is added in the grammar, we must update the following
 # lists.
-hdr_classes = [Header, TypeHeader,
-               Type, ListType, UnionTypeStmt, FuncType,
+hdr_classes = [Header, VariableType, UnionTypeStmt, FuncType,
                ShapedID,
                HeaderResults,
                FunctionHeaderStmt,
@@ -574,8 +569,10 @@ def expr(self):
 
 # Get meta-model from language description
 grammar = join(this_folder, '../grammar/headers.tx')
+types_grammar = join(this_folder, '../grammar/types.tx')
 
 meta = metamodel_from_file(grammar, classes=hdr_classes)
+types_meta = metamodel_from_file(types_grammar)
 
 def parse(filename=None, stmts=None):
     """ Parse header pragmas
diff --git a/pyccel/stdlib/external/mpi4py.py b/pyccel/stdlib/external/mpi4py.py
index ece38a8c0b..ce27510adb 100644
--- a/pyccel/stdlib/external/mpi4py.py
+++ b/pyccel/stdlib/external/mpi4py.py
@@ -70,7 +70,6 @@
 
 #===================================================================================
 
-#$ header class MPI_(public)
 #$ header method __init__(MPI_)
 
 class MPI_:
diff --git a/pyccel/stdlib/internal/lapack.pyh b/pyccel/stdlib/internal/lapack.pyh
index 4fb2e47be1..4478bf3e1d 100644
--- a/pyccel/stdlib/internal/lapack.pyh
+++ b/pyccel/stdlib/internal/lapack.pyh
@@ -283,7 +283,7 @@
 #............................................................
 #Lineaire equation
 #............................................................
-#$ header function sgetrf(int32, int32, float3232[:,:](order=F), int32, int32 [:], int32)
+#$ header function sgetrf(int32, int32, float32[:,:](order=F), int32, int32 [:], int32)
 #$ header function dgetrf(int32, int32, float64 [:,:](order=F), int32, int32 [:], int32)
 
 #$ header function sgetrs(str, int32, int32, float32 [:,:](order=F), int32, float32 [:], float32 [:,:](order=F), int32, int32)
@@ -297,7 +297,7 @@
 #$ header function dgerfs(str, int32, int32, float64 [:,:](order=F), int32, float64 [:,:](order=F), int32, int32 [:], float64 [:,:](order=F), int32, float64 [:,:](order=F), int32, float64 [:], float64 [:], float64 [:], int32 [:], int32)
 
 
-#$ header function sgetri(int32, float3232[:,:](order=F), int32, int32 [:], float3232[:], int32, int32)
+#$ header function sgetri(int32, float32[:,:](order=F), int32, int32 [:], float32[:], int32, int32)
 #$ header function dgetri(int32, float64 [:,:](order=F), int32, int32 [:], float64 [:], int32, int32)
 
 


Test patch:
diff --git a/tests/codegen/fcode/scripts/precision.py b/tests/codegen/fcode/scripts/precision.py
deleted file mode 100644
index 3ef6eb26ba..0000000000
--- a/tests/codegen/fcode/scripts/precision.py
+++ /dev/null
@@ -1,35 +0,0 @@
-# pylint: disable=missing-function-docstring, missing-module-docstring
-# pylint: disable=unused-variable
-
-#$ header function incr_(int*8)
-def incr_(x):
-    #$ header function decr_(int*8)
-    def decr_(y):
-        y = y-1
-    x = x + 1
-
-#$ header function f1(int*8, int*16, int*4) results(int*16)
-def f1(x, n=2, m=3):
-    y = x - n*m
-    return y
-
-#$ header function f2(double*8, int*4) results(double*4)
-def f2(x, m=None):
-    if m is None:
-        y = x + 1
-    else:
-        y = x - 1
-    return y
-
-
-from numpy import float32, float64 , int32, int64, int as np_int, float as np_float, complex as np_complex, complex64, complex128
-
-x1 = np_int(6)
-x2 = int32(6)
-x3 = int64(6)
-y1 = np_float(6)
-y2 = float32(6)
-y3 = float64(6)
-z1 = np_complex(6)
-z2 = complex64(6)
-z3 = complex128(6)
diff --git a/tests/epyccel/modules/generic_functions.py b/tests/epyccel/modules/generic_functions.py
index 57d18437cd..d7122e387c 100644
--- a/tests/epyccel/modules/generic_functions.py
+++ b/tests/epyccel/modules/generic_functions.py
@@ -4,7 +4,6 @@
 #$ header function gen_2(real, int)
 #$ header function gen_2(int, real)
 #$ header function gen_4(T, T)
-#$ header function tmplt_head_1(int, real)
 #$ header template T(int|real)
 #$ header template R(int|real)
 #$ header template O(real|complex)
@@ -89,8 +88,7 @@ def tst_gen_7():
 def tst_tmplt_head_1():
     x = tmplt_head_1(5, 5)
     y = tmplt_head_1(5.5, 7.3)
-    z = tmplt_head_1(5, 5.56)
-    return x, y, z
+    return x, y
 
 def tst_local_overide_1():
     x = local_overide_1(5, 4)
diff --git a/tests/epyccel/modules/generic_functions_2.py b/tests/epyccel/modules/generic_functions_2.py
index 7b739a4d3e..2e326d8338 100644
--- a/tests/epyccel/modules/generic_functions_2.py
+++ b/tests/epyccel/modules/generic_functions_2.py
@@ -152,8 +152,6 @@ def mix_float_array_1(x : 'T', a : 'float'):
 def mix_complex_array_1(x : 'T', a : 'float'):
     x[:] *= a
 
-#$ header function dup_header(float)
-#$ header function dup_header(float64)
 @template('T', ['float', 'float64'])
 def dup_header(a : 'T'):
     return a
diff --git a/tests/epyccel/recognised_functions/test_numpy_funcs.py b/tests/epyccel/recognised_functions/test_numpy_funcs.py
index 7142630857..c1d2a9cf69 100644
--- a/tests/epyccel/recognised_functions/test_numpy_funcs.py
+++ b/tests/epyccel/recognised_functions/test_numpy_funcs.py
@@ -5183,15 +5183,8 @@ def where_wrapper(cond, arr1, arr2):
 def test_numpy_linspace_scalar(language):
     from numpy import linspace
 
-    @types('int', 'int', 'int')
-    @types('int8', 'int', 'int')
-    @types('int16', 'int', 'int')
-    @types('int32', 'int', 'int')
-    @types('int64', 'int', 'int')
-    @types('float', 'int', 'int')
-    @types('float32', 'int', 'int')
-    @types('float64', 'int', 'int')
-    def get_linspace(start, steps, num):
+    @template('T', ['int', 'int8', 'int16', 'int32', 'int64', 'float', 'float32', 'float64'])
+    def get_linspace(start : 'T', steps : int, num : int):
         from numpy import linspace
         stop = start + steps
         b = linspace(start, stop, num)
diff --git a/tests/epyccel/test_array_as_func_args.py b/tests/epyccel/test_array_as_func_args.py
index 91917c25c3..64246e685f 100644
--- a/tests/epyccel/test_array_as_func_args.py
+++ b/tests/epyccel/test_array_as_func_args.py
@@ -7,17 +7,14 @@
 from numpy.random import randint, uniform
 
 from pyccel.epyccel import epyccel
-from pyccel.decorators import types
+from pyccel.decorators import template
 
 int_types = ['int8', 'int16', 'int32', 'int64']
 float_types = ['float32', 'float64']
 
 def test_array_int_1d_scalar_add(language):
-    @types( 'int8[:]' , 'int8' , 'int')
-    @types( 'int16[:]', 'int16', 'int')
-    @types( 'int32[:]', 'int32', 'int')
-    @types( 'int64[:]', 'int64', 'int')
-    def array_int_1d_scalar_add( x, a, x_len ):
+    @template('T', ['int8', 'int16', 'int32', 'int64'])
+    def array_int_1d_scalar_add(x : 'T[:]', a : 'T', x_len : int):
         for i in range(x_len):
             x[i] += a
     f1 = array_int_1d_scalar_add
@@ -35,9 +32,8 @@ def array_int_1d_scalar_add( x, a, x_len ):
         assert np.array_equal( x1, x2 )
 
 def test_array_real_1d_scalar_add(language):
-    @types( 'float32[:]', 'float32', 'int')
-    @types( 'double[:]' , 'double' , 'int')
-    def array_real_1d_scalar_add( x, a, x_len ):
+    @template('T', ['float32', 'double'])
+    def array_real_1d_scalar_add(x : 'T[:]', a : 'T', x_len : int):
         for i in range(x_len):
             x[i] += a
     f1 = array_real_1d_scalar_add
@@ -55,9 +51,8 @@ def array_real_1d_scalar_add( x, a, x_len ):
         assert np.array_equal( x1, x2 )
 
 def test_array_complex_1d_scalar_add(language):
-    @types( 'complex64[:]' , 'complex64' , 'int')
-    @types( 'complex128[:]' , 'complex128' , 'int')
-    def array_complex_1d_scalar_add( x, a, x_len ):
+    @template('T', ['complex64', 'complex128'])
+    def array_complex_1d_scalar_add(x : 'T[:]', a : 'T', x_len : int):
         for i in range(x_len):
             x[i] += a
     f1 = array_complex_1d_scalar_add
@@ -77,11 +72,8 @@ def array_complex_1d_scalar_add( x, a, x_len ):
         assert np.array_equal( x1, x2 )
 
 def test_array_int_2d_scalar_add(language):
-    @types( 'int8[:,:]' , 'int8' , 'int', 'int')
-    @types( 'int16[:,:]', 'int16', 'int', 'int')
-    @types( 'int32[:,:]', 'int32', 'int', 'int')
-    @types( 'int64[:,:]', 'int64', 'int', 'int')
-    def array_int_2d_scalar_add( x, a, d1, d2 ):
+    @template('T', ['int8', 'int16', 'int32', 'int64'])
+    def array_int_2d_scalar_add( x : 'T[:,:]', a : 'T', d1 : int, d2 : int):
         for i in range(d1):
             for j in range(d2):
                 x[i, j] += a
@@ -101,9 +93,8 @@ def array_int_2d_scalar_add( x, a, d1, d2 ):
         assert np.array_equal( x1, x2 )
 
 def test_array_real_2d_scalar_add(language):
-    @types( 'float32[:,:]', 'float32', 'int', 'int')
-    @types( 'double[:,:]' , 'double' , 'int', 'int')
-    def array_real_2d_scalar_add( x, a, d1, d2 ):
+    @template('T', ['float32', 'double'])
+    def array_real_2d_scalar_add(x : 'T[:,:]', a : 'T', d1 : int, d2 : int):
         for i in range(d1):
             for j in range(d2):
                 x[i, j] += a
@@ -123,9 +114,8 @@ def array_real_2d_scalar_add( x, a, d1, d2 ):
         assert np.array_equal( x1, x2 )
 
 def test_array_complex_2d_scalar_add(language):
-    @types( 'complex64[:,:]' , 'complex64' , 'int', 'int')
-    @types( 'complex128[:,:]' , 'complex128' , 'int', 'int')
-    def array_complex_2d_scalar_add( x, a, d1, d2 ):
+    @template('T', ['complex64', 'complex128'])
+    def array_complex_2d_scalar_add(x : 'T[:,:]', a : 'T', d1 : int, d2 : int):
         for i in range(d1):
             for j in range(d2):
                 x[i, j] += a
diff --git a/tests/epyccel/test_class_expressions.py b/tests/epyccel/test_class_expressions.py
index d3ad842f18..09bb42431b 100644
--- a/tests/epyccel/test_class_expressions.py
+++ b/tests/epyccel/test_class_expressions.py
@@ -32,6 +32,19 @@ def f(a : 'complex', b : 'complex'):
     assert r == epyc_r
     assert isinstance(r, type(epyc_r))
 
+def test_float_imag(language):
+    def f():
+        a = 1.5
+        return a.imag
+
+    epyc_f = epyccel(f, language=language)
+
+    r = f()
+    epyc_r = epyc_f()
+
+    assert r == epyc_r
+    assert isinstance(r, type(epyc_r))
+
 def test_complex_real(language):
     def f():
         a = 1+2j
diff --git a/tests/epyccel/test_epyccel_functions.py b/tests/epyccel/test_epyccel_functions.py
index 5b6734d478..bdc8bd1db0 100644
--- a/tests/epyccel/test_epyccel_functions.py
+++ b/tests/epyccel/test_epyccel_functions.py
@@ -1,5 +1,6 @@
 # pylint: disable=missing-function-docstring, missing-module-docstring
 # coding: utf-8
+import sys
 
 import pytest
 import numpy as np
@@ -343,6 +344,20 @@ def f22(a : 'complex128'):
     f = epyccel(f22, language=language)
     assert f(np.complex128(1+ 2.2j)) == f22(np.complex128(1+ 2.2j))
 
+@pytest.mark.skipif(sys.version_info < (3, 10), reason="PEP604 (writing union types as X | Y) implemented in Python 3.10")
+def test_union_type(language):
+    def square(a : int | float): #pylint: disable=unsupported-binary-operation
+        return a*a
+
+    f = epyccel(square, language=language)
+    x = np.random.randint(40)
+    y = np.random.uniform()
+
+    assert np.isclose(f(x), square(x), rtol=RTOL, atol=ATOL)
+    assert isinstance(f(x), type(square(x)))
+    assert np.isclose(f(y), square(y), rtol=RTOL, atol=ATOL)
+    assert isinstance(f(y), type(square(y)))
+
 ##==============================================================================
 ## CLEAN UP GENERATED FILES AFTER RUNNING TESTS
 ##==============================================================================
diff --git a/tests/epyccel/test_epyccel_headers.py b/tests/epyccel/test_epyccel_headers.py
index 6f4fb413bb..04f3d1c889 100644
--- a/tests/epyccel/test_epyccel_headers.py
+++ b/tests/epyccel/test_epyccel_headers.py
@@ -61,15 +61,3 @@ def stack_array_annotation():
 
     assert epyc_stack_array_annotation() == stack_array_annotation()
     assert isinstance(epyc_stack_array_annotation(), type(stack_array_annotation()))
-
-@pytest.mark.skip(reason="Failing due to #1527")
-def test_class_annoation(language):
-    def class_annotation():
-        class A:
-            def __init__(self : 'A'):
-                pass
-
-        #$ header variable myA A
-        myA = A() #pylint: disable=unused-variable
-
-    epyccel(class_annotation, language=language)
diff --git a/tests/errors/semantic/blocking/INTERFACE_WRONG_TYPES.py b/tests/errors/semantic/blocking/INTERFACE_WRONG_TYPES.py
index 5b8b57ed5a..cda405b542 100644
--- a/tests/errors/semantic/blocking/INTERFACE_WRONG_TYPES.py
+++ b/tests/errors/semantic/blocking/INTERFACE_WRONG_TYPES.py
@@ -1,9 +1,9 @@
 # pylint: disable=missing-function-docstring, missing-module-docstring
 from pyccel.decorators import template
 
-@template('z', types=['int', 'real'])
-@template('y', types=['int', 'real'])
-def multi_tmplt_1(x : 'z', y : 'z', z : 'y'):
+@template('a', types=['int', 'real'])
+@template('b', types=['int', 'real'])
+def multi_tmplt_1(x : 'a', y : 'a', z : 'b'):
     """Tests Interfaces"""
     return x + y + z
 
diff --git a/tests/errors/semantic/non_blocking/UNION_RETURN_TYPE.py b/tests/errors/semantic/non_blocking/UNION_RETURN_TYPE.py
new file mode 100644
index 0000000000..32d279be35
--- /dev/null
+++ b/tests/errors/semantic/non_blocking/UNION_RETURN_TYPE.py
@@ -0,0 +1,5 @@
+# pylint: disable=missing-function-docstring, missing-module-docstring
+
+
+def f(a : int | float, b : int | float) -> int | float: #pylint: disable=unsupported-binary-operation
+    return a+b
diff --git a/tests/errors/semantic/non_blocking/UNION_RETURN_TYPE_headers.py b/tests/errors/semantic/non_blocking/UNION_RETURN_TYPE_headers.py
new file mode 100644
index 0000000000..7e109a1cc9
--- /dev/null
+++ b/tests/errors/semantic/non_blocking/UNION_RETURN_TYPE_headers.py
@@ -0,0 +1,7 @@
+# pylint: disable=missing-function-docstring, missing-module-docstring
+
+#$ header function f(int, int) results(int)
+#$ header function f(float, float) results(float)
+
+def f(a, b):
+    return a+b
diff --git a/tests/errors/semantic/non_blocking/UNION_RETURN_TYPE_types.py b/tests/errors/semantic/non_blocking/UNION_RETURN_TYPE_types.py
new file mode 100644
index 0000000000..70ced98b53
--- /dev/null
+++ b/tests/errors/semantic/non_blocking/UNION_RETURN_TYPE_types.py
@@ -0,0 +1,7 @@
+# pylint: disable=missing-function-docstring, missing-module-docstring
+from pyccel.decorators import types
+
+@types('int', 'int', results=['int'])
+@types('float', 'float', results=['float'])
+def f(a, b):
+    return a+b
diff --git a/tests/errors/syntax_errors/functions_in_uniontype.py b/tests/errors/semantic/non_blocking/functions_in_uniontype.py
similarity index 100%
rename from tests/errors/syntax_errors/functions_in_uniontype.py
rename to tests/errors/semantic/non_blocking/functions_in_uniontype.py
diff --git a/tests/warnings/semantic/DECORATOR_WRONG_NUMBER_TYPES.py b/tests/errors/syntax_errors/DECORATOR_WRONG_NUMBER_TYPES.py
similarity index 100%
rename from tests/warnings/semantic/DECORATOR_WRONG_NUMBER_TYPES.py
rename to tests/errors/syntax_errors/DECORATOR_WRONG_NUMBER_TYPES.py
diff --git a/tests/warnings/semantic/HEADER_WRONG_NUMBER_TYPES.py b/tests/errors/syntax_errors/HEADER_WRONG_NUMBER_TYPES.py
similarity index 100%
rename from tests/warnings/semantic/HEADER_WRONG_NUMBER_TYPES.py
rename to tests/errors/syntax_errors/HEADER_WRONG_NUMBER_TYPES.py
diff --git a/tests/pyccel/scripts/classes/class_headers.py b/tests/pyccel/scripts/classes/class_headers.py
new file mode 100644
index 0000000000..ca9235df86
--- /dev/null
+++ b/tests/pyccel/scripts/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/pyccel/scripts/default_args_mod.py b/tests/pyccel/scripts/default_args_mod.py
index c012718b29..2136e15c95 100644
--- a/tests/pyccel/scripts/default_args_mod.py
+++ b/tests/pyccel/scripts/default_args_mod.py
@@ -36,3 +36,6 @@ def recursivity(x : 'float', y  : 'float' =  0.0, z  : 'bool' =  None):
 
 def print_var(n : int = 0):
     print(n)
+
+def f7(*, x  : 'float' =  1.5, y  : 'float' =  2.5):
+    return x+y
diff --git a/tests/pyccel/scripts/functions.py b/tests/pyccel/scripts/functions.py
index 3edf0ef27b..0084e8ea25 100644
--- a/tests/pyccel/scripts/functions.py
+++ b/tests/pyccel/scripts/functions.py
@@ -1,8 +1,8 @@
 # pylint: disable=missing-function-docstring, missing-module-docstring
 import numpy as np
 
-def incr_(x : int):
-    x = x + 2
+def incr_(x : np.int32):
+    x = x + np.int32(2)
     return x
 
 def helloworld():
diff --git a/tests/pyccel/scripts/generic_functions.py b/tests/pyccel/scripts/generic_functions.py
index d4ea6add57..6a3f2fbe08 100644
--- a/tests/pyccel/scripts/generic_functions.py
+++ b/tests/pyccel/scripts/generic_functions.py
@@ -6,13 +6,13 @@
 #$ header function gen_2(float, int)
 #$ header function gen_2(int, float)
 #$ header function gen_4(T, T)
-#$ header function tmplt_head_1(int, float)
 #$ header template T(int|float)
 #$ header template R(int|float)
 #$ header template O(bool|complex)
 #$ header template S(int|float)
 
-def gen_1(a : 'float'):
+@types('float', results=['float'])
+def gen_1(a):
     return a * 10
 
 def gen_2(y, x):
@@ -63,7 +63,7 @@ def tmplt_tmplt_1(x : 'z', y : 'z', z : 'R'):
 def array_elem1(x):
     return x[0]
 
-@template('k', types=['int'])
+@template('k', types='int')
 @template('g', types=['int', 'float'])
 def multi_tmplt_2(y : 'k', z : 'g'):
     return y + z
@@ -133,8 +133,7 @@ def tst_multi_tmplt_1():
 def tst_tmplt_head_1():
     x = tmplt_head_1(5, 5)
     y = tmplt_head_1(5.5, 7.3)
-    z = tmplt_head_1(5, 5.56)
-    return x * y * z
+    return x * y
 
 def tst_local_overide_1():
     x = local_overide_1(5, 4)
diff --git a/tests/pyccel/scripts/runtest_default_args.py b/tests/pyccel/scripts/runtest_default_args.py
index ab161ecae3..ff4d79daa8 100644
--- a/tests/pyccel/scripts/runtest_default_args.py
+++ b/tests/pyccel/scripts/runtest_default_args.py
@@ -3,7 +3,7 @@
 import numpy as np
 
 if __name__ == '__main__':
-    from default_args_mod import f1, f5, f3, is_nil_default_arg, recursivity, print_var
+    from default_args_mod import f1, f5, f3, is_nil_default_arg, recursivity, print_var, f7
 
     print(f1(2))
     print(f1())
@@ -34,3 +34,8 @@
 
     print_var()
     print_var(5)
+
+    print(f7(x=19.2,y=6.7))
+    print(f7(x=4.5))
+    print(f7(y = 8.2))
+    print(f7())
diff --git a/tests/pyccel/test_pyccel.py b/tests/pyccel/test_pyccel.py
index d5d09f4727..e06079f6e2 100644
--- a/tests/pyccel/test_pyccel.py
+++ b/tests/pyccel/test_pyccel.py
@@ -569,7 +569,8 @@ def test_default_arguments(language):
             dependencies = "scripts/default_args_mod.py",
             output_dtype = [int, int, float, float, float,
                 float, float, float, float, bool, bool, bool,
-                float, float, float, float],
+                float, float, float, float, float, float,
+                float, float],
             language=language)
 
 #------------------------------------------------------------------------------
@@ -787,74 +788,6 @@ def test_array_binary_op(language):
     types += [int] * 8
     pyccel_test("scripts/array_binary_operation.py", language = language, output_dtype=types)
 
-#------------------------------------------------------------------------------
-@pytest.mark.parametrize( 'language', (
-        pytest.param("c", marks = pytest.mark.c),
-        pytest.param("fortran", marks = pytest.mark.fortran)
-        # Test does not make sense in pure python
-    )
-)
-def test_headers(language):
-    test_file = "scripts/runtest_headers.py"
-    test_file = os.path.normpath(test_file)
-    test_file = get_abs_path(test_file)
-
-    header_file = 'scripts/headers.pyh'
-    header_file = os.path.normpath(header_file)
-    header_file = get_abs_path(header_file)
-
-    with open(test_file, 'w') as f:
-        code = ("from headers import f\n"
-                "def f(x):\n"
-                "    y = x\n"
-                "    return y\n"
-                "if __name__ == '__main__':\n"
-                "    print(f(1))\n")
-
-        f.write(code)
-
-    with open(header_file, 'w') as f:
-        code =("#$ header metavar ignore_at_import=True\n"
-               "#$ header function f(int)")
-
-        f.write(code)
-
-    test_file = os.path.normpath(test_file)
-    cwd = os.path.dirname(test_file)
-    cwd = get_abs_path(cwd)
-
-    pyccel_commands = " --language="+language
-
-    compile_pyccel(cwd, test_file, pyccel_commands)
-
-    lang_out = get_lang_output(test_file, language)
-    assert int(lang_out) == 1
-
-    with open(test_file, 'w') as f:
-        code = ("from headers import f\n"
-                "def f(x):\n"
-                "    y = x\n"
-                "    return y\n"
-                "if __name__ == '__main__':\n"
-                "    print(f(1.5))\n")
-
-        f.write(code)
-
-    with open(header_file, 'w') as f:
-        code =("#$ header metavar ignore_at_import=True\n"
-               "#$ header function f(float)")
-
-        f.write(code)
-
-    compile_pyccel(cwd, test_file, pyccel_commands)
-
-    lang_out = get_lang_output(test_file, language)
-    assert float(lang_out) == 1.5
-
-    with open(test_file, 'w') as f:
-        code = ("")
-        f.write(code)
-
 #------------------------------------------------------------------------------
 def test_basic_header():
     filename='scripts/basic_header.pyh'
@@ -872,6 +805,7 @@ def test_basic_header():
                                         "scripts/classes/classes_5.py",
                                         "scripts/classes/classes_6.py",
                                         "scripts/classes/classes_7.py",
+                                        "scripts/classes/class_headers.py",
                                         ] )
 @pytest.mark.parametrize( 'language', (
         pytest.param("python", marks = pytest.mark.python),