Repository: pyccel/pyccel (language: python)

Issue:
Result type of numpy.sum
```python
def array_sum(a:'float32[:]|float64[:]'):
    return a.sum()
```
Using `numpy.sum`, this function should return a float with the precision of the array, but it always returns a `float64`.
Result type of numpy.sum
```python
def array_sum(a:'float32[:]|float64[:]'):
    return a.sum()
```
Using `numpy.sum`, this function should return a float with the precision of the array, but it always returns a `float64`.
Undefined Variable error when the function definition is after the variable declaration 
Pyccel raises the error ` |fatal [semantic]: test.py [3,14]| Undefined variable (x)` when pyccelizing the following code
```python
def f():
    def g():
        print(x)
    x = 1
    g()
f()
```
while it behaves correctly if we have the following code
```python
x = 2
def f():
    def g():
        print(x)
    x = 1
    g()
f()
```
In both cases, it should print `1` 


Discussion / hints:


This is quite a tricky case to handle. We can't parse `g()` ahead of time because `x` doesn't exist and we can't parse it after the `CodeBlock` as the `CodeBlock` will have a similar problem with `g()`. As a workaround for your code in the meantime I would recommend declaring objects that you want to use in the sub-functions. E.g:
```python
def f():
    x : int
    def g():
        print(x)
    x = 1
    g()
f()
```
one way to solve this issue is to pyccelize the function definitions at the end of the CodeBlock or when it is called
because this does not work in Python
```python
x = 2
def f():
    def g():
        print(x)
    g()
    x = 1
f()
``
> This is quite a tricky case to handle. We can't parse `g()` ahead of time because `x` doesn't exist and we can't parse it after the `CodeBlock` as the `CodeBlock` will have a similar problem with `g()`. As a workaround for your code in the meantime I would recommend declaring objects that you want to use in the sub-functions. E.g:
> 
> ```python
> def f():
>     x : int
>     def g():
>         print(x)
>     x = 1
>     g()
> f()
> ```

it is not a problem that I have, it is just a bug that I found when I was making some changes to Pyccel
As I said, at the end of the CodeBlock we will have the same issue with `g()`. It should be examined when it is called for the first time, but that is a bit tricky to implement
> As I said, at the end of the CodeBlock we will have the same issue with `g()`. It should be examined when it is called for the first time, but that is a bit tricky to implement

I might be able to do it 
Do you want to be assigned to the issue?
I'm working on another issue related to the `inline` decorator, I will assign the issue to myself if I can finish the other issue quickly 
It would also solve this issue
```python
def f():
    print(1)
    g()

def g():
    print(2)
    
f()
``

Fix patch:
diff --git a/.dict_custom.txt b/.dict_custom.txt
index b25b47f277..2e5398c733 100644
--- a/.dict_custom.txt
+++ b/.dict_custom.txt
@@ -3,6 +3,8 @@ Pythran
 numba
 NumPy
 NumPy's
+CuPy
+CuPy's
 BLAS
 LAPACK
 MPI
@@ -108,3 +110,6 @@ subclasses
 oneAPI
 getter
 setter
+bitwise
+datatyping
+datatypes
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3190109063..1c1a3cf213 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,15 +13,29 @@ All notable changes to this project will be documented in this file.
 ### Fixed
 
 -   #1720 : Fix Undefined Variable error when the function definition is after the variable declaration.
+-   #1763 Use `np.result_type` to avoid mistakes in non-trivial NumPy type promotion rules.
+-   Fix some cases where a Python built-in type is returned in place of a NumPy type.
+-   Stop printing numbers with more decimal digits than their precision.
+-   Allow printing the result of a function returning multiple objects of different types.
 
 ### Changed
 -   #1720 : functions with the `@inline` decorator are no longer exposed to Python in the shared library.
 -   #1720 : Error raised when incompatible arguments are passed to an `inlined` function is now fatal.
 -   \[INTERNALS\] `FunctionDef` is annotated when it is called, or at the end of the `CodeBlock` if it is never called.
 -   \[INTERNALS\] `InlinedFunctionDef` is only annotated if it is called.
+-   \[INTERNALS\] Build `utilities.metaclasses.ArgumentSingleton` on the fly to ensure correct docstrings.
+-   \[INTERNALS\] Rewrite datatyping system. See #1722.
+-   \[INTERNALS\] Moved precision from `ast.basic.TypedAstNode` to an internal property of `ast.datatypes.FixedSizeNumericType` objects.
+-   \[INTERNALS\] Use cached `__add__` method to determine result type of arithmetic operations.
+-   \[INTERNALS\] Use cached `__and__` method to determine result type of bitwise comparison operations.
 
 ### Deprecated
 
+-   \[INTERNALS\] Remove property `ast.basic.TypedAstNode.precision`.
+-   \[INTERNALS\] Remove class `ast.datatypes.DataType` (replaced by `ast.datatypes.PrimitiveType` and `ast.datatypes.PyccelType`).
+-   \[INTERNALS\] Remove unused properties `prefix` and `alias` from `CustomDataType`.
+-   \[INTERNALS\] Remove `ast.basic.TypedAstNode._dtype`. The datatype can still be accessed as it is contained within the class type.
+
 ## \[1.11.2\] - 2024-03-05
 
 ### Added
diff --git a/bandit.yml b/bandit.yml
index 04b18d7042..915d85d514 100644
--- a/bandit.yml
+++ b/bandit.yml
@@ -10,3 +10,5 @@ skips:
   - B403  # Ignore warnings about import pickle
   - B301  # Ignore warnings about pickle.load
   - B303  # Ignore warnings about MD2, MD4, MD5, or SHA1 hash functions
+
+exclude_dirs: ['pyccel/utilities/metaclasses.py']
diff --git a/developer_docs/ast_nodes.md b/developer_docs/ast_nodes.md
index 8d6d37e0bb..6f1c3ea4db 100644
--- a/developer_docs/ast_nodes.md
+++ b/developer_docs/ast_nodes.md
@@ -10,7 +10,6 @@ The inheritance tree for a Python AST node is often more complicated than direct
 
 The class `TypedAstNode` is a super class. This class should never be used directly but provides functionalities which are common to certain AST objects. These AST nodes are those which describe objects which take up space in memory in a running program. For example a Variable requires space in memory, as does the result of a function call or an arithmetic operation, however a loop or a module does not require runtime memory to store the concept. Objects which require memory must therefore contain all information necessary to declare them in the generated code. A `TypedAstNode` therefore exposes the following properties:
 -   `dtype`
--   `precision`
 -   `rank`
 -   `shape`
 -   `order`
@@ -18,25 +17,26 @@ The class `TypedAstNode` is a super class. This class should never be used direc
 
 The contents of these types are explained in more detail below.
 
-When examining the class `TypedAstNode` you may notice that there are two methods for getting each of these properties. Each time, one is a standard method, while the other is a static class method. In general in the code you will always use the normal method. This will return the instance attribute if it is available, otherwise it will return the static class attribute. The static class method is used for type deductions when [parsing type annotations](./type_inference.md). In type annotations we do not generally have an instance of a class, however we can get access to the class itself. For instance let us consider the following type annotation:
+The class `TypedAstNode` also contains static class methods. The static class method is used for type deductions when [parsing type annotations](./type_inference.md). In type annotations we do not generally have an instance of a class, however we can get access to the class itself. The available class methods are:
+-   `static_type`
+-   `static_rank`
+-   `static_order`
+
+For instance let us consider the following type annotation:
 ```python
 a : int
 ```
-When we visit `int` in the [semantic stage](./semantic_stage.md) the `SemanticParser` will return the class `PythonInt`. This is usually used as a function (e.g to cast a variable), however here we use it to deduce the type. Following the [development conventions](./development_conventions.md#Class-variables-vs.-Instance-variables) any attributes which will remain constant over all instances of a class should be stored in static class attributes. This means that they can be accessed via these static methods. Returning to our example, a call to the function `int` always returns a scalar object with an integer type and default precision. This means that all the properties of a `TypedAstNode` can be defined without having an instance of this class. These properties cannot be defined statically for all nodes (e.g. it would not be possible for `PyccelAdd`), however generally they can be defined for the nodes which can be used in type annotations.
+When we visit `int` in the [semantic stage](./semantic_stage.md) the `SemanticParser` will return the class `PythonInt`. This is usually used as a function (e.g to cast a variable), however here we use it to deduce the type. Following the [development conventions](./development_conventions.md#Class-variables-vs.-Instance-variables) any attributes which will remain constant over all instances of a class should be stored in static class attributes. This means that they can be accessed via these static methods. Returning to our example, a call to the function `int` always returns a scalar object with the built-in `float` type. This means that all the properties of a `TypedAstNode` can be defined without having an instance of this class. These properties cannot be defined statically for all nodes (e.g. it would not be possible for `PyccelAdd`), however generally they can be defined for the nodes which can be used in type annotations.
 
 ### Class type
 
-The class type is the type reported by Python when you call the built-in function `type`. The object stored in this attribute should inherit from `pyccel.ast.datatypes.DataType`.
+The `class_type` property represents the type reported by Python when you call the built-in function `type`. This property should return an object which inherits from `pyccel.ast.datatypes.PyccelType`.
 
 ### Datatype
 
-Some types in Python are containers which contain elements of other types. This is the case for NumPy arrays, tuples, lists, etc. In this case, the class type does not provide enough information to write the declaration in the low-level target language. Additionally a data type is required. The data type is the type of an element of the container, as for the class type, the object stored in this attribute should inherit from `pyccel.ast.datatypes.DataType`. If the class type is not a container then the class type and the data type will be the same.
-
-### Precision
-
-The precision indicates the precision of the datatype. 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.
+Some types in Python are containers which contain elements of other types. This is the case for NumPy arrays, tuples, lists, etc. In this case, the class type does not provide enough information to write the declaration in the low-level target language. Additionally a data type is required. The data type is the type of an element of the container and can be accessed via the `dtype` property. As for the class type the object returned by this property should inherit from `pyccel.ast.datatypes.PyccelType`. If the class type is not a container then the class type and the data type will be the same, otherwise the class type will inherit from `pyccel.ast.datatypes.ContainerType` and the data type will inherit from `pyccel.ast.datatypes.FixedSizeType`.
 
-In Python the precision of some types depends on the system where the code is run. This is notably the case for integers which have a precision of 4 on Windows but a precision of 8 on Linux and MacOS. This is the case for native types. In order to differentiate these types from the fixed-precision objects provided by NumPy, the precision -1 is used to denote the default precision.
+A `FixedSizeType` represents a built-in scalar datatype which can be represented in memory. E.g. `int32`, `int64`. It is characterised by a primitive type which describes the category of datatype (integer, floating point, etc) and a precision. The precision 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.
 
 ### Rank
 
@@ -50,6 +50,18 @@ The shape of an array indicates the number of elements in each dimension of the
 
 The order indicates how an array is laid out in memory. This can either be row-major (C-style) ordering or column-major (Fortran-style) ordering. For more information about this, please see the [dedicated documentation](./order_docs.md).
 
+### Static Type
+
+The static type is the class type that would be assigned to an object created using an instance of this class as a type annotation.
+
+### Static rank
+
+The static rank is the rank that would be assigned to an object created using an instance of this class as a type annotation.
+
+### Static order
+
+The static order is the order that would be assigned to an object created using an instance of this class as a type annotation.
+
 ## Pyccel Internal Function
 
 The class `pyccel.ast.internals.PyccelInternalFunction` is a super class. This class should never be used directly but provides functionalities which are common to certain AST objects. These AST nodes are those which describe functions which are supported by Pyccel. For example it is used for functions from the `math` library, the `cmath` library, the `numpy` library, etc. `PyccelInternalFunction` inherits from `TypedAstNode`. The type information for the sub-class describes the type of the result of the function.
diff --git a/developer_docs/type_inference.md b/developer_docs/type_inference.md
index 47ea511ce1..bee3e4af61 100644
--- a/developer_docs/type_inference.md
+++ b/developer_docs/type_inference.md
@@ -43,3 +43,50 @@ These objects contain all the information necessary to create a Variable from a
 ### Inferring types from assignments
 
 When assignments occur in the code types must also be inferred. This allows new variables to be declared implicitly, and also enables us to verify that the types of existing variables do not change. In this case the type inference is done via the AST nodes. Each node contains the logic necessary to deduce its type information from the arguments passed to it. The resulting object, which is found on the right hand side of an assignment can be used to verify or define the type of the object on the left hand side of the assignment.
+
+### Data types in Pyccel
+
+The data types in Pyccel are designed around two super classes:
+-   `PrimitiveType` : Representing the category of datatype (integer/floating point/etc)
+-   `PyccelType` : Representing the actual type of the object in Python
+
+Subclasses of `PyccelType` generally fall into one of two categories:
+-   `FixedSizeType`
+-   `ContainerType`
+
+The types can be compared using either the `is` operator or the `==` operator. These operators have slightly different behaviour. All instances of `PyccelType` are singletons so the `is` operator tests if the types are identical. However the `==` operator tests if the types are compatible. For example `PythonNativeFloat() == NumpyFloat64Type()` will return true. This operator should therefore be used when permissive behaviour is required (e.g. when adding elements to a list of `PythonNativeFloat()` we are capable of adding an instance with the type `NumpyFloat64Type()` even if this would not be strictly homogeneous in Python).
+
+#### Fixed Size Type
+A `FixedSizeType` is an object whose size in memory is known and cannot change from one instance to another (e.g. `float64`, `int32`, `void`). In most cases the developer will need the sub-class `FixedSizeNumericType` which refers to the subset of fixed size types which contain numeric values. These objects are characterised by a `primitive_type` describing the category of datatype (integer/floating point/etc) and a `precision`. They additionally implement two magic methods:
+-   `__add__` (+)
+-   `__and__` (&)
+
+The add operator describes what happens when two numeric types are combined in an arithmetic operator. In almost all cases this is sufficient to describe all resulting datatypes. Special cases (e.g. float for integer division) are handled in the associated operator in `ast.operators` or `ast.bitwise_operators`.
+
+The and operator describes what happens when two numeric types are combined in a bitwise comparison operator. This only applies to integers and booleans.
+
+When using these operators on an unknown number of arguments it can be useful to use `NativeGeneric()` as a starting point for the sum.
+
+#### Container Type
+A `ContainerType` is an object which is comprised of `FixedSizeType` objects (e.g. `ndarray`,`list`,`tuple`, custom class). The sub-class `HomogeneousContainerType` describes containers which contain homogeneous data. These objects are characterised by an `element_type`. The elements of a `HomogeneousContainerType` are instances of `PyccelType`, but they can be either `FixedSizeType`s or `ContainerType`s.
+
+`HomogeneousContainerType`s also contain some utility functions. They implement `primitive_type` and `precision` to get the properties of the internal `FixedSizeType` (even if that type is inside another `HomogeneousContainerType`). They also implement `switch_basic_type` which creates a new `HomogeneousContainerType` which is similar to the current `HomogeneousContainerType`. The only difference is that the `FixedSizeType` is exchanged. This is useful when we want to preserve information about the container but need to change the type. For example, when we divide an integer by another we get a floating point type. When we divide a NumPy array or a CuPy array of integers by an integer (or array of integers) we get a NumPy/CuPy array of floating point numbers (with default Python precision). In order to preserve the container type we therefore call `switch_basic_type`. So for the division in the case of NumPy arrays, we want to change the type from `np.ndarray[int]` to `np.ndarray[float]`. This is done in one line:
+```python
+new_class_type = class_type.switch_basic_type(NativePythonFloat())
+```
+instead of the multiple lines that would be needed without this function. The advantage of this is seen most clearly when we consider a function acting on a more complex type e.g. `list[np.ndarray[float]]` in this case without the `switch_basic_type` function, the equivalent code would be much longer:
+```python
+new_container_types = []
+old_type = class_type
+while isinstance(old_type, ContainerType):
+    new_container_types.append(type(old_type))
+    old_type = old_type.element_type
+
+new_type = old_type
+for container in new_container_types:
+    new_type = container(new_type)
+```
+
+The `switch_basic_type` cannot be implemented generally in `PyccelType` as there is no logical interpretation for an inhomogeneous `ContainerType`, however the function is also implemented (as the identity function) for `FixedSizeType`s so `switch_basic_type` can be used without the need for type checks (generally inhomogeneous containers will not be valid arguments to classes which may need to use the `switch_basic_type` function).
+
+In order to access the internal `FixedSizeType`, `PyccelType` also implements a `datatype` property. This makes more sense in the case of a `HomogeneousContainerType` however it is also implemented (as the identity function) for `FixedSizeType`s so the low-level type can be obtained without the need for type checks.
diff --git a/pyccel/ast/basic.py b/pyccel/ast/basic.py
index e02cc66d05..4366fe1449 100644
--- a/pyccel/ast/basic.py
+++ b/pyccel/ast/basic.py
@@ -500,7 +500,7 @@ class TypedAstNode(PyccelAstNode):
     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,
+    Each typed object is described by an underlying datatype, a rank,
     a shape, and a data layout ordering.
     """
     __slots__  = ()
@@ -535,20 +535,7 @@ def dtype(self):
         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.
-
-        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
+        return self.class_type.datatype
 
     @property
     def order(self):
@@ -585,37 +572,6 @@ 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):
         """
@@ -631,7 +587,7 @@ class does not have a predetermined order.
         return cls._order # pylint: disable=no-member
 
     @classmethod
-    def static_class_type(cls):
+    def static_type(cls):
         """
         The type of the object.
 
@@ -642,14 +598,14 @@ def static_class_type(cls):
         This function is static and will return an AttributeError if the
         class does not have a predetermined order.
         """
-        return cls._class_type # pylint: disable=no-member
+        return cls._static_type # pylint: disable=no-member
 
     def copy_attributes(self, x):
         """
         Copy the attributes describing a TypedAstNode into this node.
 
         Copy the attributes which describe the TypedAstNode passed as
-        argument (dtype, precision, shape, rank, order) into this node
+        argument (dtype, shape, rank, order) into this node
         so that the two nodes can be stored in the same object.
 
         Parameters
@@ -659,8 +615,6 @@ def copy_attributes(self, x):
         """
         self._shape      = x.shape
         self._rank       = x.rank
-        self._dtype      = x.dtype
-        self._precision  = x.precision
         self._order      = x.order
         self._class_type = x.class_type
 
diff --git a/pyccel/ast/bind_c.py b/pyccel/ast/bind_c.py
index 2678a2d8cb..d1af786bab 100644
--- a/pyccel/ast/bind_c.py
+++ b/pyccel/ast/bind_c.py
@@ -12,7 +12,7 @@
 from pyccel.ast.core import Module, Deallocate
 from pyccel.ast.core import FunctionDef, ClassDef
 from pyccel.ast.core import FunctionDefArgument, FunctionDefResult
-from pyccel.ast.datatypes import DataType, NativeInteger
+from pyccel.ast.datatypes import FixedSizeType, PythonNativeInt
 from pyccel.ast.variable import Variable
 from pyccel.utilities.metaclasses import Singleton
 
@@ -35,7 +35,7 @@
 #                                    Datatypes
 # =======================================================================================
 
-class BindCPointer(DataType, metaclass = Singleton):
+class BindCPointer(FixedSizeType, metaclass = Singleton):
     """
     Datatype representing a C pointer in Fortran.
 
@@ -200,10 +200,10 @@ class method.
     def __init__(self, var, scope, original_arg_var, wrapping_bound_argument, **kwargs):
         name = var.name
         self._rank = original_arg_var.rank
-        shape   = [scope.get_temporary_variable(NativeInteger(),
+        shape   = [scope.get_temporary_variable(PythonNativeInt(),
                             name=f'{name}_shape_{i+1}')
                    for i in range(self._rank)]
-        strides = [scope.get_temporary_variable(NativeInteger(),
+        strides = [scope.get_temporary_variable(PythonNativeInt(),
                             name=f'{name}_stride_{i+1}')
                    for i in range(self._rank)]
         self._shape = shape
@@ -339,7 +339,7 @@ class BindCFunctionDefResult(FunctionDefResult):
 
     def __init__(self, var, original_res_var, scope, **kwargs):
         name = original_res_var.name
-        self._shape   = [scope.get_temporary_variable(NativeInteger(),
+        self._shape   = [scope.get_temporary_variable(PythonNativeInt(),
                             name=f'{name}_shape_{i+1}')
                          for i in range(original_res_var._rank)]
         self._original_res_var = original_res_var
diff --git a/pyccel/ast/bitwise_operators.py b/pyccel/ast/bitwise_operators.py
index 2a867f9340..a2ba0e3ad6 100644
--- a/pyccel/ast/bitwise_operators.py
+++ b/pyccel/ast/bitwise_operators.py
@@ -6,12 +6,11 @@
 Module handling all Python builtin operators
 These operators all have a precision as detailed here:
     https://docs.python.org/3/reference/expressions.html#operator-precedence
-They also have specific rules to determine the dtype, precision, rank, shape
+They also have specific rules to determine the datatype, rank, shape
 """
 from .builtins     import PythonInt
-from .datatypes    import (NativeBool, NativeInteger, NativeFloat,
-                           NativeComplex, NativeString, NativeGeneric)
-from .internals    import max_precision
+from .datatypes    import PrimitiveBooleanType, PrimitiveIntegerType
+from .datatypes    import PythonNativeBool, PythonNativeInt
 from .operators    import PyccelUnaryOperator, PyccelOperator
 
 __all__ = (
@@ -45,32 +44,28 @@ class PyccelInvert(PyccelUnaryOperator):
     __slots__ = ()
     _precedence = 14
 
-    def _calculate_dtype(self, arg):
+    def _calculate_type(self, arg):
         """
-        Get the dtype and precision.
+        Get the type of the result of the function.
 
         Arguments must be integers or booleans. Any booleans are cast
         to integers.
 
         Parameters
         ----------
-        arg : tuple of TypedAstNode
+        arg : TypedAstNode
             The argument passed to the operator.
 
         Returns
         -------
-        dtype : DataType
+        DataType
             The  datatype of the result of the operation.
-        precision : integer
-            The precision of the result of the operation.
         """
-        dtype = NativeInteger()
-        if arg.dtype not in (NativeInteger(), NativeBool()):
-            raise TypeError(f'unsupported operand type(s): {arg}')
+        dtype = PythonNativeInt()
+        assert isinstance(getattr(arg.dtype, 'primitive_type', None), (PrimitiveBooleanType, PrimitiveIntegerType))
 
-        self._args      = (PythonInt(arg) if arg.dtype is NativeBool() else arg,)
-        precision = arg.precision
-        return dtype, precision, dtype
+        self._args      = (PythonInt(arg) if arg.dtype is PythonNativeBool() else arg,)
+        return dtype
 
     def __repr__(self):
         return f'~{repr(self.args[0])}'
@@ -93,7 +88,7 @@ class PyccelBitOperator(PyccelOperator):
     _shape = None
     _rank  = 0
     _order = None
-    __slots__ = ('_dtype','_precision','_class_type')
+    __slots__ = ('_class_type',)
 
     def __init__(self, arg1, arg2):
         super().__init__(arg1, arg2)
@@ -101,78 +96,50 @@ def __init__(self, arg1, arg2):
     def _set_order(self):
         pass
 
-    def _calculate_dtype(self, *args):
+    def _calculate_type(self, arg1, arg2):
         """
-        Get the dtype and precision.
+        Get the type of the result of the function.
 
         If one argument is a string then all arguments must be strings.
 
-        If the arguments are numeric then the dtype and precision
-        match the broadest type and the largest precision.
+        If the arguments are numeric then the datatype
+        matches the broadest type.
         e.g.
             1 + 2j -> PyccelAdd(LiteralInteger, LiteralComplex) -> complex
 
         Parameters
         ----------
-        *args : tuple of TypedAstNode
-            The arguments passed to the operator.
+        arg1 : TypedAstNode
+            The first argument passed to the operator.
+        arg2 : TypedAstNode
+            The second argument passed to the operator.
 
         Returns
         -------
-        dtype : DataType
+        DataType
             The  datatype of the result of the operation.
-        precision : integer
-            The precision of the result of the operation.
         """
         try:
-            dtype = sum((a.dtype for a in args), start=NativeGeneric())
-            class_type = sum((a.class_type for a in args), start=NativeGeneric())
-        except NotImplementedError:
-            raise TypeError(f'Cannot determine the type of {args}') #pylint: disable=raise-missing-from
-
-        if dtype in (NativeString(), NativeComplex(), NativeFloat()):
-            raise TypeError(f'unsupported operand type(s): {args}')
-        elif (dtype in (NativeInteger(), NativeBool())):
-            if class_type is NativeBool():
-                class_type = NativeInteger()
-            return *self._handle_integer_type(args), class_type
-        else:
-            raise TypeError(f'Cannot determine the type of {args}')
+            class_type = arg1.class_type + arg2.class_type
+        except NotImplementedError as err:
+            raise TypeError(f'Cannot determine the type of {arg1} {self.op} {arg2}') from err # pylint: disable=no-member
 
-    def _set_shape_rank(self):
-        pass
-
-    def _handle_integer_type(self, args):
-        """
-        Set dtype and precision when the result is an integer.
+        assert isinstance(getattr(class_type, 'primitive_type', None), (PrimitiveBooleanType, PrimitiveIntegerType))
 
-        Calculate the dtype and precision of the result from the arguments in
-        the case where the result is an integer, ie. when the arguments are all
-        booleans or integers.
+        self._args = [PythonInt(a) if a.dtype is PythonNativeBool() else a for a in (arg1, arg2)]
 
-        Parameters
-        ----------
-        args : tuple of TypedAstNode
-            The arguments passed to the operator.
-
-        Returns
-        -------
-        dtype : DataType
-            The datatype of the result of the operator.
+        return class_type
 
-        precision : int
-            The precision of the result of the operator.
+    def _set_shape_rank(self):
         """
-        dtype    = NativeInteger()
-        integers = [a for a in args if a.dtype is NativeInteger()]
+        Set the shape and rank of the resulting object.
 
-        if not integers:
-            precision = -1
-        else:
-            precision = max_precision(integers)
+        Set the shape and rank of the resulting object. For a PyccelBitOperator,
+        the shape and rank are class attributes so nothing needs to be done.
+        """
 
-        self._args = [PythonInt(a) if a.dtype is NativeBool() else a for a in args]
-        return dtype, precision
+    def __repr__(self):
+        return f'{self.args[0]} {self.op} {self.args[1]}' # pylint: disable=no-member
 
 #==============================================================================
 
@@ -195,9 +162,7 @@ class PyccelRShift(PyccelBitOperator):
     """
     __slots__ = ()
     _precedence = 11
-
-    def __repr__(self):
-        return f'{self.args[0]} >> {self.args[1]}'
+    op = ">>"
 
 #==============================================================================
 
@@ -220,9 +185,7 @@ class PyccelLShift(PyccelBitOperator):
     """
     __slots__ = ()
     _precedence = 11
-
-    def __repr__(self):
-        return f'{self.args[0]} << {self.args[1]}'
+    op = "<<"
 
 #==============================================================================
 
@@ -241,36 +204,41 @@ class PyccelBitComparisonOperator(PyccelBitOperator):
         The second argument passed to the operator.
     """
     __slots__ = ()
-    def _handle_integer_type(self, args):
+    def _calculate_type(self, arg1, arg2):
         """
-        Set dtype and precision when the result is an integer.
+        Get the type of the result of the function.
 
-        Calculate the dtype and precision of the result from the arguments in
-        the case where the result is an integer, ie. when the arguments are all
-        booleans or integers.
+        If one argument is a string then all arguments must be strings.
+
+        If the arguments are numeric then the dtype
+        matches the broadest type.
+        e.g.
+            1 + 2j -> PyccelAdd(LiteralInteger, LiteralComplex) -> complex
 
         Parameters
         ----------
-        args : tuple of TypedAstNode
-            The arguments passed to the operator.
+        arg1 : TypedAstNode
+            The first argument passed to the operator.
+        arg2 : TypedAstNode
+            The second argument passed to the operator.
 
         Returns
         -------
-        dtype : DataType
-            The datatype of the result of the operator.
-
-        precision : int
-            The precision of the result of the operator.
+        DataType
+            The  datatype of the result of the operation.
         """
-        if all(a.dtype is NativeInteger() for a in args):
-            dtype = NativeInteger()
-        elif all(a.dtype is NativeBool() for a in args):
-            dtype = NativeBool()
-        else:
-            dtype = NativeInteger()
-            self._args = [PythonInt(a) if a.dtype is NativeBool() else a for a in args]
-        precision = max_precision(args, NativeInteger())
-        return dtype, precision
+        try:
+            class_type = arg1.class_type & arg2.class_type
+        except NotImplementedError as err:
+            raise TypeError(f'Cannot determine the type of {arg1} {self.op} {arg2}') from err # pylint: disable=no-member
+
+        primitive_type = class_type.primitive_type
+        assert isinstance(primitive_type, (PrimitiveBooleanType, PrimitiveIntegerType))
+
+        if isinstance(primitive_type, PrimitiveIntegerType):
+            self._args = [PythonInt(a) if a.dtype is PythonNativeBool() else a for a in (arg1, arg2)]
+
+        return class_type
 
 #==============================================================================
 
@@ -293,9 +261,7 @@ class PyccelBitXor(PyccelBitComparisonOperator):
     """
     __slots__ = ()
     _precedence = 9
-
-    def __repr__(self):
-        return f'{self.args[0]} ^ {self.args[1]}'
+    op = "^"
 
 #==============================================================================
 
@@ -318,9 +284,7 @@ class PyccelBitOr(PyccelBitComparisonOperator):
     """
     __slots__ = ()
     _precedence = 8
-
-    def __repr__(self):
-        return f'{self.args[0]} | {self.args[1]}'
+    op = "|"
 
 #==============================================================================
 
@@ -343,6 +307,4 @@ class PyccelBitAnd(PyccelBitComparisonOperator):
     """
     __slots__ = ()
     _precedence = 10
-
-    def __repr__(self):
-        return f'{self.args[0]} & {self.args[1]}'
+    op = "&"
diff --git a/pyccel/ast/builtin_methods/list_methods.py b/pyccel/ast/builtin_methods/list_methods.py
index b093e17d86..b8e3c57574 100644
--- a/pyccel/ast/builtin_methods/list_methods.py
+++ b/pyccel/ast/builtin_methods/list_methods.py
@@ -10,7 +10,7 @@
 This module contains objects which describe these methods within Pyccel's AST.
 """
 
-from pyccel.ast.datatypes import NativeVoid, NativeGeneric, NativeHomogeneousList
+from pyccel.ast.datatypes import VoidType
 from pyccel.ast.internals import PyccelInternalFunction
 
 __all__ = ('ListAppend',
@@ -76,24 +76,21 @@ class ListAppend(ListMethod):
         The argument passed to append() method.
     """
     __slots__ = ()
-    _dtype = NativeVoid()
+    _dtype = VoidType()
     _shape = None
     _order = None
     _rank = 0
-    _precision = -1
-    _class_type = NativeVoid()
+    _class_type = VoidType()
     name = 'append'
 
     def __init__(self, list_obj, new_elem) -> None:
+        expected_type = list_obj.class_type.element_type
         is_homogeneous = (
-            new_elem.dtype is not NativeGeneric() and
-            list_obj.dtype is not NativeGeneric() and
-            list_obj.dtype == new_elem.dtype and
-            list_obj.precision == new_elem.precision and
+            new_elem.class_type == expected_type and
             list_obj.rank - 1 == new_elem.rank
-            )
+        )
         if not is_homogeneous:
-            raise TypeError("Expecting an argument of the same type as the elements of the list")
+            raise TypeError(f"Expecting an argument of the same type as the elements of the list ({expected_type}) but received {new_elem.class_type}")
         super().__init__(list_obj, new_elem)
 
 #==============================================================================
@@ -116,16 +113,14 @@ class ListPop(ListMethod) :
     index_element : TypedAstNode
         The current index value for the element to be popped.
     """
-    __slots__ = ('_dtype','_precision', '_rank', '_shape', '_order')
-    _class_type = NativeHomogeneousList()
+    __slots__ = ('_class_type', '_rank', '_shape', '_order')
     name = 'pop'
 
     def __init__(self, list_obj, index_element=None) -> None:
         self._rank = list_obj.rank - 1
-        self._dtype = list_obj.dtype
-        self._precision = list_obj.precision
         self._shape = (None if len(list_obj.shape) == 1 else tuple(list_obj.shape[1:]))
         self._order = (None if self._shape is None or len(self._shape) == 1 else list_obj.order)
+        self._class_type = list_obj.class_type.element_type
         super().__init__(list_obj, index_element)
 
 #==============================================================================
@@ -148,12 +143,10 @@ class ListClear(ListMethod) :
         The list object which the method is called from.
     """
     __slots__ = ()
-    _dtype = NativeVoid()
-    _precision = -1
     _rank = 0
     _order = None
     _shape = None
-    _class_type = NativeVoid()
+    _class_type = VoidType()
     name = 'clear'
 
     def __init__(self, list_obj) -> None:
@@ -186,20 +179,16 @@ class ListInsert(ListMethod):
         The argument passed to insert() method.
     """
     __slots__ = ()
-    _dtype = NativeVoid()
     _shape = None
     _order = None
     _rank = 0
-    _precision = -1
-    _class_type = NativeVoid()
+    _class_type = VoidType()
     name = 'insert'
 
     def __init__(self, list_obj, index, new_elem) -> None:
+        expected_type = list_obj.class_type.element_type
         is_homogeneous = (
-            new_elem.dtype is not NativeGeneric() and
-            list_obj.dtype is not NativeGeneric() and
-            list_obj.dtype == new_elem.dtype and
-            list_obj.precision == new_elem.precision and
+            new_elem.class_type == expected_type and
             list_obj.rank - 1 == new_elem.rank
         )
         if not is_homogeneous:
diff --git a/pyccel/ast/builtin_methods/set_methods.py b/pyccel/ast/builtin_methods/set_methods.py
index ad28e0c658..f729044963 100644
--- a/pyccel/ast/builtin_methods/set_methods.py
+++ b/pyccel/ast/builtin_methods/set_methods.py
@@ -9,7 +9,7 @@
 
 This module contains objects which describe these methods within Pyccel's AST.
 """
-from pyccel.ast.datatypes import NativeVoid, NativeGeneric
+from pyccel.ast.datatypes import VoidType
 from pyccel.ast.internals import PyccelInternalFunction
 
 __all__ = ('SetAdd', 'SetClear', 'SetMethod', 'SetCopy')
@@ -63,20 +63,15 @@ class SetAdd(SetMethod) :
         The element that needs to be added to a set.
     """
     __slots__ = ()
-    _dtype = NativeVoid()
     _shape = None
     _order = None
     _rank = 0
-    _precision = None
-    _class_type = NativeVoid()
+    _class_type = VoidType()
     name = 'add'
 
     def __init__(self, set_variable, new_elem) -> None:
         is_homogeneous = (
-            new_elem.dtype is not NativeGeneric() and
-            set_variable.dtype is not NativeGeneric() and
-            set_variable.dtype == new_elem.dtype and
-            set_variable.precision == new_elem.precision and
+            set_variable.class_type.element_type == new_elem.class_type and
             set_variable.rank - 1 == new_elem.rank
         )
         if not is_homogeneous:
@@ -97,12 +92,10 @@ class SetClear(SetMethod):
         The set on which the method will operate.
     """
     __slots__ = ()
-    _dtype = NativeVoid()
     _shape = None
     _order = None
     _rank = 0
-    _precision = None
-    _class_type = NativeVoid()
+    _class_type = VoidType()
     name = 'clear'
 
     def __init__(self, set_variable):
@@ -121,14 +114,12 @@ class SetCopy(SetMethod):
     set_variable : TypedAstNode
         The set on which the method will operate.
     """
-    __slots__ = ("_dtype","_shape", "_order", "_rank", "_precision", "_class_type",)
+    __slots__ = ("_shape", "_order", "_rank", "_class_type",)
     name = 'copy'
 
     def __init__(self, set_variable):
-        self._dtype = set_variable._dtype
         self._shape = set_variable._shape
         self._order = set_variable._order
         self._rank = set_variable._rank
-        self._precision = set_variable._precision
         self._class_type = set_variable._class_type
         super().__init__(set_variable)
diff --git a/pyccel/ast/builtins.py b/pyccel/ast/builtins.py
index 2dc1fc0ff5..8866f4689d 100644
--- a/pyccel/ast/builtins.py
+++ b/pyccel/ast/builtins.py
@@ -15,11 +15,12 @@
 from pyccel.utilities.stage import PyccelStage
 
 from .basic     import PyccelAstNode, TypedAstNode
-from .datatypes import (NativeInteger, NativeBool, NativeFloat,
-                        NativeComplex, NativeGeneric)
-from .datatypes import NativeHomogeneousTuple, NativeInhomogeneousTuple
-from .datatypes import NativeHomogeneousList, NativeTuple, NativeHomogeneousSet
-from .internals import PyccelInternalFunction, Slice, get_final_precision, PyccelArrayShapeElement
+from .datatypes import PythonNativeInt, PythonNativeBool, PythonNativeFloat
+from .datatypes import GenericType, PythonNativeComplex, PrimitiveComplexType
+from .datatypes import HomogeneousTupleType, InhomogeneousTupleType
+from .datatypes import HomogeneousListType, HomogeneousContainerType
+from .datatypes import FixedSizeNumericType, HomogeneousSetType
+from .internals import PyccelInternalFunction, Slice, PyccelArrayShapeElement
 from .literals  import LiteralInteger, LiteralFloat, LiteralComplex, Nil
 from .literals  import Literal, LiteralImaginaryUnit, convert_to_literal
 from .literals  import LiteralString
@@ -76,12 +77,10 @@ class PythonComplexProperty(PyccelInternalFunction):
         The object which the property is called from.
     """
     __slots__ = ()
-    _dtype = NativeFloat()
-    _precision = -1
     _rank  = 0
     _shape = None
     _order = None
-    _class_type = NativeFloat()
+    _class_type = PythonNativeFloat()
 
     def __init__(self, arg):
         super().__init__(arg)
@@ -109,9 +108,9 @@ class PythonReal(PythonComplexProperty):
     __slots__ = ()
     name = 'real'
     def __new__(cls, arg):
-        if isinstance(arg.dtype, NativeBool):
+        if isinstance(arg.dtype, PythonNativeBool):
             return PythonInt(arg)
-        elif not isinstance(arg.dtype, NativeComplex):
+        elif not isinstance(arg.dtype.primitive_type, PrimitiveComplexType):
             return arg
         else:
             return super().__new__(cls)
@@ -138,7 +137,7 @@ class PythonImag(PythonComplexProperty):
     __slots__ = ()
     name = 'imag'
     def __new__(cls, arg):
-        if arg.dtype is not NativeComplex():
+        if not isinstance(arg.dtype.primitive_type, PrimitiveComplexType):
             return convert_to_literal(0, dtype = arg.dtype)
         else:
             return super().__new__(cls)
@@ -166,18 +165,16 @@ class PythonConjugate(PyccelInternalFunction):
         conjugate function.
     """
     __slots__ = ()
-    _dtype = NativeComplex()
-    _precision = -1
     _rank  = 0
     _shape = None
     _order = None
-    _class_type = NativeComplex()
+    _class_type = PythonNativeComplex()
     name = 'conjugate'
 
     def __new__(cls, arg):
-        if arg.dtype is NativeBool():
+        if arg.dtype is PythonNativeBool():
             return PythonInt(arg)
-        elif arg.dtype is not NativeComplex():
+        elif not isinstance(arg.dtype.primitive_type, PrimitiveComplexType):
             return arg
         else:
             return super().__new__(cls)
@@ -208,12 +205,11 @@ class PythonBool(PyccelInternalFunction):
     """
     __slots__ = ()
     name = 'bool'
-    _dtype = NativeBool()
-    _precision = -1
+    _static_type = PythonNativeBool()
     _rank  = 0
     _shape = None
     _order = None
-    _class_type = NativeBool()
+    _class_type = PythonNativeBool()
 
     def __new__(cls, arg):
         if getattr(arg, 'is_optional', None):
@@ -254,12 +250,11 @@ class PythonComplex(PyccelInternalFunction):
     __slots__ = ('_real_part', '_imag_part', '_internal_var', '_is_cast')
     name = 'complex'
 
-    _dtype = NativeComplex()
-    _precision = -1
+    _static_type = PythonNativeComplex()
     _rank  = 0
     _shape = None
     _order = None
-    _class_type = NativeComplex()
+    _class_type = PythonNativeComplex()
     _real_cast = PythonReal
     _imag_cast = PythonImag
     _attribute_nodes = ('_real_part', '_imag_part', '_internal_var')
@@ -284,19 +279,19 @@ def __new__(cls, arg0, arg1=LiteralFloat(0)):
             else:
                 imag_part += arg1.python_value
 
-            return LiteralComplex(real_part, imag_part, precision = cls._precision)
+            return LiteralComplex(real_part, imag_part, dtype = cls._static_type)
 
 
         # Split arguments depending on their type to ensure that the arguments are
         # either a complex and LiteralFloat(0) or 2 floats
 
-        if arg0.dtype is NativeComplex() and arg1.dtype is NativeComplex():
+        if isinstance(arg0.dtype.primitive_type, PrimitiveComplexType) and isinstance(arg1.dtype.primitive_type, PrimitiveComplexType):
             # both args are complex
             return PyccelAdd(arg0, PyccelMul(arg1, LiteralImaginaryUnit()))
         return super().__new__(cls)
 
     def __init__(self, arg0, arg1 = LiteralFloat(0)):
-        self._is_cast = arg0.dtype is NativeComplex() and \
+        self._is_cast = isinstance(arg0.dtype.primitive_type, PrimitiveComplexType) and \
                         isinstance(arg1, Literal) and arg1.python_value == 0
 
         if self._is_cast:
@@ -307,12 +302,12 @@ def __init__(self, arg0, arg1 = LiteralFloat(0)):
         else:
             self._internal_var = None
 
-            if arg0.dtype is NativeComplex() and \
+            if isinstance(arg0.dtype.primitive_type, PrimitiveComplexType) and \
                     not (isinstance(arg1, Literal) and arg1.python_value == 0):
                 # first arg is complex. Second arg is non-0
                 self._real_part = self._real_cast(arg0)
                 self._imag_part = PyccelAdd(self._imag_cast(arg0), arg1)
-            elif arg1.dtype is NativeComplex():
+            elif isinstance(arg1.dtype.primitive_type, PrimitiveComplexType):
                 if isinstance(arg0, Literal) and arg0.python_value == 0:
                     # second arg is complex. First arg is 0
                     self._real_part = PyccelUnarySub(self._imag_cast(arg1))
@@ -417,18 +412,17 @@ class PythonFloat(PyccelInternalFunction):
     """
     __slots__ = ()
     name = 'float'
-    _dtype = NativeFloat()
-    _precision = -1
+    _static_type = PythonNativeFloat()
     _rank  = 0
     _shape = None
     _order = None
-    _class_type = NativeFloat()
+    _class_type = PythonNativeFloat()
 
     def __new__(cls, arg):
-        if isinstance(arg, LiteralFloat) and arg.precision == cls._precision:
+        if isinstance(arg, LiteralFloat) and arg.dtype is cls._static_type:
             return arg
         if isinstance(arg, (LiteralInteger, LiteralFloat)):
-            return LiteralFloat(arg.python_value, precision = cls._precision)
+            return LiteralFloat(arg.python_value, dtype = cls._static_type)
         return super().__new__(cls)
 
     def __init__(self, arg):
@@ -462,16 +456,15 @@ class PythonInt(PyccelInternalFunction):
 
     __slots__ = ()
     name = 'int'
-    _dtype = NativeInteger()
-    _precision = -1
+    _static_type = PythonNativeInt()
     _rank  = 0
     _shape = None
     _order = None
-    _class_type = NativeInteger()
+    _class_type = PythonNativeInt()
 
     def __new__(cls, arg):
         if isinstance(arg, LiteralInteger):
-            return LiteralInteger(arg.python_value, precision = cls._precision)
+            return LiteralInteger(arg.python_value, dtype = cls._static_type)
         else:
             return super().__new__(cls)
 
@@ -501,7 +494,7 @@ class PythonTuple(TypedAstNode):
         The arguments passed to the tuple function.
     """
     __slots__ = ('_args','_is_homogeneous',
-            '_dtype','_precision','_rank','_shape','_order', '_class_type')
+            '_rank','_shape','_order', '_class_type')
     _iterable        = True
     _attribute_nodes = ('_args',)
 
@@ -511,48 +504,52 @@ def __init__(self, *args):
         if pyccel_stage == 'syntactic':
             return
         elif len(args) == 0:
-            self._dtype = NativeGeneric()
-            self._precision = 0
+            self._class_type = HomogeneousTupleType(GenericType())
             self._rank  = 0
             self._shape = None
             self._order = None
             self._is_homogeneous = False
             return
 
-        dtypes = set(a.dtype for a in args)
-        precisions = set(get_final_precision(a) for a in args)
-        class_types = set(a.class_type for a in args)
-        ranks = set(a.rank for a in args)
+        # Get possible datatypes
+        dtypes = [a.class_type.datatype for a in args]
+        # Extract all dtypes inside any inhomogeneous tuples
+        while any(isinstance(d, InhomogeneousTupleType) for d in dtypes):
+            dtypes = [di for d in dtypes for di in ((d_elem.datatype for d_elem in d)
+                                                    if isinstance(d, InhomogeneousTupleType)
+                                                    else [d])]
+        # Create a set of dtypes using the same key for compatible types
+        dtypes = set((d.primitive_type, d.precision) if isinstance(d, FixedSizeNumericType) else d for d in dtypes)
+
+        ranks  = set(a.rank for a in args)
         orders = set(a.order for a in args)
-        shapes = set(tuple(si if not (isinstance(si, PyccelArrayShapeElement) or \
-                           si.get_attribute_nodes(PyccelArrayShapeElement)) \
-                        else None for si in a.shape) \
-                     if a.shape is not None else None for a in args)
-        is_homogeneous = len(dtypes) == 1 and len(precisions) == 1 and \
-                         len(class_types) == 1 and len(ranks) == 1 and \
-                         len(orders) == 1 and len(shapes) == 1 and \
-                         NativeGeneric() not in dtypes
+        if len(ranks) == 1:
+            rank = next(iter(ranks))
+            shapes = tuple(set(a.shape[i] for a in args if not (isinstance(a.shape[i], PyccelArrayShapeElement) or \
+                               a.shape[i].get_attribute_nodes(PyccelArrayShapeElement))) \
+                               for i in range(rank))
+        else:
+            shapes = ()
+        is_homogeneous = len(dtypes) == 1 and len(ranks) == 1 and \
+                         len(orders) == 1 and all(len(s) <= 1 for s in shapes)
         contains_pointers = any(isinstance(a, (Variable, IndexedElement)) and a.rank>0 and \
-                            not isinstance(a.dtype, NativeHomogeneousTuple) for a in args)
+                            not isinstance(a.class_type, HomogeneousTupleType) for a in args)
 
         self._is_homogeneous = is_homogeneous
-        if is_homogeneous and not contains_pointers:
-            arg0 = args[0]
-            self._dtype = arg0.dtype
-            self._precision = arg0.precision
+        if is_homogeneous:
             inner_shape = [() if a.rank == 0 else a.shape for a in args]
-            self._rank = max(a.rank for a in args) + 1
             self._shape = (LiteralInteger(len(args)), ) + inner_shape[0]
             self._rank  = len(self._shape)
 
-            self._class_type = NativeHomogeneousTuple()
+            if contains_pointers:
+                self._class_type = InhomogeneousTupleType(*[a.class_type for a in args])
+            else:
+                self._class_type = HomogeneousTupleType(args[0].class_type)
 
         else:
-            self._rank       = 1
-            self._dtype      = NativeInhomogeneousTuple(*[a.dtype for a in args])
-            self._precision  = 0
-            self._class_type = self._dtype
-            self._shape     = (LiteralInteger(len(args)), )
+            self._rank       = 1 if not is_homogeneous else max(a.rank for a in args) + 1
+            self._class_type = InhomogeneousTupleType(*[a.class_type for a in args])
+            self._shape      = (LiteralInteger(len(args)), )
 
         self._order = None if self._rank < 2 else 'C'
 
@@ -599,7 +596,7 @@ def is_homogeneous(self):
         """
         Indicates whether the tuple is homogeneous or inhomogeneous.
 
-        Indicates whether all elements of the tuple have the same dtype, precision,
+        Indicates whether all elements of the tuple have the same dtype,
         rank, etc (homogenous) or if these values can vary (inhomogeneous).
         """
         return self._is_homogeneous
@@ -655,6 +652,7 @@ class PythonLen(PyccelInternalFunction):
         The argument whose length is being examined.
     """
     __slots__ = ()
+
     def __new__(cls, arg):
         return arg.shape[0]
 
@@ -676,9 +674,8 @@ class PythonList(TypedAstNode):
     FunctionalFor
         The `[]` function when it describes a comprehension.
     """
-    __slots__ = ('_args','_dtype','_precision','_rank','_shape','_order')
+    __slots__ = ('_args','_rank','_shape','_order', '_class_type')
     _attribute_nodes = ('_args',)
-    _class_type = NativeHomogeneousList()
 
     def __init__(self, *args):
         self._args = args
@@ -686,23 +683,19 @@ def __init__(self, *args):
         if pyccel_stage == 'syntactic':
             return
         elif len(args) == 0:
-            self._dtype = NativeGeneric()
-            self._precision = 0
             self._rank  = 0
             self._shape = None
             self._order = None
+            self._class_type = HomogeneousListType(GenericType())
             return
         arg0 = args[0]
-        precision = get_final_precision(arg0)
-        is_homogeneous = arg0.dtype is not NativeGeneric() and \
-                         all(a.dtype is not NativeGeneric() and \
-                             arg0.dtype == a.dtype and \
-                             precision == get_final_precision(a) and \
+        is_homogeneous = arg0.class_type is not GenericType() and \
+                         all(a.class_type is not GenericType() and \
+                             arg0.class_type == a.class_type and \
                              arg0.rank  == a.rank  and \
                              arg0.order == a.order for a in args[1:])
         if is_homogeneous:
-            self._dtype = arg0.dtype
-            self._precision = arg0.precision
+            dtype = arg0.class_type
 
             inner_shape = [() if a.rank == 0 else a.shape for a in args]
             self._rank = max(a.rank for a in args) + 1
@@ -712,6 +705,7 @@ def __init__(self, *args):
         else:
             raise TypeError("Can't create an inhomogeneous list")
 
+        self._class_type = HomogeneousListType(dtype)
         self._order = None if self._rank < 2 else 'C'
 
     def __iter__(self):
@@ -739,7 +733,7 @@ def is_homogeneous(self):
         """
         Indicates whether the list is homogeneous or inhomogeneous.
 
-        Indicates whether all elements of the list have the same dtype, precision,
+        Indicates whether all elements of the list have the same dtype,
         rank, etc (homogenous) or if these values can vary (inhomogeneous). Lists
         are always homogeneous.
         """
@@ -758,9 +752,8 @@ class PythonSet(TypedAstNode):
     *args : tuple of TypedAstNodes
         The arguments passed to the operator.
     """
-    __slots__ = ('_args','_dtype','_precision','_rank','_shape','_order')
+    __slots__ = ('_args','_class_type','_rank','_shape','_order')
     _attribute_nodes = ('_args',)
-    _class_type = NativeHomogeneousSet()
 
     def __init__(self, *args):
         self._args = args
@@ -768,16 +761,13 @@ def __init__(self, *args):
         if pyccel_stage == 'syntactic':
             return
         arg0 = args[0]
-        precision = get_final_precision(arg0)
-        is_homogeneous = arg0.dtype is not NativeGeneric() and \
-                         all(a.dtype is not NativeGeneric() and \
-                             arg0.dtype == a.dtype and \
-                             precision == get_final_precision(a) and \
+        is_homogeneous = arg0.class_type is not GenericType() and \
+                         all(a.class_type is not GenericType() and \
+                             arg0.class_type == a.class_type and \
                              arg0.rank  == a.rank  and \
                              arg0.order == a.order for a in args[1:])
         if is_homogeneous:
-            self._dtype = arg0.dtype
-            self._precision = arg0.precision
+            dtype = arg0.dtype
             inner_shape = [() if a.rank == 0 else a.shape for a in args]
             self._shape = (LiteralInteger(len(args)), ) + inner_shape[0]
             self._rank  = len(self._shape)
@@ -786,6 +776,7 @@ def __init__(self, *args):
         else:
             raise TypeError("Can't create an inhomogeneous set")
 
+        self._class_type = HomogeneousSetType(dtype)
         self._order = None if self._rank < 2 else 'C'
 
     def __iter__(self):
@@ -1012,15 +1003,13 @@ class PythonAbs(PyccelInternalFunction):
     x : TypedAstNode
         The argument passed to the function.
     """
-    __slots__ = ('_dtype','_precision','_rank','_shape','_order','_class_type')
+    __slots__ = ('_rank','_shape','_order','_class_type')
     name = 'abs'
     def __init__(self, x):
         self._shape     = x.shape
         self._rank      = x.rank
-        self._dtype     = NativeInteger() if x.dtype is NativeInteger() else NativeFloat()
-        self._precision = -1
         self._order     = x.order
-        self._class_type = x.class_type
+        self._class_type = PythonNativeInt() if x.dtype is PythonNativeInt() else PythonNativeFloat()
         super().__init__(x)
 
     @property
@@ -1044,7 +1033,7 @@ class PythonSum(PyccelInternalFunction):
     arg : TypedAstNode
         The argument passed to the function.
     """
-    __slots__ = ('_dtype','_precision','_class_type')
+    __slots__ = ('_class_type',)
     name   = 'sum'
     _rank  = 0
     _shape = None
@@ -1053,12 +1042,10 @@ class PythonSum(PyccelInternalFunction):
     def __init__(self, arg):
         if not isinstance(arg, TypedAstNode):
             raise TypeError(f'Unknown type of {type(arg)}.' )
-        self._dtype = arg.dtype
-        self._precision = -1
-        if isinstance(arg.class_type, (NativeHomogeneousList, NativeTuple)):
-            self._class_type = arg.dtype
+        if isinstance(arg.class_type, HomogeneousContainerType):
+            self._class_type = arg.class_type.element_type
         else:
-            self._class_type = arg.class_type
+            self._class_type = sum(arg.class_type, start=GenericType())
         super().__init__(arg)
 
     @property
@@ -1082,7 +1069,7 @@ class PythonMax(PyccelInternalFunction):
     *x : list, tuple, PythonTuple, PythonList
         The arguments passed to the funciton.
     """
-    __slots__ = ('_dtype','_precision','_class_type')
+    __slots__ = ('_class_type',)
     name   = 'max'
     _rank  = 0
     _shape = None
@@ -1098,15 +1085,13 @@ def __init__(self, *x):
             raise TypeError(f'Unknown type of {type(x)}.' )
 
         if not x.is_homogeneous:
-            types = ', '.join('{xi.dtype}({xi.precision})' for xi in x)
+            types = ', '.join(str(xi.dtype) for xi in x)
             raise PyccelError("Cannot determine final dtype of 'max' call with arguments of different "
                              f"types ({types}). Please cast arguments to the desired dtype")
-        self._dtype     = x.dtype
-        self._precision = x.precision
-        if isinstance(x.class_type, (NativeHomogeneousList, NativeTuple)):
-            self._class_type = x.dtype
+        if isinstance(x.class_type, HomogeneousContainerType):
+            self._class_type = x.class_type.element_type
         else:
-            self._class_type = x.class_type
+            self._class_type = sum(x.class_type, start=GenericType())
         super().__init__(x)
 
 
@@ -1122,7 +1107,7 @@ class PythonMin(PyccelInternalFunction):
     *x : list, tuple, PythonTuple, PythonList
         The arguments passed to the funciton.
     """
-    __slots__ = ('_dtype','_precision','_class_type')
+    __slots__ = ('_class_type',)
     name   = 'min'
     _rank  = 0
     _shape = None
@@ -1137,15 +1122,13 @@ def __init__(self, *x):
             raise TypeError(f'Unknown type of {type(x)}.' )
 
         if not x.is_homogeneous:
-            types = ', '.join(f'{xi.dtype}({xi.precision})' for xi in x)
+            types = ', '.join(str(xi.dtype) for xi in x)
             raise PyccelError("Cannot determine final dtype of 'min' call with arguments of different "
                               f"types ({types}). Please cast arguments to the desired dtype")
-        self._dtype     = x.dtype
-        self._precision = x.precision
-        if isinstance(x.class_type, (NativeHomogeneousList, NativeTuple)):
-            self._class_type = x.dtype
+        if isinstance(x.class_type, HomogeneousContainerType):
+            self._class_type = x.class_type.element_type
         else:
-            self._class_type = x.class_type
+            self._class_type = sum(x.class_type, start=GenericType())
         super().__init__(x)
 
 #==============================================================================
@@ -1210,30 +1193,17 @@ class PythonType(PyccelAstNode):
     obj : TypedAstNode
           The object whose type we wish to investigate.
     """
-    __slots__ = ('_dtype','_precision','_obj')
+    __slots__ = ('_type','_obj')
     _attribute_nodes = ('_obj',)
 
     def __init__(self, obj):
         if not isinstance (obj, TypedAstNode):
             raise PyccelError(f"Python's type function is not implemented for {type(obj)} object")
-        self._dtype = obj.dtype
-        self._precision = obj.precision
+        self._type = obj.class_type
         self._obj = obj
 
         super().__init__()
 
-    @property
-    def dtype(self):
-        """ Returns the dtype of this type
-        """
-        return self._dtype
-
-    @property
-    def precision(self):
-        """ Returns the precision of this type
-        """
-        return self._precision
-
     @property
     def arg(self):
         """ Returns the object for which the type is determined
@@ -1249,18 +1219,16 @@ def print_string(self):
         printed by Python to describe this type. This string can
         then be easily printed in each language.
         """
-        prec = self.precision
-        dtype = self.dtype.name
-        if prec in (None, 0, -1):
-            return LiteralString(f"<class '{dtype}'>")
+        return LiteralString(f"<class '{self._type}'>")
 
-        class_type = self._obj.class_type
+#==============================================================================
 
-        precision = prec * (16 if self.dtype is NativeComplex() else 8)
-        if class_type != self.dtype:
-            return LiteralString(f"<class '{class_type}' ({dtype}{precision})>")
-        else:
-            return LiteralString(f"<class 'numpy.{dtype}{precision}'>")
+DtypePrecisionToCastFunction = {
+        PythonNativeBool()    : PythonBool,
+        PythonNativeInt()     : PythonInt,
+        PythonNativeFloat()   : PythonFloat,
+        PythonNativeComplex() : PythonComplex,
+}
 
 #==============================================================================
 
diff --git a/pyccel/ast/c_concepts.py b/pyccel/ast/c_concepts.py
index 3e78e805d4..abd684742b 100644
--- a/pyccel/ast/c_concepts.py
+++ b/pyccel/ast/c_concepts.py
@@ -7,12 +7,13 @@
 Module representing object address.
 """
 
-from pyccel.utilities.metaclasses import Singleton
+from pyccel.utilities.metaclasses import ArgumentSingleton
 from .basic     import TypedAstNode, PyccelAstNode
-from .datatypes import DataType
+from .datatypes import HomogeneousContainerType, FixedSizeType, PrimitiveIntegerType
 from .literals  import LiteralString
 
 __all__ = ('CMacro',
+           'CNativeInt',
            'CStackArray',
            'CStringExpression',
            'ObjectAddress',
@@ -20,16 +21,38 @@
 
 #------------------------------------------------------------------------------
 
-class CStackArray(DataType, metaclass=Singleton):
+class CNativeInt(FixedSizeType):
+    """
+    Class representing C's native integer type.
+
+    Class representing C's native integer type.
+    """
+    __slots__ = ()
+    _name = 'int'
+    _primitive_type = PrimitiveIntegerType()
+
+#------------------------------------------------------------------------------
+
+class CStackArray(HomogeneousContainerType, metaclass=ArgumentSingleton):
     """
     A data type representing an array allocated on the stack.
 
     A data type representing an array allocated on the stack.
     E.g. `float a[4];`
+
+    Parameters
+    ----------
+    element_type : FixedSizeType
+        The type of the elements inside the array.
     """
-    __slots__ = ()
+    __slots__ = ('_element_type',)
     _name = 'c_stackarray'
 
+    def __init__(self, element_type):
+        assert isinstance(element_type, FixedSizeType)
+        self._element_type = element_type
+        super().__init__()
+
 #------------------------------------------------------------------------------
 class ObjectAddress(TypedAstNode):
     """
@@ -47,13 +70,13 @@ class ObjectAddress(TypedAstNode):
 
     Examples
     --------
-    >>> CCodePrinter._print(ObjectAddress(Variable('int','a')))
+    >>> CCodePrinter._print(ObjectAddress(Variable(PythonNativeInt(),'a')))
     '&a'
-    >>> CCodePrinter._print(ObjectAddress(Variable('int','a', memory_handling='alias')))
+    >>> CCodePrinter._print(ObjectAddress(Variable(PythonNativeInt(),'a', memory_handling='alias')))
     'a'
     """
 
-    __slots__ = ('_obj', '_rank', '_precision', '_dtype', '_shape', '_order', '_class_type')
+    __slots__ = ('_obj', '_rank', '_shape', '_order', '_class_type')
     _attribute_nodes = ('_obj',)
 
     def __init__(self, obj):
@@ -62,8 +85,6 @@ def __init__(self, obj):
         self._obj        = obj
         self._rank       = obj.rank
         self._shape      = obj.shape
-        self._precision  = obj.precision
-        self._dtype      = obj.dtype
         self._order      = obj.order
         self._class_type = obj.class_type
         super().__init__()
@@ -100,7 +121,7 @@ class PointerCast(TypedAstNode):
     cast_type : TypedAstNode
         A TypedAstNode describing the object resulting from the cast.
     """
-    __slots__ = ('_obj', '_rank', '_precision', '_dtype', '_shape', '_order',
+    __slots__ = ('_obj', '_rank', '_shape', '_order',
             '_class_type', '_cast_type')
     _attribute_nodes = ('_obj',)
 
@@ -111,8 +132,6 @@ def __init__(self, obj, cast_type):
         self._obj        = obj
         self._rank       = cast_type.rank
         self._shape      = cast_type.shape
-        self._precision  = cast_type.precision
-        self._dtype      = cast_type.dtype
         self._class_type = cast_type.class_type
         self._order      = cast_type.order
         self._cast_type  = cast_type
diff --git a/pyccel/ast/class_defs.py b/pyccel/ast/class_defs.py
index 69b2737aac..eee85e6824 100644
--- a/pyccel/ast/class_defs.py
+++ b/pyccel/ast/class_defs.py
@@ -9,16 +9,16 @@
 from pyccel.ast.builtin_methods.set_methods  import SetAdd, SetClear, SetCopy
 from pyccel.ast.builtin_methods.list_methods import ListAppend, ListInsert, ListPop, ListClear, ListExtend
 
+
 from .builtins   import PythonImag, PythonReal, PythonConjugate
 from .core       import ClassDef, PyccelFunctionDef
-from .c_concepts import CStackArray
-from .datatypes  import (NativeBool, NativeInteger, NativeFloat,
-                         NativeComplex, NativeString, NativeNumericTypes,
-                         NativeTuple, CustomDataType, NativeHomogeneousList, NativeHomogeneousSet)
+from .datatypes  import (PythonNativeBool, PythonNativeInt, PythonNativeFloat,
+                         PythonNativeComplex, StringType, TupleType, CustomDataType,
+                         HomogeneousListType, HomogeneousSetType)
 from .numpyext   import (NumpyShape, NumpySum, NumpyAmin, NumpyAmax,
                          NumpyImag, NumpyReal, NumpyTranspose,
-                         NumpyConjugate, NumpySize, NumpyResultType,
-                         NumpyArray, NumpyNDArrayType)
+                         NumpyConjugate, NumpySize, NumpyResultType, NumpyArray)
+from .numpytypes import NumpyNumericType, NumpyNDArrayType
 
 __all__ = (
     'BooleanClass',
@@ -36,7 +36,7 @@
 
 #=======================================================================================
 
-ComplexClass = ClassDef('complex', class_type = NativeComplex(),
+ComplexClass = ClassDef('complex', class_type = PythonNativeComplex(),
         methods=[
             PyccelFunctionDef('imag', func_class = PythonImag,
                 decorators={'property':'property', 'numpy_wrapper':'numpy_wrapper'}),
@@ -48,7 +48,7 @@
 
 #=======================================================================================
 
-FloatClass = ClassDef('float', class_type = NativeFloat(),
+FloatClass = ClassDef('float', class_type = PythonNativeFloat(),
         methods=[
             PyccelFunctionDef('imag', func_class = PythonImag,
                 decorators={'property':'property', 'numpy_wrapper': 'numpy_wrapper'}),
@@ -64,7 +64,7 @@
 
 #=======================================================================================
 
-IntegerClass = ClassDef('integer', class_type = NativeInteger(),
+IntegerClass = ClassDef('integer', class_type = PythonNativeInt(),
         methods=[
             PyccelFunctionDef('imag', func_class = PythonImag,
                 decorators={'property':'property', 'numpy_wrapper': 'numpy_wrapper'}),
@@ -82,12 +82,12 @@
 
 #=======================================================================================
 
-BooleanClass = ClassDef('boolean', class_type = NativeBool(),
+BooleanClass = ClassDef('boolean', class_type = PythonNativeBool(),
         superclasses=(IntegerClass,))
 
 #=======================================================================================
 
-StringClass = ClassDef('string', class_type = NativeString(),
+StringClass = ClassDef('string', class_type = StringType(),
         methods=[
                 #capitalize
                 #casefold
@@ -138,10 +138,10 @@
 
 #=======================================================================================
 
-ListClass = ClassDef('list', class_type = NativeHomogeneousList(),
+ListClass = ClassDef('list',
         methods=[
             PyccelFunctionDef('append', func_class = ListAppend),
-            PyccelFunctionDef('clear', func_class = ListClear,),
+            PyccelFunctionDef('clear', func_class = ListClear),
             PyccelFunctionDef('extend', func_class = ListExtend),
             PyccelFunctionDef('insert', func_class = ListInsert),
             PyccelFunctionDef('pop', func_class = ListPop),
@@ -149,7 +149,7 @@
 
 #=======================================================================================
 
-SetClass = ClassDef('set', class_type=NativeHomogeneousSet(),
+SetClass = ClassDef('set',
         methods=[
             PyccelFunctionDef('add', func_class = SetAdd ),
             PyccelFunctionDef('clear', func_class = SetClear),
@@ -158,7 +158,7 @@
 
 #=======================================================================================
 
-TupleClass = ClassDef('tuple', class_type = NativeTuple(),
+TupleClass = ClassDef('tuple',
         methods=[
             #index
             #count
@@ -166,7 +166,7 @@
 
 #=======================================================================================
 
-NumpyArrayClass = ClassDef('numpy.ndarray', class_type = NumpyNDArrayType(),
+NumpyArrayClass = ClassDef('numpy.ndarray',
         methods=[
             PyccelFunctionDef('shape', func_class = NumpyShape,
                 decorators = {'property': 'property', 'numpy_wrapper': 'numpy_wrapper'}),
@@ -199,37 +199,30 @@
 
 #=======================================================================================
 
-StackArrayClass = ClassDef('stack_array', class_type = CStackArray())
+StackArrayClass = ClassDef('stack_array')
 
 #=======================================================================================
 
 literal_classes = {
-        NativeBool()    : BooleanClass,
-        NativeInteger() : IntegerClass,
-        NativeFloat()   : FloatClass,
-        NativeComplex() : ComplexClass,
-        NativeString()  : StringClass
+        PythonNativeBool()    : BooleanClass,
+        PythonNativeInt()     : IntegerClass,
+        PythonNativeFloat()   : FloatClass,
+        PythonNativeComplex() : ComplexClass,
+        StringType()          : StringClass
 }
 
 #=======================================================================================
 
-def get_cls_base(dtype, precision, container_type):
+def get_cls_base(class_type):
     """
     Determine the base class of an object.
 
-    From the dtype and rank, determine the base class of an object.
+    From the type, determine the base class of an object.
 
     Parameters
     ----------
-    dtype : DataType
-        The data type of the object.
-
-    precision : int
-        The precision of the object.
-
-    container_type : DataType
-        The Python type of the object. If this is different to the dtype then
-        the object is a container.
+    class_type : DataType
+        The Python type of the object.
 
     Returns
     -------
@@ -241,22 +234,18 @@ def get_cls_base(dtype, precision, container_type):
     NotImplementedError
         Raised if the base class cannot be found.
     """
-    if isinstance(dtype, CustomDataType) and container_type is dtype:
+    if isinstance(class_type, CustomDataType):
         return None
-    elif precision in (-1, 0, None) and container_type is dtype:
-        return literal_classes[dtype]
-    elif isinstance(container_type, (*NativeNumericTypes, NumpyNDArrayType)):
+    elif class_type in literal_classes:
+        return literal_classes[class_type]
+    elif isinstance(class_type, (NumpyNumericType, NumpyNDArrayType)):
         return NumpyArrayClass
-    elif isinstance(container_type, NativeTuple):
+    elif isinstance(class_type, TupleType):
         return TupleClass
-    elif isinstance(container_type, NativeHomogeneousList):
+    elif isinstance(class_type, HomogeneousListType):
         return ListClass
-    elif isinstance(container_type, NativeHomogeneousSet):
+    elif isinstance(class_type, HomogeneousSetType):
         return SetClass
     else:
-        if container_type:
-            type_name = str(container_type)
-        else:
-            type_name = f"{dtype}({precision})"
-        raise NotImplementedError(f"No class definition found for type {type_name}")
+        raise NotImplementedError(f"No class definition found for type {class_type}")
 
diff --git a/pyccel/ast/cmathext.py b/pyccel/ast/cmathext.py
index 1d7585f656..af1901d63d 100644
--- a/pyccel/ast/cmathext.py
+++ b/pyccel/ast/cmathext.py
@@ -9,7 +9,8 @@
 
 from pyccel.ast.builtins  import PythonReal, PythonImag
 from pyccel.ast.core      import PyccelFunctionDef, Module
-from pyccel.ast.datatypes import NativeBool, NativeFloat, NativeComplex
+from pyccel.ast.datatypes import PythonNativeBool, PythonNativeFloat, PythonNativeComplex
+from pyccel.ast.datatypes import PrimitiveComplexType
 from pyccel.ast.internals import PyccelInternalFunction
 from pyccel.ast.literals  import LiteralInteger
 from pyccel.ast.operators import PyccelAnd, PyccelOr
@@ -58,9 +59,7 @@ class CmathFunctionBool(MathFunctionBase):
         The arguments passed to the function.
     """
     __slots__ = ()
-    _dtype = NativeBool()
-    _precision = -1
-    _class_type = NativeBool()
+    _class_type = PythonNativeBool()
 
 class CmathFunctionComplex(MathFunctionBase):
     """
@@ -75,12 +74,10 @@ class CmathFunctionComplex(MathFunctionBase):
         The expression passed as argument to the function.
     """
     __slots__ = ()
-    _dtype = NativeComplex()
-    _precision = -1
     _shape = None
     _rank  = 0
     _order = None
-    _class_type = NativeComplex()
+    _class_type = PythonNativeComplex()
 
     def __init__(self, z : 'TypedAstNode'):
         super().__init__(z)
@@ -357,7 +354,7 @@ class CmathIsfinite(CmathFunctionBool):
     __slots__ = ()
     name = 'isfinite'
     def __new__(cls, z):
-        if z.dtype is not NativeComplex():
+        if not isinstance(z.dtype.primitive_type, PrimitiveComplexType):
             return MathIsfinite(z)
         else:
             return PyccelAnd(MathIsfinite(PythonImag(z)), MathIsfinite(PythonReal(z)))
@@ -378,7 +375,7 @@ class CmathIsinf   (CmathFunctionBool):
     __slots__ = ()
     name = 'isinf'
     def __new__(cls, z):
-        if z.dtype is not NativeComplex():
+        if not isinstance(z.dtype.primitive_type, PrimitiveComplexType):
             return MathIsinf(z)
         else:
             return PyccelOr(MathIsinf(PythonImag(z)), MathIsinf(PythonReal(z)))
@@ -399,7 +396,7 @@ class CmathIsnan   (CmathFunctionBool):
     __slots__ = ()
     name = 'isnan'
     def __new__(cls, z):
-        if z.dtype is not NativeComplex():
+        if not isinstance(z.dtype.primitive_type, PrimitiveComplexType):
             return MathIsnan(z)
         else:
             return PyccelOr(MathIsnan(PythonImag(z)), MathIsnan(PythonReal(z)))
@@ -438,12 +435,10 @@ class CmathPolar(PyccelInternalFunction):
     """
     __slots__ = ()
     name = 'polar'
-    _dtype = NativeFloat()
-    _precision = -1
     _shape = (LiteralInteger(2),)
     _rank  = 1
     _order = None
-    _class_type = NativeFloat()
+    _class_type = PythonNativeFloat()
 
     def __init__(self, z):
         super().__init__(z)
@@ -463,12 +458,10 @@ class CmathRect(PyccelInternalFunction):
     """
     __slots__ = ()
     name = 'rect'
-    _dtype = NativeComplex()
-    _precision = -1
     _shape = None
     _rank  = 0
     _order = None
-    _class_type = NativeComplex()
+    _class_type = PythonNativeComplex()
     def __init__(self, r, phi):
         super().__init__(r, phi)
 
@@ -482,8 +475,8 @@ def __init__(self, r, phi):
             CmathPolar, CmathRect, CmathSin, CmathSinh, CmathSqrt, CmathTan, CmathTanh)]
 
 cmath_constants = { **math_constants,
-    'infj': Constant('complex', 'infj', value=cmath.infj),
-    'nanj': Constant('complex', 'nanj', value=cmath.nanj),
+    'infj': Constant(PythonNativeComplex(), 'infj', value=cmath.infj),
+    'nanj': Constant(PythonNativeComplex(), 'nanj', value=cmath.nanj),
     }
 
 cmath_mod = Module('cmath',
diff --git a/pyccel/ast/core.py b/pyccel/ast/core.py
index 9dd1dc10c6..1c4ab5a3c3 100644
--- a/pyccel/ast/core.py
+++ b/pyccel/ast/core.py
@@ -18,11 +18,11 @@
 
 from .c_concepts import PointerCast
 
-from .datatypes import (datatype, DataType, NativeSymbol, NativeHomogeneousTuple,
-                        NativeBool, NativeTuple, str_dtype, NativeInhomogeneousTuple,
-                        NativeVoid)
+from .datatypes import (PyccelType, SymbolicType, HomogeneousTupleType,
+                        PythonNativeBool, InhomogeneousTupleType,
+                        VoidType)
 
-from .internals import PyccelSymbol, PyccelInternalFunction, get_final_precision, apply_pickle
+from .internals import PyccelSymbol, PyccelInternalFunction, apply_pickle
 
 from .literals  import Nil, LiteralFalse, LiteralInteger
 from .literals  import NilArgument, LiteralTrue
@@ -200,12 +200,10 @@ class Duplicate(TypedAstNode):
     length : TypedAstNode
         The number of times the val should appear in the final object.
     """
-    __slots__ = ('_val', '_length','_dtype','_precision','_rank','_shape','_order','_class_type')
+    __slots__ = ('_val', '_length','_rank','_shape','_order','_class_type')
     _attribute_nodes = ('_val', '_length')
 
     def __init__(self, val, length):
-        self._dtype      = val.dtype
-        self._precision  = val.precision
         self._rank       = val.rank
         self._shape      = tuple(s if i!= 0 else PyccelMul(s, length, simplify=True) for i,s in enumerate(val.shape))
         self._order      = val.order
@@ -242,12 +240,10 @@ class Concatenate(TypedAstNode):
     arg2 : TypedAstNodes
            The second tuple.
     """
-    __slots__ = ('_args','_dtype','_precision','_rank','_shape','_order','_class_type')
+    __slots__ = ('_args','_rank','_shape','_order','_class_type')
     _attribute_nodes = ('_args',)
 
     def __init__(self, arg1, arg2):
-        self._dtype      = arg1.dtype
-        self._precision  = arg1.precision
         self._rank       = arg1.rank
         shape_addition   = arg2.shape[0]
         self._shape      = tuple(s if i!= 0 else PyccelAdd(s, shape_addition) for i,s in enumerate(arg1.shape))
@@ -298,6 +294,7 @@ class Assign(PyccelAstNode):
 
     Examples
     --------
+    >>> from pyccel.ast.datatypes import PythonNativeInt
     >>> from pyccel.ast.internals import symbols
     >>> from pyccel.ast.variable import Variable
     >>> from pyccel.ast.core import Assign
@@ -306,7 +303,7 @@ class Assign(PyccelAstNode):
     x := y
     >>> Assign(x, 0)
     x := 0
-    >>> A = Variable('int', 'A', rank = 2)
+    >>> A = Variable(PythonNativeInt(), 'A', rank = 2)
     >>> Assign(x, A)
     x := A
     >>> Assign(A[0,1], x)
@@ -378,19 +375,24 @@ def is_alias(self):
 
     @property
     def is_symbolic_alias(self):
-        """Returns True if the assignment is a symbolic alias."""
+        """
+        Returns True if the assignment is a symbolic alias.
+
+        Returns True if the assignment is a symbolic alias.
+        """
 
         # TODO to be improved when handling classes
+        # TODO: Is this useful?
 
         lhs = self.lhs
         rhs = self.rhs
         if isinstance(lhs, Variable):
-            return isinstance(lhs.dtype, NativeSymbol)
+            return isinstance(lhs.class_type, SymbolicType)
         elif isinstance(lhs, PyccelSymbol):
             if isinstance(rhs, PythonRange):
                 return True
             elif isinstance(rhs, Variable):
-                return isinstance(rhs.dtype, NativeSymbol)
+                return isinstance(rhs.class_type, SymbolicType)
             elif isinstance(rhs, PyccelSymbol):
                 return True
 
@@ -708,8 +710,8 @@ class AliasAssign(PyccelAstNode):
     >>> from pyccel.ast.internals import PyccelSymbol
     >>> from pyccel.ast.core import AliasAssign
     >>> from pyccel.ast.core import Variable
-    >>> n = Variable('int', 'n')
-    >>> x = Variable('int', 'x', rank=1, shape=[n])
+    >>> n = Variable(PythonNativeInt(), 'n')
+    >>> x = Variable(PythonNativeInt(), 'x', rank=1, shape=[n])
     >>> y = PyccelSymbol('y')
     >>> AliasAssign(y, x)
     """
@@ -817,8 +819,8 @@ class AugAssign(Assign):
     --------
     >>> from pyccel.ast.core import Variable
     >>> from pyccel.ast.core import AugAssign
-    >>> s = Variable('int', 's')
-    >>> t = Variable('int', 't')
+    >>> s = Variable(PythonNativeInt(), 's')
+    >>> t = Variable(PythonNativeInt(), 't')
     >>> AugAssign(s, '+', 2 * t + 1)
     s += 1 + 2*t
     """
@@ -873,8 +875,8 @@ def to_basic_assign(self):
 
 
 class While(ScopedAstNode):
-
-    """Represents a 'while' statement in the code.
+    """
+    Represents a 'while' statement in the code.
 
     Expressions are of the form:
         "while test:
@@ -883,11 +885,11 @@ class While(ScopedAstNode):
     Parameters
     ----------
     test : TypedAstNode
-        test condition given as an expression
+        Test condition given as an expression.
     body : list of Pyccel objects
-        list of statements representing the body of the While statement.
+        List of statements representing the body of the While statement.
     scope : Scope
-        The scope for the loop
+        The scope for the loop.
 
     Examples
     --------
@@ -903,7 +905,7 @@ class While(ScopedAstNode):
     def __init__(self, test, body, scope = None):
 
         if pyccel_stage == 'semantic':
-            if test.dtype is not NativeBool():
+            if test.dtype is not PythonNativeBool():
                 test = PythonBool(test)
 
         if iterable(body):
@@ -1027,12 +1029,12 @@ class Module(ScopedAstNode):
     >>> from pyccel.ast.core import ClassDef, FunctionDef, Module
     >>> from pyccel.ast.operators import PyccelAdd, PyccelMinus
     >>> from pyccel.ast.literals import LiteralInteger
-    >>> x = Variable('float', 'x')
-    >>> y = Variable('float', 'y')
-    >>> z = Variable('float', 'z')
-    >>> t = Variable('float', 't')
-    >>> a = Variable('float', 'a')
-    >>> b = Variable('float', 'b')
+    >>> x = Variable(PythonNativeFloat(), 'x')
+    >>> y = Variable(PythonNativeFloat(), 'y')
+    >>> z = Variable(PythonNativeFloat(), 'z')
+    >>> t = Variable(PythonNativeFloat(), 't')
+    >>> a = Variable(PythonNativeFloat(), 'a')
+    >>> b = Variable(PythonNativeFloat(), 'b')
     >>> body = [Assign(z,PyccelAdd(x,a))]
     >>> args = [FunctionDefArgument(arg) for arg in [x,y,a,b]]
     >>> results = [FunctionDefResult(res) for res in [z,t]]
@@ -1275,12 +1277,12 @@ class ModuleHeader(PyccelAstNode):
     >>> from pyccel.ast.core import ClassDef, FunctionDef, Module
     >>> from pyccel.ast.operators import PyccelAdd, PyccelMinus
     >>> from pyccel.ast.literals import LiteralInteger
-    >>> x = Variable('float', 'x')
-    >>> y = Variable('float', 'y')
-    >>> z = Variable('float', 'z')
-    >>> t = Variable('float', 't')
-    >>> a = Variable('float', 'a')
-    >>> b = Variable('float', 'b')
+    >>> x = Variable(PythonNativeFloat(), 'x')
+    >>> y = Variable(PythonNativeFloat(), 'y')
+    >>> z = Variable(PythonNativeFloat(), 'z')
+    >>> t = Variable(PythonNativeFloat(), 't')
+    >>> a = Variable(PythonNativeFloat(), 'a')
+    >>> b = Variable(PythonNativeFloat(), 'b')
     >>> body = [Assign(z,PyccelAdd(x,a))]
     >>> args = [FunctionDefArgument(arg) for arg in [x,y,a,b]]
     >>> results = [FunctionDefResult(res) for res in [z,t]]
@@ -1538,8 +1540,8 @@ def loop_counters(self):
 #==============================================================================
 
 class For(ScopedAstNode):
-
-    """Represents a 'for-loop' in the code.
+    """
+    Represents a 'for-loop' in the code.
 
     Expressions are of the form:
         "for target in iter:
@@ -1547,14 +1549,15 @@ class For(ScopedAstNode):
 
     Parameters
     ----------
-    target : symbol / Variable
-        symbol representing the iterator
-    iter : iterable
-        iterable object. for the moment only Range is used
-    body : list of pyccel objects
-        list of statements representing the body of the For statement.
+    target : Variable
+        Variable representing the iterator.
+    iter_obj : Iterable
+        Iterable object. Multiple iterators are supported but these are
+        translated to a range object in the Iterable class.
+    body : list[PyccelAstNode]
+        List of statements representing the body of the For statement.
     scope : Scope
-        The scope for the loop
+        The scope for the loop.
 
     Examples
     --------
@@ -1562,7 +1565,7 @@ class For(ScopedAstNode):
     >>> from pyccel.ast.core import Assign, For
     >>> from pyccel.ast.internals import symbols
     >>> i,b,e,s,x = symbols('i,b,e,s,x')
-    >>> A = Variable('int', 'A', rank = 2)
+    >>> A = Variable(PythonNativeInt(), 'A', rank = 2)
     >>> For(i, (b,e,s), [Assign(x, i), Assign(A[0, 1], x)])
     For(i, (b, e, s), (x := i, IndexedElement(A, 0, 1) := x))
     """
@@ -1936,7 +1939,7 @@ def annotation(self):
         The result annotation providing dtype information.
 
         The annotation which provides all information about the data
-        types, precision, etc, necessary to fully define the result.
+        types, rank, etc, necessary to fully define the result.
         """
         return self._annotation
 
@@ -1976,7 +1979,7 @@ class FunctionCall(TypedAstNode):
         The function where the call takes place.
     """
     __slots__ = ('_arguments','_funcdef','_interface','_func_name','_interface_name',
-                 '_dtype','_precision','_shape','_rank','_order','_class_type')
+                 '_shape','_rank','_order','_class_type')
     _attribute_nodes = ('_arguments','_funcdef','_interface')
 
     def __init__(self, func, args, current_function=None):
@@ -2050,29 +2053,23 @@ def __init__(self, func, args, current_function=None):
         self._func_name  = func.name
         n_results = len(func.results)
         if n_results == 1:
-            self._dtype      = func.results[0].var.dtype
             self._rank       = func.results[0].var.rank
             self._shape      = func.results[0].var.shape
-            self._precision  = func.results[0].var.precision
             self._order      = func.results[0].var.order
             self._class_type = func.results[0].var.class_type
         elif n_results == 0:
-            self._dtype      = NativeVoid()
             self._rank       = 0
             self._shape      = None
-            self._precision  = None
             self._order      = None
-            self._class_type = NativeVoid()
+            self._class_type = VoidType()
         else:
             dtypes = [r.var.dtype for r in func.results]
             if all(d is dtypes[0] for d in dtypes):
-                dtype = NativeHomogeneousTuple()
+                dtype = HomogeneousTupleType(dtypes[0])
             else:
-                dtype = NativeInhomogeneousTuple(*dtypes)
-            self._dtype      = dtype
+                dtype = InhomogeneousTupleType(*dtypes)
             self._rank       = 1
             self._shape      = (LiteralInteger(n_results),)
-            self._precision  = None
             self._order      = None
             self._class_type = dtype
 
@@ -2311,8 +2308,8 @@ class FunctionDef(ScopedAstNode):
     >>> from pyccel.ast.core import Assign, FunctionDef
     >>> from pyccel.ast.operators import PyccelAdd
     >>> from pyccel.ast.literals import LiteralInteger
-    >>> x = Variable('float', 'x')
-    >>> y = Variable('float', 'y')
+    >>> x = Variable(PythonNativeFloat(), 'x')
+    >>> y = Variable(PythonNativeFloat(), 'y')
     >>> args        = [FunctionDefArgument(x)]
     >>> results     = [FunctionDefResult(y)]
     >>> body        = [Assign(y,PyccelAdd(x,LiteralInteger(1)))]
@@ -2326,8 +2323,8 @@ class FunctionDef(ScopedAstNode):
     >>> from pyccel.ast.core import FunctionDef
     >>> from pyccel.ast.core import FunctionDefArgument
     >>> n = FunctionDefArgument('n', value=4)
-    >>> x = Variable('float', 'x')
-    >>> y = Variable('float', 'y')
+    >>> x = Variable(PythonNativeFloat(), 'x')
+    >>> y = Variable(PythonNativeFloat(), 'y')
     >>> args        = [x, n]
     >>> results     = [y]
     >>> body        = [Assign(y,x+n)]
@@ -2961,6 +2958,20 @@ def argument_description(self):
         """
         return self._argument_description
 
+    def __getnewargs__(self):
+        """
+          This method returns the positional and keyword arguments
+            used to create an instance of this class.
+        """
+        args = (
+        self._name,
+        self._cls_name)
+
+        kwargs = {
+            'decorators':self._decorators,
+            'argument_description':self._argument_description}
+        return args, kwargs
+
 class Interface(PyccelAstNode):
     """
     Class representing an interface function.
@@ -3135,22 +3146,32 @@ def __getnewargs__(self):
         'syntactic_node':self._syntactic_node}
         return args, kwargs
 
-    def point(self, args, use_final_precision = False):
-        """Returns the actual function that will be called, depending on the passed arguments."""
+    def point(self, args):
+        """
+        Return the actual function that will be called, depending on the passed arguments.
+
+        From the arguments passed in the function call, determine which of the FunctionDef
+        objects in the Interface is actually called.
+
+        Parameters
+        ----------
+        args : tuple[TypedAstNode]
+            The arguments passed in the function call.
+
+        Returns
+        -------
+        FunctionDef
+            The function definition which corresponds with the arguments.
+        """
         fs_args = [[j for j in i.arguments] for i in
                     self._functions]
 
-        if use_final_precision:
-            type_match = lambda dtype1, dtype2, call_arg, func_arg: \
-                    (dtype1 in dtype2 or dtype2 in dtype1) \
-                    and (call_arg.rank == func_arg.rank) \
-                    and get_final_precision(call_arg) == \
-                        get_final_precision(func_arg)
-        else:
-            type_match = lambda dtype1, dtype2, call_arg, func_arg: \
-                    (dtype1 in dtype2 or dtype2 in dtype1) \
-                    and (call_arg.rank == func_arg.rank) \
-                    and call_arg.precision == func_arg.precision
+        def type_match(call_arg, func_arg):
+            """
+            Check that the types of the arguments in the function and the call match.
+            """
+            return call_arg.class_type == func_arg.class_type \
+                    and (call_arg.rank == func_arg.rank)
 
 
         j = -1
@@ -3160,17 +3181,14 @@ def point(self, args, use_final_precision = False):
             for (x, y) in enumerate(args):
                 func_arg = i[x].var
                 call_arg = y.value
-                dtype1 = str_dtype(call_arg.dtype)
-                dtype2 = str_dtype(func_arg.dtype)
-                found = found and type_match(dtype1, dtype2, call_arg, func_arg)
+                found = found and type_match(call_arg, func_arg)
             if found:
                 break
 
-        if found:
-            return  self._functions[j]
-        else:
-            errors.report('Arguments types provided to {} are incompatible'.format(self.name),
+        if not found:
+            errors.report(f'Arguments types provided to {self.name} are incompatible',
                         severity='fatal')
+        return  self._functions[j]
 
 class FunctionAddress(FunctionDef):
     """
@@ -3212,8 +3230,8 @@ class FunctionAddress(FunctionDef):
     Examples
     --------
     >>> from pyccel.ast.core import Variable, FunctionAddress, FuncAddressDeclare, FunctionDef
-    >>> x = Variable('float', 'x')
-    >>> y = Variable('float', 'y')
+    >>> x = Variable(PythonNativeFloat(), 'x')
+    >>> y = Variable(PythonNativeFloat(), 'y')
     >>> # a function definition can have a FunctionAddress as an argument
     >>> FunctionDef('g', [FunctionAddress('f', [x], [y])], [], [])
     >>> # we can also Declare a FunctionAddress
@@ -3344,19 +3362,19 @@ class ClassDef(ScopedAstNode):
     scope : Scope
         The scope for the class contents.
 
-    class_type : DataType
+    class_type : PyccelType
         The data type associated with this class.
 
     Examples
     --------
     >>> from pyccel.ast.core import Variable, Assign
     >>> from pyccel.ast.core import ClassDef, FunctionDef
-    >>> x = Variable('float', 'x')
-    >>> y = Variable('float', 'y')
-    >>> z = Variable('float', 'z')
-    >>> t = Variable('float', 't')
-    >>> a = Variable('float', 'a')
-    >>> b = Variable('float', 'b')
+    >>> x = Variable(PythonNativeFloat(), 'x')
+    >>> y = Variable(PythonNativeFloat(), 'y')
+    >>> z = Variable(PythonNativeFloat(), 'z')
+    >>> t = Variable(PythonNativeFloat(), 't')
+    >>> a = Variable(PythonNativeFloat(), 'a')
+    >>> b = Variable(PythonNativeFloat(), 'b')
     >>> body = [Assign(y,x+a)]
     >>> translate = FunctionDef('translate', [x,y,a,b], [z,t], body)
     >>> attributes   = [x,y]
@@ -3412,13 +3430,13 @@ def __init__(
 
         if not iterable(superclasses):
             raise TypeError('superclasses must be iterable')
-        if pyccel_stage != 'syntactic':
+        if pyccel_stage.current_stage and pyccel_stage != 'syntactic':
             for s in superclasses:
                 if not isinstance(s, ClassDef):
                     raise TypeError('superclass item must be a ClassDef')
 
-            if not isinstance(class_type, DataType):
-                raise TypeError("class_type must be a DataType")
+            if not isinstance(class_type, PyccelType):
+                raise TypeError("class_type must be a PyccelType")
 
         if not iterable(interfaces):
             raise TypeError('interfaces must be iterable')
@@ -3483,9 +3501,9 @@ def name(self):
     @property
     def class_type(self):
         """
-        The DataType of an object of the described class.
+        The PyccelType of an object of the described class.
 
-        The DataType of an object of the described class.
+        The PyccelType of an object of the described class.
         """
         return self._class_type
 
@@ -3858,8 +3876,8 @@ class FuncAddressDeclare(PyccelAstNode):
     Examples
     --------
     >>> from pyccel.ast.core import Variable, FunctionAddress, FuncAddressDeclare
-    >>> x = Variable('float', 'x')
-    >>> y = Variable('float', 'y')
+    >>> x = Variable(PythonNativeFloat(), 'x')
+    >>> y = Variable(PythonNativeFloat(), 'y')
     >>> FuncAddressDeclare(FunctionAddress('f', [x], [y]))
     """
     __slots__ = ('_variable','_intent','_value','_static')
@@ -3942,9 +3960,9 @@ class Declare(PyccelAstNode):
     Examples
     --------
     >>> from pyccel.ast.core import Declare, Variable
-    >>> Declare(Variable('int', 'n'))
+    >>> Declare(Variable(PythonNativeInt(), 'n'))
     Declare(n, None)
-    >>> Declare(Variable('float', 'x'), intent='out')
+    >>> Declare(Variable(PythonNativeFloat(), 'x'), intent='out')
     Declare(x, out)
     """
     __slots__ = ('_variable','_intent','_value',
@@ -4076,18 +4094,20 @@ def expr(self):
 
 
 class Del(PyccelAstNode):
+    """
+    Represents a memory deallocation in the code.
 
-    """Represents a memory deallocation in the code.
+    Represents a memory deallocation in the code.
 
     Parameters
     ----------
-    variables : list, tuple
-        a list of pyccel variables
+    expr : Variable
+        The variable being deallocated.
 
     Examples
     --------
     >>> from pyccel.ast.core import Del, Variable
-    >>> x = Variable('float', 'x', rank=2, shape=(10,2), memory_handling='heap')
+    >>> x = Variable(PythonNativeFloat(), 'x', rank=2, shape=(10,2), memory_handling='heap')
     >>> Del([x])
     Del([x])
     """
@@ -4289,17 +4309,19 @@ class Assert(PyccelAstNode):
     """
     Represents an assert statement in the code.
 
+    Represents an assert statement in the code.
+
     Parameters
     ----------
-    test: TypedAstNode
-        boolean expression to check
+    test : TypedAstNode
+        Boolean expression to check.
     """
     __slots__ = ('_test',)
     _attribute_nodes = ('_test',)
 
     def __init__(self, test):
         if pyccel_stage != 'syntactic':
-            if test.dtype is not NativeBool():
+            if test.dtype is not PythonNativeBool():
                 test = PythonBool(test)
         self._test = test
         super().__init__()
@@ -4328,16 +4350,19 @@ class ErrorExit(Exit):
     __slots__ = ()
 
 class IfSection(PyccelAstNode):
-    """Represents a condition and associated code block
+    """
+    Represents one condition and code block in an if statement.
+
+    Represents a condition and associated code block
     in an if statement in the code.
 
     Parameters
     ----------
     cond : TypedAstNode
            A boolean expression indicating whether or not the block
-           should be executed
+           should be executed.
     body : CodeBlock
-           The code to be executed in the condition is satisfied
+           The code to be executed if the condition is satisfied.
 
     Examples
     --------
@@ -4352,7 +4377,7 @@ class IfSection(PyccelAstNode):
 
     def __init__(self, cond, body):
 
-        if pyccel_stage == 'semantic' and cond.dtype is not NativeBool():
+        if pyccel_stage == 'semantic' and cond.dtype is not PythonNativeBool():
             cond = PythonBool(cond)
         if isinstance(body, (list, tuple)):
             body = CodeBlock(body)
@@ -4446,12 +4471,10 @@ class InProgram(TypedAstNode):
     other words, a class representing the boolean:
     `__name__ == '__main__'`
     """
-    _dtype = NativeBool()
-    _precision = -1
     _rank  = 0
     _shape = None
     _order = None
-    _class_type = NativeBool()
+    _class_type = PythonNativeBool()
     _attribute_nodes = ()
     __slots__ = ()
 
diff --git a/pyccel/ast/cwrapper.py b/pyccel/ast/cwrapper.py
index 2dfaf292c0..bc796550e6 100644
--- a/pyccel/ast/cwrapper.py
+++ b/pyccel/ast/cwrapper.py
@@ -17,22 +17,23 @@
 
 from .bind_c    import BindCPointer
 
-from .datatypes import DataType, default_precision, CustomDataType
-from .datatypes import NativeInteger, NativeFloat, NativeComplex
-from .datatypes import NativeBool, NativeString, NativeVoid
+from .datatypes import FixedSizeType, CustomDataType
+from .datatypes import PythonNativeInt, PythonNativeFloat, PythonNativeComplex
+from .datatypes import PythonNativeBool, StringType, VoidType
+from .datatypes import PrimitiveBooleanType, PrimitiveIntegerType, PrimitiveFloatingPointType, PrimitiveComplexType
 
 from .core      import FunctionDefArgument, FunctionDefResult
 from .core      import FunctionDef, ClassDef
 from .core      import Module, Interface, Declare
 
-from .internals import get_final_precision, PyccelInternalFunction
+from .c_concepts import ObjectAddress, CNativeInt
+
+from .internals import PyccelInternalFunction
 
 from .literals  import LiteralString, LiteralInteger
 
 from .variable  import Variable
 
-from .c_concepts import ObjectAddress
-
 
 errors = Errors()
 
@@ -79,7 +80,7 @@
 #-------------------------------------------------------------------
 #                        Python DataTypes
 #-------------------------------------------------------------------
-class PyccelPyObject(DataType, metaclass=Singleton):
+class PyccelPyObject(FixedSizeType, metaclass=Singleton):
     """
     Datatype representing a `PyObject`.
 
@@ -89,7 +90,7 @@ class used to hold Python objects in `Python.h`.
     __slots__ = ()
     _name = 'pyobject'
 
-class PyccelPyClassType(DataType, metaclass=Singleton):
+class PyccelPyClassType(FixedSizeType, metaclass=Singleton):
     """
     Datatype representing a subclass of `PyObject`.
 
@@ -99,7 +100,7 @@ class PyccelPyClassType(DataType, metaclass=Singleton):
     __slots__ = ()
     _name = 'pyclasstype'
 
-class PyccelPyTypeObject(DataType, metaclass=Singleton):
+class PyccelPyTypeObject(FixedSizeType, metaclass=Singleton):
     """
     Datatype representing a `PyTypeObject`.
 
@@ -266,11 +267,9 @@ class PyBuildValueNode(PyccelInternalFunction):
     """
     __slots__ = ('_flags','_result_args')
     _attribute_nodes = ('_result_args',)
-    _dtype = PyccelPyObject
     _rank = 0
-    _precision = 0
     _shape = ()
-    _class_type = PyccelPyObject
+    _class_type = PyccelPyObject()
     _order = None
 
     def __init__(self, result_args = ()):
@@ -280,7 +279,7 @@ def __init__(self, result_args = ()):
             if isinstance(i.dtype, WrapperCustomDataType):
                 self._flags += 'O'
             else:
-                self._flags += pytype_parse_registry[(i.dtype, get_final_precision(i))]
+                self._flags += pytype_parse_registry[i.dtype]
         super().__init__()
 
     @property
@@ -311,11 +310,10 @@ class PyModule_AddObject(PyccelInternalFunction):
     """
     __slots__ = ('_mod_name','_name','_var')
     _attribute_nodes = ('_name','_var')
-    _dtype = NativeInteger()
-    _precision = 4
+    #_precision = 4
     _rank = 0
     _shape = None
-    _class_type = NativeInteger()
+    _class_type = PythonNativeInt()
 
     def __init__(self, mod_name, name, variable):
         if not isinstance(name, LiteralString):
@@ -363,8 +361,6 @@ class PyModule_Create(PyccelInternalFunction):
     """
     __slots__ = ('_module_def_name',)
     _attribute_nodes = ()
-    _dtype = PyccelPyObject()
-    _precision = 0
     _rank = 0
     _shape = ()
     _order = None
@@ -407,8 +403,6 @@ class PyCapsule_New(PyccelInternalFunction):
     """
     __slots__ = ('_capsule_name', '_API_var')
     _attribute_nodes = ('_API_var',)
-    _dtype = PyccelPyObject()
-    _precision = 0
     _rank = 0
     _shape = ()
     _order = None
@@ -459,8 +453,6 @@ class PyCapsule_Import(PyccelInternalFunction):
     """
     __slots__ = ('_capsule_name',)
     _attribute_nodes = ()
-    _dtype = BindCPointer()
-    _precision = 0
     _rank = 0
     _shape = ()
     _order = None
@@ -529,7 +521,7 @@ def __init__(self, name, *args, external_funcs = (), declarations = (), init_fun
         self._declarations = declarations
         if import_func is None:
             self._import_func = FunctionDef(f'{name}_import', (),
-                            (FunctionDefResult(Variable(NativeInteger(), '_', precision=-2, is_temp=True)),), ())
+                            (FunctionDefResult(Variable(CNativeInt(), '_', is_temp=True)),), ())
         else:
             self._import_func = import_func
         super().__init__(name, *args, init_func = init_func, **kwargs)
@@ -745,9 +737,9 @@ def __init__(self, original_class, struct_name, type_name, scope, **kwargs):
         self._type_object = Variable(PyccelPyClassType(), type_name)
         self._new_func = None
         self._properties = ()
-        variables = [Variable(NativeVoid(), 'instance', memory_handling='alias'),
+        variables = [Variable(VoidType(), 'instance', memory_handling='alias'),
                      Variable(PyccelPyObject(), 'referenced_objects', memory_handling='alias'),
-                     Variable(NativeBool(), 'is_alias')]
+                     Variable(PythonNativeBool(), 'is_alias')]
         scope.insert_variable(variables[0])
         scope.insert_variable(variables[1])
         scope.insert_variable(variables[2])
@@ -961,63 +953,57 @@ def declarations(self):
 # https://docs.python.org/3/c-api/refcounting.html#c.Py_INCREF
 Py_INCREF = FunctionDef(name = 'Py_INCREF',
                         body = [],
-                        arguments = [FunctionDefArgument(Variable(dtype=PyccelPyObject(), name='o', memory_handling='alias'))],
+                        arguments = [FunctionDefArgument(Variable(PyccelPyObject(), name='o', memory_handling='alias'))],
                         results = [])
 
 # https://docs.python.org/3/c-api/refcounting.html#c.Py_DECREF
 Py_DECREF = FunctionDef(name = 'Py_DECREF',
                         body = [],
-                        arguments = [FunctionDefArgument(Variable(dtype=PyccelPyObject(), name='o', memory_handling='alias'))],
+                        arguments = [FunctionDefArgument(Variable(PyccelPyObject(), name='o', memory_handling='alias'))],
                         results = [])
 
 # https://docs.python.org/3/c-api/type.html#c.PyType_Ready
 PyType_Ready = FunctionDef(name = 'PyType_Ready',
                         body = [],
-                        arguments = [FunctionDefArgument(Variable(dtype=PyccelPyObject(), name='o', memory_handling='alias'))],
-                        results = [FunctionDefResult(Variable(NativeInteger(), '_'))])
+                        arguments = [FunctionDefArgument(Variable(PyccelPyObject(), name='o', memory_handling='alias'))],
+                        results = [FunctionDefResult(Variable(PythonNativeInt(), '_'))])
 
 # https://docs.python.org/3/c-api/sys.html#PySys_GetObject
 PySys_GetObject = FunctionDef(name = 'PySys_GetObject',
                         body = [],
-                        arguments = [FunctionDefArgument(Variable(dtype=NativeString(), name='_'))],
-                        results = [FunctionDefResult(Variable(dtype=PyccelPyObject(), name='o', memory_handling='alias'))])
+                        arguments = [FunctionDefArgument(Variable(StringType(), name='_'))],
+                        results = [FunctionDefResult(Variable(PyccelPyObject(), name='o', memory_handling='alias'))])
 
 # https://docs.python.org/3/c-api/unicode.html#c.PyUnicode_FromString
 PyUnicode_FromString = FunctionDef(name = 'PyUnicode_FromString',
                         body = [],
-                        arguments = [FunctionDefArgument(Variable(dtype=NativeString(), name='_'))],
-                        results = [FunctionDefResult(Variable(dtype=PyccelPyObject(), name='o', memory_handling='alias'))])
+                        arguments = [FunctionDefArgument(Variable(StringType(), name='_'))],
+                        results = [FunctionDefResult(Variable(PyccelPyObject(), name='o', memory_handling='alias'))])
 
 # https://docs.python.org/3/c-api/list.html#c.PyList_GetItem
 PyList_GetItem = FunctionDef(name = 'PyList_GetItem',
                         body = [],
-                        arguments = [FunctionDefArgument(Variable(dtype=PyccelPyObject(), name='l', memory_handling='alias')),
-                                     FunctionDefArgument(Variable(dtype=NativeInteger(), name='i', precision=-2))],
-                        results = [FunctionDefResult(Variable(dtype=PyccelPyObject(), name='o', memory_handling='alias'))])
+                        arguments = [FunctionDefArgument(Variable(PyccelPyObject(), name='l', memory_handling='alias')),
+                                     FunctionDefArgument(Variable(CNativeInt(), name='i'))],
+                        results = [FunctionDefResult(Variable(PyccelPyObject(), name='o', memory_handling='alias'))])
 
 # https://docs.python.org/3/c-api/list.html#c.PyList_SetItem
 PyList_SetItem = FunctionDef(name = 'PyList_SetItem',
                         body = [],
-                        arguments = [FunctionDefArgument(Variable(dtype=PyccelPyObject(), name='l', memory_handling='alias')),
-                                     FunctionDefArgument(Variable(dtype=NativeInteger(), name='i', precision=-2)),
-                                     FunctionDefArgument(Variable(dtype=PyccelPyObject(), name='new_item', memory_handling='alias'))],
+                        arguments = [FunctionDefArgument(Variable(PyccelPyObject(), name='l', memory_handling='alias')),
+                                     FunctionDefArgument(Variable(CNativeInt(), name='i')),
+                                     FunctionDefArgument(Variable(PyccelPyObject(), name='new_item', memory_handling='alias'))],
                         results = [])
 
 #-------------------------------------------------------------------
 
 #using the documentation of PyArg_ParseTuple() and Py_BuildValue https://docs.python.org/3/c-api/arg.html
 pytype_parse_registry = {
-    (NativeInteger(), 4)       : 'i',
-    (NativeInteger(), 8)       : 'l',
-    (NativeInteger(), 2)       : 'h',
-    (NativeInteger(), 1)       : 'b',
-    (NativeFloat(), 8)         : 'd',
-    (NativeFloat(), 4)         : 'f',
-    (NativeComplex(), 4)       : 'O',
-    (NativeComplex(), 8)       : 'O',
-    (NativeBool(), -1)         : 'p',
-    (NativeString(), 0)        : 's',
-    (PyccelPyObject(), 0)      : 'O',
+    PythonNativeFloat()   : 'd',
+    PythonNativeComplex() : 'O',
+    PythonNativeBool()    : 'p',
+    StringType()          : 's',
+    PyccelPyObject()      : 'O',
     }
 
 #-------------------------------------------------------------------
@@ -1026,15 +1012,16 @@ def declarations(self):
 
 # Functions definitions are defined in pyccel/stdlib/cwrapper/cwrapper.c
 py_to_c_registry = {
-    (NativeBool(), -1)     : 'PyBool_to_Bool',
-    (NativeInteger(), 1)   : 'PyInt8_to_Int8',
-    (NativeInteger(), 2)   : 'PyInt16_to_Int16',
-    (NativeInteger(), 4)   : 'PyInt32_to_Int32',
-    (NativeInteger(), 8)   : 'PyInt64_to_Int64',
-    (NativeFloat(), 4)     : 'PyFloat_to_Float',
-    (NativeFloat(), 8)     : 'PyDouble_to_Double',
-    (NativeComplex(), 4)   : 'PyComplex_to_Complex64',
-    (NativeComplex(), 8)   : 'PyComplex_to_Complex128'}
+    (PrimitiveBooleanType(), -1)      : 'PyBool_to_Bool',
+    (PrimitiveIntegerType(), 1)       : 'PyInt8_to_Int8',
+    (PrimitiveIntegerType(), 2)       : 'PyInt16_to_Int16',
+    (PrimitiveIntegerType(), 4)       : 'PyInt32_to_Int32',
+    (PrimitiveIntegerType(), 8)       : 'PyInt64_to_Int64',
+    (PrimitiveFloatingPointType(), 4) : 'PyFloat_to_Float',
+    (PrimitiveFloatingPointType(), 8) : 'PyDouble_to_Double',
+    (PrimitiveComplexType(), 4)       : 'PyComplex_to_Complex64',
+    (PrimitiveComplexType(), 8)       : 'PyComplex_to_Complex128',
+    }
 
 def C_to_Python(c_object):
     """
@@ -1059,7 +1046,7 @@ def C_to_Python(c_object):
         memory_handling = 'stack'
     else:
         try :
-            cast_function = c_to_py_registry[(c_object.dtype, c_object.precision)]
+            cast_function = c_to_py_registry[c_object.dtype]
         except KeyError:
             errors.report(PYCCEL_RESTRICTION_TODO, symbol=c_object.dtype,severity='fatal')
         memory_handling = 'alias'
@@ -1067,24 +1054,17 @@ def C_to_Python(c_object):
     cast_func = FunctionDef(name = cast_function,
                        body      = [],
                        arguments = [FunctionDefArgument(c_object.clone('v', is_argument = True, memory_handling=memory_handling, new_class = Variable))],
-                       results   = [FunctionDefResult(Variable(dtype=PyccelPyObject(), name = 'o', memory_handling='alias'))])
+                       results   = [FunctionDefResult(Variable(PyccelPyObject(), name = 'o', memory_handling='alias'))])
 
     return cast_func
 
 # Functions definitions are defined in pyccel/stdlib/cwrapper/cwrapper.c
 c_to_py_registry = {
-    (NativeBool(), -1)     : 'Bool_to_PyBool',
-    (NativeInteger(), -1)  : 'Int'+str(default_precision[NativeInteger()]*8)+'_to_PyLong',
-    (NativeInteger(), 1)   : 'Int8_to_NumpyLong',
-    (NativeInteger(), 2)   : 'Int16_to_NumpyLong',
-    (NativeInteger(), 4)   : 'Int32_to_NumpyLong',
-    (NativeInteger(), 8)   : 'Int64_to_NumpyLong',
-    (NativeFloat(), 4)     : 'Float_to_NumpyDouble',
-    (NativeFloat(), 8)     : 'Double_to_NumpyDouble',
-    (NativeFloat(), -1)    : 'Double_to_PyDouble',
-    (NativeComplex(), 4)   : 'Complex64_to_NumpyComplex',
-    (NativeComplex(), 8)   : 'Complex128_to_NumpyComplex',
-    (NativeComplex(), -1)  : 'Complex128_to_PyComplex'}
+    PythonNativeBool()    : 'Bool_to_PyBool',
+    PythonNativeInt()     : 'Int'+str(PythonNativeInt().precision*8)+'_to_PyLong',
+    PythonNativeFloat()   : 'Double_to_PyDouble',
+    PythonNativeComplex() : 'Complex128_to_PyComplex',
+    }
 
 
 #-------------------------------------------------------------------
@@ -1094,13 +1074,13 @@ def C_to_Python(c_object):
 # https://docs.python.org/3/c-api/exceptions.html#c.PyErr_Occurred
 PyErr_Occurred = FunctionDef(name      = 'PyErr_Occurred',
                              arguments = [],
-                             results   = [FunctionDefResult(Variable(dtype = PyccelPyObject(), name = 'r', memory_handling = 'alias'))],
+                             results   = [FunctionDefResult(Variable(PyccelPyObject(), name = 'r', memory_handling = 'alias'))],
                              body      = [])
 
 PyErr_SetString = FunctionDef(name = 'PyErr_SetString',
               body      = [],
-              arguments = [FunctionDefArgument(Variable(dtype = PyccelPyObject(), name = 'o')),
-                           FunctionDefArgument(Variable(dtype = NativeString(), name = 's'))],
+              arguments = [FunctionDefArgument(Variable(PyccelPyObject(), name = 'o')),
+                           FunctionDefArgument(Variable(StringType(), name = 's'))],
               results   = [])
 
 PyNotImplementedError = Variable(PyccelPyObject(), name = 'PyExc_NotImplementedError')
@@ -1110,43 +1090,36 @@ def C_to_Python(c_object):
 PyObject_TypeCheck = FunctionDef(name = 'PyObject_TypeCheck',
             arguments = [FunctionDefArgument(Variable(PyccelPyObject(), 'o', memory_handling = 'alias')),
                          FunctionDefArgument(Variable(PyccelPyClassType(), 'c_type', memory_handling='alias'))],
-            results = [FunctionDefResult(Variable(NativeBool(), 'r'))],
+            results = [FunctionDefResult(Variable(PythonNativeBool(), 'r'))],
             body = [])
 
 PyList_New = FunctionDef(name = 'PyList_New',
-                    arguments = [FunctionDefArgument(Variable(NativeInteger(), 'size'), value = LiteralInteger(0))],
+                    arguments = [FunctionDefArgument(Variable(PythonNativeInt(), 'size'), value = LiteralInteger(0))],
                     results = [FunctionDefResult(Variable(PyccelPyObject(), 'r', memory_handling='alias'))],
                     body = [])
 
 PyList_Append = FunctionDef(name = 'PyList_Append',
                     arguments = [FunctionDefArgument(Variable(PyccelPyObject(), 'list', memory_handling='alias')),
                                  FunctionDefArgument(Variable(PyccelPyObject(), 'item', memory_handling='alias'))],
-                    results = [FunctionDefResult(Variable(NativeInteger(), 'i', precision=4))],
+                    results = [FunctionDefResult(Variable(CNativeInt(), 'i'))],
                     body = [])
 
 PyList_GetItem = FunctionDef(name = 'PyList_GetItem',
                     arguments = [FunctionDefArgument(Variable(PyccelPyObject(), 'list', memory_handling='alias')),
-                                 FunctionDefArgument(Variable(NativeInteger(), 'i', precision=8))],
+                                 FunctionDefArgument(Variable(PythonNativeInt(), 'i'))],
                     results = [FunctionDefResult(Variable(PyccelPyObject(), 'item', memory_handling='alias'))],
                     body = [])
 
 PyList_Size = FunctionDef(name = 'PyList_Size',
                     arguments = [FunctionDefArgument(Variable(PyccelPyObject(), 'list', memory_handling='alias'))],
-                    results = [FunctionDefResult(Variable(NativeInteger(), 'i', precision=8))],
+                    results = [FunctionDefResult(Variable(PythonNativeInt(), 'i'))],
                     body = [])
 
 
 # Functions definitions are defined in pyccel/stdlib/cwrapper/cwrapper.c
 check_type_registry = {
-    (NativeBool(), -1)     : 'PyIs_Bool',
-    (NativeInteger(), -1)  : 'PyIs_NativeInt',
-    (NativeInteger(), 1)   : 'PyIs_Int8',
-    (NativeInteger(), 2)   : 'PyIs_Int16',
-    (NativeInteger(), 4)   : 'PyIs_Int32',
-    (NativeInteger(), 8)   : 'PyIs_Int64',
-    (NativeFloat(), -1)    : 'PyIs_NativeFloat',
-    (NativeFloat(), 4)     : 'PyIs_Float',
-    (NativeFloat(), 8)     : 'PyIs_Double',
-    (NativeComplex(), -1)  : 'PyIs_NativeComplex',
-    (NativeComplex(), 4)   : 'PyIs_Complex64',
-    (NativeComplex(), 8)   : 'PyIs_Complex128'}
+    PythonNativeBool()    : 'PyIs_Bool',
+    PythonNativeInt()     : 'PyIs_NativeInt',
+    PythonNativeFloat()   : 'PyIs_NativeFloat',
+    PythonNativeComplex() : 'PyIs_NativeComplex',
+    }
diff --git a/pyccel/ast/datatypes.py b/pyccel/ast/datatypes.py
index 054d2fa546..9c90394822 100644
--- a/pyccel/ast/datatypes.py
+++ b/pyccel/ast/datatypes.py
@@ -12,115 +12,202 @@
 
 import numpy
 
-from pyccel.utilities.metaclasses import ArgumentSingleton, Singleton
+from pyccel.utilities.metaclasses import Singleton, ArgumentSingleton
+from .basic import iterable
 
-# TODO [YG, 12.03.2020] verify why we need all these types
-# NOTE: symbols not used in pyccel are commented out
 __all__ = (
-#
-# --------- CLASSES -----------
-#
-    'CustomDataType',
-    'DataType',
-    'DataTypeFactory',
-    'NativeBool',
-    'NativeComplex',
-    'NativeFloat',
-    'NativeGeneric',
-    'NativeHomogeneousList',
-    'NativeHomogeneousSet',
-    'NativeHomogeneousTuple',
-    'NativeInhomogeneousTuple',
-    'NativeInteger',
-    'NativeString',
-    'NativeSymbol',
-    'NativeTuple',
-    'NativeVoid',
-#
-# --------- FUNCTIONS -----------
-#
-    'datatype',
-    'str_dtype',
-#
-# --------- VARIABLES -----------
-#
-    'Bool',
-    'Cmplx',
-    'Generic',
-    'Int',
-    'Float',
-    'String',
-    'Void',
-	'NativeNumeric',
-#    '_Symbol',
-    'default_precision',
-    'dtype_and_precision_registry',
+        # ------------ Super classes ------------
+        'PyccelType',
+        'PrimitiveType',
+        'FixedSizeType',
+        'ContainerType',
+        # ------------ Primitive types ------------
+        'PrimitiveBooleanType',
+        'PrimitiveIntegerType',
+        'PrimitiveFloatingPointType',
+        'PrimitiveComplexType',
+        'PrimitiveCharacterType',
+        # ------------ Fixed size types ------------
+        'FixedSizeNumericType',
+        'PythonNativeNumericType',
+        'PythonNativeBool',
+        'PythonNativeInt',
+        'PythonNativeFloat',
+        'PythonNativeComplex',
+        'VoidType',
+        'GenericType',
+        'SymbolicType',
+        'CharType',
+        # ------------ Container types ------------
+        'TupleType',
+        'HomogeneousContainerType',
+        'StringType',
+        'HomogeneousTupleType',
+        'HomogeneousListType',
+        'HomogeneousSetType',
+        'CustomDataType',
+        'InhomogeneousTupleType',
+        'DictType',
+        # ---------- Functions -------------------
+        'DataTypeFactory',
 )
 
 #==============================================================================
-iso_c_binding = {
-    "integer" : {
-        1  : 'C_INT8_T',
-        2  : 'C_INT16_T',
-        4  : 'C_INT32_T',
-        8  : 'C_INT64_T',
-        16 : 'C_INT128_T'}, #no supported yet
-    "real"    : {
-        4  : 'C_FLOAT',
-        8  : 'C_DOUBLE',
-        16 : 'C_LONG_DOUBLE'},
-    "complex" : {
-        4  : 'C_FLOAT_COMPLEX',
-        8  : 'C_DOUBLE_COMPLEX',
-        16 : 'C_LONG_DOUBLE_COMPLEX'},
-    "logical" : {
-        -1 : "C_BOOL"}
-}
-iso_c_binding_shortcut_mapping = {
-    'C_INT8_T'              : 'i8',
-    'C_INT16_T'             : 'i16',
-    'C_INT32_T'             : 'i32',
-    'C_INT64_T'             : 'i64',
-    'C_INT128_T'            : 'i128',
-    'C_FLOAT'               : 'f32',
-    'C_DOUBLE'              : 'f64',
-    'C_LONG_DOUBLE'         : 'f128',
-    'C_FLOAT_COMPLEX'       : 'c32',
-    'C_DOUBLE_COMPLEX'      : 'c64',
-    'C_LONG_DOUBLE_COMPLEX' : 'c128',
-    'C_BOOL'                : 'b1'
-}
+class PrimitiveType(metaclass=Singleton):
+    """
+    Base class representing types of datatypes.
+
+    The base class representing the category of datatype to which a FixedSizeType
+    may belong (e.g. integer, floating point).
+    """
+    __slots__ = ()
+    _name = '__UNDEFINED__'
+
+    def __init__(self): #pylint: disable=useless-parent-delegation
+        # This __init__ function is required so the ArgumentSingleton can
+        # always detect a signature
+        super().__init__()
+
+    def __str__(self):
+        return self._name
+
+    def __reduce__(self):
+        """
+        Function called during pickling.
+
+        For more details see : https://docs.python.org/3/library/pickle.html#object.__reduce__.
+        This function is necessary to ensure that DataTypes remain singletons.
+
+        Returns
+        -------
+        callable
+            A callable to create the object.
+        args
+            A tuple containing any arguments to be passed to the callable.
+        """
+        return (self.__class__, ())
+
+class PrimitiveBooleanType(PrimitiveType):
+    """
+    Class representing a boolean datatype.
+
+    Class representing a boolean datatype.
+    """
+    __slots__ = ()
+    _name = 'boolean'
+
+class PrimitiveIntegerType(PrimitiveType):
+    """
+    Class representing an integer datatype.
+
+    Class representing an integer datatype.
+    """
+    __slots__ = ()
+    _name = 'integer'
+
+class PrimitiveFloatingPointType(PrimitiveType):
+    """
+    Class representing a floating point datatype.
+
+    Class representing a floating point datatype.
+    """
+    __slots__ = ()
+    _name = 'floating point'
+
+class PrimitiveComplexType(PrimitiveType):
+    """
+    Class representing a complex datatype.
+
+    Class representing a complex datatype.
+    """
+    __slots__ = ()
+    _name = 'complex'
+
+class PrimitiveCharacterType(PrimitiveType):
+    """
+    Class representing a character datatype.
+
+    Class representing a character datatype.
+    """
+    __slots__ = ()
+    _name = 'character'
 
 #==============================================================================
 
-class DataType:
+class PyccelType:
     """
-    Base class representing native datatypes.
+    Base class representing the type of an object.
+
+    Base class representing the type of an object from which all
+    types must inherit. A type must contain enough information to
+    describe the declaration type in a low-level language.
+
+    Types contain an addition operator. The operator indicates the type that
+    is expected when calling an arithmetic operator on objects of these types.
 
-    The base class from which all data types must inherit.
+    Where applicable, types also contain an and operator. The operator indicates the type that
+    is expected when calling a bitwise comparison operator on objects of these types.
     """
     __slots__ = ()
-    _name = '__UNDEFINED__'
 
     def __init__(self): #pylint: disable=useless-parent-delegation
         # This __init__ function is required so the ArgumentSingleton can
         # always detect a signature
         super().__init__()
 
+    def __str__(self):
+        return self._name #pylint: disable=no-member
+
+    def switch_basic_type(self, new_type):
+        """
+        Change the basic type to the new type.
+
+        Change the basic type to the new type. In the case of a FixedSizeType the
+        switch will replace the type completely, directly returning the new type.
+        In the case of a homogeneous container type, a new container type will be
+        returned whose underlying elements are of the new type. This method is not
+        implemented for inhomogeneous containers.
+
+        Parameters
+        ----------
+        new_type : PyccelType
+            The new basic type.
+
+        Returns
+        -------
+        PyccelType
+            The new type.
+        """
+        raise NotImplementedError(f"switch_basic_type not implemented for {type(self)}")
+
+#==============================================================================
+
+class FixedSizeType(PyccelType, metaclass=Singleton):
+    """
+    Base class representing a built-in scalar datatype.
+
+    The base class representing a built-in scalar datatype which can be
+    represented in memory. E.g. int32, int64.
+    """
+    __slots__ = ()
+
     @property
-    def name(self):
+    def datatype(self):
         """
-        Get the name of the datatype.
+        The datatype of the object.
 
-        Get the name of the datatype.
+        The datatype of the object.
         """
-        return self._name
+        return self
 
-    def __str__(self):
-        return str(self.name).lower()
+    @property
+    def primitive_type(self):
+        """
+        The datatype category of the object.
 
-    def __repr__(self):
-        return str(self.__class__.__name__)+'()'
+        The datatype category of the object (e.g. integer, floating point).
+        """
+        return self._primitive_type # pylint: disable=no-member
 
     def __reduce__(self):
         """
@@ -138,215 +225,449 @@ def __reduce__(self):
         """
         return (self.__class__, ())
 
-class NativeBool(DataType, metaclass=Singleton):
+    def switch_basic_type(self, new_type):
+        """
+        Change the basic type to the new type.
+
+        Change the basic type to the new type. In the case of a FixedSizeType the
+        switch will replace the type completely, directly returning the new type.
+
+        Parameters
+        ----------
+        new_type : PyccelType
+            The new basic type.
+
+        Returns
+        -------
+        PyccelType
+            The new type.
+        """
+        assert isinstance(new_type, FixedSizeType)
+        return new_type
+
+class FixedSizeNumericType(FixedSizeType):
     """
-    Class representing a boolean datatype.
+    Base class representing a scalar numeric datatype.
 
-    Class representing a boolean datatype.
+    The base class representing a scalar numeric datatype which can be
+    represented in memory. E.g. int32, int64.
+    """
+    __slots__ = ()
+
+    @property
+    def precision(self):
+        """
+        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. For basic types the
+        number is equivalent to the number of bytes in memory (e.g. `float64` has
+        precision = 8 as it takes up 8 bytes), however for less simple types the
+        connection is less trivial. For example `complex128` has precision = 8 as
+        it is comprised of two `float64` objects (which have precision=8).
+        It should be noted that this is not the convention chosen by NumPy (in NumPy
+        a `complex128` is so named because `16*8=precision*bits_in_a_byte=128`).
+
+        The precision in Pyccel is equivalent to the `kind` parameter in Fortran.
+        """
+        return self._precision # pylint: disable=no-member
+
+class PythonNativeNumericType(FixedSizeNumericType):
+    """
+    Base class representing a built-in scalar numeric datatype.
+
+    Base class representing a built-in scalar numeric datatype.
+    """
+    __slots__ = ()
+
+class PythonNativeBool(PythonNativeNumericType):
+    """
+    Class representing Python's native boolean type.
+
+    Class representing Python's native boolean type.
     """
     __slots__ = ()
     _name = 'bool'
+    _primitive_type = PrimitiveBooleanType()
+    _precision = -1
 
     @lru_cache
     def __add__(self, other):
-        if other in NativeNumeric:
+        if isinstance(other, PythonNativeBool):
+            return PythonNativeInt()
+        elif isinstance(other, PythonNativeNumericType):
             return other
         else:
             return NotImplemented
 
-class NativeInteger(DataType, metaclass=Singleton):
+    @lru_cache
+    def __and__(self, other):
+        if isinstance(other, PythonNativeBool):
+            return PythonNativeBool()
+        elif isinstance(other, PythonNativeNumericType):
+            return other
+        else:
+            return NotImplemented
+
+class PythonNativeInt(PythonNativeNumericType):
     """
-    Class representing an integer datatype.
+    Class representing Python's native integer type.
 
-    Class representing an integer datatype.
+    Class representing Python's native integer type.
     """
     __slots__ = ()
     _name = 'int'
+    _primitive_type = PrimitiveIntegerType()
+    _precision = numpy.dtype(int).alignment
 
     @lru_cache
     def __add__(self, other):
-        if other in NativeNumeric:
-            if other is NativeBool():
-                return self
-            else:
-                return other
+        if isinstance(other, PythonNativeBool):
+            return self
+        elif isinstance(other, PythonNativeNumericType):
+            return other
         else:
             return NotImplemented
 
-class NativeFloat(DataType, metaclass=Singleton):
+    @lru_cache
+    def __and__(self, other):
+        if isinstance(other, PythonNativeNumericType):
+            return self
+        else:
+            return NotImplemented
+
+class PythonNativeFloat(PythonNativeNumericType):
     """
-    Class representing a float datatype.
+    Class representing Python's native floating point type.
 
-    Class representing a float datatype.
+    Class representing Python's native floating point type.
     """
     __slots__ = ()
     _name = 'float'
+    _primitive_type = PrimitiveFloatingPointType()
+    _precision = 8
 
     @lru_cache
     def __add__(self, other):
-        if other in NativeNumeric:
-            if other is NativeComplex():
-                return other
-            else:
-                return self
+        if isinstance(other, PythonNativeComplex):
+            return other
+        elif isinstance(other, PythonNativeNumericType):
+            return self
         else:
             return NotImplemented
 
-class NativeComplex(DataType, metaclass=Singleton):
+class PythonNativeComplex(PythonNativeNumericType):
     """
-    Class representing a complex datatype.
+    Class representing Python's native complex type.
 
-    Class representing a complex datatype.
+    Class representing Python's native complex type.
     """
-    __slots__ = ()
+    __slots__ = ('_element_type',)
     _name = 'complex'
+    _primitive_type = PrimitiveComplexType()
+    _precision = 8
 
     @lru_cache
     def __add__(self, other):
-        if other in NativeNumeric:
+        if isinstance(other, PythonNativeNumericType):
             return self
         else:
             return NotImplemented
 
-NativeNumeric = (NativeBool(), NativeInteger(), NativeFloat(), NativeComplex())
-NativeNumericTypes = (NativeBool, NativeInteger, NativeFloat, NativeComplex)
+    @property
+    def element_type(self):
+        """
+        The type of an element of the complex.
+
+        The type of an element of the complex. In other words, the type
+        of the floats which comprise the complex type.
+        """
+        return PythonNativeFloat()
 
-class NativeString(DataType, metaclass=Singleton):
+class VoidType(FixedSizeType):
     """
-    Class representing a string datatype.
+    Class representing a void datatype.
 
-    Class representing a string datatype.
+    Class representing a void datatype. This class is especially useful
+    in the C-Python wrapper when a `void*` type is needed to collect
+    pointers from Fortran.
     """
     __slots__ = ()
-    _name = 'str'
+    _name = 'void'
+    _primitive_type = None
+
+class GenericType(FixedSizeType):
+    """
+    Class representing a generic datatype.
+
+    Class representing a generic datatype. This datatype is
+    useful for describing the type of an empty container (list/tuple/etc)
+    or an argument which can accept any type (e.g. MPI arguments).
+    """
+    __slots__ = ()
+    _name = 'Generic'
+    _primitive_type = None
 
     @lru_cache
     def __add__(self, other):
-        if isinstance(other, NativeString):
-            return self
-        else:
-            return NotImplemented
+        return other
 
-class NativeVoid(DataType, metaclass=Singleton):
+class SymbolicType(FixedSizeType):
     """
-    Class representing a void datatype.
+    Class representing the datatype of a placeholder symbol.
 
-    Class representing a void datatype. This class is especially useful
-    in the C-Python wrapper when a `void*` type is needed to collect
-    pointers from Fortran.
+    Class representing the datatype of a placeholder symbol. This type should
+    be used for objects which will not appear in the generated code but are
+    used to identify objects (e.g. Type aliases).
     """
     __slots__ = ()
-    _name = 'void'
+    _name = 'Symbolic'
+    _primitive_type = None
 
-class NativeTuple(DataType):
+class CharType(FixedSizeType):
     """
-    Base class representing tuple datatypes.
+    Class representing a char type in C/Fortran.
 
-    The class from which tuple datatypes must inherit.
+    Class representing a char type in C/Fortran. This datatype is
+    useful for describing strings.
+    """
+    __slots__ = ()
+    _name = 'char'
+    _primitive_type = PrimitiveCharacterType
 
-    Parameters
-    ----------
-    *args : tuple
-        Any arguments required by the class.
+#==============================================================================
+
+class ContainerType(PyccelType):
+    """
+    Base class representing a type which contains objects of other types.
 
-    **kwargs : dict
-        Any keyword arguments required by the class.
+    Base class representing a type which contains objects of other types.
+    E.g. classes, arrays, etc.
     """
     __slots__ = ()
-    _name = 'tuple'
 
-    @lru_cache
-    def __add__(self, other):
-        if isinstance(other, NativeTuple):
-            return self
-        else:
-            return NotImplemented
+#==============================================================================
 
-class NativeHomogeneousTuple(NativeTuple, metaclass = Singleton):
+class TupleType:
     """
-    Class representing the homogeneous tuple type.
+    Base class representing tuple datatypes.
 
-    Class representing the type of a homogeneous tuple. This
-    is a container type and should be used as the class_type.
+    The class from which tuple datatypes must inherit.
     """
     __slots__ = ()
+    _name = 'tuple'
 
-class NativeInhomogeneousTuple(NativeTuple, metaclass = ArgumentSingleton):
+#==============================================================================
+
+class HomogeneousContainerType(ContainerType):
     """
-    Class representing the inhomogeneous tuple type.
+    Base class representing a datatype which contains multiple elements of a given type.
 
-    Class representing the type of an inhomogeneous tuple. This is a
-    basic datatype as it cannot be arbitrarily indexed. It is
-    therefore parametrised by the datatypes that it contains.
+    Base class representing a datatype which contains multiple elements of a given type.
+    This is the case for objects such as arrays, lists, etc.
+    """
+    __slots__ = ()
 
-    Parameters
-    ----------
-    *dtypes : tuple[DataType, ...]
-        The datatypes stored in the inhomogeneous tuple.
+    @property
+    def datatype(self):
+        """
+        The datatype of the object.
+
+        The datatype of the object.
+        """
+        return self.element_type.datatype
+
+    @property
+    def primitive_type(self):
+        """
+        The datatype category of elements of the object.
+
+        The datatype category of elements of the object (e.g. integer, floating point).
+        """
+        return self.element_type.primitive_type
+
+    @property
+    def precision(self):
+        """
+        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. For basic types the
+        number is equivalent to the number of bytes in memory (e.g. `float64` has
+        precision = 8 as it takes up 8 bytes), however for less simple types the
+        connection is less trivial. For example `complex128` has precision = 8 as
+        it is comprised of two `float64` objects (which have precision=8).
+        It should be noted that this is not the convention chosen by NumPy (in NumPy
+        a `complex128` is so named because `16*8=precision*bits_in_a_byte=128`).
+
+        The precision in Pyccel is equivalent to the `kind` parameter in Fortran.
+        """
+        return self.element_type.precision
+
+    @property
+    def element_type(self):
+        """
+        The type of elements of the object.
+
+        The PyccelType describing an element of the container.
+        """
+        return self._element_type # pylint: disable=no-member
+
+    def __str__(self):
+        return f'{self._name}[{self._element_type}]' # pylint: disable=no-member
+
+    def __reduce__(self):
+        """
+        Function called during pickling.
+
+        For more details see : https://docs.python.org/3/library/pickle.html#object.__reduce__.
+        This function is necessary to ensure that DataTypes remain singletons.
+
+        Returns
+        -------
+        callable
+            A callable to create the object.
+        args
+            A tuple containing any arguments to be passed to the callable.
+        """
+        return (self.__class__, (self.element_type,))
+
+    def switch_basic_type(self, new_type):
+        """
+        Change the basic type to the new type.
+
+        Change the basic type to the new type. In the case of a FixedSizeType the
+        switch will replace the type completely, directly returning the new type.
+        In the case of a homogeneous container type, a new container type will be
+        returned whose underlying elements are of the new type. This method is not
+        implemented for inhomogeneous containers.
+
+        Parameters
+        ----------
+        new_type : PyccelType
+            The new basic type.
+
+        Returns
+        -------
+        PyccelType
+            The new type.
+        """
+        assert isinstance(new_type, FixedSizeType)
+        cls = type(self)
+        return cls(self.element_type.switch_basic_type(new_type))
+
+class StringType(HomogeneousContainerType, metaclass = Singleton):
     """
-    __slots__ = ('_dtypes',)
+    Class representing Python's native string type.
 
-    def __init__(self, *dtypes):
-        self._dtypes = dtypes
-        super().__init__()
+    Class representing Python's native string type.
+    """
+    __slots__ = ()
+    _name = 'str'
+    _element_type = PrimitiveCharacterType()
 
     @property
-    def name(self):
+    def datatype(self):
         """
-        The name of the datatype.
+        The datatype of the object.
+
+        The datatype of the object.
+        """
+        return self
+
+    def __str__(self):
+        return 'str'
 
-        Get the name of the datatype. This name is parametrised by the
-        datatypes in the elements of the tuple.
+    def __reduce__(self):
+        """
+        Function called during pickling.
+
+        For more details see : https://docs.python.org/3/library/pickle.html#object.__reduce__.
+        This function is necessary to ensure that DataTypes remain singletons.
 
         Returns
         -------
-        str
-            The name of the datatype.
+        callable
+            A callable to create the object.
+        args
+            A tuple containing any arguments to be passed to the callable.
         """
-        datatypes = ', '.join(d.name for d in self._dtypes)
-        return f'tuple[{datatypes}]'
+        return (self.__class__, ())
 
-    def __getitem__(self, i):
-        return self._dtypes[i]
+    @property
+    def primitive_type(self):
+        """
+        The datatype category of elements of the object.
+
+        The datatype category of elements of the object (e.g. integer, floating point).
+        """
+        return self
 
-class NativeHomogeneousList(DataType, metaclass = Singleton):
+class HomogeneousTupleType(HomogeneousContainerType, TupleType, metaclass = ArgumentSingleton):
+    """
+    Class representing the homogeneous tuple type.
+
+    Class representing the type of a homogeneous tuple. This
+    is a container type and should be used as the class_type.
+
+    Parameters
+    ----------
+    element_type : PyccelType
+        The type of the elements of the homogeneous tuple.
+    """
+    __slots__ = ('_element_type',)
+
+    def __init__(self, element_type):
+        assert isinstance(element_type, PyccelType)
+        self._element_type = element_type
+        super().__init__()
+
+    def __str__(self):
+        return f'{self._name}[{self._element_type}, ...]'
+
+class HomogeneousListType(HomogeneousContainerType, metaclass = ArgumentSingleton):
     """
     Class representing the homogeneous list type.
 
     Class representing the type of a homogeneous list. This
     is a container type and should be used as the class_type.
+
+    Parameters
+    ----------
+    element_type : PyccelType
+        The type which is stored in the homogeneous list.
     """
-    __slots__ = ()
+    __slots__ = ('_element_type',)
     _name = 'list'
 
-    @lru_cache
-    def __add__(self, other):
-        if isinstance(other, NativeHomogeneousList):
-            return self
-        else:
-            return NotImplemented
+    def __init__(self, element_type):
+        assert isinstance(element_type, PyccelType)
+        self._element_type = element_type
+        super().__init__()
 
-class NativeHomogeneousSet(DataType, metaclass = Singleton):
+class HomogeneousSetType(HomogeneousContainerType, metaclass = ArgumentSingleton):
     """
-    Class representing the homogeneous Set type.
+    Class representing the homogeneous set type.
 
-    Class representing the type of a homogeneous Set. This
+    Class representing the type of a homogeneous set. This
     is a container type and should be used as the class_type.
-    """
-    __slots__ = ()
-    _name = 'Set'
 
-class NativeSymbol(DataType, metaclass=Singleton):
+    Parameters
+    ----------
+    element_type : PyccelType
+        The type which is stored in the homogeneous set.
     """
-    Class representing a symbol datatype.
+    __slots__ = ('_element_type',)
+    _name = 'set'
 
-    Class representing a symbol datatype. This **may** be useful for
-    the sympy decorator and other symbolic manipulations.
-    """
-    __slots__ = ()
-    _name = 'Symbol'
+    def __init__(self, element_type):
+        assert isinstance(element_type, PyccelType)
+        self._element_type = element_type
+        super().__init__()
+
+#==============================================================================
 
-class CustomDataType(DataType, metaclass=Singleton):
+class CustomDataType(ContainerType, metaclass=Singleton):
     """
     Class from which user-defined types inherit.
 
@@ -355,73 +676,145 @@ class CustomDataType(DataType, metaclass=Singleton):
     """
     __slots__ = ()
 
-class NativeGeneric(DataType, metaclass = Singleton):
+    @property
+    def datatype(self):
+        """
+        The datatype of the object.
+
+        The datatype of the object.
+        """
+        return self
+
+    def __reduce__(self):
+        """
+        Function called during pickling.
+
+        For more details see : https://docs.python.org/3/library/pickle.html#object.__reduce__.
+        This function is necessary to ensure that DataTypes remain singletons.
+
+        Returns
+        -------
+        callable
+            A callable to create the object.
+        args
+            A tuple containing any arguments to be passed to the callable.
+        """
+        return (self.__class__, ())
+
+class InhomogeneousTupleType(ContainerType, TupleType, metaclass = ArgumentSingleton):
     """
-    Class representing a generic datatype.
+    Class representing the inhomogeneous tuple type.
 
-    Class representing a generic datatype. This datatype is
-    useful for describing the type of an empty container (list/tuple/etc)
-    or an argument which can accept any type (e.g. MPI arguments).
+    Class representing the type of an inhomogeneous tuple. This is a
+    basic datatype as it cannot be arbitrarily indexed. It is
+    therefore parametrised by the datatypes that it contains.
+
+    Parameters
+    ----------
+    *args : tuple of DataTypes
+        The datatypes stored in the inhomogeneous tuple.
     """
-    __slots__ = ()
-    _name = 'Generic'
+    __slots__ = ('_element_types',)
 
-    @lru_cache
-    def __add__(self, other):
-        return other
+    def __init__(self, *args):
+        self._element_types = args
+        super().__init__()
+
+    def __str__(self):
+        element_types = ', '.join(str(d) for d in self._element_types)
+        return f'tuple[{element_types}]'
+
+    def __getitem__(self, i):
+        return self._element_types[i]
+
+    def __len__(self):
+        return len(self._element_types)
+
+    def __iter__(self):
+        return self._element_types.__iter__()
+
+    def __reduce__(self):
+        """
+        Function called during pickling.
+
+        For more details see : https://docs.python.org/3/library/pickle.html#object.__reduce__.
+        This function is necessary to ensure that DataTypes remain singletons.
+
+        Returns
+        -------
+        callable
+            A callable to create the object.
+        args
+            A tuple containing any arguments to be passed to the callable.
+        """
+        return (self.__class__, tuple(self._element_types))
+
+    @property
+    def datatype(self):
+        """
+        The datatype of the object.
+
+        The datatype of the object.
+        """
+        possible_types = set(t.datatype for t in self._element_types)
+        if len(possible_types) == 1:
+            return possible_types.pop()
+        else:
+            return self
+
+class DictType(ContainerType, metaclass = ArgumentSingleton):
+    """
+    Class representing the homogeneous dictionary type.
+
+    Class representing the type of a homogeneous dict. This
+    is a container type and should be used as the class_type.
+
+    Parameters
+    ----------
+    index_type : PyccelType
+        The type of the keys of the homogeneous dictionary.
+    value_type : PyccelType
+        The type of the values of the homogeneous dictionary.
+    """
+    __slots__ = ('_index_type', '_value_type')
+    _name = 'map'
+
+    def __init__(self, index_type, value_type):
+        self._index_type = index_type
+        self._value_type = value_type
+        super().__init__()
+
+    def __str__(self):
+        return f'map[{self._index_type.name}, {self._value_type.name}]'
+
+    def __reduce__(self):
+        """
+        Function called during pickling.
+
+        For more details see : https://docs.python.org/3/library/pickle.html#object.__reduce__.
+        This function is necessary to ensure that DataTypes remain singletons.
+
+        Returns
+        -------
+        callable
+            A callable to create the object.
+        args
+            A tuple containing any arguments to be passed to the callable.
+        """
+        return (self.__class__, (self._index_type, self._value_type))
+
+    @property
+    def datatype(self):
+        """
+        The datatype of the object.
+
+        The datatype of the object.
+        """
+        return self._index_type.datatype
+
+#==============================================================================
 
-# ...
-
-
-
-Bool           = NativeBool()
-Int            = NativeInteger()
-Float          = NativeFloat()
-Cmplx          = NativeComplex()
-Void           = NativeVoid()
-String         = NativeString()
-_Symbol        = NativeSymbol()
-Generic        = NativeGeneric()
-
-dtype_and_precision_registry = { 'float' : (Float, -1),
-                                 'double' : (Float, -1),
-                                 'real' : (Float, -1),
-                                 'float32' : (Float,4),
-                                 'float64' : (Float,8),
-                                 'f4' : (Float,4),
-                                 'f8' : (Float,8),
-                                 'complex' : (Cmplx, -1),
-                                 'complex64' : (Cmplx,4),
-                                 'complex128' : (Cmplx,8),
-                                 'c8' : (Cmplx,4),
-                                 'c16' : (Cmplx,8),
-                                 'int8' :(Int,1),
-                                 'int16' : (Int,2),
-                                 'int32' : (Int,4),
-                                 'int64' : (Int,8),
-                                 'i1' :(Int,1),
-                                 'i2' : (Int,2),
-                                 'i4' : (Int,4),
-                                 'i8' : (Int,8),
-                                 'int'  :(Int, -1),
-                                 'integer' : (Int,-1),
-                                 'bool' :(Bool,-1),
-                                 'b1' :(Bool,-1),
-                                 'void' : (Void, 0),
-                                 'symbol' : (_Symbol, 0),
-                                 '*' : (Generic, 0),
-                                 'str' : (String, 0),
-                                 }
-
-default_precision = {Float : 8,
-                     Int : numpy.dtype(int).alignment,
-                     Cmplx : 8,
-                     Bool : -1}
-
-
-def DataTypeFactory(name, argnames=["_name"],
-                    BaseClass=CustomDataType,
-                    prefix=None):
+def DataTypeFactory(name, argnames = (), *, BaseClass=CustomDataType):
     """
     Create a new data class.
 
@@ -433,108 +826,58 @@ def DataTypeFactory(name, argnames=["_name"],
     name : str
         The name of the new class.
 
-    argnames : list of str
+    argnames : iterable[str]
         A list of all the arguments for the new class.
+        This can be used to create classes which are parametrised by a type.
 
     BaseClass : type inheriting from DataType
         The class from which the new type will be sub-classed.
 
-    prefix : str
-        A prefix which will be added to the class name.
-
     Returns
     -------
     type
         A new DataType class.
     """
-    def __init__(self, **kwargs):
-        for key, value in list(kwargs.items()):
+    def class_init_func(self, **kwargs):
+        """
+        The __init__ function for the new CustomDataType class.
+        """
+        for key, value in kwargs.items():
             # here, the argnames variable is the one passed to the
             # DataTypeFactory call
             if key not in argnames:
-                raise TypeError("Argument %s not valid for %s"
-                    % (key, self.__class__.__name__))
+                raise TypeError(f"Argument {key} not valid for {self.__class__.__name__}")
             setattr(self, key, value)
-        BaseClass.__init__(self)
 
-    if prefix is None:
-        prefix = 'Pyccel'
-    else:
-        prefix = 'Pyccel{0}'.format(prefix)
+        BaseClass.__init__(self) # pylint: disable=unnecessary-dunder-call
 
-    newclass = type(prefix + name, (BaseClass,),
-                    {"__init__": __init__,
-                     "_name": name,
-                     "prefix": prefix,
-                     "alias": name})
+    assert iterable(argnames)
+    assert all(isinstance(a, str) for a in argnames)
 
-    dtype_and_precision_registry[name] = (newclass(), 0)
-    return newclass
-
-def datatype(arg):
-    """
-    Get the datatype indicated by a string.
-
-    Return the datatype singleton for the dtype described
-    by the argument.
-
-    Parameters
-    ----------
-    arg : str
-        Return the singleton for the corresponding dtype.
-
-    Returns
-    -------
-    DataType
-        The data type described by the string.
-    """
-    if isinstance(arg, str):
-        if arg not in dtype_and_precision_registry:
-            raise ValueError("Unrecognized datatype " + arg)
-        return dtype_and_precision_registry[arg][0]
-    else:
-        raise TypeError('Expecting a DataType')
+    def class_name_func(self):
+        """
+        The name function for the new CustomDataType class.
+        """
+        if argnames:
+            param = ', '.join(str(getattr(self, a)) for a in argnames)
+            return f'{self._name}[{param}]' #pylint: disable=protected-access
+        else:
+            return self._name #pylint: disable=protected-access
 
-def str_dtype(dtype):
+    newclass = type(f'Pyccel{name}', (BaseClass,),
+                    {"__init__": class_init_func,
+                     "name": property(class_name_func),
+                     "_name": name})
 
-    """
-    Get a string describing a datatype.
+    return newclass
 
-    This function takes a pyccel datatype and returns a string which describes it.
+#==============================================================================
 
-    Parameters
-    ----------
-    dtype : DataType
-        The datatype.
+pyccel_type_to_original_type = {
+        PythonNativeBool()    : bool,
+        PythonNativeInt()     : int,
+        PythonNativeFloat()   : float,
+        PythonNativeComplex() : complex,
+        }
 
-    Returns
-    -------
-    str
-        A description of the data type.
-
-    Examples
-    --------
-    >>> str_dtype('int')
-    'integer'
-    >>> str_dtype(NativeInteger())
-    'integer'
-    """
-    if isinstance(dtype, str):
-        if dtype == 'int':
-            return 'integer'
-        elif dtype== 'float':
-            return 'float'
-        else:
-            return dtype
-    if isinstance(dtype, NativeInteger):
-        return 'integer'
-    elif isinstance(dtype, NativeFloat):
-        return 'float'
-    elif isinstance(dtype, NativeComplex):
-        return 'complex'
-    elif isinstance(dtype, NativeBool):
-        return 'bool'
-    elif isinstance(dtype, CustomDataType):
-        return dtype.name
-    else:
-        raise TypeError('Unknown datatype {0}'.format(str(dtype)))
+original_type_to_pyccel_type = {v: k for k,v in pyccel_type_to_original_type.items()}
diff --git a/pyccel/ast/functionalexpr.py b/pyccel/ast/functionalexpr.py
index b574305fb5..4d8e2764f0 100644
--- a/pyccel/ast/functionalexpr.py
+++ b/pyccel/ast/functionalexpr.py
@@ -55,7 +55,7 @@ class FunctionalFor(TypedAstNode):
               Index is `Dummy_0`.
     """
     __slots__ = ('_loops','_expr', '_lhs', '_indices','_index',
-            '_dtype','_precision','_rank','_shape','_order','_class_type')
+            '_rank','_shape','_order','_class_type')
     _attribute_nodes = ('_loops','_expr', '_lhs', '_indices','_index')
 
     def __init__(
@@ -74,8 +74,6 @@ def __init__(
         super().__init__()
 
         if pyccel_stage != 'syntactic':
-            self._dtype      = lhs.dtype
-            self._precision  = lhs.precision
             self._rank       = lhs.rank
             self._shape      = lhs.shape
             self._order      = lhs.order
diff --git a/pyccel/ast/internals.py b/pyccel/ast/internals.py
index bdb65be485..28fe1d5590 100644
--- a/pyccel/ast/internals.py
+++ b/pyccel/ast/internals.py
@@ -8,11 +8,10 @@
 To avoid circular imports this file should only import from basic, datatypes, and literals
 """
 
-from operator import attrgetter
 from pyccel.utilities.stage import PyccelStage
 
 from .basic     import PyccelAstNode, TypedAstNode, Immutable
-from .datatypes import NativeInteger, default_precision
+from .datatypes import PythonNativeInt, PrimitiveIntegerType
 from .literals  import LiteralInteger
 
 pyccel_stage = PyccelStage()
@@ -24,8 +23,6 @@
     'PyccelInternalFunction',
     'PyccelSymbol',
     'Slice',
-    'get_final_precision',
-    'max_precision',
 )
 
 
@@ -85,12 +82,10 @@ class PyccelArraySize(PyccelInternalFunction):
     __slots__ = ()
     name = 'size'
 
-    _dtype = NativeInteger()
-    _precision = -1
     _rank  = 0
     _shape = None
     _order = None
-    _class_type = NativeInteger()
+    _class_type = PythonNativeInt()
 
     def __init__(self, arg):
         super().__init__(arg)
@@ -133,12 +128,10 @@ class PyccelArrayShapeElement(PyccelInternalFunction):
     __slots__ = ()
     name = 'shape'
 
-    _dtype = NativeInteger()
-    _precision = -1
     _rank  = 0
     _shape = None
     _order = None
-    _class_type = NativeInteger()
+    _class_type = PythonNativeInt()
 
     def __init__(self, arg, index):
         if not isinstance(arg, TypedAstNode):
@@ -236,12 +229,9 @@ def __init__(self, start, stop, step = None, slice_type = Range):
         super().__init__()
         if pyccel_stage == 'syntactic':
             return
-        if start is not None and not (hasattr(start, 'dtype') and isinstance(start.dtype, NativeInteger)):
-            raise TypeError('Slice start must be Integer or None')
-        if stop is not None and not (hasattr(stop, 'dtype') and isinstance(stop.dtype, NativeInteger)):
-            raise TypeError('Slice stop must be Integer or None')
-        if step is not None and not (hasattr(step, 'dtype') and isinstance(step.dtype, NativeInteger)):
-            raise TypeError('Slice step must be Integer or None')
+        assert start is None or isinstance(getattr(start.dtype, 'primitive_type', None), PrimitiveIntegerType)
+        assert stop is None or isinstance(getattr(stop.dtype, 'primitive_type', None), PrimitiveIntegerType)
+        assert step is None or isinstance(getattr(step.dtype, 'primitive_type', None), PrimitiveIntegerType)
         if slice_type not in (Slice.Range, Slice.Element):
             raise TypeError('Slice type must be Range (1) or Element (0)')
 
@@ -384,56 +374,6 @@ def symbols(names):
     return tuple(symbols)
 
 
-def max_precision(objs : list, allow_native : bool = True):
-    """
-    Return the largest precision amongst the objects in the list.
-
-    Return the largest precision amongst the objects in the list.
-
-    Parameters
-    ----------
-    objs : list
-       A list of TypedAstNodes.
-
-    allow_native : bool, default=True
-        Allow the final result to be a native precision (i.e. -1).
-
-    Returns
-    -------
-    int
-        The largest precision found.
-    """
-    if allow_native and all(o.precision == -1 for o in objs):
-        return -1
-    else:
-        ndarray_list = [o for o in objs if getattr(o, 'is_ndarray', False)]
-        if ndarray_list:
-            return get_final_precision(max(ndarray_list, key=attrgetter('precision')))
-        return max(get_final_precision(o) for o in objs)
-
-
-def get_final_precision(obj):
-    """
-    Get the usable precision of an object.
-
-    Get the usable precision of an object. I.e. the precision that you
-    can use to print, e.g. 8 instead of -1 for a default precision float.
-
-    If the precision is set to the default then the value of the default
-    precision is returned, otherwise the provided precision is returned.
-
-    Parameters
-    ----------
-    obj : TypedAstNode
-        The object whose precision we want to investigate.
-
-    Returns
-    -------
-    int
-        The precision of the object to be used in the code.
-    """
-    return default_precision[obj.dtype] if obj.precision == -1 else obj.precision
-
 def apply_pickle(class_type, args, kwargs):
     """
     Utility function which recreates a class instance for pickle.
diff --git a/pyccel/ast/literals.py b/pyccel/ast/literals.py
index b81febba9e..fac53ca364 100644
--- a/pyccel/ast/literals.py
+++ b/pyccel/ast/literals.py
@@ -6,9 +6,11 @@
 """
 from pyccel.utilities.metaclasses import Singleton
 
-from .basic              import TypedAstNode, PyccelAstNode
-from .datatypes          import (NativeGeneric, NativeInteger, NativeBool, NativeFloat,
-                                  NativeComplex, NativeString)
+from .basic     import TypedAstNode, PyccelAstNode
+from .datatypes import GenericType, PythonNativeInt, PythonNativeBool
+from .datatypes import PythonNativeFloat, StringType, PythonNativeComplex
+from .datatypes import PrimitiveIntegerType, PrimitiveFloatingPointType, PrimitiveBooleanType
+from .datatypes import PrimitiveComplexType, FixedSizeNumericType
 
 __all__ = (
     'convert_to_literal',
@@ -19,7 +21,6 @@
     'LiteralFloat',
     'LiteralImaginaryUnit',
     'LiteralInteger',
-    'LiteralNumeric',
     'LiteralString',
     'LiteralTrue',
     'Nil',
@@ -52,7 +53,7 @@ def python_value(self):
         """
 
     def __repr__(self):
-        return "Literal({})".format(repr(self.python_value))
+        return f"Literal({repr(self.python_value)})"
 
     def __str__(self):
         return str(self.python_value)
@@ -67,28 +68,7 @@ def __hash__(self):
         return hash(self.python_value)
 
 #------------------------------------------------------------------------------
-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 LiteralTrue(Literal):
     """
     Class representing the Python value True.
 
@@ -96,15 +76,14 @@ class LiteralTrue(LiteralNumeric):
 
     Parameters
     ----------
-    precision : int
-        The precision of the data type.
+    dtype : FixedSizeType
+        The exact type of the literal.
     """
-    __slots__   = ()
-    _dtype      = NativeBool()
-    _class_type = NativeBool()
+    __slots__ = ('_class_type',)
 
-    def __init__(self, precision = -1):
-        super().__init__(precision)
+    def __init__(self, dtype = PythonNativeBool()):
+        self._class_type = dtype
+        super().__init__()
 
     @property
     def python_value(self):
@@ -116,7 +95,7 @@ def python_value(self):
         return True
 
 #------------------------------------------------------------------------------
-class LiteralFalse(LiteralNumeric):
+class LiteralFalse(Literal):
     """
     Class representing the Python value False.
 
@@ -124,15 +103,14 @@ class LiteralFalse(LiteralNumeric):
 
     Parameters
     ----------
-    precision : int
-        The precision of the data type.
+    dtype : FixedSizeType
+        The exact type of the literal.
     """
-    __slots__   = ()
-    _dtype      = NativeBool()
-    _class_type = NativeBool()
+    __slots__ = ('_class_type',)
 
-    def __init__(self, precision = -1):
-        super().__init__(precision)
+    def __init__(self, dtype = PythonNativeBool()):
+        self._class_type = dtype
+        super().__init__()
 
     @property
     def python_value(self):
@@ -144,7 +122,7 @@ def python_value(self):
         return False
 
 #------------------------------------------------------------------------------
-class LiteralInteger(LiteralNumeric):
+class LiteralInteger(Literal):
     """
     Class representing an integer literal in Python.
 
@@ -155,19 +133,18 @@ class LiteralInteger(LiteralNumeric):
     value : int
         The Python literal.
 
-    precision : int
-        The precision of the integer.
+    dtype : FixedSizeType
+        The exact type of the literal.
     """
-    __slots__   = ('_value',)
-    _dtype      = NativeInteger()
-    _class_type = NativeInteger()
+    __slots__   = ('_value', '_class_type')
 
-    def __init__(self, value, precision = -1):
-        super().__init__(precision)
+    def __init__(self, value, dtype = PythonNativeInt()):
         assert(value >= 0)
         if not isinstance(value, int):
             raise TypeError("A LiteralInteger can only be created with an integer")
         self._value = value
+        self._class_type = dtype
+        super().__init__()
 
     @property
     def python_value(self):
@@ -182,7 +159,7 @@ def __index__(self):
         return self.python_value
 
 #------------------------------------------------------------------------------
-class LiteralFloat(LiteralNumeric):
+class LiteralFloat(Literal):
     """
     Class representing a float literal in Python.
 
@@ -193,21 +170,20 @@ class LiteralFloat(LiteralNumeric):
     value : float
         The Python literal.
 
-    precision : int
-        The precision of the float.
+    dtype : FixedSizeType
+        The exact type of the literal.
     """
-    __slots__   = ('_value',)
-    _dtype      = NativeFloat()
-    _class_type = NativeFloat()
+    __slots__   = ('_value', '_class_type')
 
-    def __init__(self, value, *, precision = -1):
+    def __init__(self, value, dtype = PythonNativeFloat()):
         if not isinstance(value, (int, float, LiteralFloat)):
             raise TypeError("A LiteralFloat can only be created with an integer or a float")
-        super().__init__(precision)
         if isinstance(value, LiteralFloat):
             self._value = value.python_value
         else:
             self._value = float(value)
+        self._class_type = dtype
+        super().__init__()
 
     @property
     def python_value(self):
@@ -220,7 +196,7 @@ def python_value(self):
 
 
 #------------------------------------------------------------------------------
-class LiteralComplex(LiteralNumeric):
+class LiteralComplex(Literal):
     """
     Class representing a complex literal in Python.
 
@@ -234,14 +210,12 @@ class LiteralComplex(LiteralNumeric):
     imag : float
         The imaginary part of the Python literal.
 
-    precision : int
-        The precision of the complex number.
+    dtype : FixedSizeType
+        The exact type of the literal.
     """
-    __slots__   = ('_real_part','_imag_part')
-    _dtype      = NativeComplex()
-    _class_type = NativeComplex()
+    __slots__   = ('_real_part','_imag_part', '_class_type')
 
-    def __new__(cls, real, imag, precision = -1):
+    def __new__(cls, real, imag, dtype = PythonNativeComplex()):
         if cls is LiteralImaginaryUnit:
             return super().__new__(cls)
         real_part = cls._collect_python_val(real)
@@ -251,19 +225,38 @@ def __new__(cls, real, imag, precision = -1):
         else:
             return super().__new__(cls)
 
-    def __init__(self, real, imag, precision = -1):
-        super().__init__(precision)
-        self._real_part = LiteralFloat(self._collect_python_val(real), precision = precision)
-        self._imag_part = LiteralFloat(self._collect_python_val(imag), precision = precision)
+    def __init__(self, real, imag, dtype = PythonNativeComplex()):
+        self._real_part = LiteralFloat(self._collect_python_val(real),
+                                       dtype = dtype.element_type)
+        self._imag_part = LiteralFloat(self._collect_python_val(imag),
+                                       dtype = dtype.element_type)
+        self._class_type = dtype
+        super().__init__()
 
     @staticmethod
     def _collect_python_val(arg):
+        """
+        Extract the Python value from the input argument.
+
+        Extract the Python value from the input argument which can either
+        be a literal or a Python variable.
+
+        Parameters
+        ----------
+        arg : Literal | int | float
+            The Python value.
+
+        Returns
+        -------
+        float
+            The Python value of the argument.
+        """
         if isinstance(arg, Literal):
             return arg.python_value
         elif isinstance(arg, (int, float)):
             return arg
         else:
-            raise TypeError("LiteralComplex argument must be an int/float/LiteralInt/LiteralFloat")
+            raise TypeError(f"LiteralComplex argument must be an int/float/LiteralInt/LiteralFloat not a {type(arg)}")
 
     @property
     def real(self):
@@ -298,13 +291,24 @@ class LiteralImaginaryUnit(LiteralComplex):
     Class representing the Python value j.
 
     Class representing the imaginary unit j in Python.
+
+    Parameters
+    ----------
+    real : float = 0
+        The value of the real part. This argument is necessary to handle the
+        inheritance but should not be provided explicitly.
+    imag : float = 0
+        The value of the real part. This argument is necessary to handle the
+        inheritance but should not be provided explicitly.
+    dtype : FixedSizeType
+        The exact type of the literal.
     """
     __slots__ = ()
-    def __new__(cls):
-        return super().__new__(cls, 0, 1)
+    def __new__(cls, real = 0, imag = 1, dtype = PythonNativeComplex()):
+        return super().__new__(cls, 0, 1, dtype = dtype)
 
-    def __init__(self, real = 0, imag = 1, precision = -1):
-        super().__init__(0, 1)
+    def __init__(self, real = 0, imag = 1, dtype = PythonNativeComplex()):
+        super().__init__(0, 1, dtype)
 
     @property
     def python_value(self):
@@ -328,9 +332,8 @@ class LiteralString(Literal):
         The Python literal.
     """
     __slots__ = ('_string',)
-    _dtype      = NativeString()
-    _class_type = NativeString()
-    _precision  = 0
+    _class_type = StringType()
+    _static_type = StringType()
 
     def __init__(self, arg):
         super().__init__()
@@ -339,7 +342,7 @@ def __init__(self, arg):
         self._string = arg
 
     def __repr__(self):
-        return "'{}'".format(str(self.python_value))
+        return f"'{self.python_value}'"
 
     def __str__(self):
         return str(self.python_value)
@@ -368,9 +371,7 @@ class Nil(Literal, metaclass=Singleton):
     """
     __slots__ = ()
     _attribute_nodes = ()
-    _dtype = NativeGeneric()
-    _precision = 0
-    _class_type = NativeGeneric()
+    _class_type = GenericType()
 
     def __str__(self):
         return 'None'
@@ -427,7 +428,7 @@ def python_value(self):
 
 #------------------------------------------------------------------------------
 
-def convert_to_literal(value, dtype = None, precision = None):
+def convert_to_literal(value, dtype = None):
     """
     Convert a Python value to a pyccel Literal.
 
@@ -440,52 +441,52 @@ def convert_to_literal(value, dtype = None, precision = None):
     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).
 
     Returns
     -------
     Literal
         The Python value 'value' expressed as a literal
-        with the specified dtype and precision.
+        with the specified dtype.
     """
     from .operators import PyccelUnarySub # Imported here to avoid circular import
 
+    # Calculate the default datatype
     if dtype is None:
         if isinstance(value, int):
-            dtype = NativeInteger()
+            dtype = PythonNativeInt()
         elif isinstance(value, float):
-            dtype = NativeFloat()
+            dtype = PythonNativeFloat()
         elif isinstance(value, complex):
-            dtype = NativeComplex()
+            dtype = PythonNativeComplex()
         elif isinstance(value, bool):
-            dtype = NativeBool()
+            dtype = PythonNativeBool()
         elif isinstance(value, str):
-            dtype = NativeString()
+            dtype = StringType()
         else:
-            raise TypeError('Unknown type')
+            raise TypeError(f'Unknown type of object {value}')
+
+    # Resolve any datatypes which don't inherit from FixedSizeType
+    if isinstance(dtype, StringType):
+        return LiteralString(value)
 
-    if precision is None and dtype is not NativeString():
-        precision = -1
+    assert isinstance(dtype, FixedSizeNumericType)
 
-    if isinstance(dtype, NativeInteger):
+    primitive_type = dtype.primitive_type
+    if isinstance(primitive_type, PrimitiveIntegerType):
         if value >= 0:
-            literal_val = LiteralInteger(value, precision)
+            literal_val = LiteralInteger(value, dtype)
         else:
-            literal_val = PyccelUnarySub(LiteralInteger(-value, precision))
-    elif isinstance(dtype, NativeFloat):
-        literal_val = LiteralFloat(value, precision=precision)
-    elif isinstance(dtype, NativeComplex):
-        literal_val = LiteralComplex(value.real, value.imag, precision)
-    elif isinstance(dtype, NativeBool):
+            literal_val = PyccelUnarySub(LiteralInteger(-value, dtype))
+    elif isinstance(primitive_type, PrimitiveFloatingPointType):
+        literal_val = LiteralFloat(value, dtype)
+    elif isinstance(primitive_type, PrimitiveComplexType):
+        literal_val = LiteralComplex(value.real, value.imag, dtype)
+    elif isinstance(primitive_type, PrimitiveBooleanType):
         if value:
-            literal_val = LiteralTrue(precision)
+            literal_val = LiteralTrue(dtype)
         else:
-            literal_val = LiteralFalse(precision)
-    elif isinstance(dtype, NativeString):
-        literal_val = LiteralString(value)
+            literal_val = LiteralFalse(dtype)
     else:
-        raise TypeError('Unknown type')
+        raise TypeError(f'Unknown type {dtype}')
 
     return literal_val
diff --git a/pyccel/ast/macros.py b/pyccel/ast/macros.py
index e360d3b0d3..013f5c74fb 100644
--- a/pyccel/ast/macros.py
+++ b/pyccel/ast/macros.py
@@ -10,7 +10,7 @@
 from pyccel.utilities.stage import PyccelStage
 
 from .basic          import TypedAstNode
-from .datatypes      import NativeInteger, NativeGeneric
+from .datatypes      import PythonNativeInt, GenericType
 from .internals      import PyccelSymbol
 from .variable       import Variable
 
@@ -71,10 +71,8 @@ class MacroShape(Macro):
     """
     __slots__ = ('_index','_rank','_shape')
     _name      = 'shape'
-    _dtype     = NativeInteger()
-    _precision = -1
     _order     = None
-    _class_type = NativeInteger()
+    _class_type = PythonNativeInt()
 
     def __init__(self, argument, index=None):
         if index is not None:
@@ -113,12 +111,10 @@ class MacroType(Macro):
     """
     __slots__ = ()
     _name      = 'dtype'
-    _dtype     = NativeGeneric()
-    _precision = 0
     _rank      = 0
     _shape     = None
     _order     = None
-    _class_type = NativeGeneric()
+    _class_type = GenericType()
 
     def __str__(self):
         return f'MacroType({self.argument})'
@@ -138,12 +134,10 @@ class MacroCount(Macro):
     """
     __slots__ = ()
     _name      = 'count'
-    _dtype     = NativeInteger()
-    _precision = -1
     _rank      = 0
     _shape     = None
     _order     = None
-    _class_type = NativeInteger()
+    _class_type = PythonNativeInt()
 
     def __str__(self):
         return f'MacroCount({self.argument})'
diff --git a/pyccel/ast/mathext.py b/pyccel/ast/mathext.py
index e9b4446583..491288126d 100644
--- a/pyccel/ast/mathext.py
+++ b/pyccel/ast/mathext.py
@@ -8,7 +8,7 @@
 import math
 
 from pyccel.ast.core      import PyccelFunctionDef, Module
-from pyccel.ast.datatypes import NativeInteger, NativeBool, NativeFloat
+from pyccel.ast.datatypes import PythonNativeInt, PythonNativeBool, PythonNativeFloat
 from pyccel.ast.internals import PyccelInternalFunction
 from pyccel.ast.variable  import Constant
 
@@ -107,9 +107,7 @@ class MathFunctionFloat(MathFunctionBase):
     """
     __slots__ = ()
     name = 'float'
-    _dtype = NativeFloat()
-    _precision = -1
-    _class_type = NativeFloat()
+    _class_type = PythonNativeFloat()
 
 
 class MathFunctionInt(MathFunctionBase):
@@ -126,9 +124,7 @@ class MathFunctionInt(MathFunctionBase):
     """
     __slots__ = ()
     name = 'int'
-    _dtype = NativeInteger()
-    _precision = -1
-    _class_type = NativeInteger()
+    _class_type = PythonNativeInt()
 
 
 class MathFunctionBool(MathFunctionBase):
@@ -145,9 +141,7 @@ class MathFunctionBool(MathFunctionBase):
     """
     __slots__ = ()
     name = 'bool'
-    _dtype = NativeBool()
-    _precision = -1
-    _class_type = NativeBool()
+    _class_type = PythonNativeBool()
 
 #==============================================================================
 # Functions that return one value
@@ -949,11 +943,11 @@ class MathModf(MathFunctionBase):
 # Constants
 #==============================================================================
 math_constants = {
-    'e'  : Constant('float', 'e'  , value=math.e  ),
-    'pi' : Constant('float', 'pi' , value=math.pi ),
-    'inf': Constant('float', 'inf', value=math.inf),
-    'nan': Constant('float', 'nan', value=math.nan),
-    'tau': Constant('float', 'tau', value=2.*math.pi),
+    'e'  : Constant(PythonNativeFloat(), 'e'  , value=math.e  ),
+    'pi' : Constant(PythonNativeFloat(), 'pi' , value=math.pi ),
+    'inf': Constant(PythonNativeFloat(), 'inf', value=math.inf),
+    'nan': Constant(PythonNativeFloat(), 'nan', value=math.nan),
+    'tau': Constant(PythonNativeFloat(), 'tau', value=2.*math.pi),
 }
 
 math_mod = Module('math',
diff --git a/pyccel/ast/numpy_wrapper.py b/pyccel/ast/numpy_wrapper.py
index fce4170e8e..14834fe6fa 100644
--- a/pyccel/ast/numpy_wrapper.py
+++ b/pyccel/ast/numpy_wrapper.py
@@ -9,24 +9,23 @@
 
 import numpy as np
 
-from pyccel.utilities.metaclasses import Singleton
+from .datatypes         import PythonNativeBool, GenericType, VoidType, FixedSizeType
 
-from .datatypes         import (NativeInteger, NativeFloat, NativeComplex,
-                                NativeBool, NativeGeneric, NativeVoid, DataType)
+from .cwrapper          import PyccelPyObject, check_type_registry, c_to_py_registry, pytype_parse_registry
 
-from .cwrapper          import PyccelPyObject
-
-from .core              import FunctionDef, FunctionCall
+from .core              import FunctionDef
 from .core              import FunctionDefArgument, FunctionDefResult
 
-from .internals         import get_final_precision
+from .c_concepts        import CNativeInt
 
-from .literals          import LiteralInteger
+from .numpytypes        import NumpyInt8Type, NumpyInt16Type, NumpyInt32Type, NumpyInt64Type
+from .numpytypes        import NumpyFloat32Type, NumpyFloat64Type, NumpyFloat128Type
+from .numpytypes        import NumpyComplex64Type, NumpyComplex128Type, NumpyComplex256Type
+from .numpytypes        import NumpyNDArrayType
 
 from .variable          import Variable
 
 from ..errors.errors   import Errors
-from ..errors.messages import PYCCEL_RESTRICTION_TODO
 
 errors = Errors()
 
@@ -35,9 +34,6 @@
     'PyccelPyArrayObject',
     #------- CAST FUNCTIONS ------
     'pyarray_to_ndarray',
-    #-------CHECK FUNCTIONS ------
-    'array_type_check',
-    'scalar_type_check',
     #-------HELPERS ------
     'array_get_dim',
     'array_get_data',
@@ -48,7 +44,7 @@
     'get_numpy_max_acceptable_version_file',
 )
 
-class PyccelPyArrayObject(DataType, metaclass=Singleton):
+class PyccelPyArrayObject(FixedSizeType):
     """
     Datatype representing a `PyArrayObject`.
 
@@ -82,102 +78,102 @@ def get_numpy_max_acceptable_version_file():
 
 PyArray_Check = FunctionDef(name      = 'PyArray_Check',
                             body      = [],
-                            arguments = [FunctionDefArgument(Variable(dtype=PyccelPyObject(), name = 'o'))],
-                            results   = [FunctionDefResult(Variable(dtype=NativeBool(), name='b'))])
+                            arguments = [FunctionDefArgument(Variable(PyccelPyObject(), name = 'o'))],
+                            results   = [FunctionDefResult(Variable(PythonNativeBool(), name='b'))])
 
 # numpy array to c ndarray : function definition in pyccel/stdlib/cwrapper/cwrapper_ndarrays.c
 pyarray_to_ndarray = FunctionDef(
                 name      = 'pyarray_to_ndarray',
-                arguments = [FunctionDefArgument(Variable(name = 'a', dtype = PyccelPyObject(), memory_handling = 'alias'))],
+                arguments = [FunctionDefArgument(Variable(PyccelPyObject(), 'a', memory_handling = 'alias'))],
                 body      = [],
-                results   = [FunctionDefResult(Variable(name = 'array', dtype = NativeGeneric()))])
+                results   = [FunctionDefResult(Variable(NumpyNDArrayType(GenericType()), 'array'))])
 
 # numpy array check elements : function definition in pyccel/stdlib/cwrapper/cwrapper_ndarrays.c
 pyarray_check = FunctionDef(
                 name      = 'pyarray_check',
                 arguments = [
-                        FunctionDefArgument(Variable(name = 'a', dtype = PyccelPyObject(), memory_handling='alias')),
-                        FunctionDefArgument(Variable(name = 'dtype', dtype = NativeInteger())),
-                        FunctionDefArgument(Variable(name = 'rank', dtype = NativeInteger())),
-                        FunctionDefArgument(Variable(name = 'flag', dtype = NativeInteger()))
+                        FunctionDefArgument(Variable(PyccelPyObject(), 'a', memory_handling='alias')),
+                        FunctionDefArgument(Variable(CNativeInt(), 'dtype')),
+                        FunctionDefArgument(Variable(CNativeInt(), 'rank')),
+                        FunctionDefArgument(Variable(CNativeInt(), 'flag'))
                     ],
                 body      = [],
-                results   = [FunctionDefResult(Variable(name = 'b', dtype = NativeBool()))])
+                results   = [FunctionDefResult(Variable(PythonNativeBool(), 'b'))])
 
 is_numpy_array = FunctionDef(
                 name      = 'is_numpy_array',
                 arguments = [
-                        FunctionDefArgument(Variable(name = 'a', dtype = PyccelPyObject(), memory_handling='alias')),
-                        FunctionDefArgument(Variable(name = 'dtype', dtype = NativeInteger())),
-                        FunctionDefArgument(Variable(name = 'rank', dtype = NativeInteger())),
-                        FunctionDefArgument(Variable(name = 'flag', dtype = NativeInteger()))
+                        FunctionDefArgument(Variable(PyccelPyObject(), 'a', memory_handling='alias')),
+                        FunctionDefArgument(Variable(CNativeInt(), 'dtype')),
+                        FunctionDefArgument(Variable(CNativeInt(), 'rank')),
+                        FunctionDefArgument(Variable(CNativeInt(), 'flag'))
                     ],
                 body      = [],
-                results   = [FunctionDefResult(Variable(name = 'b', dtype = NativeBool()))])
+                results   = [FunctionDefResult(Variable(PythonNativeBool(), 'b'))])
 
 # Return the shape of the n-th dimension : function definition in pyccel/stdlib/cwrapper/cwrapper_ndarrays.c
 array_get_dim  = FunctionDef(name    = 'nd_ndim',
                            body      = [],
-                           arguments = [FunctionDefArgument(Variable(dtype=NativeVoid(), name = 'o', is_optional = True)),
-                                        FunctionDefArgument(Variable(dtype=NativeInteger(), name = 'idx'))],
-                           results   = [FunctionDefResult(Variable(dtype=NativeInteger(), name = 'd'))])
+                           arguments = [FunctionDefArgument(Variable(VoidType(), name = 'o', is_optional = True)),
+                                        FunctionDefArgument(Variable(CNativeInt(), name = 'idx'))],
+                           results   = [FunctionDefResult(Variable(CNativeInt(), name = 'd'))])
 
 # Return the stride of the n-th dimension : function definition in pyccel/stdlib/cwrapper/cwrapper_ndarrays.c
 array_get_c_step = FunctionDef(name    = 'nd_nstep_C',
                            body      = [],
-                           arguments = [FunctionDefArgument(Variable(dtype=NativeVoid(), name = 'o', is_optional = True)),
-                                        FunctionDefArgument(Variable(dtype=NativeInteger(), name = 'idx'))],
-                           results   = [FunctionDefResult(Variable(dtype=NativeInteger(), name = 'd'))])
+                           arguments = [FunctionDefArgument(Variable(VoidType(), name = 'o', is_optional = True)),
+                                        FunctionDefArgument(Variable(CNativeInt(), name = 'idx'))],
+                           results   = [FunctionDefResult(Variable(CNativeInt(), name = 'd'))])
 array_get_f_step = FunctionDef(name    = 'nd_nstep_F',
                            body      = [],
-                           arguments = [FunctionDefArgument(Variable(dtype=NativeVoid(), name = 'o', is_optional = True)),
-                                        FunctionDefArgument(Variable(dtype=NativeInteger(), name = 'idx'))],
-                           results   = [FunctionDefResult(Variable(dtype=NativeInteger(), name = 'd'))])
+                           arguments = [FunctionDefArgument(Variable(VoidType(), name = 'o', is_optional = True)),
+                                        FunctionDefArgument(Variable(CNativeInt(), name = 'idx'))],
+                           results   = [FunctionDefResult(Variable(CNativeInt(), name = 'd'))])
 
 # Return the data of ndarray : function definition in pyccel/stdlib/cwrapper/cwrapper_ndarrays.c
 array_get_data  = FunctionDef(name   = 'nd_data',
                            body      = [],
-                           arguments = [FunctionDefArgument(Variable(dtype=NativeVoid(), name = 'o', is_optional=True))],
-                           results   = [FunctionDefResult(Variable(dtype=NativeVoid(), name = 'v', memory_handling='alias', rank = 1, class_type = NativeVoid()))])
+                           arguments = [FunctionDefArgument(Variable(VoidType(), name = 'o', is_optional=True))],
+                           results   = [FunctionDefResult(Variable(VoidType(), name = 'v', memory_handling='alias', rank = 1))])
 
 PyArray_SetBaseObject = FunctionDef(name   = 'PyArray_SetBaseObject',
                                     body      = [],
-                                    arguments = [FunctionDefArgument(Variable(dtype=PyccelPyArrayObject(), name = 'arr', memory_handling='alias')),
-                                                 FunctionDefArgument(Variable(dtype=PyccelPyObject(), name = 'obj', memory_handling='alias'))],
-                                    results   = [FunctionDefResult(Variable(dtype=NativeInteger(), name = 'd'))])
+                                    arguments = [FunctionDefArgument(Variable(PyccelPyArrayObject(), name = 'arr', memory_handling='alias')),
+                                                 FunctionDefArgument(Variable(PyccelPyObject(), name = 'obj', memory_handling='alias'))],
+                                    results   = [FunctionDefResult(Variable(CNativeInt(), name = 'd'))])
 
 import_array = FunctionDef('import_array', (), (), ())
 
 # Basic Array Flags
 # https://numpy.org/doc/stable/reference/c-api/array.html#c.NPY_ARRAY_OWNDATA
-numpy_flag_own_data     = Variable(dtype=NativeInteger(),  name = 'NPY_ARRAY_OWNDATA')
+numpy_flag_own_data     = Variable(CNativeInt(),  name = 'NPY_ARRAY_OWNDATA')
 # https://numpy.org/doc/stable/reference/c-api/array.html#c.NPY_ARRAY_C_CONTIGUOUS
-numpy_flag_c_contig     = Variable(dtype=NativeInteger(),  name = 'NPY_ARRAY_C_CONTIGUOUS')
+numpy_flag_c_contig     = Variable(CNativeInt(),  name = 'NPY_ARRAY_C_CONTIGUOUS')
 # https://numpy.org/doc/stable/reference/c-api/array.html#c.NPY_ARRAY_F_CONTIGUOUS
-numpy_flag_f_contig     = Variable(dtype=NativeInteger(),  name = 'NPY_ARRAY_F_CONTIGUOUS')
+numpy_flag_f_contig     = Variable(CNativeInt(),  name = 'NPY_ARRAY_F_CONTIGUOUS')
 
 # Custom Array Flags defined in pyccel/stdlib/cwrapper/cwrapper_ndarrays.h
-no_type_check           = Variable(dtype=NativeInteger(),  name = 'NO_TYPE_CHECK')
-no_order_check          = Variable(dtype=NativeInteger(),  name = 'NO_ORDER_CHECK')
+no_type_check           = Variable(CNativeInt(),  name = 'NO_TYPE_CHECK')
+no_order_check          = Variable(CNativeInt(),  name = 'NO_ORDER_CHECK')
 
 # https://numpy.org/doc/stable/reference/c-api/dtype.html
-numpy_bool_type         = Variable(dtype=NativeInteger(),  name = 'NPY_BOOL', precision = 4)
-numpy_byte_type         = Variable(dtype=NativeInteger(),  name = 'NPY_BYTE', precision = 4)
-numpy_ubyte_type        = Variable(dtype=NativeInteger(),  name = 'NPY_UBYTE', precision = 4)
-numpy_short_type        = Variable(dtype=NativeInteger(),  name = 'NPY_SHORT', precision = 4)
-numpy_ushort_type       = Variable(dtype=NativeInteger(),  name = 'NPY_USHORT', precision = 4)
-numpy_int_type          = Variable(dtype=NativeInteger(),  name = 'NPY_INT32', precision = 4)
-numpy_uint_type         = Variable(dtype=NativeInteger(),  name = 'NPY_UINT', precision = 4)
-numpy_long_type         = Variable(dtype=NativeInteger(),  name = 'NPY_LONG', precision = 4)
-numpy_ulong_type        = Variable(dtype=NativeInteger(),  name = 'NPY_ULONG', precision = 4)
-numpy_longlong_type     = Variable(dtype=NativeInteger(),  name = 'NPY_INT64', precision = 4)
-numpy_ulonglong_type    = Variable(dtype=NativeInteger(),  name = 'NPY_ULONGLONG', precision = 4)
-numpy_float_type        = Variable(dtype=NativeInteger(),  name = 'NPY_FLOAT', precision = 4)
-numpy_double_type       = Variable(dtype=NativeInteger(),  name = 'NPY_DOUBLE', precision = 4)
-numpy_longdouble_type   = Variable(dtype=NativeInteger(),  name = 'NPY_LONGDOUBLE', precision = 4)
-numpy_cfloat_type       = Variable(dtype=NativeInteger(),  name = 'NPY_CFLOAT', precision = 4)
-numpy_cdouble_type      = Variable(dtype=NativeInteger(),  name = 'NPY_CDOUBLE', precision = 4)
-numpy_clongdouble_type  = Variable(dtype=NativeInteger(),  name = 'NPY_CLONGDOUBLE', precision = 4)
+numpy_bool_type         = Variable(CNativeInt(),  name = 'NPY_BOOL')
+numpy_byte_type         = Variable(CNativeInt(),  name = 'NPY_BYTE')
+numpy_ubyte_type        = Variable(CNativeInt(),  name = 'NPY_UBYTE')
+numpy_short_type        = Variable(CNativeInt(),  name = 'NPY_SHORT')
+numpy_ushort_type       = Variable(CNativeInt(),  name = 'NPY_USHORT')
+numpy_int_type          = Variable(CNativeInt(),  name = 'NPY_INT32')
+numpy_uint_type         = Variable(CNativeInt(),  name = 'NPY_UINT')
+numpy_long_type         = Variable(CNativeInt(),  name = 'NPY_LONG')
+numpy_ulong_type        = Variable(CNativeInt(),  name = 'NPY_ULONG')
+numpy_longlong_type     = Variable(CNativeInt(),  name = 'NPY_INT64')
+numpy_ulonglong_type    = Variable(CNativeInt(),  name = 'NPY_ULONGLONG')
+numpy_float_type        = Variable(CNativeInt(),  name = 'NPY_FLOAT')
+numpy_double_type       = Variable(CNativeInt(),  name = 'NPY_DOUBLE')
+numpy_longdouble_type   = Variable(CNativeInt(),  name = 'NPY_LONGDOUBLE')
+numpy_cfloat_type       = Variable(CNativeInt(),  name = 'NPY_CFLOAT')
+numpy_cdouble_type      = Variable(CNativeInt(),  name = 'NPY_CDOUBLE')
+numpy_clongdouble_type  = Variable(CNativeInt(),  name = 'NPY_CLONGDOUBLE')
 
 numpy_num_to_type = {0 : numpy_bool_type,
                      1 : numpy_byte_type,
@@ -204,124 +200,48 @@ def get_numpy_max_acceptable_version_file():
         4 : np.dtype(np.int32).num,
         8 : np.dtype(np.int64).num}
 
-numpy_dtype_registry = {('bool',-1)    : numpy_bool_type,
-                        ('int',1)      : numpy_num_to_type[numpy_int_type_precision_map[1]],
-                        ('int',2)      : numpy_num_to_type[numpy_int_type_precision_map[2]],
-                        ('int',4)      : numpy_num_to_type[numpy_int_type_precision_map[4]],
-                        ('int',8)      : numpy_num_to_type[numpy_int_type_precision_map[8]],
-                        ('int',16)     : numpy_longlong_type,
-                        ('float',4)    : numpy_float_type,
-                        ('float',8)    : numpy_double_type,
-                        ('float',16)   : numpy_longdouble_type,
-                        ('complex',4)  : numpy_cfloat_type,
-                        ('complex',8)  : numpy_cdouble_type,
-                        ('complex',16) : numpy_clongdouble_type}
+numpy_dtype_registry = {PythonNativeBool()    : numpy_bool_type,
+                        NumpyInt8Type()       : numpy_num_to_type[numpy_int_type_precision_map[1]],
+                        NumpyInt16Type()      : numpy_num_to_type[numpy_int_type_precision_map[2]],
+                        NumpyInt32Type()      : numpy_num_to_type[numpy_int_type_precision_map[4]],
+                        NumpyInt64Type()      : numpy_num_to_type[numpy_int_type_precision_map[8]],
+                        NumpyFloat32Type()    : numpy_float_type,
+                        NumpyFloat64Type()    : numpy_double_type,
+                        NumpyFloat128Type()   : numpy_longdouble_type,
+                        NumpyComplex64Type()  : numpy_cfloat_type,
+                        NumpyComplex128Type() : numpy_cdouble_type,
+                        NumpyComplex256Type() : numpy_clongdouble_type}
 
 # Needed to check for numpy arguments type
-Numpy_Bool_ref       = Variable(dtype=NativeVoid(),  name = 'Bool')
-Numpy_Int8_ref       = Variable(dtype=NativeVoid(),  name = 'Int8')
-Numpy_Int16_ref      = Variable(dtype=NativeVoid(),  name = 'Int16')
-Numpy_Int32_ref      = Variable(dtype=NativeVoid(),  name = 'Int32')
-Numpy_Int64_ref      = Variable(dtype=NativeVoid(),  name = 'Int64')
-Numpy_Float_ref      = Variable(dtype=NativeVoid(),  name = 'Float32')
-Numpy_Double_ref     = Variable(dtype=NativeVoid(),  name = 'Float64')
-Numpy_Complex64_ref  = Variable(dtype=NativeVoid(),  name = 'Complex64')
-Numpy_Complex128_ref = Variable(dtype=NativeVoid(),  name = 'Complex128')
-
-numpy_type_check_registry = {
-    (NativeInteger(), 4)       : Numpy_Int32_ref,
-    (NativeInteger(), 8)       : Numpy_Int64_ref,
-    (NativeInteger(), 2)       : Numpy_Int16_ref,
-    (NativeInteger(), 1)       : Numpy_Int8_ref,
-    (NativeFloat(), 8)         : Numpy_Double_ref,
-    (NativeFloat(), 4)         : Numpy_Float_ref,
-    (NativeComplex(), 4)       : Numpy_Complex64_ref,
-    (NativeComplex(), 8)       : Numpy_Complex128_ref,
-    (NativeBool(), 4)          : Numpy_Bool_ref
-}
-
-# helpers
-def find_in_numpy_dtype_registry(var):
-    """ Find the numpy dtype key for a given variable
-    """
-    dtype = str(var.dtype)
-    prec  = get_final_precision(var)
-    try :
-        return numpy_dtype_registry[(dtype, prec)]
-    except KeyError:
-        return errors.report(PYCCEL_RESTRICTION_TODO,
-                symbol = "{}[kind = {}]".format(dtype, prec),
-                severity='fatal')
-
-def array_type_check(py_variable, c_variable, raise_error):
-    """
-    Return the code which checks if the array has the expected type.
-
-    Returns the code which checks if the array has the expected rank,
-    datatype, precision, and order. These are determined from the
-    properties of the `c_variable` argument.
-
-    Parameters
-    ----------
-    py_variable : Variable
-            A variable containing the Python object passed into the wrapper.
-    c_variable : Variable
-            A variable containing the basic C object which will store the array.
-    raise_error : bool
-            Indicates whether an error should be raised if the type does not match.
-
-    Returns
-    -------
-    FunctionCall
-            The code necessary to validate the provided array.
-    """
-    rank     = c_variable.rank
-    type_ref = find_in_numpy_dtype_registry(c_variable)
-    flag     = no_order_check
-
-    # order flag
-    if rank > 1:
-        if c_variable.order == 'F':
-            flag = numpy_flag_f_contig
-        else:
-            flag = numpy_flag_c_contig
-
-    if raise_error:
-        return FunctionCall(pyarray_check, [py_variable, type_ref, LiteralInteger(rank), flag])
-    else:
-        return FunctionCall(is_numpy_array, [py_variable, type_ref, LiteralInteger(rank), flag])
-
-
-def scalar_type_check(py_variable, c_variable):
-    """
-    Create a FunctionCall to check the type of a Python object.
-
-    Create a FunctionCall object representing a call to a function which
-    is responsible for checking if the Python object passed as an argument
-    has a type matching that of the provided C object.
-
-    Parameters
-    ----------
-    py_variable : Variable
-        The Python argument of the check function.
-
-    c_variable : Variable
-        The variable needed for the generation of the type check.
-
-    Returns
-    -------
-    FunctionCall
-        The FunctionCall which checks the type.
-    """
-    try :
-        check_numpy_ref = numpy_type_check_registry[(c_variable.dtype, c_variable.precision)]
-    except KeyError:
-        errors.report(PYCCEL_RESTRICTION_TODO, symbol=c_variable.dtype,severity='fatal')
-
-    check_numpy_func = FunctionDef(name = 'PyArray_IsScalar',
-                              body      = [],
-                              arguments = [FunctionDefArgument(Variable(dtype=PyccelPyObject(), name = 'o', memory_handling='alias')),
-                                           FunctionDefArgument(check_numpy_ref)],
-                              results   = [FunctionDefResult(Variable(dtype=NativeBool(), name = 'r'))])
-
-    return FunctionCall(check_numpy_func, [py_variable, check_numpy_ref])
+check_type_registry.update({
+    NumpyInt8Type()       : 'PyIs_Int8',
+    NumpyInt16Type()      : 'PyIs_Int16',
+    NumpyInt32Type()      : 'PyIs_Int32',
+    NumpyInt64Type()      : 'PyIs_Int64',
+    NumpyFloat32Type()    : 'PyIs_Float',
+    NumpyFloat64Type()    : 'PyIs_Double',
+    NumpyComplex64Type()  : 'PyIs_Complex64',
+    NumpyComplex128Type() : 'PyIs_Complex128'
+    })
+
+c_to_py_registry.update({
+    NumpyInt8Type()       : 'Int8_to_NumpyLong',
+    NumpyInt16Type()      : 'Int16_to_NumpyLong',
+    NumpyInt32Type()      : 'Int32_to_NumpyLong',
+    NumpyInt64Type()      : 'Int64_to_NumpyLong',
+    NumpyFloat32Type()    : 'Float_to_NumpyDouble',
+    NumpyFloat64Type()    : 'Double_to_NumpyDouble',
+    NumpyComplex64Type()  : 'Complex64_to_NumpyComplex',
+    NumpyComplex128Type() : 'Complex128_to_NumpyComplex'
+    })
+
+pytype_parse_registry.update({
+    NumpyInt8Type()       : 'b',
+    NumpyInt16Type()      : 'h',
+    NumpyInt32Type()      : 'i',
+    NumpyInt64Type()      : 'l',
+    NumpyFloat32Type()    : 'f',
+    NumpyFloat64Type()    : 'd',
+    NumpyComplex64Type()  : 'O',
+    NumpyComplex128Type() : 'O'
+    })
diff --git a/pyccel/ast/numpyext.py b/pyccel/ast/numpyext.py
index 4fd28b584b..1af106b567 100644
--- a/pyccel/ast/numpyext.py
+++ b/pyccel/ast/numpyext.py
@@ -6,35 +6,37 @@
 #------------------------------------------------------------------------------------------#
 """ Module containing objects from the numpy module understood by pyccel
 """
-from functools import lru_cache
-
 import numpy
 
 from pyccel.errors.errors import Errors
 from pyccel.errors.messages import WRONG_LINSPACE_ENDPOINT, NON_LITERAL_KEEP_DIMS, NON_LITERAL_AXIS
 
-from pyccel.utilities.metaclasses import Singleton
 from pyccel.utilities.stage import PyccelStage
 
 from .basic          import TypedAstNode
 from .builtins       import (PythonInt, PythonBool, PythonFloat, PythonTuple,
                              PythonComplex, PythonReal, PythonImag, PythonList,
-                             PythonType, PythonConjugate)
+                             PythonType, PythonConjugate, DtypePrecisionToCastFunction)
 
 from .core           import Module, Import, PyccelFunctionDef, FunctionCall
 
-from .datatypes      import (dtype_and_precision_registry as dtype_registry, NativeHomogeneousTuple, NativeInhomogeneousTuple,
-                             default_precision, NativeInteger, DataType, NativeNumericTypes,
-                             NativeFloat, NativeComplex, NativeBool, NativeNumeric)
+from .datatypes      import PythonNativeBool, PythonNativeInt, PythonNativeFloat
+from .datatypes      import PrimitiveBooleanType, PrimitiveIntegerType, PrimitiveFloatingPointType, PrimitiveComplexType
+from .datatypes      import HomogeneousTupleType, FixedSizeNumericType, GenericType, HomogeneousContainerType
+from .datatypes      import InhomogeneousTupleType, ContainerType
 
-from .internals      import PyccelInternalFunction, Slice, get_final_precision
+from .internals      import PyccelInternalFunction, Slice
 from .internals      import PyccelArraySize, PyccelArrayShapeElement
 
 from .literals       import LiteralInteger, LiteralFloat, LiteralComplex, LiteralString, convert_to_literal
 from .literals       import LiteralTrue, LiteralFalse
 from .literals       import Nil
 from .mathext        import MathCeil
+from .numpytypes     import NumpyNumericType, NumpyInt8Type, NumpyInt16Type, NumpyInt32Type, NumpyInt64Type
+from .numpytypes     import NumpyFloat32Type, NumpyFloat64Type, NumpyFloat128Type, NumpyNDArrayType
+from .numpytypes     import NumpyComplex64Type, NumpyComplex128Type, NumpyComplex256Type, numpy_precision_map
 from .operators      import broadcast, PyccelMinus, PyccelDiv, PyccelMul, PyccelAdd
+from .type_annotations import typenames_to_dtypes as dtype_registry
 from .variable       import Variable, Constant, IndexedElement
 
 errors = Errors()
@@ -97,7 +99,6 @@
     'NumpyInt64',
     'NumpyLinspace',
     'NumpyMatmul',
-    'NumpyNDArrayType',
     'NumpyNewArray',
     'NumpyMod',
     'NumpyNonZero',
@@ -121,25 +122,26 @@
     'NumpyIsNan',
 )
 
-class NumpyNDArrayType(DataType, metaclass=Singleton):
-    """
-    Class representing the NumPy ND array type.
-
-    Class representing the NumPy ND array type.
-    """
-    __slots__ = ()
-    name = 'numpy.ndarray'
-
-    @lru_cache
-    def __add__(self, other):
-        if isinstance(other, (*NativeNumericTypes, NumpyNDArrayType)):
-            return self
-        else:
-            raise NotImplementedError(f"Can't combine {self} and {other}")
-
-    @lru_cache
-    def __radd__(self, other):
-        return self.__add__(other)
+dtype_registry.update({
+    'int8'       : NumpyInt8Type(),
+    'int16'      : NumpyInt16Type(),
+    'int32'      : NumpyInt32Type(),
+    'int64'      : NumpyInt64Type(),
+    'i1'         : NumpyInt8Type(),
+    'i2'         : NumpyInt16Type(),
+    'i4'         : NumpyInt32Type(),
+    'i8'         : NumpyInt64Type(),
+    'float32'    : NumpyFloat32Type(),
+    'float64'    : NumpyFloat64Type(),
+    'float128'   : NumpyFloat128Type(),
+    'f4'         : NumpyFloat32Type(),
+    'f8'         : NumpyFloat64Type(),
+    'complex64'  : NumpyComplex64Type(),
+    'complex128' : NumpyComplex128Type(),
+    'complex256' : NumpyComplex256Type(),
+    'c8'         : NumpyComplex64Type(),
+    'c16'        : NumpyComplex128Type(),
+    })
 
 #=======================================================================================
 def process_shape(is_scalar, shape):
@@ -187,7 +189,7 @@ def __init__(self, arg):
         self._shape = arg.shape
         self._rank  = arg.rank
         self._order = arg.order
-        self._class_type = arg.class_type
+        self._class_type = arg.class_type.switch_basic_type(self.static_type())
         super().__init__(arg)
 
     @property
@@ -201,17 +203,33 @@ def is_elemental(self):
         return True
 
 class NumpyFloat32(NumpyFloat):
-    """ Represents a call to numpy.float32() function.
+    """
+    Represents a call to numpy.float32() function.
+
+    Represents a call to numpy.float32() function.
+
+    Parameters
+    ----------
+    arg : TypedAstNode
+        The argument passed to the function.
     """
     __slots__ = ()
-    _precision = dtype_registry['float32'][1]
+    _static_type = NumpyFloat32Type()
     name = 'float32'
 
 class NumpyFloat64(NumpyFloat):
-    """ Represents a call to numpy.float64() function.
+    """
+    Represents a call to numpy.float64() function.
+
+    Represents a call to numpy.float64() function.
+
+    Parameters
+    ----------
+    arg : TypedAstNode
+        The argument passed to the function.
     """
     __slots__ = ()
-    _precision = dtype_registry['float64'][1]
+    _static_type = NumpyFloat64Type()
     name = 'float64'
 
 #=======================================================================================
@@ -232,7 +250,7 @@ def __init__(self, arg):
         self._shape = arg.shape
         self._rank  = arg.rank
         self._order = arg.order
-        self._class_type = arg.class_type
+        self._class_type = arg.class_type.switch_basic_type(self.static_type())
         super().__init__(arg)
 
     @property
@@ -264,7 +282,7 @@ def __init__(self, arg=None, base=10):
         self._shape = arg.shape
         self._rank  = arg.rank
         self._order = arg.order
-        self._class_type = arg.class_type
+        self._class_type = arg.class_type.switch_basic_type(self.static_type())
         super().__init__(arg)
 
     @property
@@ -278,31 +296,63 @@ def is_elemental(self):
         return True
 
 class NumpyInt8(NumpyInt):
-    """ Represents a call to numpy.int8() function.
+    """
+    Represents a call to numpy.int8() function.
+
+    Represents a call to numpy.int8() function.
+
+    Parameters
+    ----------
+    arg : TypedAstNode
+        The argument passed to the function.
     """
     __slots__ = ()
-    _precision = dtype_registry['int8'][1]
+    _static_type = NumpyInt8Type()
     name = 'int8'
 
 class NumpyInt16(NumpyInt):
-    """ Represents a call to numpy.int16() function.
+    """
+    Represents a call to numpy.int16() function.
+
+    Represents a call to numpy.int16() function.
+
+    Parameters
+    ----------
+    arg : TypedAstNode
+        The argument passed to the function.
     """
     __slots__ = ()
-    _precision = dtype_registry['int16'][1]
+    _static_type = NumpyInt16Type()
     name = 'int16'
 
 class NumpyInt32(NumpyInt):
-    """ Represents a call to numpy.int32() function.
+    """
+    Represents a call to numpy.int32() function.
+
+    Represents a call to numpy.int32() function.
+
+    Parameters
+    ----------
+    arg : TypedAstNode
+        The argument passed to the function.
     """
     __slots__ = ()
-    _precision = dtype_registry['int32'][1]
+    _static_type = NumpyInt32Type()
     name = 'int32'
 
 class NumpyInt64(NumpyInt):
-    """ Represents a call to numpy.int64() function.
+    """
+    Represents a call to numpy.int64() function.
+
+    Represents a call to numpy.int64() function.
+
+    Parameters
+    ----------
+    arg : TypedAstNode
+        The argument passed to the function.
     """
     __slots__ = ()
-    _precision = dtype_registry['int64'][1]
+    _static_type = NumpyInt64Type()
     name = 'int64'
 
 #==============================================================================
@@ -320,21 +370,20 @@ class NumpyReal(PythonReal):
     arg : TypedAstNode
         The argument passed to the function.
     """
-    __slots__ = ('_precision','_rank','_shape','_order','_class_type')
+    __slots__ = ('_rank','_shape','_order','_class_type')
     name = 'real'
     def __new__(cls, arg):
-        if isinstance(arg.dtype, NativeBool):
+        if isinstance(arg.dtype, PythonNativeBool):
             return NumpyInt(arg)
         else:
             return super().__new__(cls, arg)
 
     def __init__(self, arg):
         super().__init__(arg)
-        self._precision = arg.precision
         self._order = arg.order
         self._rank  = arg.rank
         self._shape = process_shape(self._rank == 0, self.internal_var.shape)
-        self._class_type = arg.class_type
+        self._class_type = arg.class_type.switch_basic_type(arg.dtype.element_type)
 
     @property
     def is_elemental(self):
@@ -359,25 +408,24 @@ class NumpyImag(PythonImag):
     arg : TypedAstNode
         The argument passed to the function.
     """
-    __slots__ = ('_precision','_rank','_shape','_order','_class_type')
+    __slots__ = ('_rank','_shape','_order','_class_type')
     name = 'imag'
     def __new__(cls, arg):
 
-        if not isinstance(arg.dtype, NativeComplex):
-            dtype = NativeInteger() if isinstance(arg.dtype, NativeBool) else arg.dtype
+        if not isinstance(arg.dtype.primitive_type, PrimitiveComplexType):
+            dtype = PythonNativeInt() if isinstance(arg.dtype, PythonNativeBool) else arg.dtype
             if arg.rank == 0:
-                return convert_to_literal(0, dtype, arg.precision)
-            dtype = DtypePrecisionToCastFunction[dtype.name][arg.precision]
+                return convert_to_literal(0, dtype)
+            dtype = DtypePrecisionToCastFunction[dtype].static_type()
             return NumpyZeros(arg.shape, dtype=dtype)
         return super().__new__(cls, arg)
 
     def __init__(self, arg):
         super().__init__(arg)
-        self._precision = arg.precision
         self._order = arg.order
         self._rank  = arg.rank
         self._shape = process_shape(self._rank == 0, self.internal_var.shape)
-        self._class_type = arg.class_type
+        self._class_type = arg.class_type.switch_basic_type(arg.dtype.element_type)
 
     @property
     def is_elemental(self):
@@ -411,7 +459,7 @@ def __init__(self, arg0, arg1 = None):
         self._shape = arg0.shape
         self._rank  = arg0.rank
         self._order = arg0.order
-        self._class_type = arg0.class_type
+        self._class_type = arg0.class_type.switch_basic_type(self.static_type())
         super().__init__(arg0)
 
     @property
@@ -425,39 +473,41 @@ def is_elemental(self):
         return True
 
 class NumpyComplex64(NumpyComplex):
-    """ Represents a call to numpy.complex64() function.
+    """
+    Represents a call to numpy.complex64() function.
+
+    Represents a call to numpy.complex64() function.
+
+    Parameters
+    ----------
+    arg0 : TypedAstNode
+        The argument passed to the function.
+
+    arg1 : TypedAstNode
+        Unused inherited argument.
     """
     __slots__ = ()
-    _precision = dtype_registry['complex64'][1]
+    _static_type = NumpyComplex64Type()
     name = 'complex64'
 
 class NumpyComplex128(NumpyComplex):
-    """ Represents a call to numpy.complex128() function.
+    """
+    Represents a call to numpy.complex128() function.
+
+    Represents a call to numpy.complex128() function.
+
+    Parameters
+    ----------
+    arg0 : TypedAstNode
+        The argument passed to the function.
+
+    arg1 : TypedAstNode
+        Unused inherited argument.
     """
     __slots__ = ()
-    _precision = dtype_registry['complex128'][1]
+    _static_type = NumpyComplex128Type()
     name = 'complex128'
 
-DtypePrecisionToCastFunction = {
-    'int' : {
-       -1 : PythonInt,
-        1 : NumpyInt8,
-        2 : NumpyInt16,
-        4 : NumpyInt32,
-        8 : NumpyInt64},
-    'float' : {
-       -1 : PythonFloat,
-        4 : NumpyFloat32,
-        8 : NumpyFloat64},
-    'complex' : {
-       -1 : PythonComplex,
-        4 : NumpyComplex64,
-        8 : NumpyComplex128,},
-    'bool':  {
-       -1 : PythonBool,
-        4 : NumpyBool}
-}
-
 #=======================================================================================
 
 class NumpyResultType(PyccelInternalFunction):
@@ -474,45 +524,17 @@ class NumpyResultType(PyccelInternalFunction):
         Any arrays and dtypes passed to the function (currently only accepts one array
         and no dtypes).
     """
-    __slots__ = ('_dtype','_precision','_class_type')
+    __slots__ = ('_class_type',)
     _rank = 0
     _shape = None
     _order = None
     name = 'result_type'
 
     def __init__(self, *arrays_and_dtypes):
-        dtype_and_precs = [process_dtype(a) if isinstance(a, PyccelFunctionDef) else (a.dtype, a.precision) \
-                            for a in arrays_and_dtypes]
-        dtypes = [d[0] for d in dtype_and_precs]
-        precisions = [d[1] for d in dtype_and_precs]
-
-        if any(d not in NativeNumeric for d in dtypes):
-            raise TypeError(f'cannot determine the type of {self}')
-        elif any(d is NativeComplex() for d in dtypes):
-            self._dtype     = NativeComplex()
-        elif any(d is NativeFloat() for d in dtypes):
-            self._dtype     = NativeFloat()
-        elif any(d is NativeInteger() for d in dtypes):
-            self._dtype     = NativeInteger()
-        else:
-            self._dtype     = NativeBool()
-        self._class_type = self._dtype
-
-        # An integer cannot be described accurately in a float of the same precision due to the mantissa
-        # If the user wants to store the maximum value of an integer they will need the next largest precision
-        if self._dtype in (NativeFloat(), NativeComplex()):
-            precisions = [p*2 if (isinstance(d, NativeInteger) and isinstance(a,PyccelFunctionDef) and p!=-1) else p \
-                            for a,d,p in zip(arrays_and_dtypes, dtypes, precisions)]
-
-        # Arrays and NumPy datatype objects have priority
-        dtype_array_precisions = [p for a,d,p in zip(arrays_and_dtypes, dtypes, precisions) \
-                                  if (getattr(a,'rank', 0) != 0 or isinstance(a,PyccelFunctionDef))]
-        if dtype_array_precisions:
-            self._precision = max(dtype_array_precisions)
-        else:
-            if -1 in precisions:
-                precisions.append(default_precision[self.dtype])
-            self._precision = max(precisions)
+        types = [a.cls_name.static_type() if isinstance(a, PyccelFunctionDef) else a.class_type for a in arrays_and_dtypes]
+        self._class_type = sum(types, start=GenericType())
+        if isinstance(self._class_type, ContainerType):
+            self._class_type = self._class_type.element_type
 
         super().__init__(*arrays_and_dtypes)
 
@@ -551,31 +573,26 @@ def process_dtype(dtype):
         if dtype.arg.rank > 0:
             errors.report("Python's type function doesn't return enough information about this object for pyccel to fully define a type",
                     symbol=dtype, severity="fatal")
-        return dtype.dtype, get_final_precision(dtype)
-    if isinstance(dtype, NumpyResultType):
-        return dtype.dtype, dtype.precision
-
-    if isinstance(dtype, PyccelFunctionDef):
-        dtype = dtype.cls_name
-
-    if dtype in (PythonInt, PythonFloat, PythonComplex, PythonBool):
-        # remove python prefix from dtype.name len("python") = 6
-        dtype = dtype.__name__.lower()[6:]
-    elif dtype in (NumpyInt, NumpyInt8, NumpyInt16, NumpyInt32, NumpyInt64, NumpyComplex, NumpyFloat,
-				  NumpyComplex128, NumpyComplex64, NumpyFloat64, NumpyFloat32):
-        # remove numpy prefix from dtype.name len("numpy") = 5
-        dtype = dtype.__name__.lower()[5:]
+        else:
+            dtype = dtype.arg.class_type
+    elif isinstance(dtype, NumpyResultType):
+        dtype =  dtype.dtype
+
+    elif isinstance(dtype, PyccelFunctionDef):
+        dtype = dtype.cls_name.static_type()
+
     elif isinstance(dtype, (LiteralString, str)):
-        dtype = str(dtype).replace('\'', '').lower()
-        if dtype not in dtype_registry:
+        try:
+            dtype = dtype_registry[str(dtype)]
+        except KeyError:
             raise TypeError(f'Unknown type of {dtype}.')
+
+    if isinstance(dtype, (NumpyNumericType, PythonNativeBool, GenericType)):
+        return dtype
+    if isinstance(dtype, FixedSizeNumericType):
+        return numpy_precision_map[(dtype.primitive_type, dtype.precision)]
     else:
         raise TypeError(f'Unknown type of {dtype}.')
-    dtype, precision = dtype_registry[dtype]
-    if precision == -1:
-        precision = default_precision[dtype]
-
-    return dtype, precision
 
 #==============================================================================
 class NumpyNewArray(PyccelInternalFunction):
@@ -589,14 +606,16 @@ class NumpyNewArray(PyccelInternalFunction):
     ----------
     *args : tuple of TypedAstNode
         The arguments of the superclass PyccelInternalFunction.
+    dtype : PyccelType
+        The datatype of the new array.
     init_dtype : PythonType, PyccelFunctionDef, LiteralString, str
         The actual dtype passed to the NumPy function.
     """
     __slots__ = ('_init_dtype','_class_type')
 
-    def __init__(self, *args, init_dtype = None):
+    def __init__(self, *args, dtype, init_dtype = None):
         self._init_dtype = init_dtype
-        self._class_type = NumpyNDArrayType()
+        self._class_type = NumpyNDArrayType(dtype) # pylint: disable=no-member
 
         super().__init__(*args)
 
@@ -644,7 +663,7 @@ class NumpyArray(NumpyNewArray):
         The minimum number of dimensions that the resulting array should
         have.
     """
-    __slots__ = ('_arg','_dtype','_precision','_shape','_rank','_order')
+    __slots__ = ('_arg','_shape','_rank','_order')
     _attribute_nodes = ('_arg',)
     name = 'array'
 
@@ -653,19 +672,16 @@ def __init__(self, arg, dtype=None, order='K', ndmin=None):
         if not isinstance(arg, (PythonTuple, PythonList, Variable, IndexedElement)):
             raise TypeError('Unknown type of  %s.' % type(arg))
 
-        is_homogeneous_tuple = isinstance(arg.class_type, NativeHomogeneousTuple)
+        is_homogeneous_tuple = isinstance(arg.class_type, HomogeneousTupleType)
         # Inhomogeneous tuples can contain homogeneous data if it is inhomogeneous due to pointers
-        if isinstance(arg.class_type, NativeInhomogeneousTuple):
+        if isinstance(arg.class_type, InhomogeneousTupleType):
             if not isinstance(arg, PythonTuple):
                 arg = PythonTuple(*arg)
             is_homogeneous_tuple = arg.is_homogeneous
 
-        is_array = (isinstance(arg, Variable) and arg.is_ndarray) or \
-                   (isinstance(arg, IndexedElement) and arg.base.is_ndarray)
-
         # TODO: treat inhomogenous lists and tuples when they have mixed ordering
-        if not (is_homogeneous_tuple or is_array or isinstance(arg, PythonList)):
-            errors.report("The argument passed to numpy.array must contain homogeneous data", symbol=arg, severity="fatal")
+        if not (is_homogeneous_tuple or isinstance(arg.class_type, HomogeneousContainerType)):
+            raise TypeError('we only accept homogeneous arguments')
 
         if not isinstance(order, (LiteralString, str)):
             raise TypeError("The order must be specified explicitly with a string.")
@@ -680,22 +696,18 @@ def __init__(self, arg, dtype=None, order='K', ndmin=None):
 
         init_dtype = dtype
 
-        if isinstance(arg.dtype, NativeInhomogeneousTuple):
+        if isinstance(arg.class_type, InhomogeneousTupleType):
             # If pseudo-inhomogeneous due to pointers, extract underlying dtype
             if dtype is None:
-                dtype = arg[0].dtype
-                prec = get_final_precision(arg[0])
-            else:
-                dtype, prec = process_dtype(dtype)
+                dtype = arg[0].class_type.datatype
+            dtype = process_dtype(dtype)
 
             shape = (LiteralInteger(len(arg)), *process_shape(False, arg[0].shape))
         else:
             # Verify dtype and get precision
             if dtype is None:
                 dtype = arg.dtype
-                prec = get_final_precision(arg)
-            else:
-                dtype, prec = process_dtype(dtype)
+            dtype = process_dtype(dtype)
 
             shape = process_shape(False, arg.shape)
 
@@ -721,10 +733,8 @@ def __init__(self, arg, dtype=None, order='K', ndmin=None):
         self._arg   = arg
         self._shape = shape
         self._rank  = rank
-        self._dtype = dtype
         self._order = order
-        self._precision = prec
-        super().__init__(init_dtype = init_dtype)
+        super().__init__(dtype = dtype, init_dtype = init_dtype)
 
     def __str__(self):
         return str(self.arg)
@@ -755,7 +765,7 @@ class NumpyArange(NumpyNewArray):
         The type of the output array, if dtype is not given,
         infer the data type from the other input arguments.
     """
-    __slots__ = ('_start','_step','_stop','_dtype','_precision','_shape')
+    __slots__ = ('_start','_step','_stop','_shape')
     _attribute_nodes = ('_start','_step','_stop')
     _rank = 1
     _order = None
@@ -774,14 +784,11 @@ def __init__(self, start, stop = None, step = None, dtype = None):
         init_dtype = dtype
         if dtype is None:
             type_info = NumpyResultType(*self.arg)
-            self._dtype = type_info.dtype
-            self._precision = type_info.precision
-        else:
-            self._dtype, self._precision = process_dtype(dtype)
+            dtype = type_info.dtype
 
         self._shape = (MathCeil(PyccelDiv(PyccelMinus(self._stop, self._start), self._step)))
         self._shape = process_shape(False, self._shape)
-        super().__init__(init_dtype = init_dtype)
+        super().__init__(dtype = process_dtype(dtype), init_dtype = init_dtype)
 
     @property
     def arg(self):
@@ -815,7 +822,7 @@ class NumpySum(PyccelInternalFunction):
     arg : list , tuple , PythonTuple, PythonList, Variable
         The argument passed to the sum function.
     """
-    __slots__ = ('_dtype','_precision','_class_type')
+    __slots__ = ('_class_type',)
     name = 'sum'
     _rank  = 0
     _shape = None
@@ -825,12 +832,12 @@ def __init__(self, arg):
         if not isinstance(arg, TypedAstNode):
             raise TypeError('Unknown type of  %s.' % type(arg))
         super().__init__(arg)
-        if isinstance(arg.dtype, NativeBool):
-            self._dtype = NativeInteger()
+        lowest_possible_type = process_dtype(PythonNativeInt())
+        if isinstance(arg.dtype.primitive_type, (PrimitiveBooleanType, PrimitiveIntegerType)) and \
+                arg.dtype.precision <= lowest_possible_type.precision:
+            self._class_type = lowest_possible_type
         else:
-            self._dtype = arg.dtype
-        self._class_type = self._dtype
-        self._precision = max(arg.precision, default_precision[self._dtype])
+            self._class_type = process_dtype(arg.dtype)
 
     @property
     def arg(self):
@@ -848,7 +855,7 @@ class NumpyProduct(PyccelInternalFunction):
     arg : list , tuple , PythonTuple, PythonList, Variable
         The argument passed to the prod function.
     """
-    __slots__ = ('_arg','_dtype','_precision','_class_type')
+    __slots__ = ('_arg','_class_type')
     name = 'product'
     _rank  = 0
     _shape = None
@@ -859,12 +866,15 @@ def __init__(self, arg):
             raise TypeError('Unknown type of  %s.' % type(arg))
         super().__init__(arg)
         self._arg = PythonList(arg) if arg.rank == 0 else self._args[0]
-        self._arg = NumpyInt(self._arg) if (isinstance(arg.dtype, NativeBool) or \
-                    (isinstance(arg.dtype, NativeInteger) and get_final_precision(self._arg) < default_precision[NativeInteger()]))\
-                    else self._arg
-        self._dtype = self._arg.dtype
-        self._class_type = self._dtype
-        self._precision = get_final_precision(self._arg)
+        lowest_possible_type = process_dtype(PythonNativeInt())
+        if isinstance(arg.dtype.primitive_type, (PrimitiveBooleanType, PrimitiveIntegerType)) and \
+                arg.dtype.precision <= lowest_possible_type.precision:
+            self._class_type = lowest_possible_type
+        else:
+            self._class_type = process_dtype(arg.dtype)
+
+        default_cast = DtypePrecisionToCastFunction[self._class_type]
+        self._arg = default_cast(self._arg) if arg.dtype != self._class_type else self._arg
 
     @property
     def arg(self):
@@ -885,7 +895,7 @@ class NumpyMatmul(PyccelInternalFunction):
     b : TypedAstNode
         The second argument of the matrix multiplication.
     """
-    __slots__ = ('_dtype','_precision','_shape','_rank','_order','_class_type')
+    __slots__ = ('_shape','_rank','_order','_class_type')
     name = 'matmul'
 
     def __init__(self, a ,b):
@@ -900,8 +910,7 @@ def __init__(self, a ,b):
 
         args      = (a, b)
         type_info = NumpyResultType(*args)
-        self._dtype = type_info.dtype
-        self._precision = type_info.precision
+        dtype = process_dtype(type_info.dtype)
 
         if not (a.shape is None or b.shape is None):
 
@@ -924,7 +933,7 @@ def __init__(self, a ,b):
         else:
             self._order = None if self._rank < 2 else 'C'
 
-        self._class_type = NumpyNDArrayType()
+        self._class_type = NumpyNDArrayType(dtype) if self.rank else dtype
 
     @property
     def a(self):
@@ -988,12 +997,12 @@ class NumpyLinspace(NumpyNewArray):
       endpoint : bool, optional
            If True, stop is the last sample. Otherwise, it is not included. Default is True.
 
-      dtype : str, DataType
+      dtype : str, PyccelType
            The type of the output array. If dtype is not given, the data type is calculated
            from start and stop, the calculated dtype will never be an integer.
     """
 
-    __slots__ = ('_dtype','_precision','_index','_start','_stop',
+    __slots__ = ('_index','_start','_stop',
             '_num','_endpoint','_shape', '_rank','_ind','_step',
             '_py_argument','_order')
     _attribute_nodes = ('_start', '_stop', '_index', '_step', '_num',
@@ -1005,7 +1014,7 @@ def __init__(self, start, stop, num=None, endpoint=True, dtype=None):
         if not num:
             num = LiteralInteger(50)
 
-        if num.rank != 0 or not isinstance(num.dtype, NativeInteger):
+        if num.rank != 0 or not isinstance(getattr(num.dtype, 'primitive_type', None), PrimitiveIntegerType):
             raise TypeError('Expecting positive integer num argument.')
 
         if any(not isinstance(arg, TypedAstNode) for arg in (start, stop, num)):
@@ -1013,18 +1022,16 @@ def __init__(self, start, stop, num=None, endpoint=True, dtype=None):
 
         init_dtype = dtype
         if dtype:
-            self._dtype, self._precision = process_dtype(dtype)
+            final_dtype = process_dtype(dtype)
         else:
             args      = (start, stop)
             type_info = NumpyResultType(*args)
-            if type_info.dtype is NativeInteger():
-                self._dtype     = NativeFloat()
-                self._precision = default_precision[self._dtype]
+            if type_info.dtype.primitive_type is PrimitiveIntegerType():
+                final_dtype = NumpyFloat64Type()
             else:
-                self._dtype = type_info.dtype
-                self._precision = type_info.precision
+                final_dtype = process_dtype(type_info.dtype)
 
-        self._index = Variable('int', 'linspace_index')
+        self._index = Variable(PythonNativeInt(), 'linspace_index')
         self._start = start
         self._stop  = stop
         self._num  = num
@@ -1033,7 +1040,7 @@ def __init__(self, start, stop, num=None, endpoint=True, dtype=None):
         elif endpoint is False:
             self._endpoint = LiteralFalse()
         else:
-            if not isinstance(endpoint.dtype, NativeBool):
+            if not isinstance(endpoint.dtype, PythonNativeBool):
                 errors.report(WRONG_LINSPACE_ENDPOINT, symbol=endpoint, severity="fatal")
             self._endpoint = endpoint
 
@@ -1053,7 +1060,7 @@ def __init__(self, start, stop, num=None, endpoint=True, dtype=None):
         else:
             self._step = PyccelDiv(PyccelMinus(self.stop, self.start), PyccelMinus(self.num, PythonInt(self.endpoint)))
 
-        super().__init__(init_dtype = init_dtype)
+        super().__init__(dtype = final_dtype, init_dtype = init_dtype)
 
     @property
     def endpoint(self):
@@ -1119,8 +1126,8 @@ class NumpyWhere(PyccelInternalFunction):
         The value if False. If `y` is provided, `x` should also be provided.
     """
 
-    __slots__ = ('_condition', '_value_true', '_value_false', '_dtype',
-                 '_rank', '_shape', '_order', '_precision','_class_type')
+    __slots__ = ('_condition', '_value_true', '_value_false',
+                 '_rank', '_shape', '_order', '_class_type')
     _attribute_nodes = ('_condition','_value_true','_value_false')
     name = 'where'
 
@@ -1139,8 +1146,7 @@ def __init__(self, condition, x, y):
 
         args      = (x, y)
         type_info = NumpyResultType(*args)
-        self._dtype = type_info.dtype
-        self._precision = type_info.precision
+        self._class_type = NumpyNDArrayType(process_dtype(type_info.dtype))
 
         shape = broadcast(x.shape, y.shape)
         shape = broadcast(condition.shape, shape)
@@ -1148,8 +1154,6 @@ def __init__(self, condition, x, y):
         self._shape = process_shape(False, shape)
         self._rank  = len(shape)
         self._order = None if self._rank < 2 else 'C'
-
-        self._class_type = NumpyNDArrayType()
         super().__init__(condition, x, y)
 
     @property
@@ -1188,15 +1192,16 @@ class NumpyRand(PyccelInternalFunction):
     """
     __slots__ = ('_shape','_rank','_order','_class_type')
     name = 'rand'
-    _dtype = NativeFloat()
-    _precision = default_precision[NativeFloat()]
 
     def __init__(self, *args):
         super().__init__(*args)
         self._rank  = len(args)
         self._shape = None if self._rank == 0 else args
         self._order = None if self._rank < 2 else 'C'
-        self._class_type = NumpyNDArrayType()
+        if self.rank == 0:
+            self._class_type = PythonNativeFloat()
+        else:
+            self._class_type = NumpyNDArrayType(NumpyFloat64Type())
 
 #==============================================================================
 class NumpyRandint(PyccelInternalFunction):
@@ -1218,8 +1223,6 @@ class NumpyRandint(PyccelInternalFunction):
     """
     __slots__ = ('_rand','_low','_high','_shape','_rank','_order','_class_type')
     name = 'randint'
-    _dtype     = NativeInteger()
-    _precision = -1
     _attribute_nodes = ('_low', '_high')
 
     def __init__(self, low, high = None, size = None):
@@ -1231,10 +1234,14 @@ def __init__(self, low, high = None, size = None):
             low  = None
 
         self._shape   = size
-        self._rank    = 0 if size is None else len(self.shape)
+        if size is None:
+            self._rank = 0
+            self._class_type = PythonNativeInt()
+        else:
+            self._rank = len(self.shape)
+            self._class_type = NumpyNDArrayType(NumpyInt64Type())
         self._order   = None if self._rank < 2 else 'C'
         self._rand    = NumpyRand() if size is None else NumpyRand(*size)
-        self._class_type = NumpyNDArrayType()
         self._low     = low
         self._high    = high
         super().__init__()
@@ -1266,7 +1273,7 @@ class NumpyFull(NumpyNewArray):
     shape : TypedAstNode
         Shape of the new array, e.g., ``(2, 3)`` or ``2``.
         For a 1D array this is either a `LiteralInteger` or an expression.
-        For a ND array this is a `TypedAstNode` with the class type NativeHomogeneousTuple.
+        For a ND array this is a `TypedAstNode` with the class type HomogeneousTupleType.
 
     fill_value : TypedAstNode
         Fill value.
@@ -1279,7 +1286,7 @@ class NumpyFull(NumpyNewArray):
         Whether to store multidimensional data in C- or Fortran-contiguous
         (row- or column-wise) order in memory.
     """
-    __slots__ = ('_fill_value','_dtype','_precision','_shape','_rank','_order')
+    __slots__ = ('_fill_value','_shape','_rank','_order')
     name = 'full'
 
     def __init__(self, shape, fill_value, dtype=None, order='C'):
@@ -1292,22 +1299,18 @@ def __init__(self, shape, fill_value, dtype=None, order='C'):
         # TODO: must get dtype from an annotated node
         if dtype is None:
             dtype = fill_value.dtype
-            precision = get_final_precision(fill_value)
-        else:
-            dtype, precision = process_dtype(dtype)
+        dtype = process_dtype(dtype)
 
         # Cast fill_value to correct type
         if fill_value:
-            if fill_value.dtype != dtype or get_final_precision(fill_value) != precision:
-                cast_func = DtypePrecisionToCastFunction[dtype.name][precision]
+            if fill_value.dtype != dtype:
+                cast_func = DtypePrecisionToCastFunction[dtype]
                 fill_value = cast_func(fill_value)
         self._shape = shape
         self._rank  = len(self._shape)
-        self._dtype = dtype
         self._order = NumpyNewArray._process_order(self._rank, order)
-        self._precision = precision
 
-        super().__init__(fill_value, init_dtype = init_dtype)
+        super().__init__(fill_value, dtype = dtype, init_dtype = init_dtype)
 
     #--------------------------------------------------------------------------
     @property
@@ -1347,55 +1350,71 @@ class NumpyEmpty(NumpyAutoFill):
     name = 'empty'
 
     def __init__(self, shape, dtype='float', order='C'):
-        if dtype in NativeNumeric:
-            precision = default_precision[dtype]
-            dtype = DtypePrecisionToCastFunction[dtype.name][precision]
         super().__init__(shape, dtype, order)
+
     @property
     def fill_value(self):
+        """
+        The value with which the array will be filled on initialisation.
+
+        The value with which the array will be filled on initialisation.
+        """
         return None
 
 #==============================================================================
 class NumpyZeros(NumpyAutoFill):
-    """ Represents a call to numpy.zeros for code generation.
+    """
+    Represents a call to numpy.zeros for code generation.
+
+    Represents a call to numpy.zeros for code generation.
+
+    Parameters
+    ----------
+    shape : TypedAstNode
+        The shape passed as argument to the function call.
+    dtype : PyccelAstNode | PyccelType | str, default = 'float'
+        The datatype specified in the argument of the function call.
+    order : str, default='C'
+        The order specified in the argument of the function call.
     """
     __slots__ = ()
     name = 'zeros'
+
     @property
     def fill_value(self):
-        dtype = self.dtype
-        if isinstance(dtype, NativeInteger):
-            value = LiteralInteger(0, precision = self.precision)
-        elif isinstance(dtype, NativeFloat):
-            value = LiteralFloat(0, precision = self.precision)
-        elif isinstance(dtype, NativeComplex):
-            value = LiteralComplex(0., 0., precision = self.precision)
-        elif isinstance(dtype, NativeBool):
-            value = LiteralFalse(precision = self.precision)
-        else:
-            raise TypeError('Unknown type')
-        return value
+        """
+        The value with which the array will be filled on initialisation.
+
+        The value with which the array will be filled on initialisation.
+        """
+        return convert_to_literal(0, self.dtype)
 
 #==============================================================================
 class NumpyOnes(NumpyAutoFill):
-    """ Represents a call to numpy.ones for code generation.
+    """
+    Represents a call to numpy.ones for code generation.
+
+    Represents a call to numpy.ones for code generation.
+
+    Parameters
+    ----------
+    shape : TypedAstNode
+        The shape passed as argument to the function call.
+    dtype : PyccelAstNode | PyccelType | str, default = 'float'
+        The datatype specified in the argument of the function call.
+    order : str, default='C'
+        The order specified in the argument of the function call.
     """
     __slots__ = ()
     name = 'ones'
     @property
     def fill_value(self):
-        dtype = self.dtype
-        if isinstance(dtype, NativeInteger):
-            value = LiteralInteger(1, precision = self.precision)
-        elif isinstance(dtype, NativeFloat):
-            value = LiteralFloat(1., precision = self.precision)
-        elif isinstance(dtype, NativeComplex):
-            value = LiteralComplex(1., 0., precision = self.precision)
-        elif isinstance(dtype, NativeBool):
-            value = LiteralTrue(precision = self.precision)
-        else:
-            raise TypeError('Unknown type')
-        return value
+        """
+        The value with which the array will be filled on initialisation.
+
+        The value with which the array will be filled on initialisation.
+        """
+        return convert_to_literal(1, self.dtype)
 
 #==============================================================================
 class NumpyFullLike(PyccelInternalFunction):
@@ -1426,7 +1445,7 @@ class constructor always returns an object of type `NumpyFull`.
     shape : PythonTuple of TypedAstNode
         Overrides the shape of the array.
         For a 1D array this is either a `LiteralInteger` or an expression.
-        For a ND array this is a `TypedAstNode` with the class type NativeHomogeneousTuple.
+        For a ND array this is a `TypedAstNode` with the class type HomogeneousTupleType.
 
     See Also
     --------
@@ -1470,7 +1489,7 @@ class constructor always returns an object of type `NumpyEmpty`.
     shape : PythonTuple of TypedAstNode
         Overrides the shape of the array.
         For a 1D array this is either a `LiteralInteger` or an expression.
-        For a ND array this is a `TypedAstNode` with the class type NativeHomogeneousTuple.
+        For a ND array this is a `TypedAstNode` with the class type HomogeneousTupleType.
 
     See Also
     --------
@@ -1516,7 +1535,7 @@ class constructor always returns an object of type `NumpyOnes`.
     shape : PythonTuple of TypedAstNode
         Overrides the shape of the array.
         For a 1D array this is either a `LiteralInteger` or an expression.
-        For a ND array this is a `TypedAstNode` with the class type NativeHomogeneousTuple.
+        For a ND array this is a `TypedAstNode` with the class type HomogeneousTupleType.
 
     See Also
     --------
@@ -1561,7 +1580,7 @@ class constructor always returns an object of type `NumpyZeros`.
     shape : PythonTuple of TypedAstNode
         Overrides the shape of the array.
         For a 1D array this is either a `LiteralInteger` or an expression.
-        For a ND array this is a `TypedAstNode` with the class type NativeHomogeneousTuple.
+        For a ND array this is a `TypedAstNode` with the class type HomogeneousTupleType.
 
     See Also
     --------
@@ -1596,16 +1615,18 @@ class NumpyNorm(PyccelInternalFunction):
         The second argument passed to the function, indicating the axis along
         which the norm should be calculated.
     """
-    __slots__ = ('_shape','_rank','_order','_arg','_precision','_class_type')
+    __slots__ = ('_shape','_rank','_order','_arg','_class_type')
     name = 'norm'
-    _dtype = NativeFloat()
 
     def __init__(self, arg, axis=None):
         super().__init__(arg, axis)
-        if not isinstance(arg.dtype, (NativeComplex, NativeFloat)):
-            arg = NumpyFloat(arg)
+        arg_dtype = arg.dtype
+        if not isinstance(arg_dtype.primitive_type, (PrimitiveFloatingPointType, PrimitiveComplexType)):
+            arg = NumpyFloat64(arg)
+            dtype = NumpyFloat64Type()
+        else:
+            dtype = numpy_precision_map[(PrimitiveFloatingPointType(), arg_dtype.precision)]
         self._arg = PythonList(arg) if arg.rank == 0 else arg
-        self._precision = get_final_precision(arg)
         if self.axis is not None:
             sh = list(arg.shape)
             del sh[self.axis]
@@ -1616,7 +1637,7 @@ def __init__(self, arg, axis=None):
             self._shape = None
             self._order = None
             self._rank  = 0
-        self._class_type = NumpyNDArrayType()
+        self._class_type = NumpyNDArrayType(dtype) if self.rank else dtype
 
     @property
     def arg(self):
@@ -1654,7 +1675,8 @@ class NumpyUfuncBase(PyccelInternalFunction):
     *args : tuple of TypedAstNode
         The arguments passed to the function.
     """
-    __slots__ = ('_dtype','_precision','_shape','_rank','_order','_class_type')
+    __slots__ = ('_shape','_rank','_order','_class_type')
+
     @property
     def is_elemental(self):
         return True
@@ -1678,33 +1700,39 @@ class NumpyUfuncUnary(NumpyUfuncBase):
         The argument passed to the function.
     """
     __slots__ = ()
+
     def __init__(self, x):
-        self._set_dtype_precision(x)
+        dtype = self._get_dtype(x)
         self._set_shape_rank(x)
         self._set_order(x)
-        if self.rank:
-            self._class_type = NumpyNDArrayType()
-        else:
-            self._class_type = self.dtype
+        self._class_type = NumpyNDArrayType(dtype) if self.rank else dtype
         super().__init__(x)
 
     def _set_shape_rank(self, x):
         self._shape      = x.shape
         self._rank       = x.rank
 
-    def _set_dtype_precision(self, x):
+    def _get_dtype(self, x):
         """
-        Use the argument to calculate the dtype and precision of the result.
+        Use the argument to calculate the dtype of the result.
 
-        Use the argument to calculate the dtype and precision of the result.
+        Use the argument to calculate the dtype of the result.
 
         Parameters
         ----------
         x : TypedAstNode
             The argument passed to the function.
+
+        Returns
+        -------
+        PyccelType
+            The dtype of the result of the function.
         """
-        self._dtype      = x.dtype if x.dtype is NativeComplex() else NativeFloat()
-        self._precision  = default_precision[self._dtype]
+        x_dtype = x.dtype
+        if not isinstance(x_dtype.primitive_type, (PrimitiveFloatingPointType, PrimitiveComplexType)):
+            return NumpyFloat64Type()
+        else:
+            return numpy_precision_map[(x_dtype.primitive_type, x_dtype.precision)]
 
     def _set_order(self, x):
         self._order      = x.order
@@ -1741,20 +1769,20 @@ class NumpyUfuncBinary(NumpyUfuncBase):
     __slots__ = ()
     def __init__(self, x1, x2):
         super().__init__(x1, x2)
-        self._set_dtype_precision(x1, x2)
+        dtype = self._get_dtype(x1, x2)
         self._set_shape_rank(x1, x2)
         self._set_order(x1, x2)
-        self._class_type = NumpyNDArrayType()
+        self._class_type = NumpyNDArrayType(dtype) if self.rank else dtype
 
     def _set_shape_rank(self, x1, x2):
         self._shape = broadcast(x1.shape, x2.shape)
         self._rank  = 0 if self._shape is None else len(self._shape)
 
-    def _set_dtype_precision(self, x1, x2):
+    def _get_dtype(self, x1, x2):
         """
-        Use the argument to calculate the dtype and precision of the result.
+        Use the argument to calculate the dtype of the result.
 
-        Use the argument to calculate the dtype and precision of the result.
+        Use the argument to calculate the dtype of the result.
 
         Parameters
         ----------
@@ -1762,9 +1790,18 @@ def _set_dtype_precision(self, x1, x2):
             The first argument passed to the function.
         x2 : TypedAstNode
             The second argument passed to the function.
+
+        Returns
+        -------
+        PyccelType
+            The dtype of the result of the function.
         """
-        self._dtype     = NativeFloat()
-        self._precision = default_precision[self._dtype]
+        if x1.dtype.primitive_type in (PrimitiveBooleanType(), PrimitiveIntegerType()) or \
+           x2.dtype.primitive_type in (PrimitiveBooleanType(), PrimitiveIntegerType()) :
+            return NumpyFloat64Type()
+        else:
+            arg_dtype = x1.dtype + x2.dtype
+            return numpy_precision_map[(PrimitiveFloatingPointType(), arg_dtype.precision)]
 
     def _set_order(self, x1, x2):
         if x1.order == x2.order:
@@ -1863,28 +1900,41 @@ class NumpyArctanh(NumpyUfuncUnary):
 
 #==============================================================================
 class NumpySign(NumpyUfuncUnary):
-    """Represent a call to the sign function in the Numpy library"""
+    """
+    Represent a call to the sign function in the Numpy library.
+
+    Represent a call to the sign function in the Numpy library.
+
+    Parameters
+    ----------
+    x : TypedAstNode
+        The argument passed to the function.
+    """
     __slots__ = ()
     name = 'sign'
-    def _set_dtype_precision(self, x):
-        if not isinstance(x.dtype, (NativeInteger, NativeFloat, NativeComplex)):
-            raise TypeError(f'{x.dtype} not supported')
-        self._dtype     = x.dtype
-        self._precision = get_final_precision(x)
+    def _get_dtype(self, x):
+        """
+        Use the argument to calculate the dtype of the result.
 
-class NumpyAbs(NumpyUfuncUnary):
-    """Represent a call to the abs function in the Numpy library"""
-    __slots__ = ()
-    name = 'abs'
-    def _set_dtype_precision(self, x):
-        self._dtype     = NativeInteger() if x.dtype is NativeInteger() else NativeFloat()
-        self._precision = get_final_precision(x)
+        Use the argument to calculate the dtype of the result.
 
-class NumpyFloor(NumpyUfuncUnary):
+        Parameters
+        ----------
+        x : TypedAstNode
+            The argument passed to the function.
+
+        Returns
+        -------
+        PyccelType
+            The dtype of the result of the function.
+        """
+        return process_dtype(x.dtype)
+
+class NumpyAbs(NumpyUfuncUnary):
     """
-    Represent a call to the floor function in the Numpy library.
+    Represent a call to the abs function in the Numpy library.
 
-    Represent a call to the floor function in the Numpy library.
+    Represent a call to the abs function in the Numpy library.
 
     Parameters
     ----------
@@ -1892,21 +1942,43 @@ class NumpyFloor(NumpyUfuncUnary):
         The argument passed to the function.
     """
     __slots__ = ()
-    name = 'floor'
-
-    def _set_dtype_precision(self, x):
+    name = 'abs'
+    def _get_dtype(self, x):
         """
-        Use the argument to calculate the dtype and precision of the result.
+        Use the argument to calculate the dtype of the result.
 
-        Use the argument to calculate the dtype and precision of the result.
+        Use the argument to calculate the dtype of the result.
 
         Parameters
         ----------
         x : TypedAstNode
             The argument passed to the function.
+
+        Returns
+        -------
+        PyccelType
+            The dtype of the result of the function.
         """
-        self._dtype     = NativeFloat()
-        self._precision = default_precision[self._dtype]
+        x_dtype = x.dtype
+        if isinstance(x_dtype.primitive_type, PrimitiveComplexType):
+            dtype = x_dtype.element_type
+        else:
+            dtype = x_dtype
+        return process_dtype(dtype)
+
+class NumpyFloor(NumpyUfuncUnary):
+    """
+    Represent a call to the floor function in the Numpy library.
+
+    Represent a call to the floor function in the Numpy library.
+
+    Parameters
+    ----------
+    x : TypedAstNode
+        The argument passed to the function.
+    """
+    __slots__ = ()
+    name = 'floor'
 
 class NumpyMod(NumpyUfuncBinary):
     """
@@ -1926,8 +1998,8 @@ class NumpyMod(NumpyUfuncBinary):
 
     def __init__(self, x1, x2):
         super().__init__(x1, x2)
-        x1 = NumpyInt(x1) if isinstance(x1.dtype, NativeBool) else x1
-        x2 = NumpyInt(x2) if isinstance(x2.dtype, NativeBool) else x2
+        x1 = NumpyInt(x1) if isinstance(x1.dtype, PythonNativeBool) else x1
+        x2 = NumpyInt(x2) if isinstance(x2.dtype, PythonNativeBool) else x2
         self._args = (x1, x2)
 
     def _set_shape_rank(self, x1, x2):
@@ -1950,7 +2022,7 @@ def _set_shape_rank(self, x1, x2):
             self._shape = shape
             self._rank  = len(shape)
 
-    def _set_dtype_precision(self, x1, x2):
+    def _get_dtype(self, x1, x2):
         """
         Set the datatype of the object.
 
@@ -1963,17 +2035,21 @@ def _set_dtype_precision(self, x1, x2):
             The first argument which helps determine the datatype.
         x2 : TypedAstNode
             The second argument which helps determine the datatype.
+
+        Returns
+        -------
+        PyccelType
+            The dtype of the result of the function.
         """
-        args      = (x1, x2)
-        if x1.dtype is NativeComplex() or x2.dtype is NativeComplex():
-            raise TypeError("Complex modulo not supported")
-        type_info = NumpyResultType(*args)
-        if type_info.dtype is NativeBool():
-            self._dtype = NativeInteger()
-            self._precision = 1
+        if isinstance(x1.dtype, PythonNativeBool) and isinstance(x2.dtype, PythonNativeBool):
+            return NumpyInt8Type()
         else:
-            self._dtype = type_info.dtype
-            self._precision = type_info.precision
+            arg_class_type = x1.class_type + x2.class_type
+            if isinstance(arg_class_type, NumpyNDArrayType):
+                arg_dtype = arg_class_type.element_type
+            else:
+                arg_dtype = arg_class_type
+            return process_dtype(arg_dtype)
 
 class NumpyAmin(PyccelInternalFunction):
     """
@@ -1986,16 +2062,14 @@ class NumpyAmin(PyccelInternalFunction):
     arg : array_like
         The input array for which the minimum argument is calculated.
     """
-    __slots__ = ('_dtype', '_precision', '_class_type')
+    __slots__ = ('_class_type',)
     name = 'amin'
     _rank = 0
     _shape = None
     _order = None
     def __init__(self, arg):
         super().__init__(arg)
-        self._dtype = arg.dtype
-        self._class_type = self._dtype
-        self._precision = max(arg.precision, default_precision[self._dtype])
+        self._class_type = arg.dtype
 
     @property
     def arg(self):
@@ -2017,16 +2091,14 @@ class NumpyAmax(PyccelInternalFunction):
     arg : array_like
         The input array for which the maximum argument is calculated.
     """
-    __slots__ = ('_dtype', '_precision', '_class_type')
+    __slots__ = ('_class_type',)
     name = 'amax'
     _rank = 0
     _shape = None
     _order = None
     def __init__(self, arg):
         super().__init__(arg)
-        self._dtype = arg.dtype
-        self._class_type = self._dtype
-        self._precision = max(arg.precision, default_precision[self._dtype])
+        self._class_type = arg.dtype
 
     @property
     def arg(self):
@@ -2043,7 +2115,18 @@ def is_elemental(self):
         return False
 
 class NumpyTranspose(NumpyUfuncUnary):
-    """Represents a call to the transpose function in the Numpy library"""
+    """
+    Represents a call to the transpose function from the Numpy library.
+
+    Represents a call to the transpose function from the Numpy library.
+
+    Parameters
+    ----------
+    x : TypedAstNode
+        The array to be transposed.
+    *axes : tuple[TypedAstNode]
+        The axes along which the user wishes to transpose their array.
+    """
     __slots__ = ()
     name = 'transpose'
 
@@ -2072,13 +2155,37 @@ def __getitem__(self, *args):
             args = args + tuple([Slice(None, None)]*(rank-len(args)))
         return NumpyTranspose(x.__getitem__(*reversed(args)))
 
-    def _set_dtype_precision(self, x):
-        self._dtype      = x.dtype
-        self._precision  = get_final_precision(x)
+    def _get_dtype(self, x):
+        """
+        Use the argument to calculate the dtype of the result.
+
+        Use the argument to calculate the dtype of the result.
+
+        Parameters
+        ----------
+        x : TypedAstNode
+            The argument passed to the function.
+
+        Returns
+        -------
+        PyccelType
+            The dtype of the result of the function.
+        """
+        return process_dtype(x.dtype)
 
     def _set_shape_rank(self, x):
-        self._shape      = tuple(reversed(x.shape))
-        self._rank       = x.rank
+        """
+        Set the shape and rank of the resulting object.
+
+        Set the shape and rank of the resulting object.
+
+        Parameters
+        ----------
+        x : TypedAstNode
+            The argument passed to the function.
+        """
+        self._shape = tuple(reversed(x.shape))
+        self._rank  = x.rank
 
     def _set_order(self, x):
         self._order = 'C' if x.order=='F' else 'F'
@@ -2101,16 +2208,15 @@ class NumpyConjugate(PythonConjugate):
     arg : TypedAstNode
         The argument passed to the function.
     """
-    __slots__ = ('_precision','_rank','_shape','_order','_class_type')
+    __slots__ = ('_rank','_shape','_order','_class_type')
     name = 'conj'
 
     def __init__(self, arg):
         super().__init__(arg)
-        self._precision = arg.precision
         self._order = arg.order
         self._rank  = self.internal_var.rank
         self._shape = process_shape(self._rank == 0, self.internal_var.shape)
-        self._class_type = NumpyNDArrayType()
+        self._class_type = NumpyNDArrayType(arg.dtype) if self.rank else arg.dtype
 
     @property
     def is_elemental(self):
@@ -2136,8 +2242,6 @@ class NumpyNonZeroElement(NumpyNewArray):
     __slots__ = ('_arr','_dim','_shape')
     _attribute_nodes = ('_arr',)
     name = 'nonzero'
-    _dtype = NativeInteger()
-    _precision = 8
     _rank = 1
     _order = None
 
@@ -2146,7 +2250,7 @@ def __init__(self, a, dim):
         self._dim = dim
 
         self._shape = (NumpyCountNonZero(a),)
-        super().__init__(a)
+        super().__init__(a, dtype = NumpyInt64Type())
 
     @property
     def array(self):
@@ -2181,11 +2285,9 @@ class NumpyNonZero(PyccelInternalFunction):
     __slots__ = ('_elements','_arr','_shape')
     _attribute_nodes = ('_elements',)
     name = 'nonzero'
-    _dtype = NativeInteger()
-    _precision = 8
     _rank  = 2
     _order = 'C'
-    _class_type = NativeHomogeneousTuple()
+    _class_type = HomogeneousTupleType(NumpyNDArrayType(NumpyInt64Type()))
 
     def __init__(self, a):
         if (a.rank > 1):
@@ -2223,15 +2325,15 @@ class NumpyCountNonZero(PyccelInternalFunction):
         An array for which the non-zero elements should be counted.
     axis : int, optional
         The dimension along which the non-zero elements are counted.
-    keepdims : NativeBool
+    keepdims : LiteralTrue | LiteralFalse
         Indicates if output arrays should have the same number of dimensions
         as arg.
     """
-    __slots__ = ('_precision', '_rank', '_shape', '_order', '_class_type', '_arr',
+    __slots__ = ('_rank', '_shape', '_order', '_class_type', '_arr',
                 '_axis', '_keep_dims')
     _attribute_nodes = ('_arr','_axis')
     name   = 'count_nonzero'
-    _dtype = NativeInteger()
+
     def __init__(self, a, axis = None, *, keepdims = LiteralFalse()):
         if not isinstance(keepdims, (LiteralTrue, LiteralFalse)):
             errors.report(NON_LITERAL_KEEP_DIMS, symbol=keepdims, severity="fatal")
@@ -2239,7 +2341,7 @@ def __init__(self, a, axis = None, *, keepdims = LiteralFalse()):
             errors.report(NON_LITERAL_AXIS, symbol=axis, severity="fatal")
 
         if keepdims.python_value:
-            self._precision = 8
+            dtype = NumpyInt64Type()
             self._rank  = a.rank
             self._order = a.order
             if axis is not None:
@@ -2247,19 +2349,20 @@ def __init__(self, a, axis = None, *, keepdims = LiteralFalse()):
                 self._shape[axis.python_value] = LiteralInteger(1)
             else:
                 self._shape = (LiteralInteger(1),)*a.rank
+            self._class_type = NumpyNDArrayType(dtype) if self._rank else dtype
         else:
-            self._precision  = -1
             if axis is not None:
+                dtype = NumpyInt64Type()
                 self._shape = list(a.shape)
                 self._shape.pop(axis.python_value)
                 self._rank  = a.rank-1
                 self._order = a.order if a.rank>2 else None
+                self._class_type = NumpyNDArrayType(dtype) if a.rank else dtype
             else:
                 self._rank  = 0
                 self._shape = None
                 self._order = None
-
-        self._class_type = NumpyNDArrayType()
+                self._class_type = PythonNativeInt()
 
         self._arr = a
         self._axis = axis
@@ -2344,10 +2447,8 @@ class NumpyIsNan(NumpyUfuncUnary):
     """
     __slots__ = ()
     name = 'isnan'
-    _dtype = NativeBool()
-    _precision = -1
 
-    def _set_dtype_precision(self, x):
+    def _get_dtype(self, x):
         """
         Use the argument to calculate the dtype and precision of the result.
 
@@ -2358,7 +2459,13 @@ def _set_dtype_precision(self, x):
         ----------
         x : TypedAstNode
             The argument passed to the function.
+
+        Returns
+        -------
+        PyccelType
+            The dtype of the result of the function.
         """
+        return PythonNativeBool()
 
 class NumpyIsInf(NumpyUfuncUnary):
     """ 
@@ -2380,10 +2487,8 @@ class NumpyIsInf(NumpyUfuncUnary):
     """
     __slots__ = ()
     name = 'isinf'
-    _dtype = NativeBool()
-    _precision = -1
 
-    def _set_dtype_precision(self, x):
+    def _get_dtype(self, x):
         """
         Use the argument to calculate the dtype and precision of the result.
 
@@ -2394,7 +2499,13 @@ def _set_dtype_precision(self, x):
         ----------
         x : TypedAstNode
             The argument passed to the function.
+
+        Returns
+        -------
+        PyccelType
+            The dtype of the result of the function.
         """
+        return PythonNativeBool()
 
 class NumpyIsFinite(NumpyUfuncUnary):
     """ 
@@ -2416,10 +2527,8 @@ class NumpyIsFinite(NumpyUfuncUnary):
     """
     __slots__ = ()
     name = 'isfinite'
-    _dtype = NativeBool()
-    _precision = -1
 
-    def _set_dtype_precision(self, x):
+    def _get_dtype(self, x):
         """
         Use the argument to calculate the dtype and precision of the result.
 
@@ -2430,7 +2539,26 @@ def _set_dtype_precision(self, x):
         ----------
         x : TypedAstNode
             The argument passed to the function.
+
+        Returns
+        -------
+        PyccelType
+            The dtype of the result of the function.
         """
+        return PythonNativeBool()
+
+#==============================================================================
+
+DtypePrecisionToCastFunction.update({
+    NumpyInt8Type()       : NumpyInt8,
+    NumpyInt16Type()      : NumpyInt16,
+    NumpyInt32Type()      : NumpyInt32,
+    NumpyInt64Type()      : NumpyInt64,
+    NumpyFloat32Type()    : NumpyFloat32,
+    NumpyFloat64Type()    : NumpyFloat64,
+    NumpyComplex64Type()  : NumpyComplex64,
+    NumpyComplex128Type() : NumpyComplex128,
+    })
 
 #==============================================================================
 # TODO split numpy_functions into multiple dictionaries following
@@ -2445,7 +2573,7 @@ def _set_dtype_precision(self, x):
      PyccelFunctionDef('randint', NumpyRandint)])
 
 numpy_constants = {
-        'pi': Constant('float', 'pi', value=numpy.pi),
+        'pi': Constant(PythonNativeFloat(), 'pi', value=numpy.pi),
     }
 
 numpy_funcs = {
diff --git a/pyccel/ast/numpytypes.py b/pyccel/ast/numpytypes.py
new file mode 100644
index 0000000000..9c9e3e8295
--- /dev/null
+++ b/pyccel/ast/numpytypes.py
@@ -0,0 +1,370 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#------------------------------------------------------------------------------------------#
+# This file is part of Pyccel which is released under MIT License. See the LICENSE file or #
+# go to https://github.com/pyccel/pyccel/blob/master/LICENSE for full license details.     #
+#------------------------------------------------------------------------------------------#
+""" Module containing types from the numpy module understood by pyccel
+"""
+from functools import lru_cache
+
+import numpy as np
+
+from pyccel.utilities.metaclasses import ArgumentSingleton
+from pyccel.utilities.stage   import PyccelStage
+
+from .datatypes import FixedSizeNumericType, HomogeneousContainerType, PythonNativeBool
+from .datatypes import PrimitiveBooleanType, PrimitiveIntegerType, PrimitiveFloatingPointType, PrimitiveComplexType
+from .datatypes import GenericType
+from .datatypes import pyccel_type_to_original_type, original_type_to_pyccel_type
+
+__all__ = (
+        'NumpyIntType',
+        'NumpyInt8Type',
+        'NumpyInt16Type',
+        'NumpyInt32Type',
+        'NumpyInt64Type',
+        'NumpyFloat32Type',
+        'NumpyFloat64Type',
+        'NumpyFloat128Type',
+        'NumpyComplex64Type',
+        'NumpyComplex128Type',
+        'NumpyComplex256Type',
+        'NumpyNDArrayType',
+        'NumpyNumericType',
+        )
+
+pyccel_stage = PyccelStage()
+
+primitive_type_precedence = [PrimitiveBooleanType(), PrimitiveIntegerType(), PrimitiveFloatingPointType(), PrimitiveComplexType()]
+
+#==============================================================================
+
+class NumpyNumericType(FixedSizeNumericType):
+    """
+    Base class representing a scalar numeric datatype defined in the numpy module.
+
+    Base class representing a scalar numeric datatype defined in the numpy module.
+    """
+    __slots__ = ()
+
+    @lru_cache
+    def __add__(self, other):
+        try:
+            return original_type_to_pyccel_type[
+                    np.result_type(pyccel_type_to_original_type[self](),
+                            pyccel_type_to_original_type[other]()).type]
+        except KeyError:
+            return NotImplemented
+
+    @lru_cache
+    def __radd__(self, other):
+        return self.__add__(other)
+
+    def __eq__(self, other):
+        if other is self:
+            return True
+        elif isinstance(other, NumpyNumericType):
+            return False
+        elif isinstance(other, FixedSizeNumericType):
+            return other.primitive_type == self.primitive_type and \
+                    other.precision == self.precision
+        else:
+            return NotImplemented
+
+    def __hash__(self):
+        return hash(f"numpy.{self}")
+
+#==============================================================================
+
+class NumpyIntType(NumpyNumericType):
+    """
+    Super class representing NumPy's integer types.
+
+    Super class representing NumPy's integer types.
+    """
+    __slots__ = ()
+    _primitive_type = PrimitiveIntegerType()
+
+    @lru_cache
+    def __and__(self, other):
+        if isinstance(other, PythonNativeBool):
+            return self
+        elif isinstance(other, FixedSizeNumericType):
+            precision = max(self.precision, other.precision)
+            return numpy_precision_map[(self._primitive_type, precision)]
+        else:
+            return NotImplemented
+
+    @lru_cache
+    def __rand__(self, other):
+        if isinstance(other, PythonNativeBool):
+            return self
+        elif isinstance(other, FixedSizeNumericType):
+            precision = max(self.precision, other.precision)
+            return numpy_precision_map[(self._primitive_type, precision)]
+        else:
+            return NotImplemented
+
+
+class NumpyInt8Type(NumpyIntType):
+    """
+    Class representing NumPy's int8 type.
+
+    Class representing NumPy's int8 type.
+    """
+    __slots__ = ()
+    _name = 'numpy.int8'
+    _precision = 1
+
+
+class NumpyInt16Type(NumpyIntType):
+    """
+    Class representing NumPy's int16 type.
+
+    Class representing NumPy's int16 type.
+    """
+    __slots__ = ()
+    _name = 'numpy.int16'
+    _precision = 2
+
+
+class NumpyInt32Type(NumpyIntType):
+    """
+    Class representing NumPy's int32 type.
+
+    Class representing NumPy's int32 type.
+    """
+    __slots__ = ()
+    _name = 'numpy.int32'
+    _precision = 4
+
+
+class NumpyInt64Type(NumpyIntType):
+    """
+    Class representing NumPy's int64 type.
+
+    Class representing NumPy's int64 type.
+    """
+    __slots__ = ()
+    _name = 'numpy.int64'
+    _precision = 8
+
+#==============================================================================
+
+class NumpyFloat32Type(NumpyNumericType):
+    """
+    Class representing NumPy's float32 type.
+
+    Class representing NumPy's float32 type.
+    """
+    __slots__ = ()
+    _name = 'numpy.float32'
+    _primitive_type = PrimitiveFloatingPointType()
+    _precision = 4
+
+
+class NumpyFloat64Type(NumpyNumericType):
+    """
+    Class representing NumPy's float64 type.
+
+    Class representing NumPy's float64 type.
+    """
+    __slots__ = ()
+    _name = 'numpy.float64'
+    _primitive_type = PrimitiveFloatingPointType()
+    _precision = 8
+
+
+class NumpyFloat128Type(NumpyNumericType):
+    """
+    Class representing NumPy's float128 type.
+
+    Class representing NumPy's float128 type.
+    """
+    __slots__ = ()
+    _name = 'numpy.float128'
+    _primitive_type = PrimitiveFloatingPointType()
+    _precision = 16
+
+#==============================================================================
+
+class NumpyComplex64Type(NumpyNumericType):
+    """
+    Class representing NumPy's complex64 type.
+
+    Class representing NumPy's complex64 type.
+    """
+    __slots__ = ()
+    _name = 'numpy.complex64'
+    _primitive_type = PrimitiveComplexType()
+    _precision = 4
+
+    @property
+    def element_type(self):
+        """
+        The type of an element of the complex.
+
+        The type of an element of the complex. In other words, the type
+        of the floats which comprise the complex type.
+        """
+        return NumpyFloat32Type()
+
+
+class NumpyComplex128Type(NumpyNumericType):
+    """
+    Class representing NumPy's complex128 type.
+
+    Class representing NumPy's complex128 type.
+    """
+    __slots__ = ()
+    _name = 'numpy.complex128'
+    _primitive_type = PrimitiveComplexType()
+    _precision = 8
+
+    @property
+    def element_type(self):
+        """
+        The type of an element of the complex.
+
+        The type of an element of the complex. In other words, the type
+        of the floats which comprise the complex type.
+        """
+        return NumpyFloat64Type()
+
+
+class NumpyComplex256Type(NumpyNumericType):
+    """
+    Class representing NumPy's complex256 type.
+
+    Class representing NumPy's complex256 type.
+    """
+    __slots__ = ()
+    _name = 'numpy.complex256'
+    _primitive_type = PrimitiveComplexType()
+    _precision = 16
+
+    @property
+    def element_type(self):
+        """
+        The type of an element of the complex.
+
+        The type of an element of the complex. In other words, the type
+        of the floats which comprise the complex type.
+        """
+        return NumpyFloat128Type()
+
+#==============================================================================
+
+class NumpyNDArrayType(HomogeneousContainerType, metaclass = ArgumentSingleton):
+    """
+    Class representing the NumPy ND array type.
+
+    Class representing the NumPy ND array type.
+
+    Parameters
+    ----------
+    dtype : NumpyNumericType | PythonNativeBool | GenericType
+        The internal datatype of the object (GenericType is allowed for external
+        libraries, e.g. MPI).
+    """
+    __slots__ = ('_element_type',)
+    _name = 'numpy.ndarray'
+
+    def __init__(self, dtype):
+        if pyccel_stage == 'semantic':
+            assert isinstance(dtype, (NumpyNumericType, PythonNativeBool, GenericType))
+        self._element_type = dtype
+        super().__init__()
+
+    @lru_cache
+    def __add__(self, other):
+        test_type = np.zeros(1, dtype = pyccel_type_to_original_type[self.element_type])
+        if isinstance(other, FixedSizeNumericType):
+            comparison_type = pyccel_type_to_original_type[other]()
+        elif isinstance(other, NumpyNDArrayType):
+            comparison_type = np.zeros(1, dtype = pyccel_type_to_original_type[other.element_type])
+        else:
+            return NotImplemented
+        result_type = original_type_to_pyccel_type[np.result_type(test_type, comparison_type).type]
+        return NumpyNDArrayType(result_type)
+
+    @lru_cache
+    def __radd__(self, other):
+        return self.__add__(other)
+
+    @lru_cache
+    def __and__(self, other):
+        elem_type = self.element_type
+        if isinstance(other, FixedSizeNumericType):
+            return NumpyNDArrayType(elem_type and other)
+        elif isinstance(other, NumpyNDArrayType):
+            return NumpyNDArrayType(elem_type+other.element_type)
+        else:
+            return NotImplemented
+
+    @lru_cache
+    def __rand__(self, other):
+        return self.__and__(other)
+
+    def switch_basic_type(self, new_type):
+        """
+        Change the basic type to the new type.
+
+        Change the basic type to the new type. A new NumpyNDArrayType will be
+        returned whose underlying elements are of the NumPy type which is
+        equivalent to the new type (e.g. PythonNativeFloat may be replaced by
+        np.float64).
+
+        Parameters
+        ----------
+        new_type : PyccelType
+            The new basic type.
+
+        Returns
+        -------
+        PyccelType
+            The new type.
+        """
+        assert isinstance(new_type, FixedSizeNumericType)
+        new_type = numpy_precision_map[(new_type.primitive_type, new_type.precision)]
+        cls = type(self)
+        return cls(self.element_type.switch_basic_type(new_type))
+
+#==============================================================================
+
+numpy_precision_map = {
+        (PrimitiveBooleanType(), -1): PythonNativeBool(),
+        (PrimitiveIntegerType(), 1): NumpyInt8Type(),
+        (PrimitiveIntegerType(), 2): NumpyInt16Type(),
+        (PrimitiveIntegerType(), 4): NumpyInt32Type(),
+        (PrimitiveIntegerType(), 8): NumpyInt64Type(),
+        (PrimitiveFloatingPointType(), 4) : NumpyFloat32Type(),
+        (PrimitiveFloatingPointType(), 8) : NumpyFloat64Type(),
+        (PrimitiveFloatingPointType(), 16): NumpyFloat128Type(),
+        (PrimitiveComplexType(), 4) : NumpyComplex64Type(),
+        (PrimitiveComplexType(), 8) : NumpyComplex128Type(),
+        (PrimitiveComplexType(), 16): NumpyComplex256Type(),
+        }
+
+numpy_type_to_original_type = {
+    NumpyInt8Type()       : np.int8,
+    NumpyInt16Type()      : np.int16,
+    NumpyInt32Type()      : np.int32,
+    NumpyInt64Type()      : np.int64,
+    NumpyFloat32Type()    : np.float32,
+    NumpyFloat64Type()    : np.float64,
+    NumpyComplex64Type()  : np.complex64,
+    NumpyComplex128Type() : np.complex128,
+    }
+
+# Large types don't exist on all systems
+if hasattr(np, 'float128'):
+    numpy_type_to_original_type.update({
+        NumpyFloat128Type()   : np.float128,
+        NumpyComplex256Type() : np.complex256,
+        })
+
+pyccel_type_to_original_type.update(numpy_type_to_original_type)
+original_type_to_pyccel_type.update({v:k for k,v in numpy_type_to_original_type.items()})
+original_type_to_pyccel_type[np.bool_] = PythonNativeBool()
diff --git a/pyccel/ast/operators.py b/pyccel/ast/operators.py
index 463d50564f..9b93b0b7ea 100644
--- a/pyccel/ast/operators.py
+++ b/pyccel/ast/operators.py
@@ -4,9 +4,9 @@
 #------------------------------------------------------------------------------------------#
 """
 Module handling all Python builtin operators
-These operators all have a precision as detailed here:
+These operators all have a precedence as detailed here:
     https://docs.python.org/3/reference/expressions.html#operator-precedence
-They also have specific rules to determine the dtype, precision, rank, shape
+They also have specific rules to determine the dtype, rank, shape, class_type
 """
 
 from pyccel.utilities.stage import PyccelStage
@@ -15,11 +15,10 @@
 
 from .basic                 import TypedAstNode
 
-from .datatypes             import (NativeBool, NativeInteger, NativeFloat,
-                                    NativeComplex, NativeString,
-                                    NativeNumeric)
+from .datatypes             import PythonNativeBool, PythonNativeFloat
+from .datatypes             import StringType, FixedSizeNumericType, ContainerType
+from .datatypes             import PrimitiveBooleanType, PrimitiveIntegerType
 
-from .internals             import max_precision
 
 from .literals              import Literal, LiteralInteger, LiteralFloat, LiteralComplex
 from .literals              import Nil, NilArgument
@@ -161,21 +160,21 @@ def __init__(self, *args):
         if pyccel_stage == 'syntactic':
             super().__init__()
             return
-        self._set_dtype()
+        self._set_type()
         self._set_shape_rank()
         # rank is None for lambda functions
         self._set_order()
         super().__init__()
 
-    def _set_dtype(self):
+    def _set_type(self):
         """
-        Set the dtype and precision of the result of the operator.
+        Set the type of the result of the operator.
 
-        Set the dtype and precision of the result of the operator. This function
-        uses the static method `_calculate_dtype` to set these values. If the
+        Set the class_type of the result of the operator. This function
+        uses the static method `_calculate_type` to set these values. If the
         values are class parameters in a sub-class, this method must be over-ridden.
         """
-        self._dtype, self._precision, self._class_type = self._calculate_dtype(*self._args)  # pylint: disable=no-member
+        self._class_type = self._calculate_type(*self._args)  # pylint: disable=no-member
 
     def _set_shape_rank(self):
         """
@@ -254,11 +253,6 @@ def _set_order(self):
         Set the order of the result of the operator.
         This is chosen to match the arguments if they are in agreement.
         Otherwise it defaults to 'C'.
-
-        Returns
-        -------
-        str | None
-            A string indicating the order or None if the rank is 1 or less.
         """
         if self.rank is not None and self.rank > 1:
             orders = [a.order for a in self._args if a.order is not None]
@@ -290,18 +284,18 @@ class PyccelUnaryOperator(PyccelOperator):
     arg : TypedAstNode
         The argument passed to the operator.
     """
-    __slots__ = ('_dtype', '_precision','_shape','_rank','_order','_class_type')
+    __slots__ = ('_shape','_rank','_order','_class_type')
 
     def __init__(self, arg):
         super().__init__(arg)
 
     @staticmethod
-    def _calculate_dtype(arg):
+    def _calculate_type(arg):
         """
-        Calculate the dtype, precision and class type of the result.
+        Calculate the dtype and class type of the result.
 
-        Calculate the dtype, precision and class type of the result.
-        These are equivalent to the dtype, precision and class type
+        Calculate the dtype and class type of the result.
+        These are equivalent to the dtype and class type
         of the only argument.
 
         Parameters
@@ -311,26 +305,33 @@ def _calculate_dtype(arg):
 
         Returns
         -------
-        dtype : DataType
-            The underlying datatype of the object.
-        precision : int
-            The precision of the datatype of the object.
-        class_type : DataType
+        DataType
             The Python type of the object.
         """
-        dtype = arg.dtype
-        precision = arg.precision
-        class_type = arg.class_type
-        return dtype, precision, class_type
+        return arg.class_type
 
     @staticmethod
-    def _calculate_shape_rank(*args):
-        """ Sets the shape and rank
+    def _calculate_shape_rank(arg):
+        """
+        Calculate the shape and rank.
+
+        Calculate the shape and rank.
         They are chosen to match the argument
+
+        Parameters
+        ----------
+        arg : TypedAstNode
+            The argument passed to the operator.
+
+        Returns
+        -------
+        shape : tuple[TypedAstNode]
+            The shape of the resulting object.
+        rank : int
+            The rank of the resulting object.
         """
-        a = args[0]
-        rank = a.rank
-        shape = a.shape
+        rank = arg.rank
+        shape = arg.shape
         return shape, rank
 
 #==============================================================================
@@ -401,15 +402,23 @@ class PyccelNot(PyccelUnaryOperator):
     """
     __slots__ = ()
     _precedence = 6
+    _class_type = PythonNativeBool()
+
+    def _set_type(self):
+        """
+        Set the type of the result of the operator.
+
+        Set the class_type of the result of the operator. Nothing needs
+        to be done here as the type is a class variable.
+        """
 
     @staticmethod
-    def _calculate_dtype(arg):
+    def _calculate_shape_rank(arg):
         """
-        Calculate the dtype, precision and class type of the result.
+        Calculate the shape and rank.
 
-        Calculate the dtype, precision and class type of the result.
-        These are the dtype, precision and class type which represent a
-        boolean.
+        Calculate the shape and rank.
+        They are chosen to match the argument
 
         Parameters
         ----------
@@ -418,22 +427,10 @@ def _calculate_dtype(arg):
 
         Returns
         -------
-        dtype : DataType
-            The underlying datatype of the object.
-        precision : int
-            The precision of the datatype of the object.
-        class_type : DataType
-            The Python type of the object.
-        """
-        dtype = NativeBool()
-        precision = -1
-        return dtype, precision, dtype
-
-    @staticmethod
-    def _calculate_shape_rank(*args):
-        """ Sets the shape and rank
-        They are chosen to match the argument unless the class has
-        a _shape or _rank member
+        shape : tuple[TypedAstNode]
+            The shape of the resulting object.
+        rank : int
+            The rank of the resulting object.
         """
         rank = 0
         shape = None
@@ -480,20 +477,20 @@ class PyccelBinaryOperator(PyccelOperator):
     arg2 : TypedAstNode
         The second argument passed to the operator.
     """
-    __slots__ = ('_dtype','_precision','_shape','_rank','_order','_class_type')
+    __slots__ = ('_shape','_rank','_order','_class_type')
 
     def __init__(self, arg1, arg2):
         super().__init__(arg1, arg2)
 
     @classmethod
-    def _calculate_dtype(cls, arg1, arg2):
+    def _calculate_type(cls, arg1, arg2):
         """
-        Sets the dtype and precision.
+        Sets the dtype and class type.
 
         If one argument is a string then all arguments must be strings
 
-        If the arguments are numeric then the dtype and precision
-        match the broadest type and the largest precision
+        If the arguments are numeric then the dtype and class type
+        match the broadest type
         e.g.
             1 + 2j -> PyccelAdd(LiteralInteger, LiteralComplex) -> complex
 
@@ -506,83 +503,53 @@ def _calculate_dtype(cls, arg1, arg2):
 
         Returns
         -------
-        dtype : DataType
-            The underlying datatype of the object.
-        precision : int
-            The precision of the datatype of the object.
-        class_type : DataType
+        DataType
             The Python type of the object.
+
+        Raises
+        ------
+        TypeError
+            Raised if the new type cannot be deduced by checking the __add__ operator
+            of the class types.
         """
         try:
-            dtype = arg1.dtype + arg2.dtype
-            class_type = arg1.class_type + arg2.class_type
+            return arg1.class_type + arg2.class_type
         except NotImplementedError:
             raise TypeError(f'Cannot determine the type of ({arg1}, {arg2})') #pylint: disable=raise-missing-from
 
-        if dtype is NativeString():
-            return *cls._handle_str_type((arg1, arg2)), class_type
-        elif dtype is NativeComplex():
-            return *cls._handle_complex_type((arg1, arg2)), class_type
-        elif dtype is NativeFloat():
-            return *cls._handle_float_type((arg1, arg2)), class_type
-        elif dtype in (NativeInteger(), NativeBool()):
-            if class_type is NativeBool():
-                class_type = NativeInteger()
-            return *cls._handle_integer_type((arg1, arg2)), class_type
-        else:
-            raise TypeError(f'Cannot determine the type of ({arg1}, {arg2})')
-
     @staticmethod
-    def _handle_str_type(strs):
+    def _calculate_shape_rank(arg1, arg2):
         """
-        Set dtype and precision when both arguments are strings
-        """
-        raise TypeError("unsupported operand type(s) for /: 'str' and 'str'")
-
-    @staticmethod
-    def _handle_complex_type(complexes):
-        """
-        Set dtype and precision when the result is a complex
-        """
-        dtype = NativeComplex()
-        precision = max_precision(complexes)
-        return dtype, precision
-
-    @staticmethod
-    def _handle_float_type(floats):
-        """
-        Set dtype and precision when the result is a float
-        """
-        dtype = NativeFloat()
-        precision = max_precision(floats)
-        return dtype, precision
-
-    @staticmethod
-    def _handle_integer_type(integers):
-        """
-        Set dtype and precision when the result is an integer
-        """
-        dtype = NativeInteger()
-        precision = max_precision(integers)
-        return dtype, precision
-
-    @staticmethod
-    def _calculate_shape_rank(*args):
-        """ Sets the shape and rank
+        Calculate the shape and rank.
 
         Strings must be scalars.
 
         For numeric types the rank and shape is determined according
-        to numpy broadcasting rules where possible
+        to NumPy broadcasting rules where possible.
+
+        Parameters
+        ----------
+        arg1 : TypedAstNode
+            The first argument passed to the operator.
+        arg2 : TypedAstNode
+            The second argument passed to the operator.
+
+        Returns
+        -------
+        shape : tuple[TypedAstNode]
+            The shape of the resulting object.
+        rank : int
+            The rank of the resulting object.
         """
-        strs = [a for a in args if a.dtype is NativeString()]
+        args = (arg1, arg2)
+        strs = [a for a in args if isinstance(a.dtype, StringType)]
         if strs:
-            other = [a for a in args if a.dtype in (NativeInteger(), NativeBool(), NativeFloat(), NativeComplex())]
+            other = [a for a in args if isinstance(a.dtype, FixedSizeNumericType)]
             assert len(other) == 0
             rank  = 0
             shape = None
         else:
-            s = broadcast(args[0].shape, args[1].shape)
+            s = broadcast(arg1.shape, arg2.shape)
 
             shape = s
             rank  = 0 if s is None else len(s)
@@ -682,15 +649,13 @@ def __new__(cls, arg1, arg2, simplify = False):
         if simplify:
             if isinstance(arg2, PyccelUnarySub):
                 return PyccelMinus(arg1, arg2.args[0], simplify = True)
-            dtype, precision, _ = cls._calculate_dtype(arg1, arg2)
+            class_type = cls._calculate_type(arg1, arg2)
             if isinstance(arg1, Literal) and isinstance(arg2, Literal):
                 return convert_to_literal(arg1.python_value + arg2.python_value,
-                                          dtype, precision)
-            if dtype == arg2.dtype and precision == arg2.precision and \
-                    isinstance(arg1, Literal) and arg1.python_value == 0:
+                                          class_type)
+            if class_type == arg2.class_type and arg1 == 0:
                 return arg2
-            if dtype == arg1.dtype and precision == arg1.precision and \
-                    isinstance(arg2, Literal) and arg2.python_value == 0:
+            if class_type == arg1.class_type and arg2 == 0:
                 return arg1
 
         if isinstance(arg1, (LiteralInteger, LiteralFloat)) and \
@@ -707,11 +672,34 @@ def __new__(cls, arg1, arg2, simplify = False):
     def __init__(self, arg1, arg2, simplify = False):
         super().__init__(arg1, arg2)
 
-    @staticmethod
-    def _handle_str_type(strs):
-        dtype = NativeString()
-        precision = None
-        return dtype, precision
+    @classmethod
+    def _calculate_type(cls, arg1, arg2):
+        """
+        Sets the dtype and class type.
+
+        If one argument is a string then all arguments must be strings
+
+        If the arguments are numeric then the dtype and class type
+        match the broadest type
+        e.g.
+            1 + 2j -> PyccelAdd(LiteralInteger, LiteralComplex) -> complex
+
+        Parameters
+        ----------
+        arg1 : TypedAstNode
+            The first argument passed to the operator.
+        arg2 : TypedAstNode
+            The second argument passed to the operator.
+
+        Returns
+        -------
+        DataType
+            The Python type of the object.
+        """
+        if arg1.dtype == arg2.dtype == StringType():
+            return arg1.dtype
+        else:
+            return super()._calculate_type(arg1, arg2)
 
     def __repr__(self):
         return f'{self.args[0]} + {self.args[1]}'
@@ -748,16 +736,16 @@ def __new__(cls, arg1, arg2, simplify = False):
             if (arg2 == 1):
                 return arg1
             if (arg1 == 0 or arg2 == 0):
-                dtype, precision, _ = cls._calculate_dtype(arg1, arg2)
-                return convert_to_literal(0, dtype, precision)
+                dtype = cls._calculate_type(arg1, arg2)
+                return convert_to_literal(0, dtype)
             if (isinstance(arg1, PyccelUnarySub) and arg1.args[0] == 1):
                 return PyccelUnarySub(arg2)
             if (isinstance(arg2, PyccelUnarySub) and arg2.args[0] == 1):
                 return PyccelUnarySub(arg1)
             if isinstance(arg1, Literal) and isinstance(arg2, Literal):
-                dtype, precision, _ = cls._calculate_dtype(arg1, arg2)
+                dtype = cls._calculate_type(arg1, arg2)
                 return convert_to_literal(arg1.python_value * arg2.python_value,
-                                          dtype, precision)
+                                          dtype)
         return super().__new__(cls)
 
     def __init__(self, arg1, arg2, simplify = False):
@@ -796,9 +784,9 @@ def __new__(cls, arg1, arg2, simplify = False):
             if isinstance(arg2, PyccelUnarySub):
                 return PyccelAdd(arg1, arg2.args[0], simplify = True)
             elif isinstance(arg1, Literal) and isinstance(arg2, Literal):
-                dtype, precision, _ = cls._calculate_dtype(arg1, arg2)
+                dtype = cls._calculate_type(arg1, arg2)
                 return convert_to_literal(arg1.python_value - arg2.python_value,
-                                          dtype, precision)
+                                          dtype)
         if isinstance(arg1, LiteralFloat) and \
             isinstance(arg2, LiteralComplex) and \
             arg2.real == LiteralFloat(0):
@@ -850,11 +838,36 @@ def __new__(cls, arg1, arg2, simplify=False):
     def __init__(self, arg1, arg2, simplify = False):
         super().__init__(arg1, arg2)
 
-    @staticmethod
-    def _handle_integer_type(integers):
-        dtype = NativeFloat()
-        precision = -1
-        return dtype, precision
+    @classmethod
+    def _calculate_type(cls, arg1, arg2):
+        """
+        Sets the dtype and class type.
+
+        If one argument is a string then all arguments must be strings
+
+        If the arguments are numeric then the dtype and class type
+        match the broadest type
+        e.g.
+            1 + 2j -> PyccelAdd(LiteralInteger, LiteralComplex) -> complex
+
+        Parameters
+        ----------
+        arg1 : TypedAstNode
+            The first argument passed to the operator.
+        arg2 : TypedAstNode
+            The second argument passed to the operator.
+
+        Returns
+        -------
+        DataType
+            The Python type of the object.
+        """
+        class_type = super()._calculate_type(arg1, arg2)
+
+        if class_type.primitive_type in (PrimitiveIntegerType(), PrimitiveBooleanType()):
+            class_type = class_type.switch_basic_type(PythonNativeFloat())
+
+        return class_type
 
     def __repr__(self):
         return f'{repr(self.args[0])} / {repr(self.args[1])}'
@@ -927,32 +940,45 @@ class PyccelComparisonOperator(PyccelBinaryOperator):
     """
     __slots__ = ()
     _precedence = 7
-    @staticmethod
-    def _calculate_dtype(*args):
+
+    @classmethod
+    def _calculate_type(cls, arg1, arg2):
         """
-        Calculate the dtype, precision and class type of the result.
+        Calculate the dtype and class type of the result.
 
-        Calculate the dtype, precision and class type of the result.
-        These are the dtype, precision and class type which represent a
+        Calculate the dtype and class type of the result.
+        These are the dtype and class type which represent a
         boolean.
 
         Parameters
         ----------
-        *args : TypedAstNode
-            The arguments passed to the operator.
+        arg1 : TypedAstNode
+            The first argument passed to the operator.
+        arg2 : TypedAstNode
+            The second argument passed to the operator.
 
         Returns
         -------
         dtype : DataType
             The underlying datatype of the object.
-        precision : int
-            The precision of the datatype of the object.
         class_type : DataType
             The Python type of the object.
         """
-        dtype = NativeBool()
-        precision = -1
-        return dtype, precision, dtype
+        dtype = PythonNativeBool()
+        possible_class_types = set(a.class_type for a in (arg1, arg2) \
+                        if isinstance(a.class_type, ContainerType))
+        if len(possible_class_types) == 0:
+            class_type = dtype
+        elif len(possible_class_types) == 1:
+            class_type = possible_class_types.pop().switch_basic_type(dtype)
+        else:
+            description = f"({arg1} {cls.op} {arg2})" # pylint: disable=no-member
+            raise NotImplementedError("Can't deduce type for comparison operator"
+                                      f" with multiple containers {description}")
+        return class_type
+
+    def __repr__(self):
+        return f'{repr(self.args[0])} {self.op} {repr(self.args[1])}' # pylint: disable=no-member
 
 #==============================================================================
 
@@ -977,6 +1003,7 @@ class PyccelEq(PyccelComparisonOperator):
         possible. False if the arguments should be preserved as they are.
     """
     __slots__ = ()
+    op = "=="
 
     def __new__(cls, arg1, arg2, simplify = False):
         if isinstance(arg1, Nil) or isinstance(arg2, Nil):
@@ -987,9 +1014,6 @@ def __new__(cls, arg1, arg2, simplify = False):
     def __init__(self, arg1, arg2, simplify = False):
         super().__init__(arg1, arg2)
 
-    def __repr__(self):
-        return f'{repr(self.args[0])} == {repr(self.args[1])}'
-
 class PyccelNe(PyccelComparisonOperator):
     """
     Class representing a call to the Python inequality operator.
@@ -1011,6 +1035,7 @@ class PyccelNe(PyccelComparisonOperator):
         possible. False if the arguments should be preserved as they are.
     """
     __slots__ = ()
+    op = "!="
 
     def __new__(cls, arg1, arg2, simplify = False):
         if isinstance(arg1, Nil) or isinstance(arg2, Nil):
@@ -1021,9 +1046,6 @@ def __new__(cls, arg1, arg2, simplify = False):
     def __init__(self, arg1, arg2, simplify = False):
         super().__init__(arg1, arg2)
 
-    def __repr__(self):
-        return f'{repr(self.args[0])} != {repr(self.args[1])}'
-
 class PyccelLt(PyccelComparisonOperator):
     """
     Class representing a call to the Python less than operator.
@@ -1042,9 +1064,7 @@ class PyccelLt(PyccelComparisonOperator):
         The second argument passed to the operator.
     """
     __slots__ = ()
-
-    def __repr__(self):
-        return f'{repr(self.args[0])} < {repr(self.args[1])}'
+    op = "<"
 
 class PyccelLe(PyccelComparisonOperator):
     """
@@ -1064,9 +1084,7 @@ class PyccelLe(PyccelComparisonOperator):
         The second argument passed to the operator.
     """
     __slots__ = ()
-
-    def __repr__(self):
-        return f'{repr(self.args[0])} <= {repr(self.args[1])}'
+    op = "<="
 
 class PyccelGt(PyccelComparisonOperator):
     """
@@ -1086,9 +1104,7 @@ class PyccelGt(PyccelComparisonOperator):
         The second argument passed to the operator.
     """
     __slots__ = ()
-
-    def __repr__(self):
-        return f'{repr(self.args[0])} > {repr(self.args[1])}'
+    op = ">"
 
 class PyccelGe(PyccelComparisonOperator):
     """
@@ -1108,9 +1124,7 @@ class PyccelGe(PyccelComparisonOperator):
         The second argument passed to the operator.
     """
     __slots__ = ()
-
-    def __repr__(self):
-        return f'{repr(self.args[0])} >= {repr(self.args[1])}'
+    op = ">="
 
 #==============================================================================
 
@@ -1126,23 +1140,36 @@ class PyccelBooleanOperator(PyccelOperator):
     *args : tuple of TypedAstNode
         The arguments passed to the operator.
     """
-    _dtype = NativeBool()
-    _precision = -1
     _rank = 0
     _shape = None
     _order = None
-    _class_type = NativeBool()
+    _class_type = PythonNativeBool()
 
     __slots__ = ()
 
     def _set_order(self):
-        pass
+        """
+        Set the order of the result.
 
-    def _set_dtype(self):
-        pass
+        Set the order of the result of the operator. Nothing needs to
+        be done here as the order is a class variable.
+        """
+
+    def _set_type(self):
+        """
+        Set the type of the result of the operator.
+
+        Set the class_type of the result of the operator. Nothing needs
+        to be done here as the type is a class variable.
+        """
 
     def _set_shape_rank(self):
-        pass
+        """
+        Set the shape and rank of the result of the operator.
+
+        Set the shape and rank of the result of the operator. Nothing needs
+        to be done here as the shape and rank are class variables.
+        """
 
 #==============================================================================
 
@@ -1163,13 +1190,14 @@ class PyccelAnd(PyccelBooleanOperator):
     """
     __slots__ = ()
     _precedence = 5
+
     def _handle_precedence(self, args):
         args = PyccelBooleanOperator._handle_precedence(self, args)
         args = tuple(PyccelAssociativeParenthesis(a) if isinstance(a, PyccelOr) else a for a in args)
         return args
 
     def __repr__(self):
-        return f'{repr(self.args[0])} and {repr(self.args[1])}'
+        return ' and '.join(repr(a) for a in self.args)
 
 #==============================================================================
 
@@ -1190,13 +1218,14 @@ class PyccelOr(PyccelBooleanOperator):
     """
     __slots__ = ()
     _precedence = 4
+
     def _handle_precedence(self, args):
         args = PyccelBooleanOperator._handle_precedence(self, args)
         args = tuple(PyccelAssociativeParenthesis(a) if isinstance(a, PyccelAnd) else a for a in args)
         return args
 
     def __repr__(self):
-        return f'{repr(self.args[0])} or {repr(self.args[1])}'
+        return ' or '.join(repr(a) for a in self.args)
 
 #==============================================================================
 
@@ -1323,7 +1352,7 @@ class IfTernaryOperator(PyccelOperator):
     >>> IfTernaryOperator(PyccelGt(n > 1),  5,  2)
     IfTernaryOperator(PyccelGt(n > 1),  5,  2)
     """
-    __slots__ = ('_dtype','_precision','_shape','_rank','_order','_class_type')
+    __slots__ = ('_shape','_rank','_order','_class_type')
     _precedence = 3
 
     def __init__(self, cond, value_true, value_false):
@@ -1333,10 +1362,8 @@ def __init__(self, cond, value_true, value_false):
             return
         if isinstance(value_true , Nil) or isinstance(value_false, Nil):
             errors.report('None is not implemented for Ternary Operator', severity='fatal')
-        if isinstance(value_true , NativeString) or isinstance(value_false, NativeString):
-            errors.report('String is not implemented for Ternary Operator', severity='fatal')
         if value_true.dtype != value_false.dtype:
-            if value_true.dtype not in NativeNumeric or value_false.dtype not in NativeNumeric:
+            if not (isinstance(value_true.dtype, FixedSizeNumericType) and isinstance(value_false.dtype, FixedSizeNumericType)):
                 errors.report('The types are incompatible in IfTernaryOperator', severity='fatal')
         if value_false.rank != value_true.rank :
             errors.report('Ternary Operator results should have the same rank', severity='fatal')
@@ -1344,12 +1371,12 @@ def __init__(self, cond, value_true, value_false):
             errors.report('Ternary Operator results should have the same shape', severity='fatal')
 
     @staticmethod
-    def _calculate_dtype(cond, value_true, value_false):
+    def _calculate_type(cond, value_true, value_false):
         """
-        Calculate the dtype, precision and class type of the result.
+        Calculate the dtype and class type of the result.
 
-        Calculate the dtype, precision and class type of the result. The dtype,
-        precision and class type are calculated from the types of the values if
+        Calculate the dtype and class type of the result. The dtype,
+        and class type are calculated from the types of the values if
         true or false.
 
         Parameters
@@ -1365,22 +1392,18 @@ def _calculate_dtype(cond, value_true, value_false):
 
         Returns
         -------
-        dtype : DataType
-            The underlying datatype of the object.
-        precision : int
-            The precision of the datatype of the object.
-        class_type : DataType
+        DataType
             The Python type of the object.
         """
-        if value_true.dtype in NativeNumeric and value_false.dtype in NativeNumeric:
-            dtype = max([value_true.dtype, value_false.dtype], key = NativeNumeric.index)
-        else:
-            dtype = value_true.dtype
+        if value_true.dtype is value_false.dtype and value_true.class_type is value_false.class_type:
+            return value_true.class_type
 
-        precision = max_precision([value_true, value_false])
+        try:
+            class_type = value_true.class_type + value_false.class_type
+        except NotImplementedError:
+            raise TypeError(f'Cannot determine the type of ({value_true}, {value_false})') #pylint: disable=raise-missing-from
 
-        class_type = value_true.class_type + value_false.class_type
-        return dtype, precision, class_type
+        return class_type
 
     @staticmethod
     def _calculate_shape_rank(cond, value_true, value_false):
diff --git a/pyccel/ast/scipyext.py b/pyccel/ast/scipyext.py
index 8bbe7adf89..4bbce04a84 100644
--- a/pyccel/ast/scipyext.py
+++ b/pyccel/ast/scipyext.py
@@ -8,11 +8,12 @@
 """
 from numpy import pi
 from .core import Module, Import
+from .datatypes import PythonNativeFloat
 from .variable import Constant
 
 __all__ = ('scipy_mod', 'scipy_pi_const')
 
-scipy_pi_const = Constant('float', 'pi', value=pi)
+scipy_pi_const = Constant(PythonNativeFloat(), 'pi', value=pi)
 
 scipy_mod = Module('scipy',
         variables = (scipy_pi_const,),
diff --git a/pyccel/ast/sympy_helper.py b/pyccel/ast/sympy_helper.py
index dd838cfdb7..d2e7b40206 100644
--- a/pyccel/ast/sympy_helper.py
+++ b/pyccel/ast/sympy_helper.py
@@ -13,14 +13,14 @@
 
 from pyccel.utilities.strings import create_incremented_string
 
+from .builtins  import PythonRange, PythonTuple
+from .core      import Iterable
+from .datatypes import PrimitiveIntegerType
 from .internals import PyccelArrayShapeElement
+from .literals  import LiteralInteger, LiteralFloat, LiteralComplex
+from .mathext   import MathCeil
 from .operators import PyccelAdd, PyccelMul, PyccelPow, PyccelUnarySub
 from .operators import PyccelDiv, PyccelMinus, PyccelAssociativeParenthesis
-from .core      import Iterable
-from .builtins  import PythonRange, PythonTuple
-from .mathext   import MathCeil
-from .literals  import LiteralInteger, LiteralFloat, LiteralComplex
-from .datatypes import NativeInteger
 from .variable  import Variable
 
 __all__ = ('sympy_to_pyccel',
@@ -99,7 +99,7 @@ def sympy_to_pyccel(expr, symbol_map):
     elif isinstance(expr, sp.ceiling):
         arg = sympy_to_pyccel(expr.args[0], symbol_map)
         # Only apply ceiling where appropriate
-        if arg.dtype is NativeInteger():
+        if getattr(arg.dtype, 'primitive_type', None) is PrimitiveIntegerType():
             return arg
         else:
             return MathCeil(arg)
diff --git a/pyccel/ast/sysext.py b/pyccel/ast/sysext.py
index 58c0f1654f..2a45b48a19 100644
--- a/pyccel/ast/sysext.py
+++ b/pyccel/ast/sysext.py
@@ -7,7 +7,7 @@
 """
 from .core import PyccelFunctionDef, Module
 from .internals import PyccelInternalFunction
-from .datatypes import NativeVoid
+from .datatypes import VoidType
 from .internals import LiteralInteger
 
 __all__ = (
@@ -18,19 +18,20 @@
 )
 
 class SysExit(PyccelInternalFunction):
-    """Represents a call to  sys.exit
+    """
+    Represents a call to sys.exit.
+
+    Represents a call to sys.exit.
 
     Parameters
     ----------
-
-    arg : TypedAstNode (optional)
-        if arg.dtype is NativeInteger it will be used as the exit_code
-        else the arg will be printed to the stderror
+    status : TypedAstNode, optional
+        If status.dtype is NativeInteger it will be used as the exit_code,
+        else the status will be printed to the stderror.
     """
     __slots__ = ()
     name      = 'exit'
-    _dtype     = NativeVoid()
-    _precision = -1
+    _class_type     = VoidType()
     _rank      = 0
     _shape     = None
     _order     = None
diff --git a/pyccel/ast/type_annotations.py b/pyccel/ast/type_annotations.py
index 315e1a0391..bafc7373ec 100644
--- a/pyccel/ast/type_annotations.py
+++ b/pyccel/ast/type_annotations.py
@@ -13,6 +13,9 @@
 
 from .core import FunctionDefArgument
 
+from .datatypes import PythonNativeBool, PythonNativeInt, PythonNativeFloat, PythonNativeComplex
+from .datatypes import VoidType, GenericType, StringType
+
 from .variable import DottedName, AnnotatedPyccelSymbol, IndexedElement
 
 __all__ = (
@@ -20,10 +23,13 @@
         'SyntacticTypeAnnotation',
         'VariableTypeAnnotation',
         'UnionTypeAnnotation',
+        'typenames_to_dtypes',
         )
 
 pyccel_stage = PyccelStage()
 
+#==============================================================================
+
 class VariableTypeAnnotation(PyccelAstNode):
     """
     A class which describes a type annotation on a variable.
@@ -33,16 +39,8 @@ class VariableTypeAnnotation(PyccelAstNode):
 
     Parameters
     ----------
-    datatype : DataType
-        The requested internal data type.
-
     class_type : DataType
-        The Python type of the variable. In the case of scalars this is equivalent to
-        the datatype. For objects in (homogeneous) containers (e.g. list/ndarray/tuple),
-        this is the type of the container.
-
-    precision : int
-        The precision of the internal datatype.
+        The requested Python type of the variable.
 
     rank : int
         The rank of the variable.
@@ -53,30 +51,18 @@ class VariableTypeAnnotation(PyccelAstNode):
     is_const : bool, default=False
         True if the variable cannot be modified, false otherwise.
     """
-    __slots__ = ('_datatype', '_class_type', '_precision', '_rank',
+    __slots__ = ('_class_type', '_rank',
                  '_order', '_is_const')
     _attribute_nodes = ()
-    def __init__(self, datatype : 'DataType', class_type : 'DataType', precision : int = -1,
+    def __init__(self, class_type : 'DataType',
             rank : int = 0, order : str = None, is_const : bool = False):
-        self._datatype = datatype
         self._class_type = class_type
-        self._precision = precision
         self._rank = rank
         self._order = order
         self._is_const = is_const
 
         super().__init__()
 
-    @property
-    def datatype(self):
-        """
-        Get the basic datatype of the object.
-
-        Get the basic datatype of the object. For objects with rank>0 this is the
-        type of one of the elements of the object.
-        """
-        return self._datatype
-
     @property
     def class_type(self):
         """
@@ -88,16 +74,6 @@ def class_type(self):
         """
         return self._class_type
 
-    @property
-    def precision(self):
-        """
-        Get the precision of the object.
-
-        Get the precision of the object. For objects with rank>0 this is the
-        precision of one of the elements of the object.
-        """
-        return self._precision
-
     @property
     def rank(self):
         """
@@ -141,21 +117,26 @@ def is_const(self, val):
         self._is_const = val
 
     def __hash__(self):
-        return hash((self.datatype, self.class_type, self.precision, self.rank, self.order))
+        return hash((self.class_type, self.rank, self.order))
 
     def __eq__(self, other):
         # Needed for set
         if isinstance(other, VariableTypeAnnotation):
-            return self.datatype == other.datatype and \
-                   self.class_type == other.class_type and \
-                   self.precision == other.precision and \
+            return self.class_type == other.class_type 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})"
+        dtype = str(self._class_type)
+        if self._rank:
+            dtype += '['+','.join(':'*self._rank)+']'
+        if self._order:
+            dtype += '(order = {self._order})'
+        return dtype
+
+#==============================================================================
 
 class FunctionTypeAnnotation(PyccelAstNode):
     """
@@ -238,6 +219,8 @@ def is_const(self, val):
             raise TypeError("Is const value should be a boolean")
         self._is_const = val
 
+#==============================================================================
+
 class UnionTypeAnnotation(PyccelAstNode):
     """
     A class which holds multiple possible type annotations.
@@ -254,7 +237,8 @@ class UnionTypeAnnotation(PyccelAstNode):
 
     def __init__(self, *type_annotations):
         annots = [ti for t in type_annotations for ti in (t.type_list if isinstance(t, UnionTypeAnnotation) else [t])]
-        self._type_annotations = tuple(set(annots))
+        # Strip out repeats
+        self._type_annotations = tuple({a: None for a in annots}.keys())
 
         super().__init__()
 
@@ -291,6 +275,8 @@ def __iter__(self):
     def __str__(self):
         return '|'.join(str(t) for t in self._type_annotations)
 
+#==============================================================================
+
 class SyntacticTypeAnnotation(PyccelAstNode):
     """
     A class describing the type annotation parsed in the syntactic stage.
@@ -348,3 +334,18 @@ def __eq__(self, o):
                     self.order == o.order
         else:
             return False
+
+#==============================================================================
+
+typenames_to_dtypes = { 'float'   : PythonNativeFloat(),
+                        'double'  : PythonNativeFloat(),
+                        'real'    : PythonNativeFloat(),
+                        'complex' : PythonNativeComplex(),
+                        'int'     : PythonNativeInt(),
+                        'integer' : PythonNativeInt(),
+                        'bool'    : PythonNativeBool(),
+                        'b1'      : PythonNativeBool(),
+                        'void'    : VoidType(),
+                        '*'       : GenericType(),
+                        'str'     : StringType(),
+                        }
diff --git a/pyccel/ast/utilities.py b/pyccel/ast/utilities.py
index c2b2daa2e2..a6195c4f15 100644
--- a/pyccel/ast/utilities.py
+++ b/pyccel/ast/utilities.py
@@ -19,7 +19,7 @@
 from .builtins      import (builtin_functions_dict,
                             PythonRange, PythonList, PythonTuple)
 from .cmathext      import cmath_mod
-from .datatypes     import NativeHomogeneousTuple
+from .datatypes     import HomogeneousTupleType, PythonNativeInt
 from .internals     import PyccelInternalFunction, Slice
 from .itertoolsext  import itertools_mod
 from .literals      import LiteralInteger, LiteralEllipsis, Nil
@@ -511,7 +511,7 @@ def collect_loops(block, indices, new_index, language_has_vectors = False, resul
                 new_level += 1
                 # If an index exists at the same depth, reuse it if not create one
                 if rank+index >= len(indices):
-                    indices.append(new_index('int','i'))
+                    indices.append(new_index(PythonNativeInt(),'i'))
                 index_var = indices[rank+index]
                 new_vars = [insert_index(v, index, index_var) for v in new_vars]
                 handled_funcs = [insert_index(v, index, index_var) for v in handled_funcs]
@@ -585,7 +585,7 @@ def collect_loops(block, indices, new_index, language_has_vectors = False, resul
 
             if not isinstance(rhs.length, LiteralInteger):
                 if len(indices) == 0:
-                    indices.append(new_index('int', 'i'))
+                    indices.append(new_index(PythonNativeInt(), 'i'))
                 idx = indices[0]
 
                 assign = Assign(lhs[Slice(PyccelMul(rhs.val.shape[0], idx, simplify=True),
@@ -675,12 +675,13 @@ def expand_inhomog_tuple_assignments(block, language_has_vectors = False):
     --------
     >>> from pyccel.ast.builtins  import PythonTuple
     >>> from pyccel.ast.core      import Assign, CodeBlock
+    >>> from pyccel.ast.datatypes import PythonNativeInt
     >>> from pyccel.ast.literals  import LiteralInteger
     >>> from pyccel.ast.utilities import expand_to_loops
     >>> from pyccel.ast.variable  import Variable
-    >>> a = Variable('int', 'a', shape=(,), rank=0)
-    >>> b = Variable('int', 'b', shape=(,), rank=0)
-    >>> c = Variable('int', 'c', shape=(,), rank=0)
+    >>> a = Variable(PythonNativeInt(), 'a', shape=(,), rank=0)
+    >>> b = Variable(PythonNativeInt(), 'b', shape=(,), rank=0)
+    >>> c = Variable(PythonNativeInt(), 'c', shape=(,), rank=0)
     >>> expr = [Assign(PythonTuple(a,b,c),PythonTuple(LiteralInteger(0),LiteralInteger(1),LiteralInteger(2))]
     >>> expand_inhomog_tuple_assignments(CodeBlock(expr))
     [Assign(a, LiteralInteger(0)), Assign(b, LiteralInteger(1)), Assign(c, LiteralInteger(2))]
@@ -688,8 +689,8 @@ def expand_inhomog_tuple_assignments(block, language_has_vectors = False):
     if not language_has_vectors:
         allocs_to_unravel = [a for a in block.get_attribute_nodes(Assign) \
                     if isinstance(a.lhs, Variable) \
-                    and isinstance(a.lhs.class_type, NativeHomogeneousTuple) \
-                    and isinstance(a.rhs.class_type, NativeHomogeneousTuple)]
+                    and isinstance(a.lhs.class_type, HomogeneousTupleType) \
+                    and isinstance(a.rhs.class_type, HomogeneousTupleType)]
         new_allocs = [(Assign(a.lhs, NumpyEmpty(a.lhs.shape,
                                      dtype=a.lhs.dtype,
                                      order=a.lhs.order)
diff --git a/pyccel/ast/variable.py b/pyccel/ast/variable.py
index 9800233a31..4d785dd8b1 100644
--- a/pyccel/ast/variable.py
+++ b/pyccel/ast/variable.py
@@ -13,14 +13,13 @@
 from pyccel.utilities.stage import PyccelStage
 
 from .basic     import PyccelAstNode, TypedAstNode
-from .datatypes import (datatype, DataType,
-                        NativeInteger, NativeBool, NativeFloat,
-                        NativeComplex, NativeHomogeneousTuple, NativeInhomogeneousTuple)
-from .internals import PyccelArrayShapeElement, Slice, get_final_precision, PyccelSymbol
+from .datatypes import PyccelType
+from .internals import PyccelArrayShapeElement, Slice, PyccelSymbol
 from .internals import apply_pickle
 from .literals  import LiteralInteger, Nil, LiteralEllipsis
 from .operators import (PyccelMinus, PyccelDiv, PyccelMul,
                         PyccelUnarySub, PyccelAdd)
+from .numpytypes import NumpyNDArrayType
 
 errors = Errors()
 pyccel_stage = PyccelStage()
@@ -45,19 +44,13 @@ class Variable(TypedAstNode):
 
     Parameters
     ----------
-    dtype : str, DataType
-        The type of the variable. Can be either a DataType,
-        or a str (bool, int, float).
+    class_type : PyccelType
+        The Python type of the variable.
 
     name : str, list, DottedName
         The name of the variable represented. This can be either a string
         or a dotted name, when using a Class attribute.
 
-    class_type : DataType
-        The Python type of the variable. In the case of scalars this is equivalent to
-        the datatype. For objects in (homogeneous) containers (e.g. list/ndarray/tuple),
-        this is the type of the container.
-
     rank : int, default: 0
         The number of dimensions for an array.
 
@@ -89,9 +82,6 @@ class Variable(TypedAstNode):
         Used for arrays. Indicates whether the data is stored in C or Fortran format in memory.
         See order_docs.md in the developer docs for more details.
 
-    precision : str, default: 0
-        Precision of the data type.
-
     is_argument : bool, default: False
         Indicates if object is the argument of a function.
 
@@ -116,16 +106,15 @@ class Variable(TypedAstNode):
     """
     __slots__ = ('_name', '_alloc_shape', '_memory_handling', '_is_const',
             '_is_target', '_is_optional', '_allows_negative_indexes',
-            '_cls_base', '_is_argument', '_is_temp','_dtype','_precision',
+            '_cls_base', '_is_argument', '_is_temp',
             '_rank','_shape','_order','_is_private','_class_type')
     _attribute_nodes = ()
 
     def __init__(
         self,
-        dtype,
+        class_type,
         name,
         *,
-        class_type = None,
         rank=0,
         memory_handling='stack',
         is_const=False,
@@ -135,7 +124,6 @@ def __init__(
         shape=None,
         cls_base=None,
         order=None,
-        precision=0,
         is_argument=False,
         is_temp =False,
         allows_negative_indexes=False
@@ -188,14 +176,8 @@ def __init__(
         self._is_temp        = is_temp
 
         # ------------ TypedAstNode Properties ---------------
-        if isinstance(dtype, str) or str(dtype) == '*':
-
-            dtype = datatype(dtype)
-        elif not isinstance(dtype, DataType):
-            raise TypeError('datatype must be an instance of DataType.')
-
-        if not isinstance(rank, int):
-            raise TypeError('rank must be an instance of int.')
+        assert isinstance(class_type, PyccelType)
+        assert isinstance(rank, int)
 
         if rank == 0:
             assert shape is None
@@ -211,26 +193,10 @@ def __init__(
         elif rank > 1:
             assert order in ('C', 'F')
 
-        if not precision:
-            if isinstance(dtype, (NativeInteger, NativeFloat, NativeComplex, NativeBool)):
-                precision = -1
-        if not isinstance(precision,int) and precision is not None:
-            raise TypeError('precision must be an integer or None.')
-
-        if rank > 0 and class_type is None:
-            raise TypeError("Multi-dimensional object requires a container type")
-        elif class_type is None:
-            class_type = dtype
-
-        if class_type is not dtype and cls_base is None:
-            raise TypeError(f"Missing class definition for type {dtype}")
-
         self._alloc_shape = shape
-        self._dtype = dtype
+        self._class_type = class_type
         self._rank  = rank
         self._shape = self.process_shape(shape)
-        self._precision = precision
-        self._class_type = class_type
         if self._rank < 2:
             self._order = None
 
@@ -458,21 +424,20 @@ def declare_as_argument(self):
 
     @property
     def is_ndarray(self):
-        """user friendly method to check if the variable is an ndarray:
-            1. have a rank > 0
-            2. dtype is one among {int, bool, float, complex}
         """
+        User friendly method to check if the variable is a numpy.ndarray.
 
-        if self.rank == 0:
-            return False
-        return isinstance(self.dtype, (NativeInteger, NativeBool,
-                          NativeFloat, NativeComplex))
+        User friendly method to check if the variable is an ndarray:
+            1. have a rank > 0
+            2. class type is NumpyNDArrayType
+        """
+        return isinstance(self.class_type, NumpyNDArrayType) and self.rank > 0
 
     def __str__(self):
         return str(self.name)
 
     def __repr__(self):
-        return '{}({}, dtype={})'.format(type(self).__name__, repr(self.name), repr(self.dtype))
+        return f'{type(self).__name__}({self.name}, type={self.class_type})'
 
     def __eq__(self, other):
         if type(self) is type(other):
@@ -483,29 +448,25 @@ def __hash__(self):
         return hash((type(self).__name__, self._name))
 
     def inspect(self):
-        """inspects the variable."""
+        """
+        Print a short summary of the Variable and its parameters.
+
+        Print a short summary of the Variable and its parameters.
+        This function is useful for debugging.
+        """
 
         print('>>> Variable')
-        print( '  name               = {}'.format(self.name))
-        print( '  dtype              = {}'.format(self.dtype))
-        print( '  precision          = {}'.format(get_final_precision(self)))
-        print( '  rank               = {}'.format(self.rank))
-        print( '  order              = {}'.format(self.order))
-        print( '  memory_handling    = {}'.format(self.memory_handling))
-        print( '  shape              = {}'.format(self.shape))
-        print( '  cls_base           = {}'.format(self.cls_base))
-        print( '  is_target          = {}'.format(self.is_target))
-        print( '  is_optional        = {}'.format(self.is_optional))
+        print(f'  name               = {self.name}')
+        print(f'  type               = {self.class_type}')
+        print(f'  rank               = {self.rank}')
+        print(f'  order              = {self.order}')
+        print(f'  memory_handling    = {self.memory_handling}')
+        print(f'  shape              = {self.shape}')
+        print(f'  cls_base           = {self.cls_base}')
+        print(f'  is_target          = {self.is_target}')
+        print(f'  is_optional        = {self.is_optional}')
         print( '<<<')
 
-    def use_exact_precision(self):
-        """
-        Change precision from default python precision to
-        equivalent numpy precision
-        """
-        if not self._is_argument:
-            self._precision = get_final_precision(self)
-
     def clone(self, name, new_class = None, **kwargs):
         """
         Create a clone of the current variable.
@@ -571,13 +532,13 @@ def __reduce_ex__(self, i):
            and its arguments.
         """
         args = (
-            self.dtype,
+            self.class_type,
             self.name)
         kwargs = {
             'rank' : self.rank,
             'memory_handling': self.memory_handling,
             'is_optional':self.is_optional,
-            'shape':self.shape,
+            'order':self.order,
             'cls_base':self.cls_base,
             }
 
@@ -683,7 +644,7 @@ class InhomogeneousTupleVariable(Variable):
         The name of the variable.
     *args : tuple
         See Variable.
-    class_type : DataType
+    class_type : PyccelType
         The Python type of the variable. In the case of scalars this is equivalent to
         the datatype. For objects in (homogeneous) containers (e.g. list/ndarray/tuple),
         this is the type of the container.
@@ -705,7 +666,7 @@ class InhomogeneousTupleVariable(Variable):
 
     def __init__(self, arg_vars, name, *args, class_type, **kwargs):
         self._vars = tuple(arg_vars)
-        super().__init__(class_type, name, *args, **kwargs, class_type = class_type)
+        super().__init__(class_type, name, *args, **kwargs)
 
     def get_vars(self):
         """ Get the variables saved internally in the tuple
@@ -787,24 +748,29 @@ def is_ndarray(self):
         return False
 
 class Constant(Variable):
-
     """
-    Class for expressing constant values (e.g. pi)
+    Class for expressing constant values (e.g. pi).
+
+    Class for expressing constant values (e.g. pi).
 
     Parameters
     ----------
-    *args, **kwargs : See pyccel.ast.variable.Variable
+    *args : tuple
+        See pyccel.ast.variable.Variable.
+
+    value : bool|int|float|complex
+        The value that the constant represents.
 
-    value : Type matching dtype
-            The value that the constant represents
+    **kwargs : dict
+        See pyccel.ast.variable.Variable.
 
     Examples
     --------
+    >>> from pyccel.ast.datatypes import PythonNativeFloat
     >>> from pyccel.ast.variable import Constant
     >>> import math
-    >>> Constant('float', 'pi' , value=math.pi )
-    Constant('pi', dtype=NativeFloat())
-
+    >>> Constant(PythonNativeFloat(), 'pi' , value=math.pi )
+    Constant('pi', type=NativeFloat())
     """
     __slots__ = ('_value',)
     # The value of a constant is not a translated object
@@ -854,15 +820,16 @@ class IndexedElement(TypedAstNode):
     Examples
     --------
     >>> from pyccel.ast.core import Variable, IndexedElement
-    >>> A = Variable('A', dtype='int', shape=(2,3), rank=2)
-    >>> i = Variable('i', dtype='int')
-    >>> j = Variable('j', dtype='int')
+    >>> from pyccel.ast.datatypes import PythonNativeInt
+    >>> A = Variable(PythonNativeInt(), 'A', shape=(2,3), rank=2)
+    >>> i = Variable(PythonNativeInt(), 'i')
+    >>> j = Variable(PythonNativeInt(), 'j')
     >>> IndexedElement(A, (i, j))
     IndexedElement(A, i, j)
     >>> IndexedElement(A, i, j) == A[i, j]
     True
     """
-    __slots__ = ('_label', '_indices','_dtype','_precision','_shape','_rank','_order','_class_type')
+    __slots__ = ('_label', '_indices','_shape','_rank','_order','_class_type')
     _attribute_nodes = ('_label', '_indices', '_shape')
 
     def __init__(self, base, *indices):
@@ -877,9 +844,6 @@ def __init__(self, base, *indices):
             super().__init__()
             return
 
-        self._dtype = base.dtype
-        self._precision = base.precision
-
         shape = base.shape
         rank  = base.rank
 
@@ -921,12 +885,15 @@ def __init__(self, base, *indices):
                 new_shape.append(_shape)
         self._rank  = len(new_shape)
         self._shape = None if self._rank == 0 else tuple(new_shape)
-        self._order = None if self.rank < 2 else base.order
 
-        if self.rank == 0:
-            self._class_type = self.dtype
-        else:
-            self._class_type = base.class_type
+        base_type = base.class_type
+        rank = base.rank
+        for _ in range(base.rank-self._rank):
+            rank -= 1
+            if not (rank and isinstance(base_type, NumpyNDArrayType)):
+                base_type = base_type.element_type
+        self._class_type = base_type
+        self._order = None if self.rank < 2 else base.order
 
         super().__init__()
 
@@ -983,8 +950,9 @@ def is_const(self):
         return self.base.is_const
 
 class DottedVariable(Variable):
-
     """
+    Class representing a dotted variable.
+
     Represents a dotted variable. This is usually
     a variable which is a member of a class
 
@@ -992,8 +960,18 @@ class DottedVariable(Variable):
     a = AClass()
     a.b = 3
 
-    In this case b is a DottedVariable where the lhs
-    is a
+    In this case b is a DottedVariable where the lhs is a.
+
+    Parameters
+    ----------
+    *args : tuple
+        See pyccel.ast.variable.Variable.
+
+    lhs : Variable
+        The Variable on the right of the '.'.
+
+    **kwargs : dict
+        See pyccel.ast.variable.Variable.
     """
     __slots__ = ('_lhs',)
     _attribute_nodes = ('_lhs',)
@@ -1028,9 +1006,9 @@ def __str__(self):
     def __repr__(self):
         lhs = repr(self.lhs)
         name = str(self.name)
-        dtype = repr(self.dtype)
+        class_type = repr(self.class_type)
         classname = type(self).__name__
-        return f'{classname}({lhs}.{name}, dtype={dtype}'
+        return f'{classname}({lhs}.{name}, type={class_type})'
 
 class AnnotatedPyccelSymbol(PyccelAstNode):
     """
diff --git a/pyccel/codegen/printing/ccode.py b/pyccel/codegen/printing/ccode.py
index c332ab4344..c083dbbbbe 100644
--- a/pyccel/codegen/printing/ccode.py
+++ b/pyccel/codegen/printing/ccode.py
@@ -25,11 +25,13 @@
 from pyccel.ast.operators import PyccelAssociativeParenthesis, PyccelMod
 from pyccel.ast.operators import PyccelUnarySub, IfTernaryOperator
 
-from pyccel.ast.datatypes import NativeInteger, NativeBool, NativeComplex, NativeVoid
-from pyccel.ast.datatypes import NativeFloat, NativeTuple, datatype, default_precision
-from pyccel.ast.datatypes import CustomDataType, NativeString, NativeHomogeneousTuple
+from pyccel.ast.datatypes import PythonNativeInt, PythonNativeBool, VoidType
+from pyccel.ast.datatypes import TupleType, FixedSizeNumericType
+from pyccel.ast.datatypes import CustomDataType, StringType, HomogeneousTupleType
+from pyccel.ast.datatypes import PrimitiveBooleanType, PrimitiveIntegerType, PrimitiveFloatingPointType, PrimitiveComplexType
+from pyccel.ast.datatypes import HomogeneousContainerType
 
-from pyccel.ast.internals import Slice, PrecomputedCode, get_final_precision, PyccelArrayShapeElement
+from pyccel.ast.internals import Slice, PrecomputedCode, PyccelArrayShapeElement
 
 from pyccel.ast.literals  import LiteralTrue, LiteralFalse, LiteralImaginaryUnit, LiteralFloat
 from pyccel.ast.literals  import LiteralString, LiteralInteger, Literal
@@ -40,6 +42,10 @@
 from pyccel.ast.numpyext import NumpyFull, NumpyArray
 from pyccel.ast.numpyext import NumpyReal, NumpyImag, NumpyFloat, NumpySize
 
+from pyccel.ast.numpytypes import NumpyInt8Type, NumpyInt16Type, NumpyInt32Type, NumpyInt64Type
+from pyccel.ast.numpytypes import NumpyFloat32Type, NumpyFloat64Type, NumpyComplex64Type, NumpyComplex128Type
+from pyccel.ast.numpytypes import NumpyNDArrayType, numpy_precision_map
+
 from pyccel.ast.utilities import expand_to_loops
 
 from pyccel.ast.variable import IndexedElement
@@ -48,7 +54,7 @@
 from pyccel.ast.variable import DottedVariable
 from pyccel.ast.variable import InhomogeneousTupleVariable
 
-from pyccel.ast.c_concepts import ObjectAddress, CMacro, CStringExpression, PointerCast
+from pyccel.ast.c_concepts import ObjectAddress, CMacro, CStringExpression, PointerCast, CNativeInt
 
 from pyccel.codegen.printing.codeprinter import CodePrinter
 
@@ -255,40 +261,38 @@ class CCodePrinter(CodePrinter):
         'tabwidth': 4,
     }
 
-    dtype_registry = {(NativeFloat(),8)   : 'double',
-                      (NativeFloat(),4)   : 'float',
-                      (NativeComplex(),8) : 'double complex',
-                      (NativeComplex(),4) : 'float complex',
-                      (NativeInteger(),4)     : 'int32_t',
-                      (NativeInteger(),8)     : 'int64_t',
-                      (NativeInteger(),2)     : 'int16_t',
-                      (NativeInteger(),1)     : 'int8_t',
-                      (NativeInteger(),-2)    : 'int',
-                      (NativeBool(),-1) : 'bool',
-                      (NativeVoid(), 0) : 'void',
+    dtype_registry = {CNativeInt()    : 'int',
+                      VoidType() : 'void',
+                      (PrimitiveComplexType(),8) : 'double complex',
+                      (PrimitiveComplexType(),4) : 'float complex',
+                      (PrimitiveFloatingPointType(),8)   : 'double',
+                      (PrimitiveFloatingPointType(),4)   : 'float',
+                      (PrimitiveIntegerType(),4)     : 'int32_t',
+                      (PrimitiveIntegerType(),8)     : 'int64_t',
+                      (PrimitiveIntegerType(),2)     : 'int16_t',
+                      (PrimitiveIntegerType(),1)     : 'int8_t',
+                      (PrimitiveBooleanType(),-1) : 'bool',
                       }
 
     ndarray_type_registry = {
-                      (NativeFloat(),8)   : 'nd_double',
-                      (NativeFloat(),4)   : 'nd_float',
-                      (NativeComplex(),8) : 'nd_cdouble',
-                      (NativeComplex(),4) : 'nd_cfloat',
-                      (NativeInteger(),8)     : 'nd_int64',
-                      (NativeInteger(),4)     : 'nd_int32',
-                      (NativeInteger(),2)     : 'nd_int16',
-                      (NativeInteger(),1)     : 'nd_int8',
-                      (NativeBool(),-1)   : 'nd_bool'}
-
-    type_to_format = {(NativeFloat(),8)   : '%.12lf',
-                      (NativeFloat(),4)   : '%.12f',
-                      (NativeComplex(),8) : '(%.12lf + %.12lfj)',
-                      (NativeComplex(),4) : '(%.12f + %.12fj)',
-                      (NativeInteger(),4)     : '%d',
-                      (NativeInteger(),8)     : LiteralString("%") + CMacro('PRId64'),
-                      (NativeInteger(),2)     : LiteralString("%") + CMacro('PRId16'),
-                      (NativeInteger(),1)     : LiteralString("%") + CMacro('PRId8'),
-                      (NativeBool(),-1)   : '%s',
-                      (NativeString(), 0) : '%s'}
+                      NumpyFloat64Type()    : 'nd_double',
+                      NumpyFloat32Type()    : 'nd_float',
+                      NumpyComplex128Type() : 'nd_cdouble',
+                      NumpyComplex64Type()  : 'nd_cfloat',
+                      NumpyInt64Type()      : 'nd_int64',
+                      NumpyInt32Type()      : 'nd_int32',
+                      NumpyInt16Type()      : 'nd_int16',
+                      NumpyInt8Type()       : 'nd_int8',
+                      PythonNativeBool()    : 'nd_bool'}
+
+    type_to_format = {(PrimitiveFloatingPointType(),8) : '%.15lf',
+                      (PrimitiveFloatingPointType(),4) : '%.6f',
+                      (PrimitiveIntegerType(),4)       : '%d',
+                      (PrimitiveIntegerType(),8)       : LiteralString("%") + CMacro('PRId64'),
+                      (PrimitiveIntegerType(),2)       : LiteralString("%") + CMacro('PRId16'),
+                      (PrimitiveIntegerType(),1)       : LiteralString("%") + CMacro('PRId8'),
+                      StringType()                  : '%s',
+                      }
 
     def __init__(self, filename, prefix_module = None):
 
@@ -361,7 +365,8 @@ def is_c_pointer(self, a):
 
         if not isinstance(a, Variable):
             return False
-        return (a.is_alias and not a.is_ndarray) or a.is_optional or \
+        return (a.is_alias and not isinstance(a.class_type, HomogeneousContainerType)) \
+                or a.is_optional or \
                 any(a is bi for b in self._additional_args for bi in b)
 
     #========================== Numpy Elements ===============================#
@@ -397,9 +402,16 @@ def copy_NumpyArray_Data(self, expr):
             return f"array_copy_data({lhs_address}, {self._print(arg)}, 0);\n"
 
         order = lhs.order
-        rhs_dtype = rhs.dtype
-        declare_dtype = self.find_in_dtype_registry(rhs_dtype, rhs.precision)
-        dtype = self.find_in_ndarray_type_registry(rhs_dtype, rhs.precision)
+        lhs_dtype = lhs.dtype
+        declare_dtype = self.find_in_dtype_registry(lhs_dtype)
+        if isinstance(lhs.class_type, NumpyNDArrayType):
+            #set dtype to the C struct types
+            dtype = self.find_in_ndarray_type_registry(lhs_dtype)
+        elif isinstance(lhs.class_type, HomogeneousTupleType):
+            dtype = self.find_in_ndarray_type_registry(numpy_precision_map[
+                        (lhs_dtype.primitive_type, lhs_dtype.precision)])
+        else:
+            raise NotImplementedError(f"Don't know how to index {lhs.class_type} type")
 
         flattened_list = self._flatten_list(arg)
         operations = ""
@@ -418,7 +430,7 @@ def copy_NumpyArray_Data(self, expr):
         num_elements = len(flattened_list)
         # Get the offset variable if it is needed
         if num_elements != 1 and not all(v.rank == 0 for v in flattened_list):
-            offset_var = self.scope.get_temporary_variable(NativeInteger(), 'offset')
+            offset_var = self.scope.get_temporary_variable(PythonNativeInt(), 'offset')
             operations += self._print(Assign(offset_var, LiteralInteger(0)))
         else:
             offset_var = LiteralInteger(0)
@@ -453,7 +465,7 @@ def copy_NumpyArray_Data(self, expr):
                 operations += f"{declare_dtype} {dummy_array_name}[] = {subset_str};\n"
 
                 copy_to_data = self._print(copy_to_data_var)
-                type_size = self._print(DottedVariable(NativeVoid(), 'type_size', lhs=copy_to))
+                type_size = self._print(DottedVariable(VoidType(), 'type_size', lhs=copy_to))
                 operations += f"memcpy(&{copy_to_data}[{offset_str}], {dummy_array_name}, {lenSubset} * {type_size});\n"
 
                 i += lenSubset
@@ -483,7 +495,7 @@ def arrayFill(self, expr):
         rhs = expr.rhs
         lhs = expr.lhs
         code_init = ''
-        declare_dtype = self.find_in_dtype_registry(rhs.dtype, rhs.precision)
+        declare_dtype = self.find_in_dtype_registry(rhs.dtype)
 
         if rhs.fill_value is not None:
             if isinstance(rhs.fill_value, Literal):
@@ -511,12 +523,17 @@ def _init_stack_array(self, expr):
             String containing the rhs of the initialization of a stack array.
         """
         var = expr
-        dtype = self.find_in_dtype_registry(var.dtype, var.precision)
-        np_dtype = self.find_in_ndarray_type_registry(var.dtype, var.precision)
+        dtype = self.find_in_dtype_registry(var.dtype)
+        if isinstance(var.class_type, NumpyNDArrayType):
+            np_dtype = self.find_in_ndarray_type_registry(var.dtype)
+        elif isinstance(var.class_type, HomogeneousContainerType):
+            np_dtype = self.find_in_ndarray_type_registry(numpy_precision_map[(var.dtype.primitive_type, var.dtype.precision)])
+        else:
+            raise NotImplementedError(f"Don't know how to index {expr.class_type} type")
         shape = ", ".join(self._print(i) for i in var.alloc_shape)
         tot_shape = self._print(functools.reduce(
             lambda x,y: PyccelMul(x,y,simplify=True), var.alloc_shape))
-        declare_dtype = self.find_in_dtype_registry(NativeInteger(), 8)
+        declare_dtype = self.find_in_dtype_registry(NumpyInt64Type())
 
         dummy_array_name = self.scope.get_new_name('array_dummy')
         buffer_array = "{dtype} {name}[{size}];\n".format(
@@ -630,10 +647,10 @@ def _handle_inline_func_call(self, expr):
     # ============ Elements ============ #
 
     def _print_PythonAbs(self, expr):
-        if expr.arg.dtype is NativeFloat():
+        if expr.arg.dtype.primitive_type is PrimitiveFloatingPointType():
             self.add_import(c_imports['math'])
             func = "fabs"
-        elif expr.arg.dtype is NativeComplex():
+        elif expr.arg.dtype.primitive_type is PrimitiveComplexType():
             self.add_import(c_imports['complex'])
             func = "cabs"
         else:
@@ -642,13 +659,13 @@ def _print_PythonAbs(self, expr):
 
     def _print_PythonMin(self, expr):
         arg = expr.args[0]
-        if arg.dtype is NativeFloat() and len(arg) == 2:
+        if arg.dtype.primitive_type is PrimitiveFloatingPointType() and len(arg) == 2:
             self.add_import(c_imports['math'])
             return "fmin({}, {})".format(self._print(arg[0]),
                                          self._print(arg[1]))
-        elif arg.dtype is NativeInteger() and len(arg) == 2:
-            arg1 = self.scope.get_temporary_variable(NativeInteger())
-            arg2 = self.scope.get_temporary_variable(NativeInteger())
+        elif arg.dtype.primitive_type is PrimitiveIntegerType() and len(arg) == 2:
+            arg1 = self.scope.get_temporary_variable(PythonNativeInt())
+            arg2 = self.scope.get_temporary_variable(PythonNativeInt())
             assign1 = Assign(arg1, arg[0])
             assign2 = Assign(arg2, arg[1])
             self._additional_code += self._print(assign1)
@@ -660,13 +677,13 @@ def _print_PythonMin(self, expr):
 
     def _print_PythonMax(self, expr):
         arg = expr.args[0]
-        if arg.dtype is NativeFloat() and len(arg) == 2:
+        if arg.dtype.primitive_type is PrimitiveFloatingPointType() and len(arg) == 2:
             self.add_import(c_imports['math'])
             return "fmax({}, {})".format(self._print(arg[0]),
                                          self._print(arg[1]))
-        elif arg.dtype is NativeInteger() and len(arg) == 2:
-            arg1 = self.scope.get_temporary_variable(NativeInteger())
-            arg2 = self.scope.get_temporary_variable(NativeInteger())
+        elif arg.dtype.primitive_type is PrimitiveIntegerType() and len(arg) == 2:
+            arg1 = self.scope.get_temporary_variable(PythonNativeInt())
+            arg2 = self.scope.get_temporary_variable(PythonNativeInt())
             assign1 = Assign(arg1, arg[0])
             assign2 = Assign(arg2, arg[1])
             self._additional_code += self._print(assign1)
@@ -678,7 +695,8 @@ def _print_PythonMax(self, expr):
 
     def _print_SysExit(self, expr):
         code = ""
-        if expr.status.dtype is not NativeInteger() or expr.status.rank > 0:
+        if not isinstance(getattr(expr.status.dtype, 'primitive_type', None), PrimitiveIntegerType) \
+                or expr.status.rank > 0:
             print_arg = FunctionCallArgument(expr.status)
             code = self._print(PythonPrint((print_arg, ), file="stderr"))
             arg = "1"
@@ -688,13 +706,13 @@ def _print_SysExit(self, expr):
 
     def _print_PythonFloat(self, expr):
         value = self._print(expr.arg)
-        type_name = self.find_in_dtype_registry(NativeFloat(), expr.precision)
+        type_name = self.find_in_dtype_registry(expr.dtype)
         return '({0})({1})'.format(type_name, value)
 
     def _print_PythonInt(self, expr):
         self.add_import(c_imports['stdint'])
         value = self._print(expr.arg)
-        type_name = self.find_in_dtype_registry(NativeInteger(), expr.precision)
+        type_name = self.find_in_dtype_registry(expr.dtype)
         return '({0})({1})'.format(type_name, value)
 
     def _print_PythonBool(self, expr):
@@ -705,13 +723,13 @@ def _print_Literal(self, expr):
         return repr(expr.python_value)
 
     def _print_LiteralInteger(self, expr):
-        if isinstance(expr, LiteralInteger) and get_final_precision(expr) == 8:
+        if isinstance(expr, LiteralInteger) and getattr(expr.dtype, 'precision', -1) == 8:
             self.add_import(c_imports['stdint'])
             return f"INT64_C({repr(expr.python_value)})"
         return repr(expr.python_value)
 
     def _print_LiteralFloat(self, expr):
-        if isinstance(expr, LiteralFloat) and get_final_precision(expr) == 4:
+        if isinstance(expr, LiteralFloat) and expr.dtype.precision == 4:
             return f"{repr(expr.python_value)}f"
         return repr(expr.python_value)
 
@@ -728,7 +746,7 @@ def _print_PythonComplex(self, expr):
         else:
             value = self._print(PyccelAssociativeParenthesis(PyccelAdd(expr.real,
                             PyccelMul(expr.imag, LiteralImaginaryUnit()))))
-        type_name = self.find_in_dtype_registry(NativeComplex(), expr.precision)
+        type_name = self.find_in_dtype_registry(expr.dtype)
         return '({0})({1})'.format(type_name, value)
 
     def _print_LiteralImaginaryUnit(self, expr):
@@ -889,12 +907,12 @@ def _print_PyccelMod(self, expr):
         first = self._print(expr.args[0])
         second = self._print(expr.args[1])
 
-        if expr.dtype is NativeInteger():
+        if expr.dtype.primitive_type is PrimitiveIntegerType():
             return "pyc_modulo({n}, {base})".format(n=first, base=second)
 
-        if expr.args[0].dtype is NativeInteger():
+        if expr.args[0].dtype.primitive_type is PrimitiveIntegerType():
             first = self._print(NumpyFloat(expr.args[0]))
-        if expr.args[1].dtype is NativeInteger():
+        if expr.args[1].dtype.primitive_type is PrimitiveIntegerType():
             second = self._print(NumpyFloat(expr.args[1]))
         return "pyc_fmodulo({n}, {base})".format(n=first, base=second)
 
@@ -902,21 +920,17 @@ def _print_PyccelPow(self, expr):
         b = expr.args[0]
         e = expr.args[1]
 
-        if expr.dtype is NativeComplex():
-            b = self._print(b if b.dtype is NativeComplex() else PythonComplex(b))
-            e = self._print(e if e.dtype is NativeComplex() else PythonComplex(e))
+        if expr.dtype.primitive_type is PrimitiveComplexType():
+            b = self._print(b if b.dtype.primitive_type is PrimitiveComplexType() else PythonComplex(b))
+            e = self._print(e if e.dtype.primitive_type is PrimitiveComplexType() else PythonComplex(e))
             self.add_import(c_imports['complex'])
             return 'cpow({}, {})'.format(b, e)
 
         self.add_import(c_imports['math'])
-        b = self._print(b if b.dtype is NativeFloat() else NumpyFloat(b))
-        e = self._print(e if e.dtype is NativeFloat() else NumpyFloat(e))
+        b = self._print(b if b.dtype.primitive_type is PrimitiveFloatingPointType() else NumpyFloat(b))
+        e = self._print(e if e.dtype.primitive_type is PrimitiveFloatingPointType() else NumpyFloat(e))
         code = 'pow({}, {})'.format(b, e)
-        if expr.dtype is NativeInteger():
-            prec  = expr.precision
-            cast_type = self.find_in_dtype_registry(expr.dtype, prec)
-            return '({}){}'.format(cast_type, code)
-        return code
+        return self._cast_to(expr, expr.dtype).format(code)
 
     def _print_Import(self, expr):
         if expr.ignore:
@@ -989,16 +1003,28 @@ def get_print_format_and_arg(self, var):
             The code which should be printed in the arguments of the generated
             print expression to print the object.
         """
-        try:
-            arg_format = self.type_to_format[(var.dtype, get_final_precision(var))]
-        except KeyError:
-            errors.report("{} type is not supported currently".format(var.dtype), severity='fatal')
-        if var.dtype is NativeComplex():
-            arg = '{}, {}'.format(self._print(NumpyReal(var)), self._print(NumpyImag(var)))
-        elif var.dtype is NativeBool():
-            arg = '{} ? "True" : "False"'.format(self._print(var))
+        if isinstance(var.dtype, FixedSizeNumericType):
+            primitive_type = var.dtype.primitive_type
+            if isinstance(primitive_type, PrimitiveComplexType):
+                _, real_part = self.get_print_format_and_arg(NumpyReal(var))
+                float_format, imag_part = self.get_print_format_and_arg(NumpyImag(var))
+                return f'({float_format} + {float_format}j)', f'{real_part}, {imag_part}'
+            elif isinstance(primitive_type, PrimitiveBooleanType):
+                return self.get_print_format_and_arg(IfTernaryOperator(var, LiteralString("True"), LiteralString("False")))
+            else:
+                try:
+                    arg_format = self.type_to_format[(primitive_type, var.dtype.precision)]
+                except KeyError:
+                    errors.report(f"Printing {var.dtype} type is not supported currently", severity='fatal')
+                arg = self._print(var)
         else:
+            try:
+                arg_format = self.type_to_format[var.dtype]
+            except KeyError:
+                errors.report(f"Printing {var.dtype} type is not supported currently", severity='fatal')
+
             arg = self._print(var)
+
         return arg_format, arg
 
     def _print_CStringExpression(self, expr):
@@ -1061,7 +1087,7 @@ def formatted_args_to_printf(args_format, args, end):
             if isinstance(f, PythonType):
                 f = f.print_string
 
-            if isinstance(f, FunctionCall) and isinstance(f.dtype, NativeTuple):
+            if isinstance(f, FunctionCall) and isinstance(f.class_type, TupleType):
                 tmp_list = [self.scope.get_temporary_variable(a.var.dtype) for a in f.funcdef.results]
                 tmp_arg_format_list = []
                 for a in tmp_list:
@@ -1077,7 +1103,7 @@ def formatted_args_to_printf(args_format, args, end):
                     code += formatted_args_to_printf(args_format, args, sep)
                     args_format = []
                     args = []
-                for_index = self.scope.get_temporary_variable(NativeInteger(), name = 'i')
+                for_index = self.scope.get_temporary_variable(PythonNativeInt(), name = 'i')
                 max_index = PyccelMinus(f.shape[0], LiteralInteger(1), simplify = True)
                 for_range = PythonRange(max_index)
                 print_body = [ FunctionCallArgument(f[for_index]) ]
@@ -1104,7 +1130,7 @@ def formatted_args_to_printf(args_format, args, end):
             code += formatted_args_to_printf(args_format, args, end)
         return code
 
-    def find_in_dtype_registry(self, dtype, prec):
+    def find_in_dtype_registry(self, dtype):
         """
         Find the corresponding C dtype in the dtype_registry.
 
@@ -1116,37 +1142,39 @@ def find_in_dtype_registry(self, dtype, prec):
         dtype : DataType
             The data type of the expression.
 
-        prec : int
-            The precision of the expression.
-
         Returns
         -------
         str
             The code which declares the datatype in C.
         """
-        if prec == -1:
-            prec = default_precision[dtype]
-
-        if dtype is NativeBool():
-            self.add_import(c_imports['stdbool'])
-        elif dtype is NativeInteger():
-            self.add_import(c_imports['stdint'])
-        elif dtype is NativeComplex():
-            self.add_import(c_imports['complex'])
+        if isinstance(dtype, FixedSizeNumericType):
+            primitive_type = dtype.primitive_type
+            if isinstance(primitive_type, PrimitiveComplexType):
+                self.add_import(c_imports['complex'])
+                return f'{self.find_in_dtype_registry(dtype.element_type)} complex'
+            elif isinstance(primitive_type, PrimitiveIntegerType):
+                self.add_import(c_imports['stdint'])
+            elif isinstance(dtype, PythonNativeBool):
+                self.add_import(c_imports['stdbool'])
+                return 'bool'
+
+            key = (primitive_type, dtype.precision)
+        else:
+            key = dtype
 
         try :
-            return self.dtype_registry[(dtype, prec)]
+            return self.dtype_registry[key]
         except KeyError:
-            errors.report(PYCCEL_RESTRICTION_TODO,
-                    symbol = "{}[kind = {}]".format(dtype, prec),
+            raise errors.report(PYCCEL_RESTRICTION_TODO, #pylint: disable=raise-missing-from
+                    symbol = dtype,
                     severity='fatal')
 
-    def find_in_ndarray_type_registry(self, dtype, prec):
+    def find_in_ndarray_type_registry(self, dtype):
         """
         Find the descriptor for the datatype in the ndarray_type_registry.
 
         Find the tag which allows the user to access data of the specified
-        type and precision within a ndarray.
+        type within a ndarray.
         Raise PYCCEL_RESTRICTION_TODO if not found.
 
         Parameters
@@ -1154,22 +1182,16 @@ def find_in_ndarray_type_registry(self, dtype, prec):
         dtype : DataType
             The data type of the expression.
 
-        prec : int
-            The precision of the expression.
-
         Returns
         -------
         str
             The code which declares the datatype in C.
         """
-        if prec == -1:
-            prec = default_precision[dtype]
-
         try :
-            return self.ndarray_type_registry[(dtype, prec)]
+            return self.ndarray_type_registry[dtype]
         except KeyError:
-            errors.report(PYCCEL_RESTRICTION_TODO,
-                    symbol = "{}[kind = {}]".format(dtype, prec),
+            raise errors.report(PYCCEL_RESTRICTION_TODO, #pylint: disable=raise-missing-from
+                    symbol = dtype,
                     severity='fatal')
 
     def get_declare_type(self, expr):
@@ -1208,22 +1230,21 @@ def get_declare_type(self, expr):
         >>> self.get_declare_type(v)
         't_ndarray*'
         """
-        dtype = expr.dtype
-        prec  = expr.precision
+        class_type = expr.class_type
         rank  = expr.rank
 
         if rank > 0:
-            if expr.is_ndarray or isinstance(expr.class_type, NativeHomogeneousTuple):
+            if expr.is_ndarray or isinstance(expr.class_type, HomogeneousContainerType):
                 if expr.rank > 15:
                     errors.report(UNSUPPORTED_ARRAY_RANK, symbol=expr, severity='fatal')
                 self.add_import(c_imports['ndarrays'])
                 dtype = 't_ndarray'
             else:
                 errors.report(PYCCEL_RESTRICTION_TODO+' (rank>0)', symbol=expr, severity='fatal')
-        elif not isinstance(dtype, CustomDataType):
-            dtype = self.find_in_dtype_registry(dtype, prec)
+        elif not isinstance(class_type, CustomDataType):
+            dtype = self.find_in_dtype_registry(class_type)
         else:
-            dtype = self._print(expr.dtype)
+            dtype = self._print(expr.class_type)
 
         if self.is_c_pointer(expr):
             return f'{dtype}*'
@@ -1305,11 +1326,11 @@ def function_signature(self, expr, print_arg_names = True):
         if n_results == 1:
             ret_type = self.get_declare_type(result_vars[0])
         elif n_results > 1:
-            ret_type = self.find_in_dtype_registry(NativeInteger(), -1)
+            ret_type = self.find_in_dtype_registry(PythonNativeInt())
             arg_vars.extend(result_vars)
             self._additional_args.append(result_vars) # Ensure correct result for is_c_pointer
         else:
-            ret_type = self.find_in_dtype_registry(NativeVoid(), 0)
+            ret_type = self.find_in_dtype_registry(VoidType())
 
         name = expr.name
         if not arg_vars:
@@ -1354,10 +1375,15 @@ def _print_IndexedElement(self, expr):
                         not isinstance(ind, LiteralInteger) and not isinstance(ind, Slice):
                     inds[i] = IfTernaryOperator(PyccelLt(ind, LiteralInteger(0)),
                         PyccelAdd(base_shape[i], ind, simplify = True), ind)
-        #set dtype to the C struct types
-        dtype = self.find_in_ndarray_type_registry(expr.dtype, expr.precision)
+        if isinstance(base.class_type, NumpyNDArrayType):
+            #set dtype to the C struct types
+            dtype = self.find_in_ndarray_type_registry(expr.dtype)
+        elif isinstance(base.class_type, HomogeneousContainerType):
+            dtype = self.find_in_ndarray_type_registry(numpy_precision_map[(expr.dtype.primitive_type, expr.dtype.precision)])
+        else:
+            raise NotImplementedError(f"Don't know how to index {expr.class_type} type")
         base_name = self._print(base)
-        if getattr(base, 'is_ndarray', False) or isinstance(base.class_type, NativeHomogeneousTuple):
+        if getattr(base, 'is_ndarray', False) or isinstance(base.class_type, HomogeneousContainerType):
             if expr.rank > 0:
                 #managing the Slice input
                 for i , ind in enumerate(inds):
@@ -1369,28 +1395,26 @@ def _print_IndexedElement(self, expr):
                             Slice.Element)
                 inds = [self._print(i) for i in inds]
                 return "array_slicing(%s, %s, %s)" % (base_name, expr.rank, ", ".join(inds))
-            inds = [self._cast_to(i, NativeInteger(), 8).format(self._print(i)) for i in inds]
+            inds = [self._cast_to(i, NumpyInt64Type()).format(self._print(i)) for i in inds]
         else:
             raise NotImplementedError(expr)
         return "GET_ELEMENT(%s, %s, %s)" % (base_name, dtype, ", ".join(inds))
 
 
-    def _cast_to(self, expr, dtype, precision):
+    def _cast_to(self, expr, dtype):
         """
         Add a cast to an expression when needed.
 
         Get a format string which provides the code to cast the object `expr`
-        to the specified dtype and precision. If the dtype and precision already
+        to the specified dtype. If the dtypes already
         match then the format string will simply print the expression.
 
         Parameters
         ----------
         expr : TypedAstNode
             The expression to be cast.
-        dtype : Datatype
+        dtype : PyccelType
             The target type of the cast.
-        precision : int
-            The target precision of the cast.
 
         Returns
         -------
@@ -1399,8 +1423,8 @@ def _cast_to(self, expr, dtype, precision):
             NB: You should insert the expression to be cast in the string
             after using this function.
         """
-        if (expr.dtype != dtype or expr.precision != precision):
-            cast=self.find_in_dtype_registry(dtype, precision)
+        if expr.dtype != dtype:
+            cast=self.find_in_dtype_registry(dtype)
             return '({}){{}}'.format(cast)
         return '{}'
 
@@ -1496,15 +1520,21 @@ def _print_Allocate(self, expr):
         if variable.rank > 0:
             #free the array if its already allocated and checking if its not null if the status is unknown
             if  (expr.status == 'unknown'):
-                shape_var = DottedVariable(NativeVoid(), 'shape', lhs = variable)
+                shape_var = DottedVariable(VoidType(), 'shape', lhs = variable)
                 free_code = f'if ({self._print(shape_var)} != NULL)\n'
                 free_code += "{{\n{}}}\n".format(self._print(Deallocate(variable)))
             elif (expr.status == 'allocated'):
                 free_code += self._print(Deallocate(variable))
             self.add_import(c_imports['ndarrays'])
             shape = ", ".join(self._print(i) for i in expr.shape)
-            dtype = self.find_in_ndarray_type_registry(variable.dtype, variable.precision)
-            shape_dtype = self.find_in_dtype_registry(NativeInteger(), 8)
+            if isinstance(variable.class_type, NumpyNDArrayType):
+                #set dtype to the C struct types
+                dtype = self.find_in_ndarray_type_registry(variable.dtype)
+            elif isinstance(variable.class_type, HomogeneousContainerType):
+                dtype = self.find_in_ndarray_type_registry(numpy_precision_map[(variable.dtype.primitive_type, variable.dtype.precision)])
+            else:
+                raise NotImplementedError(f"Don't know how to index {variable.class_type} type")
+            shape_dtype = self.find_in_dtype_registry(NumpyInt64Type())
             shape_Assign = "("+ shape_dtype +"[]){" + shape + "}"
             is_view = 'false' if variable.on_heap else 'true'
             order = "order_f" if expr.order == "F" else "order_c"
@@ -1527,7 +1557,7 @@ def _print_Deallocate(self, expr):
         if isinstance(expr.variable.dtype, CustomDataType):
             Pyccel__del = expr.variable.cls_base.scope.find('__del__').name
             return f"{Pyccel__del}({variable_address});\n"
-        elif expr.variable.is_ndarray:
+        elif isinstance(expr.variable.class_type, (NumpyNDArrayType, HomogeneousContainerType)):
             if expr.variable.is_alias:
                 return f'free_pointer({variable_address});\n'
             else:
@@ -1570,14 +1600,14 @@ def _print_NumpyUfuncBase(self, expr):
             errors.report(PYCCEL_RESTRICTION_TODO, severity='fatal')
         args = []
         for arg in expr.args:
-            if arg.dtype is NativeComplex():
+            if arg.dtype.primitive_type is PrimitiveComplexType():
                 self.add_import(c_imports['complex'])
                 try:
                     func_name = numpy_ufunc_to_c_complex[type_name]
                     args.append(self._print(arg))
                 except KeyError:
                     errors.report(INCOMPATIBLE_TYPEVAR_TO_FUNC.format(type_name) ,severity='fatal')
-            elif arg.dtype is not NativeFloat():
+            elif arg.dtype.primitive_type is not PrimitiveFloatingPointType():
                 args.append(self._print(NumpyFloat(arg)))
             else :
                 args.append(self._print(arg))
@@ -1607,13 +1637,13 @@ def _print_NumpySign(self, expr):
 
         """
         self.add_import(c_imports['numpy_c'])
-        dtype = expr.dtype
+        primitive_type = expr.dtype.primitive_type
         func = ''
-        if isinstance(dtype, NativeInteger):
+        if isinstance(primitive_type, PrimitiveIntegerType):
             func = 'isign'
-        elif isinstance(dtype, NativeFloat):
+        elif isinstance(primitive_type, PrimitiveFloatingPointType):
             func = 'fsign'
-        elif isinstance(dtype, NativeComplex):
+        elif isinstance(primitive_type, PrimitiveComplexType):
             func = 'csign'
 
         return f'{func}({self._print(expr.args[0])})'
@@ -1675,24 +1705,25 @@ def _print_MathFunctionBase(self, expr):
         if func_name.startswith("pyc"):
             self.add_import(c_imports['pyc_math_c'])
         else:
-            if expr.dtype is NativeComplex():
+            if expr.dtype.primitive_type is PrimitiveComplexType():
                 self.add_import(c_imports['complex'])
             else:
                 self.add_import(c_imports['math'])
-        if expr.dtype is NativeComplex():
+        if expr.dtype.primitive_type is PrimitiveComplexType():
             args = [self._print(a) for a in expr.args]
         else:
             args = []
             for arg in expr.args:
-                if arg.dtype is not NativeFloat() and not func_name.startswith("pyc"):
+                if arg.dtype.primitive_type is not PrimitiveFloatingPointType() \
+                        and not func_name.startswith("pyc"):
                     args.append(self._print(NumpyFloat(arg)))
                 else:
                     args.append(self._print(arg))
         code_args = ', '.join(args)
-        if expr.dtype is NativeInteger():
-            cast_type = self.find_in_dtype_registry(NativeInteger(), expr.precision)
-            return '({0}){1}({2})'.format(cast_type, func_name, code_args)
-        return '{0}({1})'.format(func_name, code_args)
+        if expr.dtype.primitive_type is PrimitiveIntegerType():
+            cast_type = self.find_in_dtype_registry(expr.dtype)
+            return f'({cast_type}){func_name}({code_args})'
+        return f'{func_name}({code_args})'
 
     def _print_MathIsfinite(self, expr):
         """Convert a Python expression with a math isfinite function call to C
@@ -1700,7 +1731,7 @@ def _print_MathIsfinite(self, expr):
         # add necessary include
         self.add_import(c_imports['math'])
         arg = expr.args[0]
-        if arg.dtype is NativeInteger():
+        if arg.dtype.primitive_type is PrimitiveIntegerType():
             code_arg = self._print(NumpyFloat(arg))
         else:
             code_arg = self._print(arg)
@@ -1712,7 +1743,7 @@ def _print_MathIsinf(self, expr):
         # add necessary include
         self.add_import(c_imports['math'])
         arg = expr.args[0]
-        if arg.dtype is NativeInteger():
+        if arg.dtype.primitive_type is PrimitiveIntegerType():
             code_arg = self._print(NumpyFloat(arg))
         else:
             code_arg = self._print(arg)
@@ -1724,7 +1755,7 @@ def _print_MathIsnan(self, expr):
         # add necessary include
         self.add_import(c_imports['math'])
         arg = expr.args[0]
-        if arg.dtype is NativeInteger():
+        if arg.dtype.primitive_type is PrimitiveIntegerType():
             code_arg = self._print(NumpyFloat(arg))
         else:
             code_arg = self._print(arg)
@@ -1736,7 +1767,7 @@ def _print_MathTrunc(self, expr):
         # add necessary include
         self.add_import(c_imports['math'])
         arg = expr.args[0]
-        if arg.dtype is NativeInteger():
+        if arg.dtype.primitive_type is PrimitiveIntegerType():
             code_arg = self._print(NumpyFloat(arg))
         else:
             code_arg = self._print(arg)
@@ -1768,19 +1799,18 @@ def _print_NumpySum(self, expr):
         '''
         if not isinstance(expr.arg, (NumpyArray, Variable, IndexedElement)):
             raise TypeError(f'Expecting a NumpyArray, given {type(expr.arg)}')
-        dtype, prec, name = (expr.arg.dtype,
-                             expr.arg.precision,
-                             self._print(expr.arg))
-        if prec == -1:
-            prec = default_precision[dtype]
+        dtype = expr.arg.dtype
+        primitive_type = dtype.primitive_type
+        prec  = dtype.precision
+        name  = self._print(expr.arg)
 
-        if isinstance(dtype, NativeInteger):
+        if isinstance(primitive_type, PrimitiveIntegerType):
             return f'numpy_sum_int{prec * 8}({name})'
-        elif isinstance(dtype, NativeFloat):
+        elif isinstance(primitive_type, PrimitiveFloatingPointType):
             return f'numpy_sum_float{prec * 8}({name})'
-        elif isinstance(dtype, NativeComplex):
+        elif isinstance(primitive_type, PrimitiveComplexType):
             return f'numpy_sum_complex{prec * 16}({name})'
-        elif isinstance(dtype, NativeBool):
+        elif isinstance(primitive_type, PrimitiveBooleanType):
             return f'numpy_sum_bool({name})'
         raise NotImplementedError('Sum not implemented for argument')
 
@@ -1789,15 +1819,16 @@ def _print_NumpyAmax(self, expr):
         Convert a call to numpy.max to the equivalent function in C.
         '''
         dtype = expr.arg.dtype
-        prec  = expr.arg.precision
+        primitive_type = dtype.primitive_type
+        prec  = dtype.precision
         name  = self._print(expr.arg)
-        if isinstance(dtype, NativeInteger):
+        if isinstance(primitive_type, PrimitiveIntegerType):
             return f'numpy_amax_int{prec * 8}({name})'
-        elif isinstance(dtype, NativeFloat):
+        elif isinstance(primitive_type, PrimitiveFloatingPointType):
             return f'numpy_amax_float{prec * 8}({name})'
-        elif isinstance(dtype, NativeComplex):
+        elif isinstance(primitive_type, PrimitiveComplexType):
             return f'numpy_amax_complex{prec * 16}({name})'
-        elif isinstance(dtype, NativeBool):
+        elif isinstance(primitive_type, PrimitiveBooleanType):
             return f'numpy_amax_bool({name})'
 
     def _print_NumpyAmin(self, expr):
@@ -1805,15 +1836,16 @@ def _print_NumpyAmin(self, expr):
         Convert a call to numpy.min to the equivalent function in C.
         '''
         dtype = expr.arg.dtype
-        prec  = expr.arg.precision
+        primitive_type = dtype.primitive_type
+        prec  = dtype.precision
         name  = self._print(expr.arg)
-        if isinstance(dtype, NativeInteger):
+        if isinstance(primitive_type, PrimitiveIntegerType):
             return f'numpy_amin_int{prec * 8}({name})'
-        elif isinstance(dtype, NativeFloat):
+        elif isinstance(primitive_type, PrimitiveFloatingPointType):
             return f'numpy_amin_float{prec * 8}({name})'
-        elif isinstance(dtype, NativeComplex):
+        elif isinstance(primitive_type, PrimitiveComplexType):
             return f'numpy_amin_complex{prec * 16}({name})'
-        elif isinstance(dtype, NativeBool):
+        elif isinstance(primitive_type, PrimitiveBooleanType):
             return f'numpy_amin_bool({name})'
 
     def _print_NumpyLinspace(self, expr):
@@ -1829,7 +1861,7 @@ def _print_NumpyLinspace(self, expr):
             else:
                 cond_template = lhs + ' = {cond} ? {stop} : ' + lhs
 
-        v = self._cast_to(expr.stop, expr.dtype, expr.precision).format(self._print(expr.stop))
+        v = self._cast_to(expr.stop, expr.dtype).format(self._print(expr.stop))
 
         init_value = template.format(
             start = self._print(expr.start),
@@ -2008,7 +2040,7 @@ def _print_PyccelMul(self, expr):
         return ' * '.join(self._print(a) for a in expr.args)
 
     def _print_PyccelDiv(self, expr):
-        if all(a.dtype is NativeInteger() for a in expr.args):
+        if all(a.dtype.primitive_type is PrimitiveIntegerType() for a in expr.args):
             args = [NumpyFloat(a) for a in expr.args]
         else:
             args = expr.args
@@ -2019,10 +2051,11 @@ def _print_PyccelFloorDiv(self, expr):
         # the result type of the floor division is dependent on the arguments
         # type, if all arguments are integers the result is integer otherwise
         # the result type is float
-        need_to_cast = all(a.dtype is NativeInteger() for a in expr.args)
-        code = ' / '.join(self._print(a if a.dtype is NativeFloat() else NumpyFloat(a)) for a in expr.args)
+        need_to_cast = all(a.dtype.primitive_type is PrimitiveIntegerType() for a in expr.args)
+        code = ' / '.join(self._print(a if a.dtype.primitive_type is PrimitiveFloatingPointType()
+                                        else NumpyFloat(a)) for a in expr.args)
         if (need_to_cast):
-            cast_type = self.find_in_dtype_registry(NativeInteger(), expr.precision)
+            cast_type = self.find_in_dtype_registry(expr.dtype)
             return "({})floor({})".format(cast_type, code)
         return "floor({})".format(code)
 
@@ -2033,17 +2066,17 @@ def _print_PyccelLShift(self, expr):
         return ' << '.join(self._print(a) for a in expr.args)
 
     def _print_PyccelBitXor(self, expr):
-        if expr.dtype is NativeBool():
+        if expr.dtype is PythonNativeBool():
             return '{0} != {1}'.format(self._print(expr.args[0]), self._print(expr.args[1]))
         return ' ^ '.join(self._print(a) for a in expr.args)
 
     def _print_PyccelBitOr(self, expr):
-        if expr.dtype is NativeBool():
+        if expr.dtype is PythonNativeBool():
             return ' || '.join(self._print(a) for a in expr.args)
         return ' | '.join(self._print(a) for a in expr.args)
 
     def _print_PyccelBitAnd(self, expr):
-        if expr.dtype is NativeBool():
+        if expr.dtype is PythonNativeBool():
             return ' && '.join(self._print(a) for a in expr.args)
         return ' & '.join(self._print(a) for a in expr.args)
 
@@ -2064,7 +2097,7 @@ def _print_AugAssign(self, expr):
         lhs = expr.lhs
         rhs = expr.rhs
 
-        if op == '%' and isinstance(lhs.dtype, NativeFloat):
+        if op == '%' and isinstance(lhs.dtype.primitive_type, PrimitiveFloatingPointType):
             _expr = expr.to_basic_assign()
             expr.invalidate_node()
             return self._print(_expr)
@@ -2077,7 +2110,7 @@ def _print_Assign(self, expr):
         prefix_code = ''
         lhs = expr.lhs
         rhs = expr.rhs
-        if isinstance(rhs, FunctionCall) and isinstance(rhs.dtype, NativeTuple):
+        if isinstance(rhs, FunctionCall) and isinstance(rhs.class_type, TupleType):
             self._temporary_args = [ObjectAddress(a) for a in lhs]
             return prefix_code+'{};\n'.format(self._print(rhs))
         # Inhomogenous tuples are unravelled and therefore do not exist in the c printer
@@ -2241,7 +2274,7 @@ def _handle_is_operator(self, Op, expr):
             rhs = self._print(rhs)
             return '{} {} {}'.format(lhs, Op, rhs)
 
-        if (a.dtype is NativeBool() and b.dtype is NativeBool()):
+        if (a.dtype is PythonNativeBool() and b.dtype is PythonNativeBool()):
             return '{} {} {}'.format(lhs, Op, rhs)
         else:
             errors.report(PYCCEL_RESTRICTION_IS_ISNOT,
@@ -2494,8 +2527,6 @@ def ccode(expr, filename, assign_to=None, **settings):
         the expression is assigned. Can be a string, ``Symbol``,
         ``MatrixSymbol``, or ``Indexed`` type. This is helpful in case of
         line-wrapping, or for expressions that generate multi-line statements.
-    precision : integer, optional
-        The precision for numbers such as pi [default=15].
     user_functions : dict, optional
         A dictionary where keys are ``FunctionClass`` instances and values are
         their string representations. Alternatively, the dictionary value can
diff --git a/pyccel/codegen/printing/cwrappercode.py b/pyccel/codegen/printing/cwrappercode.py
index 7289b51ac1..10334e8b89 100644
--- a/pyccel/codegen/printing/cwrappercode.py
+++ b/pyccel/codegen/printing/cwrappercode.py
@@ -49,10 +49,10 @@ class CWrapperCodePrinter(CCodePrinter):
             Any additional arguments which are necessary for CCodePrinter.
     """
     dtype_registry = {**CCodePrinter.dtype_registry,
-                      (PyccelPyObject() , 0) : 'PyObject',
-                      (PyccelPyArrayObject() , 0) : 'PyArrayObject',
-                      (PyccelPyTypeObject() , 0) : 'PyTypeObject',
-                      (BindCPointer()   , 0) : 'void'}
+                      PyccelPyObject() : 'PyObject',
+                      PyccelPyArrayObject() : 'PyArrayObject',
+                      PyccelPyTypeObject() : 'PyTypeObject',
+                      BindCPointer()  : 'void'}
 
     def __init__(self, filename, target_language, **settings):
         CCodePrinter.__init__(self, filename, **settings)
diff --git a/pyccel/codegen/printing/fcode.py b/pyccel/codegen/printing/fcode.py
index 0b895065a1..121587a135 100644
--- a/pyccel/codegen/printing/fcode.py
+++ b/pyccel/codegen/printing/fcode.py
@@ -7,19 +7,20 @@
 www.fortran90.org as much as possible."""
 
 
+import functools
 import string
 import re
-from itertools import chain
 from collections import OrderedDict
+from itertools import chain
 
-import functools
+import numpy as np
 
 from pyccel.ast.basic import TypedAstNode
 
 from pyccel.ast.bind_c import BindCPointer, BindCFunctionDef, BindCFunctionDefArgument, BindCModule, BindCClassDef
 
 from pyccel.ast.builtins import PythonInt, PythonType, PythonPrint, PythonRange
-from pyccel.ast.builtins import PythonTuple
+from pyccel.ast.builtins import PythonTuple, DtypePrecisionToCastFunction
 from pyccel.ast.builtins import PythonBool, PythonAbs
 
 from pyccel.ast.core import FunctionDef
@@ -32,15 +33,13 @@
 from pyccel.ast.core import Assign, AliasAssign, Declare, Deallocate
 from pyccel.ast.core import FunctionCall, PyccelFunctionDef
 
-from pyccel.ast.datatypes import NativeSymbol, NativeString, str_dtype
-from pyccel.ast.datatypes import NativeInteger, NativeBool, NativeFloat, NativeComplex
-from pyccel.ast.datatypes import iso_c_binding
-from pyccel.ast.datatypes import iso_c_binding_shortcut_mapping
-from pyccel.ast.datatypes import NativeNumeric
-from pyccel.ast.datatypes import CustomDataType
+from pyccel.ast.datatypes import PrimitiveBooleanType, PrimitiveIntegerType, PrimitiveFloatingPointType, PrimitiveComplexType
+from pyccel.ast.datatypes import SymbolicType, StringType, FixedSizeNumericType
+from pyccel.ast.datatypes import PythonNativeInt
+from pyccel.ast.datatypes import CustomDataType, InhomogeneousTupleType
+from pyccel.ast.datatypes import pyccel_type_to_original_type
 
 from pyccel.ast.internals import Slice, PrecomputedCode, PyccelArrayShapeElement
-from pyccel.ast.internals import get_final_precision
 
 from pyccel.ast.itertoolsext import Product
 
@@ -58,7 +57,6 @@
 from pyccel.ast.numpyext import NumpyNonZero
 from pyccel.ast.numpyext import NumpySign
 from pyccel.ast.numpyext import NumpyIsFinite, NumpyIsNan
-from pyccel.ast.numpyext import DtypePrecisionToCastFunction
 
 from pyccel.ast.operators import PyccelAdd, PyccelMul, PyccelMinus, PyccelAnd
 from pyccel.ast.operators import PyccelMod, PyccelNot, PyccelAssociativeParenthesis
@@ -174,13 +172,39 @@
     '__del__' : 'free',
 }
 
-type_to_print_format = {
-        ('float'): 'F0.12',
-        ('complex'): '"(",F0.12," + ",F0.12,")"',
-        ('int'): 'I0',
-        ('bool'): 'A',
-        ('str'): 'A',
-        ('tuple'):  '*'
+#==============================================================================
+iso_c_binding = {
+    PrimitiveIntegerType() : {
+        1  : 'C_INT8_T',
+        2  : 'C_INT16_T',
+        4  : 'C_INT32_T',
+        8  : 'C_INT64_T',
+        16 : 'C_INT128_T'}, #not supported yet
+    PrimitiveFloatingPointType() : {
+        4  : 'C_FLOAT',
+        8  : 'C_DOUBLE',
+        16 : 'C_LONG_DOUBLE'}, #not supported yet
+    PrimitiveComplexType() : {
+        4  : 'C_FLOAT_COMPLEX',
+        8  : 'C_DOUBLE_COMPLEX',
+        16 : 'C_LONG_DOUBLE_COMPLEX'}, #not supported yet
+    PrimitiveBooleanType() : {
+        -1 : "C_BOOL"}
+}
+
+iso_c_binding_shortcut_mapping = {
+    'C_INT8_T'              : 'i8',
+    'C_INT16_T'             : 'i16',
+    'C_INT32_T'             : 'i32',
+    'C_INT64_T'             : 'i64',
+    'C_INT128_T'            : 'i128',
+    'C_FLOAT'               : 'f32',
+    'C_DOUBLE'              : 'f64',
+    'C_LONG_DOUBLE'         : 'f128',
+    'C_FLOAT_COMPLEX'       : 'c32',
+    'C_DOUBLE_COMPLEX'      : 'c64',
+    'C_LONG_DOUBLE_COMPLEX' : 'c128',
+    'C_BOOL'                : 'b1'
 }
 
 inc_keyword = (r'do\b', r'if\b',
@@ -292,10 +316,23 @@ def _format_code(self, lines):
 
     def print_kind(self, expr):
         """
-        Prints the kind(precision) of a literal value or its shortcut if possible
+        Print the kind(precision) of a literal value or its shortcut if possible.
+
+        Print the kind(precision) of a literal value or its shortcut if possible.
+
+        Parameters
+        ----------
+        expr : TypedAstNode
+            The object whose precision should be investigated.
+
+        Returns
+        -------
+        str
+            The code for the kind parameter.
         """
-        precision = get_final_precision(expr)
-        constant_name = iso_c_binding[self._print(expr.dtype)][precision]
+        dtype = expr.dtype
+        constant_name = iso_c_binding[dtype.primitive_type][dtype.precision]
+
         constant_shortcut = iso_c_binding_shortcut_mapping[constant_name]
         if constant_shortcut not in self.scope.all_used_symbols and constant_name != constant_shortcut:
             self._constantImports.setdefault('ISO_C_Binding', set())\
@@ -702,7 +739,7 @@ def _print_PythonPrint(self, expr):
                     args_format = []
                     args = []
                 loop_scope = self.scope.create_new_loop_scope()
-                for_index = self.scope.get_temporary_variable(NativeInteger(), name='i')
+                for_index = self.scope.get_temporary_variable(PythonNativeInt(), name='i')
                 max_index = PyccelMinus(f.shape[0], LiteralInteger(1), simplify=True)
                 for_range = PythonRange(max_index)
                 print_body = [FunctionCallArgument(f[for_index])]
@@ -781,8 +818,11 @@ def _formatted_args_to_print(self, fargs_format, fargs, fend, fsep, expr):
                 .add(("stdout", "output_unit"))
         return f"write(stdout, '({args_formatting})', advance=\"{advance}\") {args_code}\n"
 
-    def _get_print_format_and_arg(self,var):
-        """ Get the format string and the printable argument for an object.
+    def _get_print_format_and_arg(self, var, var_code = None):
+        """
+        Get the format string and the printable argument for an object.
+
+        Get the format string and the printable argument for an object.
         In other words get arg_format and arg such that var can be printed
         by doing:
 
@@ -791,32 +831,56 @@ def _get_print_format_and_arg(self,var):
         Parameters
         ----------
         var : TypedAstNode
-              The object to be printed
+            The object to be printed.
+        var_code : str, optional
+            The code which will print the variable (this is mostly useful when calling
+            this function recursively, e.g. to print an inhomogenoeus tuple of function
+            call results).
 
-        Results
+        Returns
         -------
         arg_format : str
-                     The format string
-        arg        : str
-                     The fortran code which represents var
+            The format string.
+        arg : str
+            The Fortran code which represents var.
         """
+        if var_code is None:
+            var_code = self._print(var)
+        arg = var_code
+
         var_type = var.dtype
-        try:
-            arg_format = type_to_print_format[str(var_type)]
-        except KeyError:
-            errors.report("{} type is not supported currently".format(var_type), severity='fatal')
-
-        if var_type is NativeComplex():
-            arg = '{}, {}'.format(self._print(NumpyReal(var)), self._print(NumpyImag(var)))
-        elif var_type is NativeBool():
-            if isinstance(var, LiteralTrue):
-                arg = "'True'"
-            elif isinstance(var, LiteralFalse):
-                arg = "'False'"
+        if isinstance(var.class_type, StringType):
+            arg_format = 'A'
+        elif isinstance(var, FunctionCall) and len(var.funcdef.results)>1 or \
+                isinstance(var.class_type, InhomogeneousTupleType):
+            var_elem_code = var_code[1:-1].split(', ')
+            args_and_formats = [self._get_print_format_and_arg(v.var, c) for v,c in zip(var.funcdef.results, var_elem_code)]
+            formats = ',", ",'.join(af[0] for af in args_and_formats)
+            arg_format = f'"(",{formats},")"'
+            arg = ', '.join(af[1] for af in args_and_formats)
+        elif isinstance(var_type, FixedSizeNumericType):
+            if isinstance(var_type.primitive_type, PrimitiveComplexType):
+                float_format, real_arg = self._get_print_format_and_arg(NumpyReal(var))
+                imag_arg = self._print(NumpyImag(var))
+                arg_format = f'"(",{float_format}," + ",{float_format},"j)"'
+                arg = f'{real_arg}, {imag_arg}'
+            elif isinstance(var_type.primitive_type, PrimitiveFloatingPointType):
+                dps = np.finfo(pyccel_type_to_original_type[var_type]).precision
+                arg_format = f'F0.{dps}'
+            elif isinstance(var_type.primitive_type, PrimitiveIntegerType):
+                arg_format = 'I0'
+            elif isinstance(var_type.primitive_type, PrimitiveBooleanType):
+                arg_format = 'A'
+                if isinstance(var, LiteralTrue):
+                    arg = "'True'"
+                elif isinstance(var, LiteralFalse):
+                    arg = "'False'"
+                else:
+                    arg = f'merge("True ", "False", {var_code})'
             else:
-                arg = 'merge("True ", "False", {})'.format(self._print(var))
+                errors.report(f"Printing {var_type} type is not supported currently", severity='fatal')
         else:
-            arg = self._print(var)
+            errors.report(f"Printing {var_type} type is not supported currently", severity='fatal')
 
         return arg_format, arg
 
@@ -940,9 +1004,9 @@ def _print_PythonImag(self, expr):
 
     def _print_NumpySum(self, expr):
         """Fortran print."""
-
         rhs_code = self._print(expr.arg)
-        if isinstance(expr.arg.dtype, NativeBool):
+        dtype = expr.arg.dtype.primitive_type
+        if isinstance(dtype, PrimitiveBooleanType):
             return 'count({0})'.format(rhs_code)
         return 'sum({0})'.format(rhs_code)
 
@@ -958,9 +1022,9 @@ def _print_NumpyMatmul(self, expr):
         b_code = self._print(expr.b)
 
         if expr.rank == 0:
-            if isinstance(expr.a.dtype, NativeBool):
+            if isinstance(expr.a.dtype.primitive_type, PrimitiveBooleanType):
                 a_code = self._print(PythonInt(expr.a))
-            if isinstance(expr.b.dtype, NativeBool):
+            if isinstance(expr.b.dtype.primitive_type, PrimitiveBooleanType):
                 b_code = self._print(PythonInt(expr.b))
             return 'sum({}*{})'.format(a_code, b_code)
         if expr.a.order and expr.b.order:
@@ -979,7 +1043,7 @@ def _print_NumpyEmpty(self, expr):
 
     def _print_NumpyNorm(self, expr):
         """Fortran print."""
-        arg = PythonAbs(expr.arg) if isinstance(expr.arg.dtype, NativeComplex) else expr.arg
+        arg = PythonAbs(expr.arg) if isinstance(expr.arg.dtype.primitive_type, PrimitiveComplexType) else expr.arg
         if expr.axis:
             axis = expr.axis
             if arg.order != 'F':
@@ -994,8 +1058,8 @@ def _print_NumpyNorm(self, expr):
 
     def _print_NumpyLinspace(self, expr):
 
-        if expr.stop.dtype != expr.dtype or expr.precision != expr.stop.precision:
-            cast_func = DtypePrecisionToCastFunction[expr.dtype.name][expr.precision]
+        if expr.stop.dtype != expr.dtype:
+            cast_func = DtypePrecisionToCastFunction[expr.dtype]
             st = cast_func(expr.stop)
             v = self._print(st)
         else:
@@ -1025,7 +1089,7 @@ def _print_NumpyLinspace(self, expr):
             var = expr.ind
         else:
             template = '[(({start} + {index}*{step}), {index} = {zero},{end})]'
-            var = self.scope.get_temporary_variable('int', 'linspace_index') 
+            var = self.scope.get_temporary_variable(PythonNativeInt(), 'linspace_index')
 
         init_value = template.format(
             start = self._print(expr.start),
@@ -1046,10 +1110,10 @@ def _print_NumpyLinspace(self, expr):
 
     def _print_NumpyNonZeroElement(self, expr):
 
-        ind   = self._print(self.scope.get_temporary_variable('int'))
+        ind   = self._print(self.scope.get_temporary_variable(PythonNativeInt()))
         array = expr.array
 
-        if array.dtype is NativeBool():
+        if isinstance(array.dtype.primitive_type, PrimitiveBooleanType):
             mask  = self._print(array)
         else:
             mask  = self._print(NumpyBool(array))
@@ -1066,7 +1130,7 @@ def _print_NumpyCountNonZero(self, expr):
         axis  = expr.axis
         array = expr.array
 
-        if array.dtype is NativeBool():
+        if isinstance(array.dtype.primitive_type, PrimitiveBooleanType):
             mask  = self._print(array)
         else:
             mask  = self._print(NumpyBool(array))
@@ -1105,13 +1169,13 @@ def _print_NumpyWhere(self, expr):
         value_true  = expr.value_true
         value_false = expr.value_false
         try :
-            cast_func = DtypePrecisionToCastFunction[expr.dtype.name][expr.precision]
+            cast_func = DtypePrecisionToCastFunction[expr.dtype]
         except KeyError:
             errors.report(PYCCEL_RESTRICTION_TODO, severity='fatal')
 
-        if value_true.dtype != expr.dtype or value_true.precision != expr.precision:
+        if value_true.dtype != expr.dtype:
             value_true = cast_func(value_true)
-        if value_false.dtype != expr.dtype or value_false.precision != expr.precision:
+        if value_false.dtype != expr.dtype:
             value_false = cast_func(value_false)
 
         condition   = self._print(expr.condition)
@@ -1179,7 +1243,7 @@ def _print_NumpyArange(self, expr):
         start  = self._print(expr.start)
         step   = self._print(expr.step)
         shape  = PyccelMinus(expr.shape[0], LiteralInteger(1), simplify = True)
-        index  = self.scope.get_temporary_variable('int')
+        index  = self.scope.get_temporary_variable(PythonNativeInt())
 
         code = '[({start} + {step} * {index}, {index} = {0}, {shape}, {1})]'
         code = code.format(self._print(LiteralInteger(0)),
@@ -1218,7 +1282,7 @@ def _print_PyccelArrayShapeElement(self, expr):
 
     def _print_PythonInt(self, expr):
         value = self._print(expr.arg)
-        if (expr.arg.dtype is NativeBool()):
+        if isinstance(expr.arg.dtype.primitive_type, PrimitiveBooleanType):
             code = 'MERGE(1_{0}, 0_{1}, {2})'.format(self.print_kind(expr), self.print_kind(expr),value)
         else:
             code  = 'Int({0}, {1})'.format(value, self.print_kind(expr))
@@ -1226,7 +1290,7 @@ def _print_PythonInt(self, expr):
 
     def _print_PythonFloat(self, expr):
         value = self._print(expr.arg)
-        if (expr.arg.dtype is NativeBool()):
+        if isinstance(expr.arg.dtype.primitive_type, PrimitiveBooleanType):
             code = 'MERGE(1.0_{0}, 0.0_{1}, {2})'.format(self.print_kind(expr), self.print_kind(expr),value)
         else:
             code  = 'Real({0}, {1})'.format(value, self.print_kind(expr))
@@ -1238,10 +1302,9 @@ def _print_MathFloor(self, expr):
 
         # math.floor on integer argument is identity,
         # but we need parentheses around expressions
-        if arg.dtype is NativeInteger():
+        if isinstance(arg.dtype.primitive_type, PrimitiveIntegerType):
             return '({})'.format(arg_code)
 
-        prec = expr.precision
         prec_code = self.print_kind(expr)
         return 'floor({}, kind={})'.format(arg_code, prec_code)
 
@@ -1258,7 +1321,7 @@ def _print_PythonComplex(self, expr):
         return code
 
     def _print_PythonBool(self, expr):
-        if isinstance(expr.arg.dtype, NativeBool):
+        if isinstance(expr.arg.dtype.primitive_type, PrimitiveBooleanType):
             return 'logical({}, kind = {prec})'.format(self._print(expr.arg), prec = self.print_kind(expr))
         else:
             return '({} /= 0)'.format(self._print(expr.arg))
@@ -1271,7 +1334,7 @@ def _print_NumpyRand(self, expr):
         if (not self._additional_code):
             self._additional_code = ''
         var = self.scope.get_temporary_variable(expr.dtype, memory_handling = 'stack',
-                shape = expr.shape, precision = expr.precision,
+                shape = expr.shape,
                 order = expr.order, rank = expr.rank)
 
         self._additional_code = self._additional_code + self._print(Assign(var,expr)) + '\n'
@@ -1297,12 +1360,12 @@ def _print_NumpyFull(self, expr):
     
     def _print_NumpyAmax(self, expr):
         array_arg = expr.arg
-        if array_arg.dtype is NativeBool():
+        if isinstance(array_arg.dtype.primitive_type, PrimitiveBooleanType):
             arg_code = self._print(NumpyInt32(array_arg))
         else:
             arg_code = self._print(array_arg)
 
-        if array_arg.dtype is NativeComplex():
+        if isinstance(array_arg.dtype.primitive_type, PrimitiveComplexType):
             self._additional_imports.add(Import('pyc_math_f90', Module('pyc_math_f90',(),())))
             return f'amax({array_arg})'
         else:
@@ -1310,12 +1373,12 @@ def _print_NumpyAmax(self, expr):
     
     def _print_NumpyAmin(self, expr):
         array_arg = expr.arg
-        if array_arg.dtype is NativeBool():
+        if isinstance(array_arg.dtype.primitive_type, PrimitiveBooleanType):
             arg_code = self._print(NumpyInt32(array_arg))
         else:
             arg_code = self._print(array_arg)
 
-        if array_arg.dtype is NativeComplex():
+        if isinstance(array_arg.dtype.primitive_type, PrimitiveComplexType):
             self._additional_imports.add(Import('pyc_math_f90', Module('pyc_math_f90',(),())))
             return f'amin({array_arg})'
         else:
@@ -1363,7 +1426,7 @@ def _print_MacroShape(self, expr):
     # ...
     def _print_MacroType(self, expr):
         dtype = self._print(expr.argument.dtype)
-        prec  = expr.argument.precision
+        prec  = expr.argument.dtype.precision
 
         if dtype == 'integer':
             if prec==4:
@@ -1401,7 +1464,7 @@ def _print_Declare(self, expr):
         # ... ignored declarations
         var = expr.variable
         expr_dtype      = var.dtype
-        if isinstance(expr_dtype, NativeSymbol):
+        if isinstance(expr_dtype, SymbolicType):
             return ''
 
         # meta-variables
@@ -1435,25 +1498,21 @@ def _print_Declare(self, expr):
 
         # ... print datatype
         if isinstance(expr_dtype, CustomDataType):
-            name   = expr_dtype.__class__.__name__
-            prefix = expr_dtype.prefix
-            alias  = expr_dtype.alias
+            name   = expr_dtype.name
 
             if var.is_argument:
                 sig = 'class'
             else:
                 sig = 'type'
-
-            if alias is None:
-                name = name.replace(prefix, '')
-            else:
-                name = alias
             dtype = f'{sig}({name})'
+        elif isinstance(expr_dtype, FixedSizeNumericType):
+            dtype = self._print(expr_dtype.primitive_type)
+            dtype += f'({self.print_kind(var)})'
         else:
-            dtype = self._print(expr_dtype)
 
         # ...
-            if isinstance(expr_dtype, NativeString):
+            if isinstance(expr_dtype, StringType):
+                dtype = self._print(expr_dtype)
 
                 if intent_in:
                     dtype = dtype[:9] +'(len =*)'
@@ -1462,7 +1521,7 @@ def _print_Declare(self, expr):
                 dtype = 'type(c_ptr)'
                 self._constantImports.setdefault('ISO_C_Binding', set()).add('c_ptr')
             else:
-                dtype += '({0})'.format(self.print_kind(expr.variable))
+                dtype = self._print(expr_dtype)
 
         code_value = ''
         if expr.value:
@@ -1544,7 +1603,7 @@ def _print_Declare(self, expr):
 #                severity='fatal')
 
         mod_str = ''
-        if expr.module_variable and not is_private and (rank == 0) and expr_dtype in NativeNumeric:
+        if expr.module_variable and not is_private and isinstance(expr.variable.class_type, FixedSizeNumericType):
             mod_str = ', bind(c)'
 
         # Construct declaration
@@ -1663,7 +1722,7 @@ def _print_Assign(self, expr):
             return 'call {0}({1})\n'.format(rhs_code, code_args)
 
         if (isinstance(expr.lhs, Variable) and
-              expr.lhs.dtype == NativeSymbol()):
+              isinstance(expr.lhs.dtype, SymbolicType)):
             return ''
 
         # Right-hand side code
@@ -1765,19 +1824,19 @@ def _print_DeallocatePointer(self, expr):
 
 #------------------------------------------------------------------------------
 
-    def _print_NativeBool(self, expr):
+    def _print_PrimitiveBooleanType(self, expr):
         return 'logical'
 
-    def _print_NativeInteger(self, expr):
+    def _print_PrimitiveIntegerType(self, expr):
         return 'integer'
 
-    def _print_NativeFloat(self, expr):
+    def _print_PrimitiveFloatingPointType(self, expr):
         return 'real'
 
-    def _print_NativeComplex(self, expr):
+    def _print_PrimitiveComplexType(self, expr):
         return 'complex'
 
-    def _print_NativeString(self, expr):
+    def _print_StringType(self, expr):
         return 'character(len=280)'
         #TODO fix improve later
 
@@ -1811,7 +1870,7 @@ def _print_Interface(self, expr):
         else:
             funcs = [f for f in expr.functions if f is \
                     expr.point([FunctionCallArgument(a.var.clone('arg_'+str(i))) \
-                        for i,a in enumerate(f.arguments)], use_final_precision = True)]
+                        for i,a in enumerate(f.arguments)])]
 
         if expr.is_argument:
             funcs_sigs = []
@@ -2405,7 +2464,7 @@ def _print_PyccelIs(self, expr):
         if isinstance(rhs_var, Nil):
             return '.not. '+ self._handle_not_none(lhs, lhs_var)
 
-        if (a.dtype is NativeBool() and b.dtype is NativeBool()):
+        if all(isinstance(var.dtype.primitive_type, PrimitiveBooleanType) for var in (a, b)):
             return f'{lhs} .eqv. {rhs}'
 
         errors.report(PYCCEL_RESTRICTION_IS_ISNOT,
@@ -2422,7 +2481,7 @@ def _print_PyccelIsNot(self, expr):
         if isinstance(rhs_var, Nil):
             return self._handle_not_none(lhs, lhs_var)
 
-        if a.dtype is NativeBool() and b.dtype is NativeBool():
+        if all(isinstance(var.dtype.primitive_type, PrimitiveBooleanType) for var in (a, b)):
             return f'{lhs} .neqv. {rhs}'
 
         errors.report(PYCCEL_RESTRICTION_IS_ISNOT,
@@ -2453,13 +2512,13 @@ def _print_If(self, expr):
 
     def _print_IfTernaryOperator(self, expr):
 
-        cond = PythonBool(expr.cond) if not isinstance(expr.cond.dtype, NativeBool) else expr.cond
+        cond = PythonBool(expr.cond) if not isinstance(expr.cond.dtype.primitive_type, PrimitiveBooleanType) else expr.cond
         value_true = expr.value_true
         value_false = expr.value_false
 
         if value_true.dtype != value_false.dtype :
             try :
-                cast_func = DtypePrecisionToCastFunction[expr.dtype.name][expr.precision]
+                cast_func = DtypePrecisionToCastFunction[expr.dtype]
             except KeyError:
                 errors.report(PYCCEL_RESTRICTION_TODO, severity='fatal')
             value_true = cast_func(value_true) if value_true.dtype != expr.dtype else value_true
@@ -2478,35 +2537,35 @@ def _print_PyccelPow(self, expr):
         return '{} ** {}'.format(base_c, e_c)
 
     def _print_PyccelAdd(self, expr):
-        if expr.dtype is NativeString():
+        if isinstance(expr.dtype, StringType):
             return '//'.join('trim('+self._print(a)+')' for a in expr.args)
         else:
-            args = [PythonInt(a) if a.dtype is NativeBool() else a for a in expr.args]
+            args = [PythonInt(a) if isinstance(a.dtype.primitive_type, PrimitiveBooleanType) else a for a in expr.args]
             return ' + '.join(self._print(a) for a in args)
 
     def _print_PyccelMinus(self, expr):
-        args = [PythonInt(a) if a.dtype is NativeBool() else a for a in expr.args]
+        args = [PythonInt(a) if isinstance(a.dtype.primitive_type, PrimitiveBooleanType) else a for a in expr.args]
         args_code = [self._print(a) for a in args]
 
         return ' - '.join(args_code)
 
     def _print_PyccelMul(self, expr):
-        args = [PythonInt(a) if a.dtype is NativeBool() else a for a in expr.args]
+        args = [PythonInt(a) if isinstance(a.dtype.primitive_type, PrimitiveBooleanType) else a for a in expr.args]
         args_code = [self._print(a) for a in args]
         return ' * '.join(a for a in args_code)
 
     def _print_PyccelDiv(self, expr):
-        if all(a.dtype in (NativeBool(), NativeInteger()) for a in expr.args):
+        if all(isinstance(a.dtype.primitive_type, (PrimitiveBooleanType, PrimitiveIntegerType)) for a in expr.args):
             args = [NumpyFloat(a) for a in expr.args]
         else:
             args = expr.args
         return ' / '.join(self._print(a) for a in args)
 
     def _print_PyccelMod(self, expr):
-        is_float = expr.dtype is NativeFloat()
+        is_float = isinstance(expr.dtype.primitive_type, PrimitiveFloatingPointType)
 
         def correct_type_arg(a):
-            if is_float and a.dtype is NativeInteger():
+            if is_float and isinstance(a.dtype.primitive_type, PrimitiveIntegerType):
                 return NumpyFloat(a)
             else:
                 return a
@@ -2521,11 +2580,11 @@ def correct_type_arg(a):
     def _print_PyccelFloorDiv(self, expr):
 
         code     = self._print(expr.args[0])
-        adtype   = expr.args[0].dtype
-        is_float = expr.dtype is NativeFloat()
+        adtype   = expr.args[0].dtype.primitive_type
+        is_float = isinstance(expr.dtype.primitive_type, PrimitiveFloatingPointType)
         for b in expr.args[1:]:
-            bdtype    = b.dtype
-            if adtype is NativeInteger() and bdtype is NativeInteger():
+            bdtype    = b.dtype.primitive_type
+            if all(isinstance(dtype, PrimitiveIntegerType) for dtype in (adtype, bdtype)):
                 b = NumpyFloat(b)
             c = self._print(b)
             adtype = bdtype
@@ -2541,17 +2600,17 @@ def _print_PyccelLShift(self, expr):
         return 'LSHIFT({}, {})'.format(self._print(expr.args[0]), self._print(expr.args[1]))
 
     def _print_PyccelBitXor(self, expr):
-        if expr.dtype is NativeBool():
+        if isinstance(expr.dtype.primitive_type, PrimitiveBooleanType):
             return ' .neqv. '.join(self._print(a) for a in expr.args)
         return 'IEOR({}, {})'.format(self._print(expr.args[0]), self._print(expr.args[1]))
 
     def _print_PyccelBitOr(self, expr):
-        if expr.dtype is NativeBool():
+        if isinstance(expr.dtype.primitive_type, PrimitiveBooleanType):
             return ' .or. '.join(self._print(a) for a in expr.args)
         return 'IOR({}, {})'.format(self._print(expr.args[0]), self._print(expr.args[1]))
 
     def _print_PyccelBitAnd(self, expr):
-        if expr.dtype is NativeBool():
+        if isinstance(expr.dtype.primitive_type, PrimitiveBooleanType):
             return ' .and. '.join(self._print(a) for a in expr.args)
         return 'IAND({}, {})'.format(self._print(expr.args[0]), self._print(expr.args[1]))
 
@@ -2568,60 +2627,60 @@ def _print_PyccelUnarySub(self, expr):
         return '-{}'.format(self._print(expr.args[0]))
 
     def _print_PyccelAnd(self, expr):
-        args = [a if a.dtype is NativeBool() else PythonBool(a) for a in expr.args]
+        args = [a if isinstance(a.dtype.primitive_type, PrimitiveBooleanType) else PythonBool(a) for a in expr.args]
         return ' .and. '.join(self._print(a) for a in args)
 
     def _print_PyccelOr(self, expr):
-        args = [a if a.dtype is NativeBool() else PythonBool(a) for a in expr.args]
+        args = [a if isinstance(a.dtype.primitive_type, PrimitiveBooleanType) else PythonBool(a) for a in expr.args]
         return ' .or. '.join(self._print(a) for a in args)
 
     def _print_PyccelEq(self, expr):
         lhs = self._print(expr.args[0])
         rhs = self._print(expr.args[1])
-        a = expr.args[0].dtype
-        b = expr.args[1].dtype
+        a = expr.args[0].dtype.primitive_type
+        b = expr.args[1].dtype.primitive_type
 
-        if a is NativeBool() and b is NativeBool():
+        if all(isinstance(var, PrimitiveBooleanType) for var in (a, b)):
             return '{} .eqv. {}'.format(lhs, rhs)
         return '{0} == {1}'.format(lhs, rhs)
 
     def _print_PyccelNe(self, expr):
         lhs = self._print(expr.args[0])
         rhs = self._print(expr.args[1])
-        a = expr.args[0].dtype
-        b = expr.args[1].dtype
+        a = expr.args[0].dtype.primitive_type
+        b = expr.args[1].dtype.primitive_type
 
-        if a is NativeBool() and b is NativeBool():
+        if all(isinstance(var, PrimitiveBooleanType) for var in (a, b)):
             return '{} .neqv. {}'.format(lhs, rhs)
         return '{0} /= {1}'.format(lhs, rhs)
 
     def _print_PyccelLt(self, expr):
-        args = [PythonInt(a) if a.dtype is NativeBool() else a for a in expr.args]
+        args = [PythonInt(a) if isinstance(a.dtype.primitive_type, PrimitiveBooleanType) else a for a in expr.args]
         lhs = self._print(args[0])
         rhs = self._print(args[1])
         return '{0} < {1}'.format(lhs, rhs)
 
     def _print_PyccelLe(self, expr):
-        args = [PythonInt(a) if a.dtype is NativeBool() else a for a in expr.args]
+        args = [PythonInt(a) if isinstance(a.dtype.primitive_type, PrimitiveBooleanType) else a for a in expr.args]
         lhs = self._print(args[0])
         rhs = self._print(args[1])
         return '{0} <= {1}'.format(lhs, rhs)
 
     def _print_PyccelGt(self, expr):
-        args = [PythonInt(a) if a.dtype is NativeBool() else a for a in expr.args]
+        args = [PythonInt(a) if isinstance(a.dtype.primitive_type, PrimitiveBooleanType) else a for a in expr.args]
         lhs = self._print(args[0])
         rhs = self._print(args[1])
         return '{0} > {1}'.format(lhs, rhs)
 
     def _print_PyccelGe(self, expr):
-        args = [PythonInt(a) if a.dtype is NativeBool() else a for a in expr.args]
+        args = [PythonInt(a) if isinstance(a.dtype.primitive_type, PrimitiveBooleanType) else a for a in expr.args]
         lhs = self._print(args[0])
         rhs = self._print(args[1])
         return '{0} >= {1}'.format(lhs, rhs)
 
     def _print_PyccelNot(self, expr):
         a = self._print(expr.args[0])
-        if (expr.args[0].dtype is not NativeBool()):
+        if not isinstance(expr.args[0].dtype.primitive_type, PrimitiveBooleanType):
             return '{} == 0'.format(a)
         return '.not. {}'.format(a)
 
@@ -2633,12 +2692,12 @@ def _print_SysExit(self, expr):
         exit_code = expr.status
         if isinstance(exit_code, LiteralInteger):
             arg = exit_code.python_value
-        elif exit_code.dtype is not NativeInteger() or exit_code.rank > 0:
+        elif not isinstance(getattr(exit_code.dtype, 'primitive_type'), PrimitiveIntegerType) or exit_code.rank > 0:
             print_arg = FunctionCallArgument(exit_code)
             code = self._print(PythonPrint((print_arg, ), file="stderr"))
             arg = "1"
         else:
-            if exit_code.precision != 4:
+            if exit_code.dtype.precision != 4:
                 exit_code = NumpyInt32(exit_code)
             arg = self._print(exit_code)
         return f'{code}stop {arg}\n'
@@ -2651,7 +2710,7 @@ def _print_NumpyUfuncBase(self, expr):
             self._print_not_supported(expr)
         if func_name.startswith('ieee_'):
             self._constantImports.setdefault('ieee_arithmetic', set()).add(func_name)
-        args = [self._print(NumpyFloat(a) if a.dtype is NativeInteger() else a)\
+        args = [self._print(NumpyFloat(a) if isinstance(a.dtype.primitive_type, PrimitiveIntegerType) else a)\
 				for a in expr.args]
         code_args = ', '.join(args)
         code = '{0}({1})'.format(func_name, code_args)
@@ -2686,12 +2745,12 @@ def _print_NumpySign(self, expr):
         """
         arg = expr.args[0]
         arg_code = self._print(arg)
-        if isinstance(expr.dtype, NativeComplex):
+        if isinstance(expr.dtype.primitive_type, PrimitiveComplexType):
             func = PyccelFunctionDef('numpy_sign', NumpySign)
             self._additional_imports.add(Import('numpy_f90', AsName(func, 'numpy_sign')))
             return f'numpy_sign({arg_code})'
         else:
-            cast_func = DtypePrecisionToCastFunction[expr.dtype.name][expr.precision]
+            cast_func = DtypePrecisionToCastFunction[expr.dtype]
             # The absolute value of the result (0 if the argument is 0, 1 otherwise)
             abs_result = self._print(cast_func(PythonBool(arg)))
             return f'sign({abs_result}, {arg_code})'
@@ -2740,7 +2799,7 @@ def _print_MathFunctionBase(self, expr):
         args = []
         for arg in expr.args:
             if arg.dtype != expr.dtype:
-                cast_func = DtypePrecisionToCastFunction[expr.dtype.name][expr.precision]
+                cast_func = DtypePrecisionToCastFunction[expr.dtype]
                 args.append(self._print(cast_func(arg)))
             else:
                 args.append(self._print(arg))
@@ -2752,7 +2811,7 @@ def _print_MathCeil(self, expr):
         Fortran function call"""
         # add necessary include
         arg = expr.args[0]
-        if arg.dtype is NativeInteger():
+        if isinstance(arg.dtype.primitive_type, PrimitiveIntegerType):
             code_arg = self._print(NumpyFloat(arg))
         else:
             code_arg = self._print(arg)
@@ -2763,7 +2822,7 @@ def _print_MathIsnan(self, expr):
         Fortran function call"""
         # add necessary include
         arg = expr.args[0]
-        if arg.dtype is NativeInteger():
+        if isinstance(arg.dtype.primitive_type, PrimitiveIntegerType):
             code_arg = self._print(NumpyFloat(arg))
         else:
             code_arg = self._print(arg)
@@ -2774,7 +2833,7 @@ def _print_MathTrunc(self, expr):
         Fortran function call"""
         # add necessary include
         arg = expr.args[0]
-        if arg.dtype is NativeInteger():
+        if isinstance(arg.dtype.primitive_type, PrimitiveIntegerType):
             code_arg = self._print(NumpyFloat(arg))
         else:
             code_arg = self._print(arg)
@@ -2790,7 +2849,8 @@ def _print_MathPow(self, expr):
 
     def _print_NumpySqrt(self, expr):
         arg = expr.args[0]
-        if arg.dtype is NativeInteger() or arg.dtype is NativeBool():
+        dtype = arg.dtype.primitive_type
+        if isinstance(dtype, (PrimitiveIntegerType, PrimitiveBooleanType)):
             arg = NumpyFloat(arg)
         code_args = self._print(arg)
         code = 'sqrt({})'.format(code_args)
@@ -3220,8 +3280,6 @@ def fcode(expr, filename, assign_to=None, **settings):
         the expression is assigned. Can be a string, ``Symbol``,
         ``MatrixSymbol``, or ``Indexed`` type. This is helpful in case of
         line-wrapping, or for expressions that generate multi-line statements.
-    precision : integer, optional
-        The precision for numbers such as pi [default=15].
     user_functions : dict, optional
         A dictionary where keys are ``FunctionClass`` instances and values are
         their string representations. Alternatively, the dictionary value can
diff --git a/pyccel/codegen/printing/pycode.py b/pyccel/codegen/printing/pycode.py
index eb40c63d3a..dda7cc8df1 100644
--- a/pyccel/codegen/printing/pycode.py
+++ b/pyccel/codegen/printing/pycode.py
@@ -8,16 +8,16 @@
 from pyccel.decorators import __all__ as pyccel_decorators
 
 from pyccel.ast.builtins   import PythonMin, PythonMax, PythonType, PythonBool, PythonInt, PythonFloat
-from pyccel.ast.builtins   import PythonComplex
+from pyccel.ast.builtins   import PythonComplex, DtypePrecisionToCastFunction
 from pyccel.ast.core       import CodeBlock, Import, Assign, FunctionCall, For, AsName, FunctionAddress
 from pyccel.ast.core       import IfSection, FunctionDef, Module, PyccelFunctionDef
-from pyccel.ast.datatypes  import NativeHomogeneousTuple
+from pyccel.ast.datatypes  import HomogeneousTupleType
 from pyccel.ast.functionalexpr import FunctionalFor
 from pyccel.ast.literals   import LiteralTrue, LiteralString
 from pyccel.ast.literals   import LiteralInteger, LiteralFloat, LiteralComplex
 from pyccel.ast.numpyext   import numpy_target_swap
 from pyccel.ast.numpyext   import NumpyArray, NumpyNonZero, NumpyResultType
-from pyccel.ast.numpyext   import DtypePrecisionToCastFunction
+from pyccel.ast.numpytypes import NumpyNumericType
 from pyccel.ast.variable   import DottedName, Variable
 from pyccel.ast.utilities  import builtin_import_registry as pyccel_builtin_import_registry
 from pyccel.ast.utilities  import decorators_mod
@@ -37,7 +37,7 @@
 # The keys are modules from which the target is imported
 # The values are a dictionary whose keys are object aliases and whose values
 # are the names used in pyccel
-import_object_swap = { 'numpy': numpy_target_swap}
+import_object_swap = {'numpy': numpy_target_swap}
 import_target_swap = {
         'numpy' : {'double'     : 'float64',
                    'prod'       : 'product',
@@ -216,8 +216,8 @@ def _print_dtype_argument(self, expr, init_dtype):
             dtype = self._get_numpy_name(init_dtype.cls_name)
         else:
             dtype = self._print(expr.dtype)
-            if expr.precision != -1:
-                dtype = self._get_numpy_name(DtypePrecisionToCastFunction[expr.dtype.name][expr.precision])
+            if isinstance(expr.dtype, NumpyNumericType):
+                dtype = self._get_numpy_name(DtypePrecisionToCastFunction[expr.dtype])
         return f"dtype = {dtype}"
 
     def _print_Header(self, expr):
@@ -227,17 +227,8 @@ def _print_tuple(self, expr):
         fs = ', '.join(self._print(f) for f in expr)
         return '({0})'.format(fs)
 
-    def _print_NativeBool(self, expr):
-        return 'bool'
-
-    def _print_NativeInteger(self, expr):
-        return 'int'
-
-    def _print_NativeFloat(self, expr):
-        return 'float'
-
-    def _print_NativeComplex(self, expr):
-        return 'complex'
+    def _print_FixedSizeType(self, expr):
+        return str(expr)
 
     def _print_Variable(self, expr):
         return expr.name
@@ -257,7 +248,7 @@ def _print_FunctionDefArgument(self, expr):
             var = expr.var
             def get_type_annotation(var):
                 if isinstance(var, Variable):
-                    type_annotation = str(var.dtype)
+                    type_annotation = str(var.class_type)
                     if var.rank:
                         type_annotation += '[' + ','.join(':' for _ in range(var.rank)) + ']'
                     return f"'{type_annotation}'"
@@ -293,7 +284,7 @@ def _print_IndexedElement(self, expr):
                 indices = indices[0]
 
             indices = [self._print(i) for i in indices]
-            if expr.pyccel_staging != 'syntactic' and isinstance(expr.base.class_type, NativeHomogeneousTuple):
+            if expr.pyccel_staging != 'syntactic' and isinstance(expr.base.class_type, HomogeneousTupleType):
                 indices = ']['.join(i for i in indices)
             else:
                 indices = ','.join(i for i in indices)
@@ -457,13 +448,13 @@ def _print_PythonBool(self, expr):
 
     def _print_PythonInt(self, expr):
         name = 'int'
-        if expr.precision != -1:
+        if isinstance(expr.dtype, NumpyNumericType):
             name = self._get_numpy_name(expr)
         return '{}({})'.format(name, self._print(expr.arg))
 
     def _print_PythonFloat(self, expr):
         name = 'float'
-        if expr.precision != -1:
+        if isinstance(expr.dtype, NumpyNumericType):
             name = self._get_numpy_name(expr)
         return '{}({})'.format(name, self._print(expr.arg))
 
@@ -475,7 +466,7 @@ def _print_PythonComplex(self, expr):
             return '{}({}, {})'.format(name, self._print(expr.real), self._print(expr.imag))
 
     def _print_NumpyComplex(self, expr):
-        if expr.precision != -1:
+        if isinstance(expr.dtype, NumpyNumericType):
             name = self._get_numpy_name(expr)
         else:
             name = 'complex'
@@ -927,13 +918,9 @@ def _print_IfTernaryOperator(self, expr):
 
     def _print_Literal(self, expr):
         dtype = expr.dtype
-        precision = expr.precision
 
-        if not isinstance(expr, (LiteralInteger, LiteralFloat, LiteralComplex)) or \
-                precision == -1:
-            return repr(expr.python_value)
-        else:
-            cast_func = DtypePrecisionToCastFunction[dtype.name][precision]
+        if isinstance(dtype, NumpyNumericType):
+            cast_func = DtypePrecisionToCastFunction[dtype]
             type_name = cast_func.__name__.lower()
             is_numpy  = type_name.startswith('numpy')
             cast_name = cast_func.name
@@ -943,6 +930,8 @@ def _print_Literal(self, expr):
                         source = 'numpy',
                         target = AsName(cast_func, cast_name))
             return '{}({})'.format(name, repr(expr.python_value))
+        else:
+            return repr(expr.python_value)
 
     def _print_Print(self, expr):
         args = []
diff --git a/pyccel/codegen/wrapper/c_to_python_wrapper.py b/pyccel/codegen/wrapper/c_to_python_wrapper.py
index 357258be0b..2b7e8d5ad1 100644
--- a/pyccel/codegen/wrapper/c_to_python_wrapper.py
+++ b/pyccel/codegen/wrapper/c_to_python_wrapper.py
@@ -29,10 +29,9 @@
 from pyccel.ast.cwrapper      import PyList_New, PyList_Append, PyList_Size, PyList_GetItem, PyList_SetItem
 from pyccel.ast.cwrapper      import PyccelPyTypeObject, PyCapsule_New, PyCapsule_Import
 from pyccel.ast.cwrapper      import PySys_GetObject, PyUnicode_FromString, PyGetSetDefElement
-from pyccel.ast.c_concepts    import ObjectAddress, PointerCast, CStackArray
-from pyccel.ast.datatypes     import NativeVoid, NativeInteger, CustomDataType, DataTypeFactory
-from pyccel.ast.datatypes     import NativeNumeric
-from pyccel.ast.internals     import get_final_precision
+from pyccel.ast.c_concepts    import ObjectAddress, PointerCast, CStackArray, CNativeInt
+from pyccel.ast.datatypes     import VoidType, PythonNativeInt, CustomDataType, DataTypeFactory
+from pyccel.ast.datatypes     import FixedSizeNumericType
 from pyccel.ast.literals      import Nil, LiteralTrue, LiteralString, LiteralInteger
 from pyccel.ast.literals      import LiteralFalse
 from pyccel.ast.numpyext      import NumpyNDArrayType
@@ -105,16 +104,16 @@ def get_new_PyObject(self, name, dtype = None, is_temp = False):
             The new variable.
         """
         if isinstance(dtype, CustomDataType):
-            var = Variable(dtype=self._python_object_map[dtype],
-                            name=self.scope.get_new_name(name),
-                            memory_handling='alias',
-                            cls_base = self.scope.find(dtype.name, 'classes', raise_if_missing = True),
-                            is_temp=is_temp)
+            var = Variable(self._python_object_map[dtype],
+                           self.scope.get_new_name(name),
+                           memory_handling='alias',
+                           cls_base = self.scope.find(dtype.name, 'classes', raise_if_missing = True),
+                           is_temp=is_temp)
         else:
-            var = Variable(dtype=PyccelPyObject(),
-                            name=self.scope.get_new_name(name),
-                            memory_handling='alias',
-                            is_temp=is_temp)
+            var = Variable(PyccelPyObject(),
+                           self.scope.get_new_name(name),
+                           memory_handling='alias',
+                           is_temp=is_temp)
         self.scope.insert_variable(var)
         return var
 
@@ -272,25 +271,22 @@ def _get_check_function(self, py_obj, arg, raise_error):
             python_cls_base = self.scope.find(dtype.name, 'classes', raise_if_missing = True)
             func_call = FunctionCall(PyObject_TypeCheck, [py_obj, python_cls_base.type_object])
         elif rank == 0:
-            prec  = arg.precision
             try :
-                cast_function = check_type_registry[(dtype, prec)]
+                cast_function = check_type_registry[dtype]
             except KeyError:
-                errors.report(f"Can't check the type of {dtype.name}[kind = {prec}]\n"+PYCCEL_RESTRICTION_TODO,
+                errors.report(f"Can't check the type of {dtype}\n"+PYCCEL_RESTRICTION_TODO,
                         symbol=arg, severity='fatal')
             func = FunctionDef(name = cast_function,
                                body      = [],
-                               arguments = [FunctionDefArgument(Variable(dtype=PyccelPyObject(), name = 'o', memory_handling='alias'))],
-                               results   = [FunctionDefResult(Variable(dtype=dtype, name = 'v', precision = prec))])
+                               arguments = [FunctionDefArgument(Variable(PyccelPyObject(), name = 'o', memory_handling='alias'))],
+                               results   = [FunctionDefResult(Variable(dtype, name = 'v'))])
 
             func_call = FunctionCall(func, [py_obj])
         else:
-            np_dtype = str(dtype)
-            prec  = get_final_precision(arg)
             try :
-                type_ref = numpy_dtype_registry[(np_dtype, prec)]
+                type_ref = numpy_dtype_registry[dtype]
             except KeyError:
-                errors.report(f"Can't check the type of an array of {dtype.name}[kind = {prec}]\n"+PYCCEL_RESTRICTION_TODO,
+                errors.report(f"Can't check the type of an array of {dtype}\n"+PYCCEL_RESTRICTION_TODO,
                         symbol=arg, severity='fatal')
 
             # order flag
@@ -307,7 +303,7 @@ def _get_check_function(self, py_obj, arg, raise_error):
             func_call = FunctionCall(check_func, [py_obj, type_ref, LiteralInteger(rank), flag])
 
         if raise_error:
-            message = LiteralString(f"Expected an argument of type {dtype.name} for argument {arg.name}")
+            message = LiteralString(f"Expected an argument of type {dtype} for argument {arg.name}")
             python_error = FunctionCall(PyErr_SetString, [PyTypeError, message])
             error_code = (python_error,)
 
@@ -368,7 +364,7 @@ def f(a, b):
         func_scope = self.scope.new_child_scope(name)
         self.scope = func_scope
         orig_funcs = [getattr(func, 'original_function', func) for func in funcs]
-        type_indicator = Variable('int', self.scope.get_new_name('type_indicator'))
+        type_indicator = Variable(PythonNativeInt(), self.scope.get_new_name('type_indicator'))
 
         # Initialise the argument_type_flags
         argument_type_flags = {func : 0 for func in funcs}
@@ -381,7 +377,7 @@ def f(a, b):
             # Get the relevant typed arguments from the original functions
             interface_args = [getattr(func.arguments[i], 'original_function_argument_variable', func.arguments[i].var) for func in orig_funcs]
             # Get the type key
-            interface_types = [(a.dtype, a.precision, a.rank, a.order) for a in interface_args]
+            interface_types = [(str(a.dtype), a.rank, a.order) for a in interface_args]
             # Get a dictionary mapping each unique type key to an example argument
             type_to_example_arg = dict(zip(interface_types, interface_args))
             # Get a list of unique keys
@@ -531,15 +527,15 @@ def _incref_return_pointer(self, ref_obj, return_var, orig_var):
                                     (ObjectAddress(PointerCast(return_var, PyArray_SetBaseObject.arguments[0].var)),
                                      ObjectAddress(PointerCast(ref_obj, PyArray_SetBaseObject.arguments[1].var))))
             return [FunctionCall(Py_INCREF, (ref_obj,)),
-                    If(IfSection(PyccelLt(save_ref_call,LiteralInteger(0, precision=-2)),
+                    If(IfSection(PyccelLt(save_ref_call,LiteralInteger(0, dtype=CNativeInt())),
                                       [Return([self._error_exit_code])]))]
         elif isinstance(orig_var.dtype, CustomDataType):
             ref_attribute = return_var.cls_base.scope.find('referenced_objects', 'variables', raise_if_missing = True)
             ref_list = ref_attribute.clone(ref_attribute.name, new_class = DottedVariable, lhs = return_var)
             save_ref_call = FunctionCall(PyList_Append, (ref_list, ObjectAddress(PointerCast(ref_obj, ref_list))))
-            return [If(IfSection(PyccelLt(save_ref_call,LiteralInteger(0, precision=-2)),
+            return [If(IfSection(PyccelLt(save_ref_call,LiteralInteger(0, dtype=CNativeInt())),
                                       [Return([self._error_exit_code])]))]
-        elif orig_var.class_type in NativeNumeric:
+        elif isinstance(orig_var.class_type, FixedSizeNumericType):
             return []
         else:
             raise NotImplementedError(f"Unsure how to preserve references for attribute of type {type(orig_var.class_type)}")
@@ -615,8 +611,8 @@ def _build_module_init_function(self, expr, imports):
         # Create necessary variables
         module_var = self.get_new_PyObject("mod")
         API_var_name = self.scope.get_new_name(f'Py{mod_name}_API')
-        API_var = Variable(BindCPointer(), API_var_name, rank=1, shape = (n_classes,),
-                                    class_type = CStackArray(), cls_base = StackArrayClass)
+        API_var = Variable(CStackArray(BindCPointer()), API_var_name, rank=1, shape = (n_classes,),
+                                    cls_base = StackArrayClass)
         self.scope.insert_variable(API_var)
         capsule_obj = self.get_new_PyObject(self.scope.get_new_name('c_api_object'))
 
@@ -712,32 +708,32 @@ def _build_module_import_function(self, expr):
         func_name = self.scope.get_new_name(f'{mod_name}_import')
 
         API_var_name = self.scope.get_new_name(f'Py{mod_name}_API')
-        API_var = Variable(BindCPointer(), API_var_name, rank=1, shape = (None,),
-                                    class_type = CStackArray(), cls_base = StackArrayClass,
+        API_var = Variable(CStackArray(BindCPointer()), API_var_name, rank=1, shape = (None,),
+                                    cls_base = StackArrayClass,
                                     memory_handling = 'alias')
         self.scope.insert_variable(API_var)
 
         func_scope = self.scope.new_child_scope(func_name)
         self.scope = func_scope
 
-        ok_code = LiteralInteger(0, precision=-2)
-        error_code = PyccelUnarySub(LiteralInteger(1, precision=-2))
+        ok_code = LiteralInteger(0, dtype=CNativeInt())
+        error_code = PyccelUnarySub(LiteralInteger(1, dtype=CNativeInt()))
 
         # Create variables to temporarily modify the Python path so the file will be discovered
         current_path = func_scope.get_temporary_variable(PyccelPyObject(), 'current_path', memory_handling='alias')
         stash_path = func_scope.get_temporary_variable(PyccelPyObject(), 'stash_path', memory_handling='alias')
 
         body = [AliasAssign(current_path, FunctionCall(PySys_GetObject, [LiteralString("path")])),
-                AliasAssign(stash_path, FunctionCall(PyList_GetItem, [current_path, LiteralInteger(0, precision=-2)])),
+                AliasAssign(stash_path, FunctionCall(PyList_GetItem, [current_path, LiteralInteger(0, dtype=CNativeInt())])),
                 FunctionCall(Py_INCREF, [stash_path]),
                 FunctionCall(PyList_SetItem, [current_path,
-                                              LiteralInteger(0, precision=-2),
+                                              LiteralInteger(0, dtype=CNativeInt()),
                                               FunctionCall(PyUnicode_FromString, [LiteralString(self._file_location)])]),
                 AliasAssign(API_var, PyCapsule_Import(self.scope.get_python_name(mod_name))),
-                FunctionCall(PyList_SetItem, [current_path, LiteralInteger(0, precision=-2), stash_path]),
+                FunctionCall(PyList_SetItem, [current_path, LiteralInteger(0, dtype=CNativeInt()), stash_path]),
                 Return([IfTernaryOperator(PyccelIsNot(API_var, Nil()), ok_code, error_code)])]
 
-        result = func_scope.get_temporary_variable(NativeInteger(), precision=-2)
+        result = func_scope.get_temporary_variable(CNativeInt())
         self.exit_scope()
         import_func = FunctionDef(func_name, (), (FunctionDefResult(result),), body, is_static=True, scope = func_scope)
 
@@ -807,7 +803,7 @@ def _get_class_allocator(self, class_dtype, func = None):
         func_scope = self.scope.new_child_scope(func_name)
         self.scope = func_scope
 
-        self_var = Variable(dtype=PyccelPyTypeObject(), name=self.scope.get_new_name('self'),
+        self_var = Variable(PyccelPyTypeObject(), name=self.scope.get_new_name('self'),
                               memory_handling='alias')
         self.scope.insert_variable(self_var)
         func_args = [self_var] + [self.get_new_PyObject(n) for n in ("args", "kwargs")]
@@ -867,7 +863,7 @@ def _get_class_initialiser(self, init_function, cls_dtype):
         func_name = self.scope.get_new_name(original_name+'_wrapper')
         func_scope = self.scope.new_child_scope(func_name)
         self.scope = func_scope
-        self._error_exit_code = PyccelUnarySub(LiteralInteger(1, precision=-2))
+        self._error_exit_code = PyccelUnarySub(LiteralInteger(1, dtype=CNativeInt()))
 
         is_bind_c_function_def = isinstance(init_function, BindCFunctionDef)
 
@@ -896,8 +892,7 @@ def _get_class_initialiser(self, init_function, cls_dtype):
         func_args = [FunctionDefArgument(a) for a in func_args]
 
         # Get the results of the PyFunctionDef
-        python_result_variable = Variable(NativeInteger(), self.scope.get_new_name(),
-                                          precision = -2, is_temp = True)
+        python_result_variable = Variable(CNativeInt(), self.scope.get_new_name(), is_temp = True)
 
         # Get the code required to extract the C-compatible arguments from the Python arguments
         body += [l for a in python_args for l in self._wrap(a)]
@@ -925,7 +920,7 @@ def _get_class_initialiser(self, init_function, cls_dtype):
 
         # Pack the Python compatible results of the function into one argument.
         func_results = [FunctionDefResult(python_result_variable)]
-        body.append(Return([LiteralInteger(0, precision=-2)]))
+        body.append(Return([LiteralInteger(0, dtype=CNativeInt())]))
 
         self.exit_scope()
         for a in python_args:
@@ -1164,7 +1159,7 @@ def _wrap_Interface(self, expr):
         # Get python arguments which will be passed to FunctionDefs
         python_arg_objs = [self._python_object_map[a] for a in python_args]
 
-        type_indicator = Variable('int', self.scope.get_new_name('type_indicator'))
+        type_indicator = Variable(PythonNativeInt(), self.scope.get_new_name('type_indicator'))
         self.scope.insert_variable(type_indicator)
 
         self.exit_scope()
@@ -1295,7 +1290,7 @@ def _wrap_FunctionDef(self, expr):
         for r in original_c_results:
             if isinstance(r, BindCFunctionDefResult):
                 orig_var = r.original_function_result_variable
-                if orig_var.rank == 0 and orig_var.dtype in NativeNumeric:
+                if orig_var.rank == 0 and isinstance(orig_var.dtype, FixedSizeNumericType):
                     c_result_names.append(orig_var.name)
                     continue
             c_result_names.append(r.var.name)
@@ -1431,7 +1426,7 @@ def _wrap_FunctionDefArgument(self, expr):
             if isinstance(orig_var.dtype, CustomDataType):
                 kwargs['memory_handling']='alias'
                 if isinstance(expr, BindCFunctionDefArgument):
-                    kwargs['dtype'] = NativeVoid()
+                    kwargs['class_type'] = VoidType()
 
             arg_var = orig_var.clone(self.scope.get_expected_name(expr.var.name), new_class = Variable,
                                     **kwargs)
@@ -1457,8 +1452,8 @@ def _wrap_FunctionDefArgument(self, expr):
                 cast_type = collect_arg
                 cast = []
             else:
-                cast_type = Variable(dtype=self._python_object_map[dtype],
-                                    name=self.scope.get_new_name(collect_arg.name),
+                cast_type = Variable(self._python_object_map[dtype],
+                                    self.scope.get_new_name(collect_arg.name),
                                     memory_handling='alias',
                                     cls_base = self.scope.find(dtype.name, 'classes', raise_if_missing = True))
                 self.scope.insert_variable(cast_type)
@@ -1467,15 +1462,14 @@ def _wrap_FunctionDefArgument(self, expr):
             cast_c_res = PointerCast(c_res, orig_var)
             cast.append(AliasAssign(arg_var, cast_c_res))
         elif arg_var.rank == 0:
-            prec  = get_final_precision(orig_var)
             try :
-                cast_function = py_to_c_registry[(dtype, prec)]
+                cast_function = py_to_c_registry[(dtype.primitive_type, dtype.precision)]
             except KeyError:
                 errors.report(PYCCEL_RESTRICTION_TODO, symbol=dtype,severity='fatal')
             cast_func = FunctionDef(name = cast_function,
                                body      = [],
-                               arguments = [FunctionDefArgument(Variable(dtype=PyccelPyObject(), name = 'o', memory_handling='alias'))],
-                               results   = [FunctionDefResult(Variable(dtype=dtype, name = 'v', precision = prec))])
+                               arguments = [FunctionDefArgument(Variable(PyccelPyObject(), name = 'o', memory_handling='alias'))],
+                               results   = [FunctionDefResult(Variable(dtype, name = 'v'))])
             cast = [Assign(arg_var, FunctionCall(cast_func, [collect_arg]))]
         else:
             cast = [Assign(arg_var, FunctionCall(pyarray_to_ndarray, [collect_arg]))]
@@ -1662,7 +1656,7 @@ def _wrap_BindCFunctionDefResult(self, expr):
             self.scope.insert_variable(c_res, orig_var_name)
 
             body.append(Allocate(c_res, shape = shape_vars, order = orig_var.order, status='unallocated'))
-            body.append(AliasAssign(DottedVariable(NativeVoid(), 'raw_data', memory_handling = 'alias', lhs=c_res), arg_var))
+            body.append(AliasAssign(DottedVariable(VoidType(), 'raw_data', memory_handling = 'alias', lhs=c_res), arg_var))
         elif isinstance(orig_var.dtype, CustomDataType):
             c_res = expr.var.clone(self.scope.get_expected_name(var_name), is_argument = False, memory_handling='alias')
             self.scope.insert_variable(c_res, var_name)
@@ -1745,10 +1739,10 @@ def _wrap_BindCArrayVariable(self, expr):
         v = expr.original_variable
 
         # Get pointer to store raw array data
-        var = self.scope.get_temporary_variable(dtype_or_var = NativeVoid(),
+        var = self.scope.get_temporary_variable(dtype_or_var = VoidType(),
                 name = v.name + '_data', memory_handling = 'alias')
         # Create variables to store the shape of the array
-        shape = [self.scope.get_temporary_variable(NativeInteger(),
+        shape = [self.scope.get_temporary_variable(PythonNativeInt(),
                 v.name+'_size') for _ in range(v.rank)]
         # Get the bind_c function which wraps a fortran array and returns c objects
         var_wrapper = expr.wrapper_function
@@ -1760,7 +1754,7 @@ def _wrap_BindCArrayVariable(self, expr):
                 name = v.name, memory_handling = 'alias')
         alloc = Allocate(nd_var, shape=shape, order=nd_var.order, status='unallocated')
         # Save raw_data into ndarray to obtain useable pointer
-        set_data = AliasAssign(DottedVariable(NativeVoid(), 'raw_data',
+        set_data = AliasAssign(DottedVariable(VoidType(), 'raw_data',
                 memory_handling = 'alias', lhs=nd_var), var)
 
         # Save the ndarray to vars_to_wrap to be handled as if it came from C
@@ -1806,7 +1800,7 @@ def _wrap_DottedVariable(self, expr):
         getter_scope = self.scope.new_child_scope(getter_name)
         self.scope = getter_scope
         getter_args = [self.get_new_PyObject('self_obj', dtype = lhs.dtype),
-                       getter_scope.get_temporary_variable(NativeVoid(), memory_handling='alias')]
+                       getter_scope.get_temporary_variable(VoidType(), memory_handling='alias')]
         self.scope.insert_symbol(expr.name)
         getter_result = self.get_new_PyObject(expr.name, dtype = expr.dtype)
         get_val_result = FunctionDefResult(expr.clone(expr.name, new_class = Variable))
@@ -1847,19 +1841,19 @@ def _wrap_DottedVariable(self, expr):
         # ----------------------------------------------------------------------------------
         #                        Create setter
         # ----------------------------------------------------------------------------------
-        self._error_exit_code = PyccelUnarySub(LiteralInteger(1, precision=-2))
+        self._error_exit_code = PyccelUnarySub(LiteralInteger(1, dtype=CNativeInt()))
         setter_name = self.scope.get_new_name(f'{class_type.name}_{expr.name}_setter')
         setter_scope = self.scope.new_child_scope(setter_name)
         self.scope = setter_scope
         setter_args = [self.get_new_PyObject('self_obj', dtype = lhs.dtype),
                        self.get_new_PyObject(f'{expr.name}_obj'),
-                       setter_scope.get_temporary_variable(NativeVoid(), memory_handling='alias')]
-        setter_result = [FunctionDefResult(setter_scope.get_temporary_variable(NativeInteger(), precision=-2))]
+                       setter_scope.get_temporary_variable(VoidType(), memory_handling='alias')]
+        setter_result = [FunctionDefResult(setter_scope.get_temporary_variable(CNativeInt()))]
         self.scope.insert_symbol(expr.name)
         new_set_val_arg = FunctionDefArgument(expr.clone(expr.name, new_class = Variable))
         self._python_object_map[new_set_val_arg] = setter_args[1]
 
-        if (expr.rank == 0 and expr.dtype in NativeNumeric) or expr.is_alias:
+        if isinstance(expr.class_type, FixedSizeNumericType) or expr.is_alias:
             class_obj = Variable(lhs.dtype, self.scope.get_new_name('self'), memory_handling='alias')
             self.scope.insert_variable(class_obj)
 
@@ -1880,7 +1874,7 @@ def _wrap_DottedVariable(self, expr):
                                                               cast_type = lhs)),
                            *self._incref_return_pointer(setter_args[1], setter_args[0], expr.lhs),
                            update,
-                           Return((LiteralInteger(0, precision=-2),))]
+                           Return((LiteralInteger(0, dtype=CNativeInt()),))]
         else:
             setter_body = [FunctionCall(PyErr_SetString, [PyAttributeError,
                                         LiteralString("Can't reallocate memory via Python interface.")]),
@@ -1937,7 +1931,7 @@ def _wrap_BindCClassProperty(self, expr):
             self.scope.insert_symbol(r.var.name)
 
         getter_args = [self.get_new_PyObject('self_obj', dtype = class_type),
-                       getter_scope.get_temporary_variable(NativeVoid(), memory_handling='alias')]
+                       getter_scope.get_temporary_variable(VoidType(), memory_handling='alias')]
         getter_result = self.get_new_PyObject(f'{name}_obj',
                                         dtype = getattr(get_val_result, 'original_function_result_variable', get_val_result.var).dtype)
 
@@ -1977,7 +1971,7 @@ def _wrap_BindCClassProperty(self, expr):
         # ----------------------------------------------------------------------------------
         #                        Create setter
         # ----------------------------------------------------------------------------------
-        self._error_exit_code = PyccelUnarySub(LiteralInteger(1, precision=-2))
+        self._error_exit_code = PyccelUnarySub(LiteralInteger(1, dtype=CNativeInt()))
         setter_name = self.scope.get_new_name(f'{class_type.name}_{name}_setter')
         setter_scope = self.scope.new_child_scope(setter_name)
         self.scope = setter_scope
@@ -1994,13 +1988,13 @@ def _wrap_BindCClassProperty(self, expr):
 
         setter_args = [self.get_new_PyObject('self_obj', dtype = class_type),
                        self.get_new_PyObject(f'{name}_obj'),
-                       setter_scope.get_temporary_variable(NativeVoid(), memory_handling='alias')]
-        setter_result = [FunctionDefResult(setter_scope.get_temporary_variable(NativeInteger(), precision=-2))]
+                       setter_scope.get_temporary_variable(VoidType(), memory_handling='alias')]
+        setter_result = [FunctionDefResult(setter_scope.get_temporary_variable(CNativeInt()))]
 
         self._python_object_map[self_arg] = setter_args[0]
         self._python_object_map[set_val_arg] = setter_args[1]
 
-        if (wrapped_var.rank == 0 and wrapped_var.dtype in NativeNumeric) or wrapped_var.is_alias:
+        if isinstance(wrapped_var.class_type, FixedSizeNumericType) or wrapped_var.is_alias:
             arg_code = [l for a in original_args for l in self._wrap(a)]
 
             func_call_args = [self.scope.find(n.var.name, category='variables', raise_if_missing = True) for n in f_wrapped_args]
@@ -2008,7 +2002,7 @@ def _wrap_BindCClassProperty(self, expr):
             setter_body = [*arg_code,
                            FunctionCall(expr.setter, func_call_args),
                            *self._save_referenced_objects(expr.setter, setter_args),
-                           Return((LiteralInteger(0, precision=-2),))]
+                           Return((LiteralInteger(0, dtype=CNativeInt()),))]
         else:
             setter_body = [FunctionCall(PyErr_SetString, [PyAttributeError,
                                         LiteralString("Can't reallocate memory via Python interface.")]),
diff --git a/pyccel/codegen/wrapper/fortran_to_c_wrapper.py b/pyccel/codegen/wrapper/fortran_to_c_wrapper.py
index d9b9d2b254..125796efd1 100644
--- a/pyccel/codegen/wrapper/fortran_to_c_wrapper.py
+++ b/pyccel/codegen/wrapper/fortran_to_c_wrapper.py
@@ -17,7 +17,7 @@
 from pyccel.ast.core import Allocate, EmptyNode, FunctionAddress
 from pyccel.ast.core import If, IfSection, Import, Interface, FunctionDefArgument
 from pyccel.ast.core import AsName, Module, AliasAssign, FunctionDefResult
-from pyccel.ast.datatypes import NativeNumeric, CustomDataType
+from pyccel.ast.datatypes import CustomDataType, FixedSizeNumericType
 from pyccel.ast.internals import Slice
 from pyccel.ast.literals import LiteralInteger, Nil, LiteralTrue
 from pyccel.ast.operators import PyccelIsNot, PyccelMul
@@ -397,8 +397,8 @@ def _wrap_FunctionDefResult(self, expr):
             scope.insert_variable(local_var, name)
 
             # Create the C-compatible data pointer
-            bind_var = Variable(dtype=BindCPointer(),
-                                name=scope.get_new_name('bound_'+name),
+            bind_var = Variable(BindCPointer(),
+                                scope.get_new_name('bound_'+name),
                                 is_const=False, memory_handling='alias')
             scope.insert_variable(bind_var)
 
@@ -450,7 +450,7 @@ def _wrap_Variable(self, expr):
             The AST object describing the code which must be printed in
             the wrapping module to expose the variable.
         """
-        if expr.rank == 0 and expr.dtype in NativeNumeric:
+        if isinstance(expr.class_type, FixedSizeNumericType):
             return expr.clone(expr.name, new_class = BindCVariable)
         else:
             scope = self.scope
@@ -461,8 +461,8 @@ def _wrap_Variable(self, expr):
             func_scope.imports['variables'][expr.name] = expr
 
             # Create the data pointer
-            bind_var = Variable(dtype=BindCPointer(),
-                                name=scope.get_new_name('bound_'+expr.name),
+            bind_var = Variable(BindCPointer(),
+                                scope.get_new_name('bound_'+expr.name),
                                 is_const=True, memory_handling='alias')
             func_scope.insert_variable(bind_var)
 
@@ -600,8 +600,8 @@ def _wrap_ClassDef(self, expr):
         func_scope.insert_variable(local_var)
 
         # Create the C-compatible data pointer
-        bind_var = Variable(dtype=BindCPointer(),
-                            name=func_scope.get_new_name('bound_'+name),
+        bind_var = Variable(BindCPointer(),
+                            func_scope.get_new_name('bound_'+name),
                             is_const=False, memory_handling='alias')
         func_scope.insert_variable(bind_var)
 
diff --git a/pyccel/parser/semantic.py b/pyccel/parser/semantic.py
index b746978838..1b80eb206e 100644
--- a/pyccel/parser/semantic.py
+++ b/pyccel/parser/semantic.py
@@ -62,20 +62,19 @@
 
 from pyccel.ast.class_defs import NumpyArrayClass, TupleClass, get_cls_base
 
-from pyccel.ast.datatypes import str_dtype, DataType
-from pyccel.ast.datatypes import NativeSymbol, DataTypeFactory, CustomDataType
-from pyccel.ast.datatypes import default_precision, dtype_and_precision_registry
-from pyccel.ast.datatypes import (NativeInteger, NativeBool, NativeHomogeneousList,
-                                  NativeFloat, NativeString, NativeInhomogeneousTuple,
-                                  NativeGeneric, NativeComplex, NativeTuple,
-                                  NativeVoid, NativeHomogeneousTuple)
+from pyccel.ast.datatypes import CustomDataType, PyccelType, TupleType, VoidType, GenericType
+from pyccel.ast.datatypes import PrimitiveIntegerType, HomogeneousListType, StringType, SymbolicType
+from pyccel.ast.datatypes import PythonNativeBool, PythonNativeInt, PythonNativeFloat
+from pyccel.ast.datatypes import DataTypeFactory, PrimitiveFloatingPointType
+from pyccel.ast.datatypes import InhomogeneousTupleType, HomogeneousTupleType
+from pyccel.ast.datatypes import PrimitiveComplexType, FixedSizeNumericType
 
 from pyccel.ast.functionalexpr import FunctionalSum, FunctionalMax, FunctionalMin, GeneratorComprehension, FunctionalFor
 
 from pyccel.ast.headers import FunctionHeader, MethodHeader, Header
 from pyccel.ast.headers import MacroFunction, MacroVariable
 
-from pyccel.ast.internals import PyccelInternalFunction, Slice, PyccelSymbol, get_final_precision
+from pyccel.ast.internals import PyccelInternalFunction, Slice, PyccelSymbol
 from pyccel.ast.itertoolsext import Product
 
 from pyccel.ast.literals import LiteralTrue, LiteralFalse
@@ -89,7 +88,9 @@
 from pyccel.ast.numpyext import NumpyWhere, NumpyArray
 from pyccel.ast.numpyext import NumpyTranspose, NumpyConjugate
 from pyccel.ast.numpyext import NumpyNewArray, NumpyNonZero, NumpyResultType
-from pyccel.ast.numpyext import DtypePrecisionToCastFunction, NumpyNDArrayType
+from pyccel.ast.numpyext import process_dtype as numpy_process_dtype
+
+from pyccel.ast.numpytypes import NumpyNDArrayType
 
 from pyccel.ast.omp import (OMP_For_Loop, OMP_Simd_Construct, OMP_Distribute_Construct,
                             OMP_TaskLoop_Construct, OMP_Sections_Construct, Omp_End_Clause,
@@ -102,7 +103,7 @@
 from pyccel.ast.sympy_helper import sympy_to_pyccel, pyccel_to_sympy
 
 from pyccel.ast.type_annotations import VariableTypeAnnotation, UnionTypeAnnotation, SyntacticTypeAnnotation
-from pyccel.ast.type_annotations import FunctionTypeAnnotation
+from pyccel.ast.type_annotations import FunctionTypeAnnotation, typenames_to_dtypes
 
 from pyccel.ast.typingext import TypingFinal
 
@@ -405,7 +406,7 @@ def check_for_variable(self, name):
             try:
                 class_def = prefix.cls_base
             except AttributeError:
-                class_def = get_cls_base(prefix.dtype, prefix.precision, prefix.class_type) or \
+                class_def = get_cls_base(prefix.class_type) or \
                             self.scope.find(prefix.class_type.name, 'classes')
 
             attr_name = name.name[-1]
@@ -500,7 +501,7 @@ def get_class_construct(self, name):
 
         Returns
         -------
-        DataType
+        PyccelType
             The datatype for the class.
 
         Raises
@@ -599,9 +600,9 @@ def _garbage_collector(self, expr):
         if len(expr.body)>0 and not isinstance(expr.body[-1], Return):
             for i in self._allocs[-1]:
                 if isinstance(i, DottedVariable):
-                    if isinstance(i.lhs.dtype, CustomDataType) and self._current_function != '__del__':
+                    if isinstance(i.lhs.class_type, CustomDataType) and self._current_function != '__del__':
                         continue
-                if isinstance(i.dtype, CustomDataType) and i.is_alias:
+                if isinstance(i.class_type, CustomDataType) and i.is_alias:
                     continue
                 deallocs.append(Deallocate(i))
         self._allocs.pop()
@@ -635,7 +636,7 @@ class instance persists after a call to a class method, and the arguments of a
         """
         for i in self._allocs[-1]:
             if isinstance(i, DottedVariable):
-                if isinstance(i.lhs.dtype, CustomDataType) and self._current_function != '__del__':
+                if isinstance(i.lhs.class_type, CustomDataType) and self._current_function != '__del__':
                     continue
             if i in exceptions:
                 continue
@@ -643,7 +644,7 @@ class instance persists after a call to a class method, and the arguments of a
         targets = {t[0]:t[1] for target_list in self._pointer_targets[-1].values() for t in target_list}
         for i in self._allocs[-1]:
             if isinstance(i, DottedVariable):
-                if isinstance(i.lhs.dtype, CustomDataType) and self._current_function != '__del__':
+                if isinstance(i.lhs.class_type, CustomDataType) and self._current_function != '__del__':
                     continue
             if i in exceptions:
                 continue
@@ -712,7 +713,6 @@ def _infer_type(self, expr):
         Create a dictionary describing all the type information that can be
         inferred about the expression `expr`. This includes information about:
         - `datatype`
-        - `precision`
         - `rank`
         - `shape`
         - `order`
@@ -732,8 +732,6 @@ def _infer_type(self, expr):
             Dictionary containing all the type information which was inferred.
         """
         d_var = {
-                'datatype' : expr.dtype,
-                'precision': expr.precision,
                 'shape'    : expr.shape,
                 'rank'     : expr.rank,
                 'order'    : expr.order,
@@ -743,7 +741,7 @@ def _infer_type(self, expr):
         if isinstance(expr, Variable):
             d_var['memory_handling'] = expr.memory_handling
             d_var['class_type'     ] = expr.class_type
-            d_var['cls_base'       ] = expr.cls_base or self.scope.find(expr.dtype.name, 'classes')
+            d_var['cls_base'       ] = expr.cls_base or self.scope.find(str(expr.dtype), 'classes')
             d_var['is_target'      ] = expr.is_target
             return d_var
 
@@ -786,7 +784,7 @@ def _infer_type(self, expr):
         elif isinstance(expr, TypedAstNode):
 
             d_var['memory_handling'] = 'heap' if expr.rank > 0 else 'stack'
-            d_var['cls_base'   ] = get_cls_base(expr.dtype, expr.precision, expr.class_type)
+            d_var['cls_base'   ] = get_cls_base(expr.class_type)
             return d_var
 
         else:
@@ -905,11 +903,11 @@ def is_int(a):
                 severity='error')
 
         for arg in var[indices].indices:
-            if not isinstance(arg, (Slice, LiteralEllipsis)) and not \
-                (hasattr(arg, 'dtype') and isinstance(arg.dtype, NativeInteger)):
+            if not isinstance(arg, (Slice, LiteralEllipsis)) and not (hasattr(arg, 'dtype') and
+                    isinstance(getattr(arg.dtype, 'primitive_type', None), PrimitiveIntegerType)):
                 errors.report(INVALID_INDICES, symbol=var[indices],
-                bounding_box=(self.current_ast_node.lineno, self.current_ast_node.col_offset),
-                severity='error')
+                    bounding_box=(self.current_ast_node.lineno, self.current_ast_node.col_offset),
+                    severity='error')
         return var[indices]
 
     def _create_PyccelOperator(self, expr, visited_args):
@@ -952,7 +950,7 @@ def _create_Duplicate(self, val, length):
         ----------
         val : PyccelAstNode
             The tuple object. This object should have a class type which inherits from
-            NativeTuple.
+            TupleType.
 
         length : LiteralInteger | TypedAstNode
             The number of times the tuple is duplicated.
@@ -964,12 +962,12 @@ def _create_Duplicate(self, val, length):
         """
         # Arguments have been visited in PyccelMul
 
-        if not isinstance(val.class_type, (NativeTuple, NativeHomogeneousList)):
+        if not isinstance(val.class_type, (TupleType, HomogeneousListType)):
             errors.report("Unexpected Duplicate", symbol=Duplicate(val, length),
                 bounding_box=(self.current_ast_node.lineno, self.current_ast_node.col_offset),
                 severity='fatal')
 
-        if isinstance(val.class_type, NativeHomogeneousTuple):
+        if isinstance(val.class_type, HomogeneousTupleType):
             return Duplicate(val, length)
         else:
             if isinstance(length, LiteralInteger):
@@ -1024,19 +1022,24 @@ def _handle_function_args(self, arguments):
 
     def get_type_description(self, var, include_rank = True):
         """
-        Provides a text description of the type of a variable
-        (useful for error messages)
+        Get a text description of the type of a variable.
+
+        Get a text description of the type of a variable.
+        This is notably useful for error messages.
+
         Parameters
         ----------
-        var          : Variable
-                       The variable to describe
-        include_rank : bool
-                       Indicates whether rank information should be included
-                       Default : True
+        var : Variable
+            The variable to describe.
+        include_rank : bool, default=True
+            Indicates whether rank information should be included.
+
+        Returns
+        -------
+        str
+            A description of the variable type.
         """
-        dtype = var.dtype
-        prec  = get_final_precision(var)
-        descr = f'{dtype}{(prec * 2 if isinstance(dtype, NativeComplex) else prec) * 8 if prec else ""}'
+        descr = str(var.class_type)
         if include_rank and var.rank>0:
             dims = ','.join(':'*var.rank)
             descr += f'[{dims}]'
@@ -1070,12 +1073,10 @@ def _check_argument_compatibility(self, input_args, func_args, func, elemental,
         """
         if elemental:
             def incompatible(i_arg, f_arg):
-                return (i_arg.dtype is not f_arg.dtype or \
-                        get_final_precision(i_arg) != get_final_precision(f_arg))
+                return i_arg.class_type.datatype != f_arg.class_type.datatype
         else:
             def incompatible(i_arg, f_arg):
-                return (i_arg.dtype is not f_arg.dtype or \
-                        get_final_precision(i_arg) != get_final_precision(f_arg) or
+                return (i_arg.class_type != f_arg.class_type or
                         i_arg.rank != f_arg.rank)
 
         err_msgs = []
@@ -1086,7 +1087,7 @@ def incompatible(i_arg, f_arg):
             # Ignore types which cannot be compared
             if (i_arg is Nil()
                     or isinstance(f_arg, FunctionAddress)
-                    or f_arg.dtype is NativeGeneric()):
+                    or f_arg.class_type is GenericType()):
                 continue
 
             # Check for compatibility
@@ -1294,7 +1295,7 @@ def _annotate_the_called_function_def(self, old_func, function_call_args=None):
             scope.imports['functions'][old_func.name] = func
         return func
 
-    def _create_variable(self, name, dtype, rhs, d_lhs, arr_in_multirets=False):
+    def _create_variable(self, name, class_type, rhs, d_lhs, arr_in_multirets=False):
         """
         Create a new variable.
 
@@ -1310,8 +1311,8 @@ def _create_variable(self, name, dtype, rhs, d_lhs, arr_in_multirets=False):
         name : str
             The name of the new variable.
 
-        dtype : DataType
-            The data type of the new variable.
+        class_type : PyccelType
+            The type of the new variable.
 
         rhs : Variable
             The value assigned to the lhs. This is required to call
@@ -1351,13 +1352,13 @@ def _create_variable(self, name, dtype, rhs, d_lhs, arr_in_multirets=False):
                     self._ensure_target( r, elem_d_lhs )
                 if elem_d_lhs_ref is None:
                     elem_d_lhs_ref = elem_d_lhs.copy()
-                    is_homogeneous = elem_d_lhs['datatype'] is not NativeGeneric()
+                    is_homogeneous = getattr(elem_d_lhs['class_type'], 'datatype', GenericType()) is not GenericType()
                 elif elem_d_lhs != elem_d_lhs_ref:
                     is_homogeneous = False
 
-                elem_dtype = elem_d_lhs.pop('datatype')
+                elem_type = elem_d_lhs.pop('class_type')
 
-                var = self._create_variable(elem_name, elem_dtype, r, elem_d_lhs)
+                var = self._create_variable(elem_name, elem_type, r, elem_d_lhs)
                 elem_vars.append(var)
 
             if any(v.is_alias for v in elem_vars):
@@ -1366,12 +1367,12 @@ def _create_variable(self, name, dtype, rhs, d_lhs, arr_in_multirets=False):
                 d_lhs['memory_handling'] = d_lhs.get('memory_handling', False) or 'heap'
 
             if is_homogeneous and not (d_lhs['memory_handling'] == 'alias' and isinstance(rhs, PythonTuple)):
-                lhs = Variable(dtype, name, **d_lhs, is_temp=is_temp)
+                lhs = Variable(class_type, name, **d_lhs, is_temp=is_temp)
             else:
-                lhs = InhomogeneousTupleVariable(elem_vars, name, **d_lhs, is_temp=is_temp)
+                lhs = InhomogeneousTupleVariable(elem_vars, name, class_type = class_type, **d_lhs, is_temp=is_temp)
 
         else:
-            lhs = Variable(dtype, name, **d_lhs, is_temp=is_temp)
+            lhs = Variable(class_type, name, **d_lhs, is_temp=is_temp)
 
         return lhs
 
@@ -1395,7 +1396,7 @@ def _ensure_target(self, rhs, d_lhs):
             d_lhs['memory_handling'] = 'alias'
             rhs.internal_var.is_target = True
 
-        if isinstance(rhs, Variable) and (rhs.is_ndarray or isinstance(rhs.dtype, CustomDataType)):
+        if isinstance(rhs, Variable) and (rhs.is_ndarray or isinstance(rhs.class_type, CustomDataType)):
             d_lhs['memory_handling'] = 'alias'
             rhs.is_target = not rhs.is_alias
 
@@ -1451,7 +1452,7 @@ def _assign_lhs_variable(self, lhs, d_var, rhs, new_expressions, is_augassign,ar
             name = lhs
             if lhs == '_':
                 name = self.scope.get_new_name()
-            dtype = d_var.pop('datatype')
+            dtype = d_var.pop('class_type')
 
             d_lhs = d_var.copy()
             # ISSUES #177: lhs must be a pointer when rhs is heap array
@@ -1537,7 +1538,7 @@ def _assign_lhs_variable(self, lhs, d_var, rhs, new_expressions, is_augassign,ar
                 # ...
                 # Add memory allocation if needed
                 array_declared_in_function = (isinstance(rhs, FunctionCall) and not isinstance(rhs.funcdef, PyccelFunctionDef) \
-                                            and not getattr(rhs.funcdef, 'is_elemental', False) and not isinstance(lhs.class_type, NativeHomogeneousTuple)) or arr_in_multirets
+                                            and not getattr(rhs.funcdef, 'is_elemental', False) and not isinstance(lhs.class_type, HomogeneousTupleType)) or arr_in_multirets
                 if lhs.on_heap and not array_declared_in_function:
                     if self.scope.is_loop:
                         # Array defined in a loop may need reallocation at every cycle
@@ -1568,7 +1569,7 @@ def _assign_lhs_variable(self, lhs, d_var, rhs, new_expressions, is_augassign,ar
 
                 # ...
                 # Add memory deallocation
-                if isinstance(lhs.dtype, CustomDataType) or not lhs.on_stack:
+                if isinstance(lhs.class_type, CustomDataType) or not lhs.on_stack:
                     if isinstance(lhs, InhomogeneousTupleVariable):
                         args = [v for v in lhs.get_vars() if v.rank>0]
                         new_args = []
@@ -1620,7 +1621,7 @@ def _assign_lhs_variable(self, lhs, d_var, rhs, new_expressions, is_augassign,ar
 
         return lhs
 
-    def _ensure_inferred_type_matches_existing(self, dtype, d_var, var, is_augassign, new_expressions, rhs):
+    def _ensure_inferred_type_matches_existing(self, class_type, d_var, var, is_augassign, new_expressions, rhs):
         """
         Ensure that the inferred type matches the existing variable.
 
@@ -1632,8 +1633,8 @@ def _ensure_inferred_type_matches_existing(self, dtype, d_var, var, is_augassign
 
         Parameters
         ----------
-        dtype : DataType
-            The inferred DataType.
+        class_type : PyccelType
+            The inferred PyccelType.
         d_var : dict
             The inferred information about the variable. Usually created by the _infer_type function.
         var : Variable
@@ -1647,18 +1648,15 @@ def _ensure_inferred_type_matches_existing(self, dtype, d_var, var, is_augassign
             If is_augassign is False, this value is not used.
         """
 
-        precision = d_var.get('precision', 0)
-        internal_precision = default_precision[dtype] if precision == -1 else precision
-
         # TODO improve check type compatibility
-        if not hasattr(var, 'dtype'):
+        if not isinstance(var, Variable):
             name = var.name
-            errors.report(INCOMPATIBLE_TYPES_IN_ASSIGNMENT.format(type(var), dtype),
-                    symbol=f'{name}={dtype}',
+            errors.report(INCOMPATIBLE_TYPES_IN_ASSIGNMENT.format(type(var), class_type),
+                    symbol=f'{name}={class_type}',
                     bounding_box=(self.current_ast_node.lineno, self.current_ast_node.col_offset),
                     severity='fatal')
 
-        elif not is_augassign and var.is_ndarray and var.is_target:
+        if not is_augassign and var.is_ndarray and var.is_target:
             errors.report(ARRAY_ALREADY_IN_USE,
                 bounding_box=(self.current_ast_node.lineno,
                     self.current_ast_node.col_offset),
@@ -1682,40 +1680,21 @@ def _ensure_inferred_type_matches_existing(self, dtype, d_var, var, is_augassign
             # to remove memory leaks
             new_expressions.append(Deallocate(var))
 
-        elif str(dtype) != str(var.dtype) or \
-                internal_precision != get_final_precision(var) or \
-                d_var['class_type'] != var.class_type:
+        elif class_type != var.class_type:
             if is_augassign:
                 tmp_result = PyccelAdd(var, rhs)
-                result_dtype = str(tmp_result.dtype)
-                result_precision = get_final_precision(tmp_result)
-                raise_error = (str(var.dtype) != result_dtype or \
-                        get_final_precision(var) != result_precision)
-            elif str(dtype) == str(var.dtype) and \
-                    internal_precision == get_final_precision(var) and \
-                    var.rank == 0:
+                result_type = tmp_result.class_type
+                raise_error = var.class_type != result_type
+            elif class_type == var.class_type and var.rank == 0:
                 # Don't complain about non-numpy and numpy scalars
                 raise_error = False
             else:
                 raise_error = True
 
             if raise_error:
-                # Get type name from cast function (handles precision implicitly)
-                try:
-                    d1 = DtypePrecisionToCastFunction[var.dtype.name][var.precision].name
-                except KeyError:
-                    d1 = str(var.dtype)
-                try:
-                    d2 = DtypePrecisionToCastFunction[dtype.name][precision].name
-                except KeyError:
-                    d2 = str(dtype)
-
-                c1 = d1 if var.class_type == var.dtype else f"{var.class_type}[{d1}]"
-                c2 = d2 if d_var['class_type'] == dtype else f"{d_var['class_type']}[{d2}]"
-
                 name = var.name
                 rhs_str = str(rhs)
-                errors.report(INCOMPATIBLE_TYPES_IN_ASSIGNMENT.format(c1, c2),
+                errors.report(INCOMPATIBLE_TYPES_IN_ASSIGNMENT.format(var.class_type, class_type),
                     symbol=f'{name}={rhs_str}',
                     bounding_box=(self.current_ast_node.lineno, self.current_ast_node.col_offset),
                     severity='error')
@@ -1800,12 +1779,9 @@ def format_shape(s):
                 new_expressions.append(Allocate(var,
                     shape=d_var['shape'], order=d_var['order'],
                     status=status))
-            elif isinstance(var.dtype, CustomDataType) and not var.is_alias:
+            elif isinstance(var.class_type, CustomDataType) and not var.is_alias:
                 new_expressions.append(Deallocate(var))
 
-        if var.precision == -1 and precision != var.precision:
-            var.use_exact_precision()
-
     def _assign_GeneratorComprehension(self, lhs_name, expr):
         """
         Visit the GeneratorComprehension node.
@@ -1830,7 +1806,7 @@ def _assign_GeneratorComprehension(self, lhs_name, expr):
         loop = expr.loops
         nlevels = 0
         # Create throw-away variable to help obtain result type
-        index   = Variable('int',self.scope.get_new_name('to_delete'), is_temp=True)
+        index   = Variable(PythonNativeInt(),self.scope.get_new_name('to_delete'), is_temp=True)
         self.scope.insert_variable(index)
         new_expr = []
         while isinstance(loop, For):
@@ -1874,17 +1850,31 @@ def _assign_GeneratorComprehension(self, lhs_name, expr):
         if isinstance(result, CodeBlock):
             result = result.body[-1]
 
+        # Create start value
+        if isinstance(expr, FunctionalSum):
+            dtype = result.dtype
+            if isinstance(dtype, PythonNativeBool):
+                val = LiteralInteger(0, dtype)
+            else:
+                val = convert_to_literal(0, dtype)
+            d_var = self._infer_type(PyccelAdd(result, val))
+        elif isinstance(expr, FunctionalMin):
+            val = math_constants['inf']
+            d_var = self._infer_type(result)
+        elif isinstance(expr, FunctionalMax):
+            val = PyccelUnarySub(math_constants['inf'])
+            d_var = self._infer_type(result)
+
         # Infer the final dtype of the expression
-        d_var = self._infer_type(result)
-        dtype = d_var.pop('datatype')
+        class_type = d_var.pop('class_type')
         d_var['is_temp'] = expr.lhs.is_temp
 
         lhs  = self.check_for_variable(lhs_name)
         if lhs:
-            self._ensure_inferred_type_matches_existing(dtype, d_var, lhs, False, new_expr, None)
+            self._ensure_inferred_type_matches_existing(class_type, d_var, lhs, False, new_expr, None)
         else:
             lhs_name = self.scope.get_expected_name(lhs_name)
-            lhs = Variable(dtype, lhs_name, **d_var)
+            lhs = Variable(class_type, lhs_name, **d_var)
             self.scope.insert_variable(lhs)
 
         # Iterate over the loops
@@ -1902,15 +1892,6 @@ def _assign_GeneratorComprehension(self, lhs_name, expr):
                 loop.body.insert2body(e, back=False)
                 e.loops[-1].scope.update_parent_scope(loop.scope, is_loop = True)
 
-        if isinstance(expr, FunctionalSum):
-            val = LiteralInteger(0)
-            if str_dtype(dtype) in ['float', 'complex']:
-                val = LiteralFloat(0.0)
-        elif isinstance(expr, FunctionalMin):
-            val = math_constants['inf']
-        elif isinstance(expr, FunctionalMax):
-            val = PyccelUnarySub(math_constants['inf'])
-
         # Initialise result with correct initial value
         stmt = Assign(lhs, val)
         stmt.set_current_ast(expr.python_ast)
@@ -1994,23 +1975,17 @@ def _get_indexed_type(self, base, args, expr):
 
         if all(isinstance(a, Slice) for a in args):
             if isinstance(base, VariableTypeAnnotation):
-                dtype = base.datatype
-                prec = base.precision
-                class_type = base.class_type
-                if class_type is dtype:
-                    class_type = NumpyNDArrayType()
+                dtype = base.class_type
+                class_type = NumpyNDArrayType(numpy_process_dtype(dtype))
                 if base.rank != 0:
                     raise errors.report("Can't index a vector type",
                             severity='fatal', symbol=expr)
             elif isinstance(base, PyccelFunctionDef):
                 dtype_cls = base.cls_name
-                dtype = dtype_cls.static_dtype()
-                prec = dtype_cls.static_precision()
-                class_type = NumpyNDArrayType()
+                dtype = numpy_process_dtype(dtype_cls.static_type())
+                class_type = NumpyNDArrayType(dtype)
             rank = len(args)
-            if prec == -1:
-                prec = default_precision[dtype]
-            return VariableTypeAnnotation(dtype, class_type, prec, rank)
+            return VariableTypeAnnotation(class_type, rank)
 
         if not any(isinstance(a, Slice) for a in args):
             if isinstance(base, PyccelFunctionDef):
@@ -2026,17 +2001,17 @@ def _get_indexed_type(self, base, args, expr):
                 internal_datatypes = self._visit(syntactic_annotation)
                 type_annotations = []
                 if dtype_cls is PythonTupleFunction:
-                    class_type = NativeHomogeneousTuple()
+                    class_type = HomogeneousTupleType
                 elif dtype_cls is PythonList:
-                    class_type = NativeHomogeneousList()
+                    class_type = HomogeneousListType
                 else:
                     raise errors.report(f"Unknown annotation base {base}\n"+PYCCEL_RESTRICTION_TODO,
                             severity='fatal', symbol=expr)
                 for u in internal_datatypes.type_list:
                     rank = u.rank+1
                     order = None if rank == 1 else 'C'
-                    type_annotations.append(VariableTypeAnnotation(u.datatype, class_type,
-                        u.precision, rank, order, u.is_const))
+                    type_annotations.append(VariableTypeAnnotation(class_type(u.class_type),
+                        rank, order, u.is_const))
                 return UnionTypeAnnotation(*type_annotations)
             else:
                 raise errors.report("Cannot handle non-homogenous type index\n"+PYCCEL_RESTRICTION_TODO,
@@ -2142,7 +2117,7 @@ def _visit_Module(self, expr):
 
         if not all(isinstance(l, comment_types) for l in init_func_body):
             # If there are any initialisation statements then create an initialisation function
-            init_var = Variable(NativeBool(), self.scope.get_new_name('initialised'),
+            init_var = Variable(PythonNativeBool(), self.scope.get_new_name('initialised'),
                                 is_private=True)
             init_func_name = self.scope.get_new_name(name_suffix+'__init')
             # Ensure that the function is correctly defined within the namespaces
@@ -2231,16 +2206,16 @@ def _visit_Module(self, expr):
                         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, class_type = t.class_type,
+                            args = [Variable(t.class_type, PyccelSymbol(f'anon_{i}'),
+                                shape = None, rank = t.rank, order = t.order,
                                 is_const = t.is_const, is_optional = False,
-                                cls_base = t.datatype if t.rank == 0 else NumpyNDArrayType(),
+                                cls_base = t.class_type if t.rank == 0 else NumpyNDArrayType(numpy_process_dtype(t.class_type.element_type)),
                                 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, class_type = t.class_type,
-                                cls_base = t.datatype if t.rank == 0 else NumpyNDArrayType(),
+                            results = [Variable(t.class_type, PyccelSymbol(f'result_{i}'),
+                                shape = None, rank = t.rank, order = t.order,
+                                cls_base = t.class_type if t.rank == 0 else NumpyNDArrayType(numpy_process_dtype(t.class_type.element_type)),
                                 is_const = t.is_const, is_optional = False,
                                 memory_handling = 'heap' if t.rank > 0 else 'stack') for i,t in enumerate(types)]
 
@@ -2327,7 +2302,7 @@ def generate_and_assign_temp_var():
             return FunctionCallArgument(self._visit(tmp_var))
         if isinstance(value, (PyccelArithmeticOperator, PyccelInternalFunction)) and value.rank:
             a = generate_and_assign_temp_var()
-        elif isinstance(value, FunctionCall) and isinstance(value.dtype, CustomDataType):
+        elif isinstance(value, FunctionCall) and isinstance(value.class_type, CustomDataType):
             if not value.funcdef.results[0].var.is_alias:
                 a = generate_and_assign_temp_var()
         return a
@@ -2358,12 +2333,9 @@ def _visit_FunctionDefArgument(self, expr):
         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)
+                dtype = v.class_type
+                if isinstance(value, Literal) and value is not Nil():
+                    value = convert_to_literal(value.python_value, dtype)
                 clone_var = v.clone(v.name, is_optional = is_optional, is_argument = True)
                 args.append(FunctionDefArgument(clone_var, bound_argument = bound_argument,
                                         value = value, kwonly = kwonly, annotation = expr.annotation))
@@ -2508,10 +2480,10 @@ def _visit_IndexedElement(self, expr):
         # TODO check consistency of indices with shape/rank
         args = [self._visit(idx) for idx in expr.indices]
 
-        if (len(args) == 1 and isinstance(getattr(args[0], 'class_type', None), NativeTuple)):
+        if (len(args) == 1 and isinstance(getattr(args[0], 'class_type', None), TupleType)):
             args = args[0]
 
-        elif any(isinstance(getattr(a, 'class_type', None), NativeTuple) for a in args):
+        elif any(isinstance(getattr(a, 'class_type', None), TupleType) for a in args):
             n_exprs = None
             for a in args:
                 if getattr(a, 'shape', None) and isinstance(a.shape[0], LiteralInteger):
@@ -2545,7 +2517,7 @@ def _visit_PyccelSymbol(self, expr):
             if name == 'real':
                 var = numpy_funcs['float']
             elif name == '*':
-                return NativeGeneric()
+                return GenericType()
 
         if var is None:
             if name == '_':
@@ -2613,13 +2585,11 @@ def _visit_AnnotatedPyccelSymbol(self, expr):
                 address = FunctionAddress(name, args, results)
                 possible_args.append(address)
             elif isinstance(t, VariableTypeAnnotation):
-                dtype = t.datatype
-                prec  = t.precision
                 rank  = t.rank
                 class_type = t.class_type
-                cls_base = get_cls_base(dtype, prec, class_type) or self.scope.find(class_type.name, 'classes')
-                v = var_class(dtype, name, precision = prec, cls_base = cls_base,
-                        shape = None, rank = rank, order = t.order, class_type = t.class_type,
+                cls_base = get_cls_base(class_type) or self.scope.find(class_type.name, 'classes')
+                v = var_class(class_type, name, cls_base = cls_base,
+                        shape = None, rank = rank, order = t.order,
                         is_const = t.is_const, is_optional = False,
                         memory_handling = array_memory_handling if rank > 0 else 'stack',
                         **kwargs)
@@ -2643,12 +2613,8 @@ def _visit_SyntacticTypeAnnotation(self, expr):
 
         if isinstance(visited_dtype, PyccelFunctionDef):
             dtype_cls = visited_dtype.cls_name
-            dtype = dtype_cls.static_dtype()
-            prec = dtype_cls.static_precision()
-            class_type = dtype_cls.static_class_type()
-            if not isinstance(class_type, DataType):
-                class_type = dtype
-            return UnionTypeAnnotation(VariableTypeAnnotation(dtype, class_type, prec, 0, None))
+            class_type = dtype_cls.static_type()
+            return UnionTypeAnnotation(VariableTypeAnnotation(class_type))
         elif isinstance(visited_dtype, VariableTypeAnnotation):
             if visited_dtype.rank > 1:
                 visited_dtype.order = order or visited_dtype.order or 'C'
@@ -2658,12 +2624,9 @@ def _visit_SyntacticTypeAnnotation(self, expr):
         elif isinstance(visited_dtype, ClassDef):
             # TODO: Improve when #1676 is merged
             dtype = self.get_class_construct(visited_dtype.name)
-            prec = 0
-            rank = 0
-            order = None
-            return UnionTypeAnnotation(VariableTypeAnnotation(dtype, dtype, prec, rank, order))
-        elif isinstance(visited_dtype, DataType):
-            return UnionTypeAnnotation(VariableTypeAnnotation(visited_dtype, visited_dtype, -1, 0, None))
+            return UnionTypeAnnotation(VariableTypeAnnotation(dtype))
+        elif isinstance(visited_dtype, PyccelType):
+            return UnionTypeAnnotation(VariableTypeAnnotation(visited_dtype))
         else:
             raise errors.report(PYCCEL_RESTRICTION_TODO + ' Could not deduce type information',
                     severity='fatal', symbol=expr)
@@ -2735,10 +2698,10 @@ def _visit_DottedName(self, expr):
                     severity='fatal')
 
         d_var = self._infer_type(first)
-        dtype = d_var['datatype']
-        cls_base = get_cls_base(dtype, d_var['precision'], d_var['class_type'])
+        class_type = d_var['class_type']
+        cls_base = get_cls_base(class_type)
         if cls_base is None:
-            cls_base = self.scope.find(dtype.name, 'classes')
+            cls_base = self.scope.find(class_type.name, 'classes')
 
         # look for a class method
         if isinstance(rhs, FunctionCall):
@@ -2848,15 +2811,15 @@ def _visit_PyccelOperator(self, expr):
 
     def _visit_PyccelAdd(self, expr):
         args = [self._visit(a) for a in expr.args]
-        if isinstance(args[0].class_type, (NativeTuple, NativeHomogeneousList)):
-            is_inhomogeneous = any(isinstance(a.class_type, NativeInhomogeneousTuple) for a in args)
+        if isinstance(args[0].class_type, (TupleType, HomogeneousListType)):
+            is_inhomogeneous = any(isinstance(a.class_type, InhomogeneousTupleType) for a in args)
             if is_inhomogeneous:
                 def get_vars(a):
                     if isinstance(a, InhomogeneousTupleVariable):
                         return a.get_vars()
                     elif isinstance(a, PythonTuple):
                         return a.args
-                    elif isinstance(a.class_type, NativeHomogeneousTuple):
+                    elif isinstance(a.class_type, HomogeneousTupleType):
                         n_vars = a.shape[0]
                         if not isinstance(a.shape[0], (LiteralInteger, int)):
                             errors.report("Can't create an inhomogeneous tuple using a homogeneous tuple of unknown size",
@@ -2875,9 +2838,9 @@ def get_vars(a):
 
     def _visit_PyccelMul(self, expr):
         args = [self._visit(a) for a in expr.args]
-        if isinstance(args[0].class_type, (NativeTuple, NativeHomogeneousList)):
+        if isinstance(args[0].class_type, (TupleType, HomogeneousListType)):
             expr_new = self._create_Duplicate(args[0], args[1])
-        elif isinstance(args[1].class_type, (NativeTuple, NativeHomogeneousList)):
+        elif isinstance(args[1].class_type, (TupleType, HomogeneousListType)):
             expr_new = self._create_Duplicate(args[1], args[0])
         else:
             expr_new = self._create_PyccelOperator(expr, args)
@@ -2922,7 +2885,7 @@ def _visit_MathSqrt(self, expr):
             mul1, mul2 = arg.value.args
             mul1_syn, mul2_syn = expr.args[0].value.args
             is_abs = False
-            if mul1 is mul2 and mul1.dtype in (NativeInteger(), NativeFloat()):
+            if mul1 is mul2 and isinstance(mul1.dtype.primitive_type, (PrimitiveIntegerType, PrimitiveFloatingPointType)):
                 pyccel_stage.set_stage('syntactic')
 
                 fabs_name = self.scope.get_new_name('fabs')
@@ -2957,7 +2920,7 @@ def _visit_MathSqrt(self, expr):
         elif isinstance(arg.value, PyccelPow):
             base, exponent = arg.value.args
             base_syn, _ = expr.args[0].value.args
-            if exponent == 2 and base.dtype in (NativeInteger(), NativeFloat()):
+            if exponent == 2 and isinstance(base.dtype.primitive_type, (PrimitiveIntegerType, PrimitiveFloatingPointType)):
                 pyccel_stage.set_stage('syntactic')
 
                 fabs_name = self.scope.get_new_name('fabs')
@@ -2977,8 +2940,8 @@ def _visit_CmathPolar(self, expr):
         z = arg.value
         x = PythonReal(z)
         y = PythonImag(z)
-        x_var = self.scope.get_temporary_variable(z, dtype=NativeFloat())
-        y_var = self.scope.get_temporary_variable(z, dtype=NativeFloat())
+        x_var = self.scope.get_temporary_variable(z, class_type=PythonNativeFloat())
+        y_var = self.scope.get_temporary_variable(z, class_type=PythonNativeFloat())
         self._additional_exprs[-1].append(Assign(x_var, x))
         self._additional_exprs[-1].append(Assign(y_var, y))
         r = MathSqrt(PyccelAdd(PyccelMul(x_var,x_var), PyccelMul(y_var,y_var)))
@@ -3000,7 +2963,7 @@ def _visit_CmathRect(self, expr):
     def _visit_CmathPhase(self, expr):
         arg, = self._handle_function_args(expr.args) #pylint: disable=unbalanced-tuple-unpacking
         var = arg.value
-        if var.dtype is not NativeComplex():
+        if not isinstance(var.dtype.primitive_type, PrimitiveComplexType):
             return LiteralFloat(0.0)
         else:
             self.insert_import('math', AsName(MathAtan2, 'atan2'))
@@ -3102,13 +3065,13 @@ def _visit_FunctionCall(self, expr):
                     severity='error')
             dtype = method.arguments[0].var.class_type
             cls_def = method.arguments[0].var.cls_base
-            d_var = {'datatype': dtype,
+            d_var = {'class_type' : dtype,
                     'memory_handling':'stack',
                     'shape' : None,
                     'rank' : 0,
                     'is_target' : False,
                     'cls_base' : cls_def,
-                    'class_type' : dtype}
+                    }
             new_expression = []
 
             lhs = expr.get_user_nodes(Assign)[0].lhs
@@ -3180,7 +3143,7 @@ def _visit_Assign(self, expr):
                 rhs = genexp.lhs
         elif isinstance(rhs, IfTernaryOperator):
             value_true  = self._visit(rhs.value_true)
-            if value_true.rank > 0 or value_true.dtype is NativeString():
+            if value_true.rank > 0 or value_true.dtype is StringType():
                 # Temporarily deactivate type checks to construct syntactic assigns
                 pyccel_stage.set_stage('syntactic')
                 assign_true  = Assign(lhs, rhs.value_true, python_ast = python_ast)
@@ -3307,7 +3270,7 @@ def _visit_Assign(self, expr):
             d_var  = self._infer_type(rhs)
             if d_var['memory_handling'] == 'alias' and not isinstance(lhs, IndexedElement):
                 rhs = rhs.internal_var
-        elif isinstance(rhs, PyccelInternalFunction) and isinstance(rhs.dtype, NativeVoid):
+        elif isinstance(rhs, PyccelInternalFunction) and isinstance(rhs.dtype, VoidType):
             if expr.lhs.is_temp:
                 return rhs
             else:
@@ -3318,7 +3281,7 @@ def _visit_Assign(self, expr):
             d_list = d_var if isinstance(d_var, list) else [d_var]
 
             for d in d_list:
-                name = d['datatype'].__class__.__name__
+                name = d['class_type'].__class__.__name__
 
                 if name.startswith('Pyccel'):
                     name = name[6:]
@@ -3378,7 +3341,7 @@ def _visit_Assign(self, expr):
                     new_lhs.append( self._assign_lhs_variable(l, d, r, new_expressions, isinstance(expr, AugAssign),arr_in_multirets=r.rank>0 ) )
                 lhs = PythonTuple(*new_lhs)
 
-            elif isinstance(rhs.class_type, NativeHomogeneousTuple):
+            elif isinstance(rhs.class_type, HomogeneousTupleType):
                 new_lhs = []
                 d_var = self._infer_type(rhs[0])
                 new_rhs = []
@@ -3446,7 +3409,7 @@ def _visit_Assign(self, expr):
             for l,r in zip(lhs, rhs):
                 # Split assign (e.g. for a,b = 1,c)
                 if isinstance(l, (PythonTuple, InhomogeneousTupleVariable)) \
-                        and isinstance(r.class_type,(NativeTuple, NativeHomogeneousList)) \
+                        and isinstance(r.class_type,(TupleType, HomogeneousListType)) \
                         and not isinstance(r, FunctionCall):
                     new_lhs.extend(l)
                     new_rhs.extend(r)
@@ -3542,7 +3505,7 @@ def _visit_For(self, expr):
         iterator = expr.target
 
         if iterable.num_loop_counters_required:
-            indices = [Variable('int', self.scope.get_new_name(), is_temp=True)
+            indices = [Variable(PythonNativeInt(), self.scope.get_new_name(), is_temp=True)
                         for i in range(iterable.num_loop_counters_required)]
             iterable.set_loop_counter(*indices)
         else:
@@ -3628,35 +3591,34 @@ def _visit_FunctionalFor(self, expr):
             i += 1
             a     = self._visit(body.iterable)
             if isinstance(a, PythonRange):
-                var   = self._create_variable(var, 'int', start, {})
+                var   = self._create_variable(var, PythonNativeInt(), start, {})
                 dvar  = self._infer_type(var)
                 stop  = a.stop
                 start = a.start
                 step  = a.step
             elif isinstance(a, (PythonZip, PythonEnumerate)):
                 dvar  = self._infer_type(a.element)
-                dtype = dvar.pop('datatype')
+                class_type = dvar.pop('class_type')
                 if dvar['rank'] > 0:
                     dvar['rank' ] -= 1
                     dvar['shape'] = (dvar['shape'])[1:]
                 if dvar['rank'] == 0:
                     dvar['memory_handling'] = 'stack'
-                var  = Variable(dtype, var, **dvar)
+                var  = Variable(class_type, var, **dvar)
                 stop = a.element.shape[0]
             elif isinstance(a, Variable):
                 dvar  = self._infer_type(a)
-                dtype = dvar.pop('datatype')
+                class_type = dvar.pop('class_type').element_type
                 if dvar['rank'] == 1:
                     dvar['rank']  = 0
                     dvar['shape'] = None
-                    dvar['class_type'] = dtype
                 if dvar['rank'] > 1:
                     dvar['rank'] -= 1
                     dvar['shape'] = (dvar['shape'])[1:]
                 if dvar['rank'] == 0:
                     dvar['memory_handling'] = 'stack'
 
-                var  = Variable(dtype, var, **dvar)
+                var  = Variable(class_type, var, **dvar)
                 stop = a.shape[0]
             else:
                 errors.report(PYCCEL_RESTRICTION_TODO,
@@ -3751,9 +3713,9 @@ def _visit_FunctionalFor(self, expr):
         target = self._visit(target)
         d_var = self._infer_type(target)
 
-        dtype = d_var['datatype']
+        class_type = d_var['class_type']
 
-        if dtype is NativeGeneric():
+        if class_type is GenericType():
             errors.report(LIST_OF_TUPLES,
                           bounding_box=(self.current_ast_node.lineno, self.current_ast_node.col_offset),
                           severity='fatal')
@@ -3767,9 +3729,9 @@ def _visit_FunctionalFor(self, expr):
         else:
             d_var['order'] = None
         d_var['shape'] = shape
-        cls_type = NativeHomogeneousList()
-        d_var['class_type'] = cls_type
-        d_var['cls_base'] = get_cls_base(dtype, d_var['precision'], cls_type)
+        class_type = HomogeneousListType(class_type)
+        d_var['class_type'] = class_type
+        d_var['cls_base'] = get_cls_base(class_type)
 
         # ...
         # TODO [YG, 30.10.2020]:
@@ -3878,7 +3840,7 @@ def _visit_If(self, expr):
 
     def _visit_IfTernaryOperator(self, expr):
         value_true  = self._visit(expr.value_true)
-        if value_true.rank > 0 or value_true.dtype is NativeString():
+        if value_true.rank > 0 or value_true.dtype is StringType():
             lhs = PyccelSymbol(self.scope.get_new_name(), is_temp=True)
             # Temporarily deactivate type checks to construct syntactic assigns
             pyccel_stage.set_stage('syntactic')
@@ -4003,7 +3965,7 @@ def _visit_FunctionDef(self, expr, annotate=False, function_call_args=None):
         current_class = expr.get_direct_user_nodes(lambda u: isinstance(u, ClassDef))
         cls_name = current_class[0].name if current_class else None
         if cls_name:
-            bound_class = self.scope.find(cls_name, 'classes')
+            bound_class = self.scope.find(cls_name, 'classes', raise_if_missing = True)
 
         not_used = [d for d in decorators if d not in def_decorators.__all__]
         if len(not_used) >= 1:
@@ -4285,7 +4247,7 @@ def _visit_PythonPrint(self, expr):
 
         def is_symbolic(var):
             return isinstance(var, Variable) \
-                and isinstance(var.dtype, NativeSymbol)
+                and isinstance(var.dtype, SymbolicType)
 
         # TODO fix: not yet working because of mpi examples
 #        if not test:
@@ -4317,8 +4279,9 @@ def _visit_ClassDef(self, expr):
         name = self.scope.get_expected_name(expr.name)
 
         #  create a new Datatype for the current class
-        dtype = DataTypeFactory(name, '_name')
-        self.scope.cls_constructs[name] = dtype()
+        dtype = DataTypeFactory(name)()
+        typenames_to_dtypes[name] = dtype
+        self.scope.cls_constructs[name] = dtype
 
         parent = self._find_superclasses(expr)
 
@@ -4339,14 +4302,14 @@ def _visit_ClassDef(self, expr):
         docstring = self._visit(expr.docstring) if expr.docstring else expr.docstring
 
         cls = ClassDef(name, attributes, [], superclasses=parent, scope=scope,
-                docstring = docstring, class_type = dtype())
+                docstring = docstring, class_type = dtype)
         self.scope.parent_scope.insert_class(cls)
 
         methods = list(expr.methods)
         init_func = None
 
         if not any(method.name == '__init__' for method in methods):
-            argument = FunctionDefArgument(Variable(cls.name, 'self', cls_base = cls), bound_argument = True)
+            argument = FunctionDefArgument(Variable(dtype, 'self', cls_base = cls), bound_argument = True)
             self.scope.insert_symbol('__init__')
             scope = self.create_new_function_scope('__init__')
             init_func = FunctionDef('__init__', [argument], (), [], cls_name=cls.name, scope=scope)
@@ -4368,9 +4331,9 @@ def _visit_ClassDef(self, expr):
                 methods.pop(i)
 
                 # create a new attribute to check allocation
-                deallocater_lhs = Variable(cls.name, 'self', cls_base = cls, is_argument=True)
+                deallocater_lhs = Variable(dtype, 'self', cls_base = cls, is_argument=True)
                 deallocater = DottedVariable(lhs = deallocater_lhs, name = self.scope.get_new_name('is_freed'),
-                                             dtype = NativeBool(), is_private=True)
+                                             class_type = PythonNativeBool(), is_private=True)
                 cls.add_new_attribute(deallocater)
                 deallocater_assign = Assign(deallocater, LiteralFalse())
                 init_func.body.insert2body(deallocater_assign, back=False)
@@ -4385,7 +4348,7 @@ def _visit_ClassDef(self, expr):
             self._visit_FunctionDef(i, annotate=True)
 
         if not any(method.name == '__del__' for method in methods):
-            argument = FunctionDefArgument(Variable(cls.name, 'self', cls_base = cls), bound_argument = True)
+            argument = FunctionDefArgument(Variable(dtype, 'self', cls_base = cls), bound_argument = True)
             self.scope.insert_symbol('__del__')
             scope = self.create_new_function_scope('__del__')
             del_method = FunctionDef('__del__', [argument], (), [Pass()], scope=scope)
@@ -4400,7 +4363,7 @@ def _visit_ClassDef(self, expr):
                 for attr in cls.attributes:
                     if not attr.on_stack:
                         attribute.append(attr)
-                    elif isinstance(attr.dtype, CustomDataType) and not attr.is_alias:
+                    elif isinstance(attr.class_type, CustomDataType) and not attr.is_alias:
                         attribute.append(attr)
                 if attribute:
                     # Create a new list that store local attributes
@@ -4460,12 +4423,11 @@ def _visit_PyccelIs(self, expr):
             elif IsClass == PyccelIsNot:
                 return LiteralTrue()
 
-        if (isinstance(var1.dtype, NativeBool) and
-            isinstance(var2.dtype, NativeBool)):
+        if (isinstance(var1.dtype, PythonNativeBool) and
+            isinstance(var2.dtype, PythonNativeBool)):
             return IsClass(var1, var2)
 
-        lst = [NativeString(), NativeComplex(), NativeFloat(), NativeInteger()]
-        if (var1.dtype in lst):
+        if isinstance(var1.class_type, (StringType, FixedSizeNumericType)):
             errors.report(PYCCEL_RESTRICTION_PRIMITIVE_IMMUTABLE, symbol=expr,
                 severity='error')
             return IsClass(var1, var2)


Test patch:
diff --git a/tests/ast/test_basic.py b/tests/ast/test_basic.py
index 5002997a3d..a49e5a9235 100644
--- a/tests/ast/test_basic.py
+++ b/tests/ast/test_basic.py
@@ -6,6 +6,7 @@
 
 from pyccel.ast.basic       import PyccelAstNode
 from pyccel.ast.core        import Assign, Return, FunctionDef, AugAssign, FunctionDefArgument
+from pyccel.ast.datatypes   import PythonNativeInt
 from pyccel.ast.literals    import LiteralInteger
 from pyccel.ast.operators   import PyccelOperator, PyccelAdd, PyccelMinus, PyccelMul
 from pyccel.ast.variable    import Variable
@@ -38,12 +39,12 @@ def test_get_attribute_nodes():
 
     assert all(isinstance(a, Variable) for a in atts)
 
-    expected = [Variable('int', 'a'),
-                Variable('int', 'b'),
-                Variable('int', 'c'),
-                Variable('int', 'd'),
-                Variable('int', 'e'),
-                Variable('int', 'g')]
+    expected = [Variable(PythonNativeInt(), 'a'),
+                Variable(PythonNativeInt(), 'b'),
+                Variable(PythonNativeInt(), 'c'),
+                Variable(PythonNativeInt(), 'd'),
+                Variable(PythonNativeInt(), 'e'),
+                Variable(PythonNativeInt(), 'g')]
 
     for e in expected:
         assert e in atts
@@ -60,15 +61,15 @@ def test_get_attribute_nodes_exclude():
     minus = atts[0]
     assert isinstance(minus, PyccelMinus)==1
 
-    a = Variable('int', 'a')
-    b = Variable('int', 'b')
+    a = Variable(PythonNativeInt(), 'a')
+    b = Variable(PythonNativeInt(), 'b')
     assert minus.args[0] == a
     assert minus.args[1] == b
 
 def test_get_user_nodes():
     filename = os.path.join(path_dir, "math.py")
 
-    interesting_var = Variable('int', 'a')
+    interesting_var = Variable(PythonNativeInt(), 'a')
 
     fst = get_functions(filename)[0]
     atts = set(fst.get_attribute_nodes(Variable))
@@ -85,7 +86,7 @@ def test_get_user_nodes():
 def test_get_user_nodes_excluded():
     filename = os.path.join(path_dir, "math.py")
 
-    interesting_var = Variable('int', 'a')
+    interesting_var = Variable(PythonNativeInt(), 'a')
 
     fst = get_functions(filename)[0]
     atts = set(fst.get_attribute_nodes(Variable))
@@ -99,7 +100,7 @@ def test_get_user_nodes_excluded():
 def test_get_all_user_nodes():
     filename = os.path.join(path_dir, "math.py")
 
-    interesting_var = Variable('int', 'b')
+    interesting_var = Variable(PythonNativeInt(), 'b')
 
     fst = get_functions(filename)[0]
     atts = set(fst.get_attribute_nodes(Variable))
@@ -116,7 +117,7 @@ def test_get_all_user_nodes():
 def test_get_direct_user_nodes():
     filename = os.path.join(path_dir, "math.py")
 
-    interesting_var = Variable('int', 'a')
+    interesting_var = Variable(PythonNativeInt(), 'a')
 
     fst = get_functions(filename)[0]
     atts = set(fst.get_attribute_nodes(Variable))
@@ -133,8 +134,8 @@ def test_get_direct_user_nodes():
 def test_substitute():
     filename = os.path.join(path_dir, "math.py")
 
-    interesting_var = Variable('int', 'a')
-    new_var = Variable('int', 'Z')
+    interesting_var = Variable(PythonNativeInt(), 'a')
+    new_var = Variable(PythonNativeInt(), 'Z')
 
     fst = get_functions(filename)[0]
     atts = set(fst.get_attribute_nodes(Variable))
@@ -155,8 +156,8 @@ def test_substitute():
 def test_substitute_exclude():
     filename = os.path.join(path_dir, "math.py")
 
-    interesting_var = Variable('int', 'a')
-    new_var = Variable('int', 'Z')
+    interesting_var = Variable(PythonNativeInt(), 'a')
+    new_var = Variable(PythonNativeInt(), 'Z')
 
     fst = get_functions(filename)[0]
     atts = set(fst.get_attribute_nodes(Variable))
diff --git a/tests/epyccel/modules/arrays.py b/tests/epyccel/modules/arrays.py
index 13a192f388..c169b54bc3 100644
--- a/tests/epyccel/modules/arrays.py
+++ b/tests/epyccel/modules/arrays.py
@@ -324,20 +324,20 @@ def array_float_4d_C_array_initialization(x : 'float[:,:,:]', y : 'float[:,:,:]'
 
 def array_float_nested_C_array_initialization(x : 'float[:,:,:]', y : 'float[:,:]', z : 'float[:,:]', a : 'float[:,:,:,:]'):
     from numpy import array
-    tmp      = array([x, [y, z, z], x], dtype='float')
+    tmp      = array((x, (y, z, z), x), dtype='float')
     a[:,:,:,:] = tmp[:,:,:,:]
 
 def array_float_nested_C_array_initialization_2(a : 'float[:,:,:]', e : 'float[:,:]', f : 'float[:]', x : 'float[:,:,:,:]'):
     from numpy import array
-    tmp      = array([[e, [f, f]], a, [[f, f], [f, f]]], dtype='float')
+    tmp      = array(((e, (f, f)), a, ((f, f), (f, f))), dtype='float')
     x[:,:,:,:] = tmp[:,:,:,:]
 
 def array_float_nested_C_array_initialization_3(a : 'float[:,:,:]', e : 'float[:,:]', x : 'float[:,:,:,:]'):
     from numpy import array
-    tmp      = array([[e, [[1., 2., 3.], [1., 2., 3.]]],
+    tmp      = array(((e, ((1., 2., 3.), (1., 2., 3.))),
                        a,
-                       [[[1., 2., 3.], [1., 2., 3.]],
-                        [[1., 2., 3.], [1., 2., 3.]]]], dtype='float')
+                       (((1., 2., 3.), (1., 2., 3.)),
+                        ((1., 2., 3.), (1., 2., 3.)))), dtype='float')
     x[:,:,:,:] = tmp[:,:,:,:]
 
 ##==============================================================================
@@ -346,20 +346,20 @@ def array_float_nested_C_array_initialization_3(a : 'float[:,:,:]', e : 'float[:
 
 def array_float_nested_F_array_initialization(x : 'float[:,:,:]', y : 'float[:,:]', z : 'float[:,:]', a : 'float[:,:,:,:](order=F)'):
     from numpy import array
-    tmp      = array([x, [y, z, z], x], dtype='float', order="F")
+    tmp      = array((x, (y, z, z), x), dtype='float', order="F")
     a[:,:,:,:] = tmp[:,:,:,:]
 
 def array_float_nested_F_array_initialization_2(a : 'float[:,:,:]', e : 'float[:,:]', f : 'float[:]', x : 'float[:,:,:,:](order=F)'):
     from numpy import array
-    tmp      = array([[e, [f, f]], a, [[f, f], [f, f]]], dtype='float', order="F")
+    tmp      = array(((e, (f, f)), a, ((f, f), (f, f))), dtype='float', order="F")
     x[:,:,:,:] = tmp[:,:,:,:]
 
 def array_float_nested_F_array_initialization_3(a : 'float[:,:,:]', e : 'float[:,:]', x : 'float[:,:,:,:](order=F)'):
     from numpy import array
-    tmp      = array([[e, [[1., 2., 3.], [1., 2., 3.]]],
+    tmp      = array(((e, ((1., 2., 3.), (1., 2., 3.))),
                        a,
-                       [[[1., 2., 3.], [1., 2., 3.]],
-                        [[1., 2., 3.], [1., 2., 3.]]]], dtype='float', order="F")
+                       (((1., 2., 3.), (1., 2., 3.)),
+                        ((1., 2., 3.), (1., 2., 3.)))), dtype='float', order="F")
     x[:,:,:,:] = tmp[:,:,:,:]
 
 ##==============================================================================
diff --git a/tests/epyccel/modules/tuples.py b/tests/epyccel/modules/tuples.py
index c3d4917ff9..32c79c9cc9 100644
--- a/tests/epyccel/modules/tuples.py
+++ b/tests/epyccel/modules/tuples.py
@@ -15,6 +15,9 @@
         'homogeneous_tuple_2_levels',
         'tuple_unpacking_1',
         'tuple_unpacking_2',
+        'tuple_unpacking_3',
+        'tuple_unpacking_4',
+        'tuple_unpacking_5',
         'tuple_name_clash',
         'tuples_as_indexing_basic',
         'tuples_as_indexing_var',
@@ -133,6 +136,11 @@ def tuple_unpacking_3(x : 'int[:,:]'):
 def tuple_unpacking_4(x : 'int[:,:]'):
     x[:,0], x[0,:] = 2, 3
 
+def tuple_unpacking_5():
+    ai = (1,2,3)
+    a,b,c = ai
+    return a,b,c
+
 def tuple_name_clash():
     ai = (1+2j, False, 10.4)
     ai_0 = 44
diff --git a/tests/epyccel/recognised_functions/test_numpy_funcs.py b/tests/epyccel/recognised_functions/test_numpy_funcs.py
index bd83df74cd..7f256d72f9 100644
--- a/tests/epyccel/recognised_functions/test_numpy_funcs.py
+++ b/tests/epyccel/recognised_functions/test_numpy_funcs.py
@@ -2404,6 +2404,16 @@ def sum_call(x : 'float[:]'):
     x = rand(10)
     assert(isclose(f1(x), sum_call(x), rtol=RTOL, atol=ATOL))
 
+def test_sum_type(language):
+    def sum_call(x : 'float32[:]'):
+        from numpy import sum as np_sum
+        return np_sum(x)
+
+    f1 = epyccel(sum_call, language = language)
+    x = rand(10).astype(np.float32)
+    assert isclose(f1(x), sum_call(x), rtol=RTOL32, atol=ATOL32)
+    assert matching_types(f1(x), sum_call(x))
+
 def test_sum_phrase(language):
     def sum_phrase(x : 'float[:]', y : 'float[:]'):
         from numpy import sum as np_sum
@@ -5152,7 +5162,6 @@ def calculate_max_values(min_for_type, max_for_type):
         cast = type(min_for_type)
         min_test = -np.sqrt(abs(min_for_type) / size[0])
         max_test = np.sqrt(abs(max_for_type) / size[0])
-        print(min_test, max_test, cast(min_test), cast(max_test))
         return cast(min_test), cast(max_test)
 
     integer8 = randint(*calculate_max_values(min_int8, max_int8), size=size, dtype=np.int8)
@@ -5391,10 +5400,7 @@ def get_linspace(start : 'T', steps : int, num : int):
         from numpy import linspace
         stop = start + steps
         b = linspace(start, stop, num)
-        x = 0.0
-        for bi in b:
-            x += bi
-        return x
+        return b
 
     def test_linspace(start : 'complex64', end : 'complex64'):
         from numpy import linspace
@@ -5447,29 +5453,29 @@ def test_linspace_type2(start : 'int', end : 'int', result : 'complex128[:]'):
     assert (np.allclose(x, out))
     arr = np.zeros
     x = randint(1, 60)
-    assert np.isclose(epyccel_func(integer8, x, 30), get_linspace(integer8, x, 30), rtol=RTOL, atol=ATOL)
-    assert matching_types(epyccel_func(integer8, x, 100), get_linspace(integer8, x, 100))
+    assert np.allclose(epyccel_func(integer8, x, 30), get_linspace(integer8, x, 30), rtol=RTOL, atol=ATOL)
+    assert matching_types(epyccel_func(integer8, x, 100)[0], get_linspace(integer8, x, 100)[0])
     x = randint(100, 200)
-    assert np.isclose(epyccel_func(integer, x, 30), get_linspace(integer, x, 30), rtol=RTOL, atol=ATOL)
-    assert matching_types(epyccel_func(integer, x, 100), get_linspace(integer, x, 100))
+    assert np.allclose(epyccel_func(integer, x, 30), get_linspace(integer, x, 30), rtol=RTOL, atol=ATOL)
+    assert matching_types(epyccel_func(integer, x, 100)[0], get_linspace(integer, x, 100)[0])
     x = randint(100, 200)
-    assert np.isclose(epyccel_func(integer16, x, 30), get_linspace(integer16, x, 30), rtol=RTOL, atol=ATOL)
-    assert matching_types(epyccel_func(integer16, x, 100), get_linspace(integer16, x, 100))
+    assert np.allclose(epyccel_func(integer16, x, 30), get_linspace(integer16, x, 30), rtol=RTOL, atol=ATOL)
+    assert matching_types(epyccel_func(integer16, x, 100)[0], get_linspace(integer16, x, 100)[0])
     x = randint(100, 200)
-    assert np.isclose(epyccel_func(integer32, x, 30), get_linspace(integer32, x, 30), rtol=RTOL, atol=ATOL)
-    assert matching_types(epyccel_func(integer32, x, 100), get_linspace(integer32, x, 100))
+    assert np.allclose(epyccel_func(integer32, x, 30), get_linspace(integer32, x, 30), rtol=RTOL, atol=ATOL)
+    assert matching_types(epyccel_func(integer32, x, 100)[0], get_linspace(integer32, x, 100)[0])
     x = randint(100, 200)
-    assert np.isclose(epyccel_func(integer64, x, 200), get_linspace(integer64, x, 200), rtol=RTOL, atol=ATOL)
-    assert matching_types(epyccel_func(integer64, x, 100), get_linspace(integer64, x, 100))
+    assert np.allclose(epyccel_func(integer64, x, 200), get_linspace(integer64, x, 200), rtol=RTOL, atol=ATOL)
+    assert matching_types(epyccel_func(integer64, x, 100)[0], get_linspace(integer64, x, 100)[0])
     x = randint(100, 200)
-    assert np.isclose(epyccel_func(fl, x, 100), get_linspace(fl, x, 100), rtol=RTOL, atol=ATOL)
-    assert matching_types(epyccel_func(fl, x, 100), get_linspace(fl, x, 100))
+    assert np.allclose(epyccel_func(fl, x, 100), get_linspace(fl, x, 100), rtol=RTOL, atol=ATOL)
+    assert matching_types(epyccel_func(fl, x, 100)[0], get_linspace(fl, x, 100)[0])
     x = randint(100, 200)
-    assert np.isclose(epyccel_func(fl32, x, 200), get_linspace(fl32, x, 200), rtol=RTOL, atol=ATOL)
-    assert matching_types(epyccel_func(fl32, x, 100), get_linspace(fl32, x, 100))
+    assert np.allclose(epyccel_func(fl32, x, 200), get_linspace(fl32, x, 200), rtol=RTOL, atol=ATOL)
+    assert matching_types(epyccel_func(fl32, x, 100)[0], get_linspace(fl32, x, 100)[0])
     x = randint(100, 200)
-    assert np.isclose(epyccel_func(fl64, x, 200), get_linspace(fl64, x, 200), rtol=RTOL, atol=ATOL)
-    assert matching_types(epyccel_func(fl64, x, 100), get_linspace(fl64, x, 100))
+    assert np.allclose(epyccel_func(fl64, x, 200), get_linspace(fl64, x, 200), rtol=RTOL, atol=ATOL)
+    assert matching_types(epyccel_func(fl64, x, 100)[0], get_linspace(fl64, x, 100)[0])
 
     epyccel_func1 = epyccel(test_linspace, language=language)
     epyccel_func2 = epyccel(test_linspace2, language=language)
diff --git a/tests/epyccel/test_epyccel_generators.py b/tests/epyccel/test_epyccel_generators.py
index 12c999a575..b34ecac734 100644
--- a/tests/epyccel/test_epyccel_generators.py
+++ b/tests/epyccel/test_epyccel_generators.py
@@ -65,6 +65,17 @@ def f(a : 'int[:]'):
 
     assert f(x) == f_epyc(x)
 
+def test_sum_var5(language):
+    def f(a : 'bool[:]'):
+        return sum(ai for ai in a)
+
+    n = randint(1,50)
+    x = np.ones(n, dtype=bool)
+
+    f_epyc = epyccel(f, language = language)
+
+    assert f(x) == f_epyc(x)
+
 @pytest.mark.parametrize( 'language', (
         pytest.param("fortran", marks = pytest.mark.fortran),
         pytest.param("c", marks = [
@@ -121,15 +132,15 @@ def f(b : 'float[:]'):
     )
 )
 def test_expression2(language):
-    def f(b : 'int[:]'):
-        def incr(x : int):
+    def f(b : 'int64[:]'):
+        def incr(x : 'int64'):
             y = x + 1
             return y
         n = b.shape[0]
         return 5+incr(2+incr(6+sum(b[i] for i in range(n))))
 
     n = randint(1,10)
-    x = randint(100,size=n)
+    x = randint(100,size=n).astype(np.int64)
 
     f_epyc = epyccel(f, language = language)
 
@@ -147,7 +158,7 @@ def f(a : 'float[:,:,:,:]'):
 
 def test_nested_generators2(language):
     def f(a : 'float[:,:,:,:]'):
-        return min(min(sum(min(max(a[i,k,o,l]*l for i in range(5)) for k in range(5)) for o in range(5)) for l in range(5)),0.)
+        return min(min(sum(min(max(a[i,k,o,l]*l for i in range(5)) for k in range(5)) for o in range(5)) for l in range(5)), 0.)
 
     x = randint(0, 50, size=(5,5,5,5)).astype(float)
 
@@ -173,4 +184,17 @@ def f(a : 'float[:,:,:,:]'):
 
     f_epyc = epyccel(f, language = language)
 
-    assert f(x) == f_epyc(x)
\ No newline at end of file
+    assert f(x) == f_epyc(x)
+
+def test_sum_range_overwrite(language):
+    def f(a0 : 'int[:]'):
+        v = sum(a0[i] for i in range(len(a0)))
+        v = sum(a0[i] for i in range(len(a0)))
+        return v
+
+    n = randint(1,50)
+    x = randint(100,size=n)
+
+    f_epyc = epyccel(f, language = language)
+
+    assert f(x) == f_epyc(x)
diff --git a/tests/epyccel/test_epyccel_lists.py b/tests/epyccel/test_epyccel_lists.py
index c8a463d377..9664d6fd50 100644
--- a/tests/epyccel/test_epyccel_lists.py
+++ b/tests/epyccel/test_epyccel_lists.py
@@ -82,16 +82,6 @@ def pop_last_element():
     assert isinstance(python_result, type(pyccel_result))
     assert np.array_equal(python_result, pyccel_result)
 
-def test_pop_list_of_tuples_and_lists(language) :
-    def pop_last_element():
-        a = [(True, False), [True, False]]
-        return a.pop()
-    epyc_last_element = epyccel(pop_last_element, language = language)
-    pyccel_result = epyc_last_element()
-    python_result = pop_last_element()
-    assert isinstance(python_result, type(pyccel_result))
-    assert python_result == pyccel_result
-
 def test_pop_specific_index(language) :
     def pop_specific_index():
         a = [1j,3j,45j]
@@ -173,16 +163,6 @@ def f():
     epyc_f = epyccel(f, language=language)
     assert f() == epyc_f()
 
-def test_append_range_tuple(language):
-    def f():
-        a = [[1, 2, 3]]
-        for i in range(0, 1000):
-            a.append((i, i + 1))
-        return a
-
-    epyc_f = epyccel(f, language=language)
-    assert f() == epyc_f()
-
 def test_append_bool(language):
     def f():
         a = [True, True, True]
@@ -359,16 +339,6 @@ def f():
     epyc_f = epyccel(f, language=language)
     assert f() == epyc_f()
 
-def test_insert_range_tuple(language):
-    def f():
-        a = [[1, 2, 3]]
-        for i in range(4, 1000):
-            a.insert(i, (i, i + 1))
-        return a
-
-    epyc_f = epyccel(f, language=language)
-    assert f() == epyc_f()
-
 def test_insert_user_defined_objects(language):
     import modules.list_user_defined_objs as mod
 
diff --git a/tests/errors/semantic/blocking/APPEND_LIST_INCOMPATIBLE_TYPES_0.py b/tests/errors/semantic/blocking/APPEND_LIST_INCOMPATIBLE_TYPES_0.py
index 3b3b48a57f..b49e7625bf 100644
--- a/tests/errors/semantic/blocking/APPEND_LIST_INCOMPATIBLE_TYPES_0.py
+++ b/tests/errors/semantic/blocking/APPEND_LIST_INCOMPATIBLE_TYPES_0.py
@@ -2,6 +2,6 @@
 
 import numpy as np
 a = [1,2,3]
-b = np.int32(4)
+b = np.int16(4)
 a.append(b)
 
diff --git a/tests/errors/semantic/blocking/APPEND_LIST_INCOMPATIBLE_TYPES_3.py b/tests/errors/semantic/blocking/APPEND_LIST_INCOMPATIBLE_TYPES_3.py
new file mode 100644
index 0000000000..d14ecef456
--- /dev/null
+++ b/tests/errors/semantic/blocking/APPEND_LIST_INCOMPATIBLE_TYPES_3.py
@@ -0,0 +1,5 @@
+# pylint: disable=missing-function-docstring, missing-module-docstring
+
+a = [[1, 2, 3]]
+a.append((4, 7))
+
diff --git a/tests/errors/semantic/blocking/INHOMOGENEOUS_MAX.py b/tests/errors/semantic/blocking/INHOMOGENEOUS_MAX.py
new file mode 100644
index 0000000000..8629fc504f
--- /dev/null
+++ b/tests/errors/semantic/blocking/INHOMOGENEOUS_MAX.py
@@ -0,0 +1,3 @@
+# pylint: disable=missing-function-docstring, missing-module-docstring
+
+a = max(3, 4.5)
diff --git a/tests/errors/semantic/blocking/INHOMOGENEOUS_MIN.py b/tests/errors/semantic/blocking/INHOMOGENEOUS_MIN.py
new file mode 100644
index 0000000000..c1617dab1a
--- /dev/null
+++ b/tests/errors/semantic/blocking/INHOMOGENEOUS_MIN.py
@@ -0,0 +1,3 @@
+# pylint: disable=missing-function-docstring, missing-module-docstring
+
+a = min(True, 5+4j)
diff --git a/tests/errors/semantic/blocking/INSERT_LIST_INCOMPATIBLE_TYPES_0.py b/tests/errors/semantic/blocking/INSERT_LIST_INCOMPATIBLE_TYPES_0.py
index fa1a3e074a..787add75ef 100644
--- a/tests/errors/semantic/blocking/INSERT_LIST_INCOMPATIBLE_TYPES_0.py
+++ b/tests/errors/semantic/blocking/INSERT_LIST_INCOMPATIBLE_TYPES_0.py
@@ -3,6 +3,6 @@
 import numpy as np
 
 a = [1,2,3]
-b = np.int32(4)
+b = np.int16(4)
 a.insert(3, b)
 
diff --git a/tests/errors/semantic/blocking/INSERT_LIST_INCOMPATIBLE_TYPES_4.py b/tests/errors/semantic/blocking/INSERT_LIST_INCOMPATIBLE_TYPES_4.py
new file mode 100644
index 0000000000..897e3e939f
--- /dev/null
+++ b/tests/errors/semantic/blocking/INSERT_LIST_INCOMPATIBLE_TYPES_4.py
@@ -0,0 +1,5 @@
+# pylint: disable=missing-function-docstring, missing-module-docstring
+
+a = [[1, 2, 3]]
+a.insert(0, (4, 7))
+
diff --git a/tests/errors/semantic/non_blocking/INCOMPATIBLE_ARGUMENT3.py b/tests/errors/semantic/non_blocking/INCOMPATIBLE_ARGUMENT3.py
new file mode 100644
index 0000000000..72102d8880
--- /dev/null
+++ b/tests/errors/semantic/non_blocking/INCOMPATIBLE_ARGUMENT3.py
@@ -0,0 +1,10 @@
+# pylint: disable=missing-function-docstring, missing-module-docstring, missing-class-docstring
+
+class A:
+    def f(self, x : int):
+        y = x + 1
+        return y
+
+my_a = A()
+z = my_a.f(2.3)
+print(z)
diff --git a/tests/pyccel/scripts/print_tuples.py b/tests/pyccel/scripts/print_tuples.py
index 735d2b96da..0971513de3 100644
--- a/tests/pyccel/scripts/print_tuples.py
+++ b/tests/pyccel/scripts/print_tuples.py
@@ -1,5 +1,9 @@
 # pylint: disable=missing-function-docstring, missing-module-docstring
 # ------------------------------- Strings ------------------------------------
+
+def f():
+    return 1, True
+
 if __name__ == '__main__':
     print(())
     print((1,))
@@ -8,5 +12,6 @@
     print(((1,2),(3,)))
     print((((1,),2),(3,)))
     print((1, True))
-    print((1, True), sep=",")
+    print((1, False), sep=",")
     print((1, True), end="!\n")
+    print(f())
diff --git a/tests/pyccel/test_pyccel.py b/tests/pyccel/test_pyccel.py
index 4422ec7c88..1297224d6a 100644
--- a/tests/pyccel/test_pyccel.py
+++ b/tests/pyccel/test_pyccel.py
@@ -50,12 +50,9 @@ def insert_pyccel_folder(abs_path):
 
 #------------------------------------------------------------------------------
 def get_python_output(abs_path, cwd = None):
-    if cwd is None:
-        p = subprocess.Popen([sys.executable , "%s" % abs_path], stdout=subprocess.PIPE, universal_newlines=True)
-    else:
-        p = subprocess.Popen([sys.executable , "%s" % abs_path], stdout=subprocess.PIPE, universal_newlines=True, cwd=cwd)
-    out, _ = p.communicate()
-    assert(p.returncode==0)
+    with subprocess.Popen([sys.executable , abs_path], stdout=subprocess.PIPE, universal_newlines=True, cwd=cwd) as p:
+        out, _ = p.communicate()
+        assert(p.returncode==0)
     return out
 
 #------------------------------------------------------------------------------
diff --git a/tests/warnings/test_warnings.py b/tests/warnings/test_warnings.py
index 354f259fa3..795931ad84 100644
--- a/tests/warnings/test_warnings.py
+++ b/tests/warnings/test_warnings.py
@@ -77,7 +77,8 @@ def test_semantic_warnings(f):
 
 
 @pytest.mark.parametrize("f", [HIGH_ORDER_FUNCTIONS_IN_CLASS_FUNCS])
-def test_cwrapper_warnings(f, language):
+@pytest.mark.c
+def test_cwrapper_warnings(f):
     with pytest.warns(UserWarning):
         epyccel(f, language='c')