Repository: pyccel/pyccel (python)

Issue:
Add preliminary support for isinstance 
### Relevant Discussion

Discussed in Pyccel meeting

### Describe the feature
We would like to recognise and support `isinstance` as a compile time object.

### Test Code
Provide code which does not currently work but which should do when this issue is fixed:
```python
def my_print(a : int | float):
    if isinstance(a, int):
        print("Found int!")
    else:
        print("Found float!")
```

### Proposed Solution
- [ ] Add a class `PythonIsInstance` in `pyccel/ast/builtins.py` to represent `isinstance`
- [ ] In `pyccel/parser/semantic.py` add a method called `_build_PythonIsInstance`
    - [ ] wrap the second argument of the function in a `SyntacticTypeAnnotation` before visiting it to access the class type
    - [ ] use the `class_type` property of the visited first argument and the visited `SyntacticTypeAnnotation` to translate `isinstance` to `LiteralTrue` or `LiteralFalse`


Pull request:
Add preliminary support for `isinstance`
Add preliminary support for `isinstance`. Fixes #2182 

**Commit Summary**
- Allow a tuple to contain symbolic objects (such as `PyccelFunctionDef`)
- Add static types for lists/sets/dicts/tuples
- Add `PythonIsInstance` class
- Add simplification to `PyccelOr` and `PyccelAnd` to remove unnecessary combinations with `True`/`False`
- Simplify assert printing by printing `assert True` as a comment
- Allow `PyccelOr` to be used for types outside of a type annotation
- Add `_build_PythonIsInstance` to change `isinstance` call to literal boolean

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ed7d74c4..91669b4a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -68,6 +68,7 @@ All notable changes to this project will be documented in this file.
 -   #2099 : Fix translation of modules containing `__all__`.
 -   #983 : Add support for built-in function `round`.
 -   Add support for `type` as a type annotation.
+-   #2182 : Add support for `isinstance`.
 -   \[INTERNALS\] Add abstract class `SetMethod` to handle calls to various set methods.
 -   \[INTERNALS\] Added `container_rank` property to `ast.datatypes.PyccelType` objects.
 -   \[INTERNALS\] Add a `__call__` method to `FunctionDef` to create `FunctionCall` instances.
diff --git a/docs/builtin-functions.md b/docs/builtin-functions.md
index 8033e2ed..7db19f07 100644
--- a/docs/builtin-functions.md
+++ b/docs/builtin-functions.md
@@ -38,7 +38,7 @@ Python contains a limited number of builtin functions defined [here](https://doc
 | `id` | No |
 | `input` | No |
 | **`int`** | **Yes** |
-| `isinstance` | No |
+| **`isinstance`** | **Yes** |
 | `issubclass` | No |
 | `iter` | No |
 | **`len`** | **Yes** |
diff --git a/pyccel/ast/builtins.py b/pyccel/ast/builtins.py
index dfb78a7e..1cfd39cc 100644
--- a/pyccel/ast/builtins.py
+++ b/pyccel/ast/builtins.py
@@ -18,7 +18,7 @@ from .basic     import PyccelAstNode, TypedAstNode
 from .datatypes import PythonNativeInt, PythonNativeBool, PythonNativeFloat
 from .datatypes import GenericType, PythonNativeComplex
 from .datatypes import PrimitiveBooleanType, PrimitiveComplexType
-from .datatypes import HomogeneousTupleType, InhomogeneousTupleType
+from .datatypes import HomogeneousTupleType, InhomogeneousTupleType, TupleType
 from .datatypes import HomogeneousListType, HomogeneousContainerType
 from .datatypes import FixedSizeNumericType, HomogeneousSetType, SymbolicType
 from .datatypes import DictType, VoidType, TypeAlias, StringType
@@ -45,6 +45,7 @@ __all__ = (
     'PythonFloat',
     'PythonImag',
     'PythonInt',
+    'PythonIsInstance',
     'PythonLen',
     'PythonList',
     'PythonListFunction',
@@ -602,6 +603,12 @@ class PythonTuple(TypedAstNode):
         # 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)
 
+        if any(isinstance(d, SymbolicType) for d in dtypes):
+            self._class_type = InhomogeneousTupleType(*[a.class_type for a in args])
+            self._shape = (LiteralInteger(len(args)),)
+            self._is_homogeneous = False
+            return
+
         ranks  = set(a.rank for a in args)
         orders = set(a.order for a in args)
         if len(ranks) == 1:
@@ -700,6 +707,7 @@ class PythonTupleFunction(TypedAstNode):
     """
     __slots__ = ()
     _attribute_nodes = ()
+    _static_type = TupleType
 
 #==============================================================================
 class PythonLen(PyccelFunction):
@@ -826,6 +834,7 @@ class PythonListFunction(PyccelFunction):
     name = 'list'
     __slots__ = ('_class_type', '_shape')
     _attribute_nodes = ()
+    _static_type = HomogeneousListType
 
     def __new__(cls, arg = None):
         if arg is None:
@@ -933,12 +942,12 @@ class PythonSetFunction(PyccelFunction):
 
     Parameters
     ----------
-    arg : TypedAstNode
+    copied_obj : TypedAstNode
         The argument passed to the function call.
     """
-
     __slots__ = ('_shape', '_class_type')
     name = 'set'
+    _static_type = HomogeneousSetType
 
     def __init__(self, copied_obj):
         self._class_type = copied_obj.class_type
@@ -1048,6 +1057,7 @@ class PythonDictFunction(PyccelFunction):
     """
     __slots__ = ('_shape', '_class_type')
     name = 'dict'
+    _static_type = DictType
 
     def __new__(cls, *args, **kwargs):
         if len(args) == 0:
@@ -1693,6 +1703,27 @@ class VariableIterator(Iterable):
         """
         return self.get_python_iterable_item()
 
+#==============================================================================
+class PythonIsInstance(PyccelFunction):
+    """
+    Represents a call to Python's `isinstance` function.
+
+    Represents a call to Python's `isinstance` function which checks if an
+    object has a specified type. This class exists to find a definition of
+    `isinstance` in builtin_functions_dict but it should not be instantiated.
+
+    Parameters
+    ----------
+    obj : TypedAstNode
+        The object whose type should be checked.
+    class_or_tuple : TypedAstNode
+        A class or a tuple of classes describing the acceptable types for
+        the object.
+    """
+    __slots__ = ()
+    def __init__(self, obj, class_or_tuple):
+        super().__init__(obj, class_or_tuple)
+
 #==============================================================================
 
 DtypePrecisionToCastFunction = {
@@ -1705,25 +1736,26 @@ DtypePrecisionToCastFunction = {
 #==============================================================================
 
 builtin_functions_dict = {
-    'abs'      : PythonAbs,
-    'bool'     : PythonBool,
-    'complex'  : PythonComplex,
-    'dict'     : PythonDictFunction,
-    'enumerate': PythonEnumerate,
-    'float'    : PythonFloat,
-    'int'      : PythonInt,
-    'len'      : PythonLen,
-    'list'     : PythonListFunction,
-    'map'      : PythonMap,
-    'max'      : PythonMax,
-    'min'      : PythonMin,
-    'not'      : PyccelNot,
-    'range'    : PythonRange,
-    'round'    : PythonRound,
-    'set'      : PythonSetFunction,
-    'str'      : LiteralString,
-    'sum'      : PythonSum,
-    'tuple'    : PythonTupleFunction,
-    'type'     : PythonType,
-    'zip'      : PythonZip,
+    'abs'        : PythonAbs,
+    'bool'       : PythonBool,
+    'complex'    : PythonComplex,
+    'dict'       : PythonDictFunction,
+    'enumerate'  : PythonEnumerate,
+    'float'      : PythonFloat,
+    'int'        : PythonInt,
+    'isinstance' : PythonIsInstance,
+    'len'        : PythonLen,
+    'list'       : PythonListFunction,
+    'map'        : PythonMap,
+    'max'        : PythonMax,
+    'min'        : PythonMin,
+    'not'        : PyccelNot,
+    'range'      : PythonRange,
+    'round'      : PythonRound,
+    'set'        : PythonSetFunction,
+    'str'        : LiteralString,
+    'sum'        : PythonSum,
+    'tuple'      : PythonTupleFunction,
+    'type'       : PythonType,
+    'zip'        : PythonZip,
 }
diff --git a/pyccel/ast/core.py b/pyccel/ast/core.py
index 8f9b4d58..8f176ddb 100644
--- a/pyccel/ast/core.py
+++ b/pyccel/ast/core.py
@@ -20,7 +20,7 @@ from .builtins  import PythonBool, PythonTuple
 from .c_concepts import PointerCast
 
 from .datatypes import (PyccelType, HomogeneousTupleType, VoidType, CustomDataType,
-                        PythonNativeBool, InhomogeneousTupleType)
+                        PythonNativeBool, InhomogeneousTupleType, SymbolicType)
 
 from .internals import PyccelSymbol, PyccelFunction, Iterable
 
@@ -2712,6 +2712,8 @@ class PyccelFunctionDef(FunctionDef):
         different default values.
     """
     __slots__ = ('_argument_description',)
+    class_type = SymbolicType()
+
     def __init__(self, name, func_class, *, decorators = {}, argument_description = {}):
         assert isinstance(func_class, type) and \
                 issubclass(func_class, (PyccelFunction, TypedAstNode, Iterable))
diff --git a/pyccel/ast/operators.py b/pyccel/ast/operators.py
index da13c69f..9d406943 100644
--- a/pyccel/ast/operators.py
+++ b/pyccel/ast/operators.py
@@ -19,9 +19,8 @@ from .datatypes             import PythonNativeBool, PythonNativeFloat
 from .datatypes             import StringType, FixedSizeNumericType, ContainerType
 from .datatypes             import PrimitiveBooleanType, PrimitiveIntegerType
 
-
 from .literals              import Literal, LiteralInteger, LiteralFloat, LiteralComplex
-from .literals              import Nil, NilArgument
+from .literals              import Nil, NilArgument, LiteralTrue, LiteralFalse
 from .literals              import convert_to_literal
 
 errors = Errors()
@@ -1147,10 +1146,27 @@ class PyccelAnd(PyccelBooleanOperator):
     ----------
     *args : tuple of TypedAstNode
         The arguments passed to the operator.
+    simplify : bool
+        True if the expression should be simplified to be as compact/readable as
+        possible. False if the arguments should be preserved as they are.
     """
     __slots__ = ()
     _precedence = 5
 
+    def __new__(cls, *args, simplify = False):
+        if simplify:
+            if any(isinstance(a, LiteralFalse) for a in args):
+                return LiteralFalse()
+            if all(isinstance(a, LiteralTrue) for a in args):
+                return LiteralTrue()
+        return super().__new__(cls)
+
+    def __init__(self, *args, simplify = False):
+        if simplify:
+            args = tuple(a for a in args if not isinstance(a, LiteralTrue))
+        super().__init__(*args)
+
+
     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)
@@ -1175,10 +1191,26 @@ class PyccelOr(PyccelBooleanOperator):
     ----------
     *args : tuple of TypedAstNode
         The arguments passed to the operator.
+    simplify : bool
+        True if the expression should be simplified to be as compact/readable as
+        possible. False if the arguments should be preserved as they are.
     """
     __slots__ = ()
     _precedence = 4
 
+    def __new__(cls, *args, simplify = False):
+        if simplify:
+            if any(isinstance(a, LiteralTrue) for a in args):
+                return LiteralTrue()
+            elif all(isinstance(a, LiteralFalse) for a in args):
+                return LiteralFalse()
+        return super().__new__(cls)
+
+    def __init__(self, *args, simplify = False):
+        if simplify:
+            args = tuple(a for a in args if not isinstance(a, LiteralFalse))
+        super().__init__(*args)
+
     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)
diff --git a/pyccel/codegen/printing/ccode.py b/pyccel/codegen/printing/ccode.py
index 7ea56269..02e29a9c 100644
--- a/pyccel/codegen/printing/ccode.py
+++ b/pyccel/codegen/printing/ccode.py
@@ -3,9 +3,11 @@
 # 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/devel/LICENSE for full license details.      #
 #------------------------------------------------------------------------------------------#
+import ast
 import functools
 from itertools import chain, product
 import re
+import sys
 from packaging.version import Version
 
 import numpy as np
@@ -2666,9 +2668,14 @@ class CCodePrinter(CodePrinter):
         return '/*' + comments + '*/\n'
 
     def _print_Assert(self, expr):
+        if isinstance(expr.test, LiteralTrue):
+            if sys.version_info < (3, 9):
+                return ''
+            else:
+                return '//' + ast.unparse(expr.python_ast) + '\n' #pylint: disable=no-member
         condition = self._print(expr.test)
         self.add_import(c_imports['assert'])
-        return "assert({0});\n".format(condition)
+        return f"assert({condition});\n"
 
     def _print_PyccelSymbol(self, expr):
         return expr
diff --git a/pyccel/codegen/printing/fcode.py b/pyccel/codegen/printing/fcode.py
index 9c218fa2..400cf7e8 100644
--- a/pyccel/codegen/printing/fcode.py
+++ b/pyccel/codegen/printing/fcode.py
@@ -6,9 +6,10 @@
 """Print to F90 standard. Trying to follow the information provided at
 www.fortran90.org as much as possible."""
 
-
+import ast
 import functools
 import string
+import sys
 import re
 from collections import OrderedDict
 from itertools import chain
@@ -2958,12 +2959,15 @@ class FCodePrinter(CodePrinter):
         return 'STOP'
 
     def _print_Assert(self, expr):
-        prolog = "if ( .not. ({0})) then".format(self._print(expr.test))
-        body = 'stop 1'
-        epilog = 'end if'
-        return ('{prolog}\n'
-                '{body}\n'
-                '{epilog}\n').format(prolog=prolog, body=body, epilog=epilog)
+        if isinstance(expr.test, LiteralTrue):
+            if sys.version_info < (3, 9):
+                return ''
+            else:
+                return '!' + ast.unparse(expr.python_ast) + '\n' #pylint: disable=no-member
+        test_code = self._print(expr.test)
+        return (f"if ( .not. ({test_code})) then\n"
+                'stop 1\n'
+                'end if\n')
 
     def _handle_not_none(self, lhs, lhs_var):
         """
diff --git a/pyccel/parser/semantic.py b/pyccel/parser/semantic.py
index ef5e4bc8..5bd2b9c4 100644
--- a/pyccel/parser/semantic.py
+++ b/pyccel/parser/semantic.py
@@ -102,7 +102,7 @@ from pyccel.ast.omp import (OMP_For_Loop, OMP_Simd_Construct, OMP_Distribute_Con
                             OMP_Single_Construct)
 
 from pyccel.ast.operators import PyccelArithmeticOperator, PyccelIs, PyccelIsNot, IfTernaryOperator, PyccelUnarySub
-from pyccel.ast.operators import PyccelNot, PyccelAdd, PyccelMinus, PyccelMul, PyccelPow
+from pyccel.ast.operators import PyccelNot, PyccelAdd, PyccelMinus, PyccelMul, PyccelPow, PyccelOr
 from pyccel.ast.operators import PyccelAssociativeParenthesis, PyccelDiv, PyccelIn
 
 from pyccel.ast.sympy_helper import sympy_to_pyccel, pyccel_to_sympy
@@ -934,12 +934,13 @@ class SemanticParser(BasicParser):
             The new operator.
         """
         arg1 = visited_args[0]
-        if isinstance(arg1, FunctionDef):
-            msg = ("Function found in a mathematical operation. "
-                   "Are you trying to declare a type? "
-                   "If so then the type object must be used as a type hint.")
-            errors.report(msg,
-                    severity='fatal', symbol=expr)
+        if all(isinstance(a, PyccelFunctionDef) for a in visited_args):
+            try:
+                possible_types = [a.cls_name.static_type() for a in visited_args]
+            except AttributeError:
+                errors.report("Unrecognised type in type union statement",
+                        severity='fatal', symbol=expr)
+            return UnionTypeAnnotation(*[VariableTypeAnnotation(t) for t in possible_types])
         class_type = arg1.class_type
         class_base = self.scope.find(str(class_type), 'classes') or get_cls_base(class_type)
         magic_method_name = magic_method_map.get(type(expr), None)
@@ -5574,7 +5575,7 @@ class SemanticParser(BasicParser):
 
         Parameters
         ----------
-        expr : DottedName
+        expr : FunctionCall
             The syntactic node that represent the call to `len()`.
 
         function_call_args : iterable[FunctionCallArgument]
@@ -5659,3 +5660,67 @@ class SemanticParser(BasicParser):
             else:
                 self._additional_exprs[-1].extend(body)
                 return lhs_semantic_var
+
+    def _build_PythonIsInstance(self, expr, function_call_args):
+        """
+        Method to visit a PythonIsInstance node.
+
+        The purpose of this `_build` method is to construct a literal boolean indicating
+        whether or not the expression has the expected type.
+            The syntactic node that represent the call to `isinstance()`.
+
+        Parameters
+        ----------
+        expr : FunctionCall
+            The syntactic node that represent the call to `PythonSetFunction`.
+
+        function_call_args : iterable[FunctionCallArgument]
+            The semantic arguments passed to the function.
+
+        Returns
+        -------
+        Literal
+            A LiteralTrue or LiteralFalse node describing the result of the `isinstance`
+            call.
+        """
+        obj = function_call_args[0].value
+        class_or_tuple = function_call_args[1].value
+        if isinstance(class_or_tuple, PythonTuple):
+            obj_arg = function_call_args[0]
+            return PyccelOr(*[self._build_PythonIsInstance(expr, [obj_arg, FunctionCallArgument(class_type)]) \
+                                for class_type in class_or_tuple], simplify=True)
+        elif isinstance(class_or_tuple, UnionTypeAnnotation):
+            obj_arg = function_call_args[0]
+            return PyccelOr(*[self._build_PythonIsInstance(expr, [obj_arg, FunctionCallArgument(var_annot)]) \
+                                for var_annot in class_or_tuple.type_list], simplify=True)
+        else:
+            if isinstance(class_or_tuple, VariableTypeAnnotation):
+                expected_type = class_or_tuple.class_type
+            else:
+                class_type = class_or_tuple.cls_name
+                try:
+                    expected_type = class_type.static_type()
+                except AttributeError:
+                    expected_type = None
+
+            if isinstance(expected_type, type):
+                return convert_to_literal(isinstance(obj.class_type, expected_type))
+
+            elif expected_type:
+                class_type = obj.class_type
+                cls_base_to_insert = [self.scope.find(str(class_type), 'classes') or get_cls_base(class_type)]
+                possible_types = {class_type}
+                while cls_base_to_insert:
+                    cls_base = cls_base_to_insert.pop()
+                    class_type = cls_base.class_type
+                    possible_types.add(class_type)
+                    cls_base_to_insert.extend(cls_base.superclasses)
+
+                possible_types.discard(None)
+
+                return convert_to_literal(expected_type in possible_types)
+
+            else:
+                errors.report(f"Type {class_or_tuple} is not handled in isinstance call.",
+                        severity='error', symbol=expr)
+                return LiteralTrue()


Test patch:
diff --git a/tests/epyccel/test_base.py b/tests/epyccel/test_base.py
index 03faab35..8edb925c 100644
--- a/tests/epyccel/test_base.py
+++ b/tests/epyccel/test_base.py
@@ -131,13 +131,6 @@ def test_same_float(language):
     test = epyccel_test(base.isnot_same_float, lang=language)
     test.compare_epyccel( 22.2 )
 
-@pytest.mark.parametrize( 'language', [
-        pytest.param("c", marks = [
-            pytest.mark.xfail(reason="Strings are not yet implemented for C language"),
-            pytest.mark.c]),
-        pytest.param("fortran", marks = pytest.mark.fortran)
-    ]
-)
 def test_same_string(language):
     test = epyccel_test(base.is_same_string, lang=language)
     test.compare_epyccel()
diff --git a/tests/epyccel/test_builtins.py b/tests/epyccel/test_builtins.py
index 54de6b57..4c202c48 100644
--- a/tests/epyccel/test_builtins.py
+++ b/tests/epyccel/test_builtins.py
@@ -1,4 +1,5 @@
 # pylint: disable=missing-function-docstring, missing-module-docstring
+import sys
 import pytest
 from numpy.random import randint, uniform
 from numpy import iinfo, finfo
@@ -686,3 +687,115 @@ def test_round_ndigits_bool(language):
     round_ndigits_output = round_ndigits()
     assert all(o == r for o, r in zip(f_output, round_ndigits_output))
     assert all(isinstance(o, type(r)) for o, r in zip(f_output, round_ndigits_output))
+
+@pytest.mark.parametrize( 'language', (
+        pytest.param("fortran", marks = [pytest.mark.fortran]),
+        pytest.param("c", marks = [pytest.mark.c]),
+        pytest.param("python", marks = [
+            pytest.mark.skip(reason=("isinstance is evaluated during translation so Python translation "
+                "gives wrong results. See #802")),
+            pytest.mark.python]
+        )
+    )
+)
+def test_isinstance_native(language):
+    def isinstance_test(a : 'bool | int | float | complex'):
+        return isinstance(a, bool), isinstance(a, int), isinstance(a, float), isinstance(a, complex)
+
+    f = epyccel(isinstance_test, language=language)
+    assert f(True) == isinstance_test(True)
+    assert f(False) == isinstance_test(False)
+    assert f(4) == isinstance_test(6)
+    assert f(3.9) == isinstance_test(6.7)
+    assert f(1+2j) == isinstance_test(6.5+8.3j)
+
+def test_isinstance_containers(language):
+    def isinstance_tup(a : int, b : int):
+        container = (a, b)
+        return (isinstance(container, tuple), isinstance(container, list),
+                isinstance(container, set), isinstance(container, dict))
+    def isinstance_lst(a : int, b : int):
+        container = [a, b]
+        return (isinstance(container, tuple), isinstance(container, list),
+                isinstance(container, set), isinstance(container, dict))
+    def isinstance_set(a : int, b : int):
+        container = {a, b}
+        return (isinstance(container, tuple), isinstance(container, list),
+                isinstance(container, set), isinstance(container, dict))
+    def isinstance_dict(a : int, b : int):
+        container = {a: False, b: True}
+        return (isinstance(container, tuple), isinstance(container, list),
+                isinstance(container, set), isinstance(container, dict))
+
+    test_funcs = (isinstance_tup, isinstance_lst, isinstance_set, isinstance_dict)
+
+    for f in test_funcs:
+        f_epyc = epyccel(f, language=language)
+
+        assert f(2, 5) == f_epyc(2, 5)
+
+@pytest.mark.parametrize( 'language', (
+        pytest.param("fortran", marks = [pytest.mark.fortran]),
+        pytest.param("c", marks = [pytest.mark.c]),
+        pytest.param("python", marks = [
+            pytest.mark.skip(reason=("isinstance is evaluated during translation so Python translation "
+                "gives wrong results. See #802")),
+            pytest.mark.python]
+        )
+    )
+)
+def test_isinstance_numpy(language):
+    def isinstance_test(a : 'int32 | int64 | int | float32'):
+        import numpy as np #pylint:disable=reimported
+        return isinstance(a, np.int32), isinstance(a, np.int64), isinstance(a, int), isinstance(a, np.float32)
+
+    f = epyccel(isinstance_test, language=language)
+    assert f(np.int32(4)) == isinstance_test(np.int32(4))
+    assert f(np.int64(4)) == isinstance_test(np.int64(4))
+    assert f(4) == isinstance_test(4)
+    assert f(np.float32(4)) == isinstance_test(np.float32(4))
+
+@pytest.mark.parametrize( 'language', (
+        pytest.param("fortran", marks = [pytest.mark.fortran]),
+        pytest.param("c", marks = [pytest.mark.c]),
+        pytest.param("python", marks = [
+            pytest.mark.skip(reason=("isinstance is evaluated during translation so Python translation "
+                "gives wrong results. See #802")),
+            pytest.mark.python]
+        )
+    )
+)
+def test_isinstance_tuple(language):
+    def isinstance_test(a : 'bool | int | float | complex'):
+        return (isinstance(a, (bool, int)), isinstance(a, (bool, float)), isinstance(a, (int, complex)),
+                isinstance(a, (tuple, list)))
+
+    f = epyccel(isinstance_test, language=language)
+    assert f(True) == isinstance_test(True)
+    assert f(False) == isinstance_test(False)
+    assert f(4) == isinstance_test(6)
+    assert f(3.9) == isinstance_test(6.7)
+    assert f(1+2j) == isinstance_test(6.5+8.3j)
+
+@pytest.mark.skipif(sys.version_info < (3, 10), reason="Union of types implemented in Python 3.10")
+@pytest.mark.parametrize( 'language', (
+        pytest.param("fortran", marks = [pytest.mark.fortran]),
+        pytest.param("c", marks = [pytest.mark.c]),
+        pytest.param("python", marks = [
+            pytest.mark.skip(reason=("isinstance is evaluated during translation so Python translation "
+                "gives wrong results. See #802")),
+            pytest.mark.python]
+        )
+    )
+)
+def test_isinstance_union(language):
+    def isinstance_test(a : bool | int | float | complex): #pylint: disable=unsupported-binary-operation
+        return (isinstance(a, bool | int), isinstance(a, bool | float), isinstance(a, int | complex), #pylint: disable=unsupported-binary-operation
+                isinstance(a, tuple | list)) #pylint: disable=unsupported-binary-operation
+
+    f = epyccel(isinstance_test, language=language)
+    assert f(True) == isinstance_test(True)
+    assert f(False) == isinstance_test(False)
+    assert f(4) == isinstance_test(6)
+    assert f(3.9) == isinstance_test(6.7)
+    assert f(1+2j) == isinstance_test(6.5+8.3j)
diff --git a/tests/pyccel/test_pyccel.py b/tests/pyccel/test_pyccel.py
index 33965015..395ec9cc 100644
--- a/tests/pyccel/test_pyccel.py
+++ b/tests/pyccel/test_pyccel.py
@@ -999,8 +999,6 @@ def test_assert(language, test_file):
 
     cwd = get_abs_path(test_dir)
 
-    if not language:
-        language = "fortran"
     pyccel_commands = " --language="+language
     pyccel_commands += " --output="+ output_dir