Repository: pyccel/pyccel
Pull request #2073 (2024-11-20 12:45:59)

Add support for `dict.items`
Make `Iterable` into a super-class instead of a storage class. Functions that can be used in an iterable context must inherit from this class and define useful methods. The method `get_python_iterable_item` must be defined. If the iterable can be converted to a range the methods `get_range` and `get_assign_targets` must also be defined. Organising iterables via inheritance instead of storing the object in the `Iterable` class avoids problems with circular includes as the low-level `Iterable` object no longer depends on a large number of other ast modules.

The new setup is used to add support for `dict.items`.

**Commit Summary**
- Add an `Iterable` sub-class describing `dict.items`.
- Change method names in `Iterable` sub-classes
- Remove unnecessary methods `length` and `__getitem__` in `Iterable` sub-classes
- Add `Iterable` as a super class of objects that can be iterated over.
- Add a `prefer_inhomogeneous` argument to `PythonTuple` which allows the developer to force the creation of an inhomogeneous tuple (e.g. for the target when iterating over an enumerate of integers).
- Add a `VariableIterator` class to represent the `iter()` function that is called implicitly in a for loop
- Move `Iterable` class from `ast.core` to `ast.internals`
- Remove unnecessary line for `Iterable` in sympy_helper.
- Save the `For` target as a tuple after the semantic stage
- Fix insertion of an inhomogeneous tuple into a loop scope.
- Create `_get_for_iterators` function to unify handling of iterators in FunctionalFor/GeneratorExpressions/For loops
- Add tests

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

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

EmilyBourne: /bot checklist
EmilyBourne: /bot run macosx
EmilyBourne: /bot run docs pylint pyccel_lint
EmilyBourne: /bot run docs pylint pyccel_lint coverage
EmilyBourne: /bot run docs pylint pyccel_lint coverage
EmilyBourne: /bot run docs pylint pyccel_lint coverage
EmilyBourne: /bot run coverage
EmilyBourne: /bot run coverage
EmilyBourne: /bot run coverage
pyccel-bot[bot]: It seems like you have forgotten to complete your checklist for this pull request. Please check the list [here](https://github.com/pyccel/pyccel/pull/2073#issuecomment-2488496504), fix the missing items and let me know when everything is ticked and fixed with `/bot mark as ready`.

pyccel-bot[bot]: @EmilyBourne, @jalalium has a few questions/comments about your code. Can you go through and see if you agree with them. If not go ahead and explain why. Once you've adressed all the comments let me know with `/bot mark as ready` and we will see if we can get approval.

pyccel-bot[bot]: Hey @yguclu, @EmilyBourne, this PR is looking pretty good. @EmilyBourne and @jalalium think it is ready to merge. Could you add your expertise to confirm that this follows all the coding conventions and fits in Pyccel's future plans? Thanks :smile:


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f57e8b0b..d598c757 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -42,6 +42,7 @@ All notable changes to this project will be documented in this file.
 -   #1918 : Add support for set method `clear()`.
 -   #1918 : Add support for set method `copy()`.
 -   #1753 : Add support for set method `union()`.
+-   #1884 : Add support for dict method `items()`.
 -   #1936 : Add missing C output for inline decorator example in documentation
 -   #1937 : Optimise `pyccel.ast.basic.PyccelAstNode.substitute` method.
 -   #1544 : Add support for `typing.TypeAlias`.
@@ -110,6 +111,7 @@ All notable changes to this project will be documented in this file.
 -   \[INTERNALS\] Stop using ndarrays as an intermediate step to call Fortran code.
 -   \[INTERNALS\] Stop using ndarrays as an intermediate step to return arrays from Fortran code.
 -   \[INTERNALS\] Unify the strategy for handling additional imports in the printing stage for different languages.
+-   \[INTERNALS\] Make `Iterable` into a super-class instead of a storage class.
 
 ### Deprecated
 
diff --git a/docs/builtin-functions.md b/docs/builtin-functions.md
index 38358c4f..cc613789 100644
--- a/docs/builtin-functions.md
+++ b/docs/builtin-functions.md
@@ -123,7 +123,7 @@ Python contains a limited number of builtin functions defined [here](https://doc
 | `clear` | Python-only |
 | `copy` | Python-only |
 | `get` | Python-only |
-| `items` | No |
+| `items` | **Yes** |
 | `keys` | No |
 | `pop` | Python-only |
 | `popitem` | Python-only |
diff --git a/pyccel/ast/builtin_methods/dict_methods.py b/pyccel/ast/builtin_methods/dict_methods.py
index 4af7c271..dd8a9607 100644
--- a/pyccel/ast/builtin_methods/dict_methods.py
+++ b/pyccel/ast/builtin_methods/dict_methods.py
@@ -10,13 +10,15 @@ always available.
 This module contains objects which describe these methods within Pyccel's AST.
 """
 
-from pyccel.ast.datatypes import InhomogeneousTupleType, VoidType
-from pyccel.ast.internals import PyccelFunction
+from pyccel.ast.datatypes import InhomogeneousTupleType, VoidType, SymbolicType
+from pyccel.ast.internals import PyccelFunction, Iterable
+from pyccel.ast.variable  import IndexedElement
 
 
 __all__ = ('DictClear',
            'DictCopy',
            'DictGet',
+           'DictItems',
            'DictMethod',
            'DictPop',
            'DictPopitem',
@@ -260,7 +262,6 @@ class DictClear(DictMethod) :
         super().__init__(dict_obj)
 
 #==============================================================================
-
 class DictCopy(DictMethod):
     """
     Represents a call to the .copy() method.
@@ -280,3 +281,49 @@ class DictCopy(DictMethod):
         dict_type = dict_obj.class_type
         self._class_type = dict_type
         super().__init__(dict_obj)
+
+#==============================================================================
+class DictItems(Iterable):
+    """
+    Represents a call to the .items() method.
+
+    Represents a call to the .items() method which iterates over a dictionary.
+
+    Parameters
+    ----------
+    dict_obj : TypedAstNode
+        The object from which the method is called.
+    """
+    __slots__ = ('_dict_obj',)
+    _attribute_nodes = Iterable._attribute_nodes + ("_dict_obj",)
+    _shape = None
+    _class_type = SymbolicType()
+    name = 'items'
+
+    def __init__(self, dict_obj):
+        self._dict_obj = dict_obj
+        super().__init__(1)
+
+    @property
+    def variable(self):
+        """
+        Get the object representing the dict.
+
+        Get the object representing the dict.
+        """
+        return self._dict_obj
+
+    def get_python_iterable_item(self):
+        """
+        Get the item of the iterable that will be saved to the loop targets.
+
+        Returns two objects that could be a key and a value of the dictionary.
+        These elements are used to determine the types of the loop targets.
+
+        Returns
+        -------
+        list[TypedAstNode]
+            A list of objects that should be assigned to variables.
+        """
+        item = DictPopitem(self._dict_obj)
+        return [IndexedElement(item, 0), IndexedElement(item, 1)]
diff --git a/pyccel/ast/builtins.py b/pyccel/ast/builtins.py
index 0aaaf5c0..75c33192 100644
--- a/pyccel/ast/builtins.py
+++ b/pyccel/ast/builtins.py
@@ -20,8 +20,8 @@ from .datatypes import GenericType, PythonNativeComplex, PrimitiveComplexType
 from .datatypes import HomogeneousTupleType, InhomogeneousTupleType
 from .datatypes import HomogeneousListType, HomogeneousContainerType
 from .datatypes import FixedSizeNumericType, HomogeneousSetType, SymbolicType
-from .datatypes import DictType
-from .internals import PyccelFunction, Slice, PyccelArrayShapeElement
+from .datatypes import DictType, VoidType
+from .internals import PyccelFunction, Slice, PyccelArrayShapeElement, Iterable
 from .literals  import LiteralInteger, LiteralFloat, LiteralComplex, Nil
 from .literals  import Literal, LiteralImaginaryUnit, convert_to_literal
 from .literals  import LiteralString
@@ -60,6 +60,7 @@ __all__ = (
     'PythonTupleFunction',
     'PythonType',
     'PythonZip',
+    'VariableIterator',
     'builtin_functions_dict',
 )
 
@@ -347,7 +348,7 @@ class PythonComplex(PyccelFunction):
         return f"complex({self.real}, {self.imag})"
 
 #==============================================================================
-class PythonEnumerate(PyccelFunction):
+class PythonEnumerate(Iterable):
     """
     Represents a call to Python's native `enumerate()` function.
 
@@ -362,7 +363,7 @@ class PythonEnumerate(PyccelFunction):
         The start value of the enumeration index.
     """
     __slots__ = ('_element','_start')
-    _attribute_nodes = ('_element','_start')
+    _attribute_nodes = Iterable._attribute_nodes + ('_element','_start')
     name = 'enumerate'
     _class_type = SymbolicType()
     _shape = ()
@@ -373,7 +374,7 @@ class PythonEnumerate(PyccelFunction):
             raise TypeError('Expecting an arg of valid type')
         self._element = arg
         self._start   = start or LiteralInteger(0)
-        super().__init__()
+        super().__init__(int(self.start != 0))
 
     @property
     def element(self):
@@ -390,15 +391,53 @@ class PythonEnumerate(PyccelFunction):
         """
         return self._start
 
-    def __getitem__(self, index):
-        return [PyccelAdd(index, self.start, simplify=True),
-                self.element[index]]
+    def get_python_iterable_item(self):
+        """
+        Get the item of the iterable that will be saved to the loop targets.
 
-    @property
-    def length(self):
-        """ Return the length of the enumerated object
+        Returns two objects that could be elements of the enumerate.
+
+        Returns
+        -------
+        list[TypedAstNode]
+            A list of objects that should be assigned to variables.
+        """
+        index = self._indices[0]
+        return [index, self.element[index]]
+
+    def get_assign_targets(self):
+        """
+        Get objects that should be assigned to variables to use the enumerate targets.
+
+        Get objects that should be assigned to variables to use the enumerate targets.
+        If the start of the enumerate is 0 then the only object that needs to be assigned
+        is the element of the variable, however if the indexing is offset compared to
+        the variable then both the index and the variable need to be created.
+
+        Returns
+        -------
+        list[TypedAstNode]
+            A list of objects that should be assigned to variables.
+        """
+        index = self._indices[0]
+        if index.is_temp:
+            return [PyccelAdd(index, self.start, simplify=True),
+                    self.element[index]]
+        else:
+            return [self.element[index]]
+
+    def get_range(self):
         """
-        return PythonLen(self.element)
+        Get a range that can be used to iterate over the enumerate iterable.
+
+        Get a range that can be used to iterate over the enumerate iterable.
+
+        Returns
+        -------
+        PythonRange
+            A range that can be used to iterate over the enumerate iterable.
+        """
+        return PythonRange(PythonLen(self.element))
 
 #==============================================================================
 class PythonFloat(PyccelFunction):
@@ -491,12 +530,15 @@ class PythonTuple(TypedAstNode):
     ----------
     *args : tuple of TypedAstNode
         The arguments passed to the tuple function.
+    prefer_inhomogeneous : bool, default=False
+        A boolean that can be used to ensure that the tuple is stocked as an
+        inhomogeneous object even if it could be homogeneous.
     """
     __slots__ = ('_args','_is_homogeneous', '_shape', '_class_type')
     _iterable = True
     _attribute_nodes = ('_args',)
 
-    def __init__(self, *args):
+    def __init__(self, *args, prefer_inhomogeneous = False):
         self._args = args
         super().__init__()
         if pyccel_stage == 'syntactic':
@@ -521,7 +563,7 @@ class PythonTuple(TypedAstNode):
                                for i in range(rank))
         else:
             shapes = ()
-        is_homogeneous = len(dtypes) == 1 and len(ranks) == 1 and \
+        is_homogeneous = (not prefer_inhomogeneous) and 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.class_type, HomogeneousTupleType) for a in args)
@@ -978,7 +1020,7 @@ class PythonDictFunction(PyccelFunction):
         return self._args[0]
 
 #==============================================================================
-class PythonMap(PyccelFunction):
+class PythonMap(Iterable):
     """
     Class representing a call to Python's builtin map function.
 
@@ -993,7 +1035,7 @@ class PythonMap(PyccelFunction):
         The arguments to which the function will be applied.
     """
     __slots__ = ('_func','_func_args')
-    _attribute_nodes = ('_func','_func_args')
+    _attribute_nodes = Iterable._attribute_nodes + ('_func','_func_args')
     name = 'map'
     _class_type = SymbolicType()
     _shape = ()
@@ -1001,7 +1043,7 @@ class PythonMap(PyccelFunction):
     def __init__(self, func, func_args):
         self._func = func
         self._func_args = func_args
-        super().__init__()
+        super().__init__(1)
 
     @property
     def func(self):
@@ -1015,14 +1057,48 @@ class PythonMap(PyccelFunction):
         """
         return self._func_args
 
-    def __getitem__(self, index):
-        return self.func, IndexedElement(self.func_args, index)
+    def get_python_iterable_item(self):
+        """
+        Get the item of the iterable that will be saved to the loop targets.
+
+        This is the function calling the element of the variable that is found
+        at the index.
 
-    @property
-    def length(self):
-        """ Return the length of the resulting object
+        Returns
+        -------
+        list[TypedAstNode]
+            A list of objects that should be assigned to variables.
+        """
+        idx = self._indices[0] if len(self._indices)==1 else self._indices
+        return [self.func(IndexedElement(self.func_args, idx))]
+
+    def get_range(self):
+        """
+        Get a range that can be used to iterate over the map iterable.
+
+        Get a range that can be used to iterate over the map iterable.
+
+        Returns
+        -------
+        PythonRange
+            A range that can be used to iterate over the map iterable.
+        """
+        return PythonRange(PythonLen(self.func_args))
+
+    def get_assign_targets(self):
+        """
+        Get objects that should be assigned to variables to use the map target.
+
+        Get objects that should be assigned to variables to use the map targets.
+        This is the function calling the element of the variable that is found
+        at the index.
+
+        Returns
+        -------
+        list[TypedAstNode]
+            A list of objects that should be assigned to variables.
         """
-        return PythonLen(self.func_args)
+        return self.get_python_iterable_item()
 
 #==============================================================================
 class PythonPrint(PyccelAstNode):
@@ -1065,7 +1141,7 @@ class PythonPrint(PyccelAstNode):
         return self._file
 
 #==============================================================================
-class PythonRange(PyccelFunction):
+class PythonRange(Iterable):
     """
     Class representing a range.
 
@@ -1083,10 +1159,8 @@ class PythonRange(PyccelFunction):
         If three arguments are passed then they represent the start, end and step of the interval.
     """
     __slots__ = ('_start','_stop','_step')
-    _attribute_nodes = ('_start', '_stop', '_step')
+    _attribute_nodes = Iterable._attribute_nodes + ('_start', '_stop', '_step')
     name = 'range'
-    _class_type = SymbolicType()
-    _shape = ()
 
     def __init__(self, *args):
         # Define default values
@@ -1107,7 +1181,7 @@ class PythonRange(PyccelFunction):
         else:
             raise ValueError('Range has at most 3 arguments')
 
-        super().__init__()
+        super().__init__(0)
 
     @property
     def start(self):
@@ -1137,12 +1211,51 @@ class PythonRange(PyccelFunction):
         """
         return self._step
 
-    def __getitem__(self, index):
-        return index
+    def get_range(self):
+        """
+        Get this range.
+
+        Get this range. This method is used to allow this class to be handled
+        like other iterables which can be converted to PythonRange objects.
+
+        Returns
+        -------
+        PythonRange
+            This object.
+        """
+        return self
+
+    def get_python_iterable_item(self):
+        """
+        Get the item of the iterable that will be saved to the loop targets.
+
+        Returns an element of the range indexed with the iterators
+        previously provided via the set_loop_counters method
+        (useful to determine the dtype etc of the loop iterator).
+
+        Returns
+        -------
+        list[TypedAstNode]
+            A list of objects that should be assigned to variables.
+        """
+        return self._indices
 
+    def get_assign_targets(self):
+        """
+        Get objects that should be assigned to variables to use the range.
+
+        This method is used to allow this class to be handled like other iterables
+        which can be converted to PythonRange objects.
+
+        Returns
+        -------
+        list[TypedAstNode]
+            An empty list.
+        """
+        return []
 
 #==============================================================================
-class PythonZip(PyccelFunction):
+class PythonZip(Iterable):
     """
     Represents a call to Python `zip` for code generation.
 
@@ -1153,7 +1266,8 @@ class PythonZip(PyccelFunction):
     *args : tuple of TypedAstNode
         The arguments passed to the function.
     """
-    __slots__ = ('_length', '_class_type')
+    __slots__ = ('_length', '_class_type','_args')
+    _attribute_nodes = Iterable._attribute_nodes + ('_args', '_length')
     name = 'zip'
 
     def __init__(self, *args):
@@ -1161,7 +1275,7 @@ class PythonZip(PyccelFunction):
             raise TypeError('args must be a list or tuple')
         elif len(args) < 2:
             raise ValueError('args must be of length > 2')
-        super().__init__(*args)
+        self._args = args
         if pyccel_stage == 'syntactic':
             self._length = None
             return
@@ -1170,18 +1284,61 @@ class PythonZip(PyccelFunction):
             if lengths:
                 self._length = min(lengths)
             else:
-                self._length = self.args[0].shape[0]
+                self._length = PythonMin(*[PythonLen(a) for a in self.args])
             self._class_type = InhomogeneousTupleType(*[a.class_type for a in args])
+        super().__init__(1)
 
     @property
-    def length(self):
-        """ Length of the shortest zip argument
+    def args(self):
         """
-        return self._length
+        The arguments passed to the function.
+
+        Tuple containing all the arguments passed to the function call.
+        """
+        return self._args
+
+    def get_python_iterable_item(self):
+        """
+        Get the item of the iterable that will be saved to the loop targets.
 
-    def __getitem__(self, index):
+        Returns an element of the zip indexed with the iterators
+        previously provided via the set_loop_counters method
+        (useful to determine the dtype etc of the loop iterator).
+
+        Returns
+        -------
+        list[TypedAstNode]
+            A list of objects that should be assigned to variables.
+        """
+        index = self._indices[0]
         return [a[index] for a in self.args]
 
+    def get_assign_targets(self):
+        """
+        Get objects that should be assigned to variables to use the zip targets.
+
+        Get objects that should be assigned to variables to use the zip targets.
+
+        Returns
+        -------
+        list[TypedAstNode]
+            A list of objects that should be assigned to variables.
+        """
+        return self.get_python_iterable_item()
+
+    def get_range(self):
+        """
+        Get a range that can be used to iterate over the zip iterable.
+
+        Get a range that can be used to iterate over the zip iterable.
+
+        Returns
+        -------
+        PythonRange
+            A range that can be used to iterate over the zip iterable.
+        """
+        return PythonRange(self._length)
+
 #==============================================================================
 class PythonAbs(PyccelFunction):
     """
@@ -1407,6 +1564,80 @@ class PythonType(PyccelFunction):
         """
         return LiteralString(f"<class '{self._type}'>")
 
+#==============================================================================
+class VariableIterator(Iterable):
+    """
+    Represents a call to Python's `iter` function on a variable.
+
+    Represents a call to Python's `iter` function on a variable. This is
+    useful for for loops as this function is called implicitly.
+
+    Parameters
+    ----------
+    var : Variable
+        The Variable that is iterated over.
+    """
+    __slots__ = ('_var',)
+    _attribute_nodes = Iterable._attribute_nodes + ('_var',)
+
+    def __init__(self, var):
+        assert isinstance(var, TypedAstNode)
+        assert var.class_type is not VoidType()
+        assert var.rank > 0
+        self._var = var
+        super().__init__(1)
+
+    @property
+    def variable(self):
+        """
+        The variable being iterated over.
+
+        The variable being iterated over.
+        """
+        return self._var
+
+    def get_range(self):
+        """
+        Get a range that can be used to iterate over the variable.
+
+        Get a range that can be used to iterate over the variable.
+
+        Returns
+        -------
+        PythonRange
+            A range that can be used to iterate over the enumerate iterable.
+        """
+        return PythonRange(self.variable.shape[0])
+
+    def get_python_iterable_item(self):
+        """
+        Get the item of the iterable that will be saved to the loop targets.
+
+        Returns an element of the variable indexed with the iterators
+        previously provided via the set_loop_counters method
+        (useful to determine the dtype etc of the loop iterator).
+
+        Returns
+        -------
+        list[TypedAstNode]
+            A list of objects that should be assigned to variables.
+        """
+        return [self._var[self._indices[0]]]
+
+    def get_assign_targets(self):
+        """
+        Get objects that should be assigned to variables to use the variable iterator targets.
+
+        Returns an element of the variable indexed with the iterators
+        previously provided via the set_loop_counters method.
+
+        Returns
+        -------
+        list[TypedAstNode]
+            A list of objects that should be assigned to variables.
+        """
+        return self.get_python_iterable_item()
+
 #==============================================================================
 
 DtypePrecisionToCastFunction = {
diff --git a/pyccel/ast/class_defs.py b/pyccel/ast/class_defs.py
index a1997888..da4bee0b 100644
--- a/pyccel/ast/class_defs.py
+++ b/pyccel/ast/class_defs.py
@@ -12,7 +12,7 @@ from pyccel.ast.builtin_methods.list_methods import (ListAppend, ListInsert, Lis
                                                      ListClear, ListExtend, ListRemove,
                                                      ListCopy, ListSort)
 from pyccel.ast.builtin_methods.dict_methods import (DictPop, DictPopitem, DictGet, DictClear,DictCopy,
-                                                     DictSetDefault)
+                                                     DictSetDefault, DictItems)
 
 from .builtins   import PythonImag, PythonReal, PythonConjugate
 from .core       import ClassDef, PyccelFunctionDef
@@ -178,6 +178,7 @@ DictClass = ClassDef('dict',
             PyccelFunctionDef('copy', func_class = DictCopy),
             PyccelFunctionDef('clear', func_class = DictClear),
             PyccelFunctionDef('get', func_class = DictGet),
+            PyccelFunctionDef('items', func_class = DictItems),
             PyccelFunctionDef('pop', func_class = DictPop),
             PyccelFunctionDef('popitem', func_class = DictPopitem),
             PyccelFunctionDef('setdefault', func_class = DictSetDefault),
diff --git a/pyccel/ast/core.py b/pyccel/ast/core.py
index b50d8579..7d3c85e3 100644
--- a/pyccel/ast/core.py
+++ b/pyccel/ast/core.py
@@ -15,15 +15,14 @@ from .basic     import PyccelAstNode, TypedAstNode, iterable, ScopedAstNode
 
 from .bitwise_operators import PyccelBitOr, PyccelBitAnd
 
-from .builtins  import (PythonEnumerate, PythonLen, PythonMap, PythonTuple,
-                        PythonRange, PythonZip, PythonBool)
+from .builtins  import PythonBool, PythonTuple
 
 from .c_concepts import PointerCast
 
 from .datatypes import (PyccelType, HomogeneousTupleType, VoidType,
                         PythonNativeBool, InhomogeneousTupleType)
 
-from .internals import PyccelSymbol, PyccelFunction, apply_pickle
+from .internals import PyccelSymbol, PyccelFunction, apply_pickle, Iterable
 
 from .literals  import Nil, LiteralFalse, LiteralInteger, LiteralString
 from .literals  import NilArgument, LiteralTrue
@@ -79,7 +78,6 @@ __all__ = (
     'InProgram',
     'InlineFunctionDef',
     'Interface',
-    'Iterable',
     'Module',
     'ModuleHeader',
     'Pass',
@@ -105,7 +103,6 @@ __all__ = (
 
 #==============================================================================
 
-
 class AsName(PyccelAstNode):
     """
     Represents a renaming of an object, used with Import.
@@ -1354,147 +1351,6 @@ class Program(ScopedAstNode):
         self._imports = tuple(i for i in self.imports if i.source != name)
 
 
-#==============================================================================
-class Iterable(PyccelAstNode):
-    """
-    Wrapper around iterable types helping to convert between those types and a range.
-
-    Wrapper around iterable types helping to convert between those
-    types and a range (necessary in low level languages, e.g. C and Fortran).
-
-    Parameters
-    ----------
-    iterable : acceptable_iterator_type
-               The iterator being wrapped
-               The type must be in acceptable_iterator_types or the class must
-               implement the following functions:
-               - n_indices
-               - to_range
-    """
-    acceptable_iterator_types = (Variable, PythonMap, PythonZip, PythonEnumerate, PythonRange, IndexedElement)
-    __slots__ = ('_iterable','_indices','_num_indices_required')
-    _attribute_nodes = ('_iterable','_indices')
-
-    def __init__(self, iterable):
-        self._iterable = iterable
-        self._indices  = None
-
-        if isinstance(iterable, PythonRange):
-            self._num_indices_required = 0
-        elif isinstance(iterable, PythonEnumerate):
-            self._num_indices_required = int(iterable.start != 0)
-        elif isinstance(iterable, self.acceptable_iterator_types):
-            self._num_indices_required = 1
-        elif hasattr(iterable, 'n_indices') and hasattr(iterable, 'to_range'):
-            self._num_indices_required = iterable.n_indices
-        else:
-            raise TypeError(f"Unknown iterator type {type(iterable)}")
-
-        super().__init__()
-
-    @property
-    def num_loop_counters_required(self):
-        """ Number of iterators which must be generated in order to
-        convert this iterable to a range
-        """
-        return self._num_indices_required
-
-    def set_loop_counter(self, *indices):
-        """ Set the iterator(s) for the generated range
-        These are iterators generated by pyccel so are not
-        needed for python code
-        """
-        assert self._indices is None
-        for i in indices:
-            i.set_current_user_node(self)
-        self._indices = indices
-
-    def unset_loop_counter(self, invalidate = True):
-        """ Remove the iterator(s) set for the generated range
-        """
-        for i in self._indices:
-            i.remove_user_node(self, invalidate)
-        self._indices = None
-
-    def get_assigns(self, target):
-        """ Returns a list containing any assigns necessary to initialise
-        the loop iterators/targets when using a range iterable
-
-        Parameters
-        ----------
-        target : Variable or list of Variables
-                 The index(es) over which the loop iterates
-
-        Results
-        -------
-        assigns : list of Assign
-                  The assignments necessary to define target
-        """
-        iterable = self._iterable
-        if isinstance(iterable, PythonRange):
-            return []
-        range_element = self.get_target_from_range()
-        if self.num_loop_counters_required == 0:
-            target = target[1:]
-            range_element = range_element[1:]
-        if isinstance(target, (tuple, list)):
-            return [AliasAssign(t, r) if t.is_alias else Assign(t, r) for t, r in zip(target, range_element)]
-        else:
-            return [AliasAssign(target, range_element) if target.is_alias else Assign(target, range_element)]
-
-    def get_target_from_range(self):
-        """ Returns an element of the range indexed with the iterators
-        previously provided via the set_loop_counters method
-        (useful for get_assigns and to determine the dtype etc of the
-        loop iterator)
-        """
-        idx = self._indices[0] if len(self._indices)==1 else self._indices
-        range_base = self._iterable[idx]
-        if isinstance(self._iterable, PythonMap):
-            return FunctionCall(range_base[0], [range_base[1]])
-        else:
-            return range_base
-
-    def get_range(self):
-        """
-        Get the range required for this iterable.
-
-        Returns the range which is necessary in a low-level language to iterate over
-        the wrapped iterable.
-
-        Returns
-        -------
-        PythonRange
-            The range which should be used in the code.
-        """
-        if isinstance(self._iterable, PythonRange):
-            return self._iterable
-        elif hasattr(self._iterable, 'to_range'):
-            return self._iterable.to_range()
-        else:
-            length = getattr(self._iterable, '__len__',
-                    getattr(self._iterable, 'length', None))
-
-            # Only create PythonLen if length is None to avoid unnecessary TypeErrors
-            if length is None:
-                length = PythonLen(self._iterable)
-
-            if callable(length):
-                length = length()
-            return PythonRange(length)
-
-    @property
-    def iterable(self):
-        """ Returns the iterable being wrapped
-        """
-        return self._iterable
-
-    @property
-    def loop_counters(self):
-        """ Returns the iterator(s) of the generated range
-        """
-        return self._indices
-
 #==============================================================================
 
 class For(ScopedAstNode):
@@ -1537,11 +1393,8 @@ class For(ScopedAstNode):
         body,
         scope = None
         ):
-        if pyccel_stage != "syntactic":
-            if not isinstance(iter_obj, Iterable):
-                iter_obj = Iterable(iter_obj)
-                if iter_obj.num_loop_counters_required!=0:
-                    raise TypeError('iter_obj must be an iterable')
+        assert pyccel_stage == "syntactic" or isinstance(iter_obj, Iterable)
+        assert pyccel_stage == "syntactic" or iterable(target)
 
         if iterable(body):
             body = CodeBlock(body)
@@ -2903,7 +2756,7 @@ class PyccelFunctionDef(FunctionDef):
     __slots__ = ('_argument_description',)
     def __init__(self, name, func_class, *, decorators = {}, argument_description = {}):
         assert isinstance(func_class, type) and \
-                issubclass(func_class, (PyccelFunction, TypedAstNode))
+                issubclass(func_class, (PyccelFunction, TypedAstNode, Iterable))
         assert isinstance(argument_description, dict)
         arguments = ()
         results = ()
diff --git a/pyccel/ast/internals.py b/pyccel/ast/internals.py
index 4d30b32c..70adc56b 100644
--- a/pyccel/ast/internals.py
+++ b/pyccel/ast/internals.py
@@ -11,12 +11,13 @@ To avoid circular imports this file should only import from basic, datatypes, an
 from pyccel.utilities.stage import PyccelStage
 
 from .basic     import PyccelAstNode, TypedAstNode, Immutable
-from .datatypes import PythonNativeInt, PrimitiveIntegerType
+from .datatypes import PythonNativeInt, PrimitiveIntegerType, SymbolicType
 from .literals  import LiteralInteger
 
 pyccel_stage = PyccelStage()
 
 __all__ = (
+    'Iterable',
     'PrecomputedCode',
     'PyccelArrayShapeElement',
     'PyccelArraySize',
@@ -342,6 +343,94 @@ class PrecomputedCode(PyccelAstNode):
         return self._code
 
 
+class Iterable(TypedAstNode):
+    """
+    Wrapper around iterable types helping to convert between those types and a range.
+
+    Wrapper around iterable types helping to convert between those
+    types and a range (necessary in low level languages, e.g. C and Fortran).
+
+    If an iterable can be iterated over using a range then this can be done automatically
+    by defining the following 3 functions:
+    - get_range : Returns the range required for the iteration.
+    - get_python_iterable_item : Returns the item of the iterable that will be saved to the
+        variables which are the loop targets.
+        E.g. for the loop:
+        >>> for idx, v in enumerate(var)
+
+        this function should return the range index `idx` (this may be set using
+        set_loop_counter) and var[idx].
+        These objects are used for type deductions.
+    - get_assign_targets : Returns any objects that should be assigned to targets
+        E.g. for the loop:
+        >>> for idx, v in enumerate(var)
+
+        The object `var[idx]` is returned. The printer is then responsible for
+        creating the Assign(v, var[idx])
+        E.g. for the loop:
+        >>> for r,p in zip(r_var,p_var)
+
+        The objects `r_var[idx]` and `p_var[idx]` are returned. The index is retrieved
+        from this class where it was set using set_loop_counter.
+        The printer is then responsible for creating the Assign(r, r_var[idx]) and
+        Assign(p, p_var[idx]).
+
+    Parameters
+    ----------
+    num_indices_required : int
+        The number of indices that the semantic stage should generate to correctly
+        iterate over the object.
+    """
+    __slots__ = ('_indices', '_num_indices_required')
+    _attribute_nodes = ('_indices',)
+    _class_type = SymbolicType()
+    _shape = None
+
+    def __init__(self, num_indices_required):
+        assert isinstance(num_indices_required, int)
+        self._indices  = None
+        self._num_indices_required = num_indices_required
+
+        super().__init__()
+
+    @property
+    def num_loop_counters_required(self):
+        """
+        Number of indices that should be generate by the semantic stage.
+
+        Number of indices which must be generated in order to convert this
+        iterable to a range. This is usually 1.
+        """
+        return self._num_indices_required
+
+    def set_loop_counter(self, *indices):
+        """
+        Set the iterator(s) for the generated range.
+
+        These are iterators generated by Pyccel that were not needed for the
+        original Python code. Ideally they will also not be necessary in the
+        generated Python code so these objects should only be inserted into
+        the scope during printing.
+
+        Parameters
+        ----------
+        *indices : TypedAstNode
+            The iterator(s) generated by Pyccel.
+        """
+        assert self._indices is None
+        for i in indices:
+            i.set_current_user_node(self)
+        self._indices = indices
+
+    @property
+    def loop_counters(self):
+        """
+        Returns the iterator(s) of the generated range.
+
+        Returns the iterator(s) of the generated range.
+        """
+        return self._indices
+
 def symbols(names):
     """
     Transform strings into instances of PyccelSymbol class.
diff --git a/pyccel/ast/itertoolsext.py b/pyccel/ast/itertoolsext.py
index 8450e084..557aaec8 100644
--- a/pyccel/ast/itertoolsext.py
+++ b/pyccel/ast/itertoolsext.py
@@ -7,14 +7,14 @@ This module represent a call to the itertools functions for code generation.
 """
 from .builtins  import PythonLen, PythonRange
 from .core      import PyccelFunctionDef, Module
-from .internals import PyccelFunction
+from .internals import Iterable
 
 __all__ = (
     'Product',
     'itertools_mod',
 )
 
-class Product(PyccelFunction):
+class Product(Iterable):
     """
     Represents a call to itertools.product for code generation.
 
@@ -38,29 +38,26 @@ class Product(PyccelFunction):
 
     def __init__(self, *args):
         self._elements = args
-        super().__init__(args)
+        super().__init__(len(args))
 
     @property
     def elements(self):
         """get expression's elements"""
         return self._elements
 
-    def __getitem__(self, indices):
-        return [elem[idx] for idx, elem in zip(indices, self.elements)]
-
-    def to_range(self):
-        """ Get the range iterator(s) needed to access all elements of Product
+    def get_python_iterable_item(self):
         """
-        lengths = [getattr(e, '__len__',
-                getattr(e, 'length', PythonLen(e))) for e in self.elements]
-        lengths = [l() if callable(l) else l for l in lengths]
-        return [PythonRange(l) for l in lengths]
+        Get the item of the iterable that will be saved to the loop targets.
 
-    @property
-    def n_indices(self):
-        """ Number of indices required
+        This is an element from each of the variables indexed using the
+        iterators previously provided via the set_loop_counters method.
+
+        Returns
+        -------
+        list[TypedAstNode]
+            A list of objects that should be assigned to variables.
         """
-        return len(self._elements)
+        return [elem[idx] for idx, elem in zip(self._indices, self.elements)]
 
 #==============================================================================
 itertools_mod = Module('itertools',(),
diff --git a/pyccel/ast/sympy_helper.py b/pyccel/ast/sympy_helper.py
index bbede81c..f2c80cdf 100644
--- a/pyccel/ast/sympy_helper.py
+++ b/pyccel/ast/sympy_helper.py
@@ -14,7 +14,6 @@ from sympy.core.numbers import One, NegativeOne, Zero, Half
 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
@@ -194,9 +193,6 @@ def pyccel_to_sympy(expr, symbol_map, used_names):
         symbol_map[sym] = expr
         return sym
 
-    elif isinstance(expr, Iterable):
-        return pyccel_to_sympy(expr.iterable, symbol_map, used_names)
-
     elif isinstance(expr, PythonRange):
         start = pyccel_to_sympy(expr.start, symbol_map, used_names)
         stop  = pyccel_to_sympy(expr.stop , symbol_map, used_names)
diff --git a/pyccel/ast/utilities.py b/pyccel/ast/utilities.py
index f2a7ad6b..34aceb13 100644
--- a/pyccel/ast/utilities.py
+++ b/pyccel/ast/utilities.py
@@ -694,7 +694,7 @@ def insert_fors(blocks, indices, scope, level = 0):
     else:
         body = CodeBlock(body, unravelled = True)
         loop_scope = scope.create_new_loop_scope()
-        return [For(indices[level],
+        return [For([indices[level]],
                     PythonRange(0,blocks.length),
                     body,
                     scope = loop_scope)]
diff --git a/pyccel/codegen/printing/ccode.py b/pyccel/codegen/printing/ccode.py
index 3c0614a8..8af2c019 100644
--- a/pyccel/codegen/printing/ccode.py
+++ b/pyccel/codegen/printing/ccode.py
@@ -15,9 +15,11 @@ from pyccel.ast.basic     import ScopedAstNode
 from pyccel.ast.bind_c    import BindCPointer
 
 from pyccel.ast.builtins  import PythonRange, PythonComplex
-from pyccel.ast.builtins  import PythonPrint, PythonType
+from pyccel.ast.builtins  import PythonPrint, PythonType, VariableIterator
 from pyccel.ast.builtins  import PythonList, PythonTuple, PythonSet, PythonDict, PythonLen
 
+from pyccel.ast.builtin_methods.dict_methods  import DictItems
+
 from pyccel.ast.core      import Declare, For, CodeBlock
 from pyccel.ast.core      import FuncAddressDeclare, FunctionCall, FunctionCallArgument
 from pyccel.ast.core      import Allocate, Deallocate
@@ -1215,7 +1217,7 @@ class CCodePrinter(CodePrinter):
 
                 for_body  = [PythonPrint(print_body, file=expr.file)]
                 for_scope = self.scope.create_new_loop_scope()
-                for_loop  = For(for_index, for_range, for_body, scope=for_scope)
+                for_loop  = For((for_index,), for_range, for_body, scope=for_scope)
                 for_end   = FunctionCallArgument(LiteralString(']'+end if i == len(orig_args)-1 else ']'), keyword='end')
 
                 body = CodeBlock([PythonPrint([ FunctionCallArgument(LiteralString('[')), empty_end],
@@ -2329,38 +2331,53 @@ class CCodePrinter(CodePrinter):
         self.set_scope(expr.scope)
 
         iterable = expr.iterable
-        iterable_type = iterable.iterable.class_type
         indices = iterable.loop_counters
 
-        if isinstance(iterable_type, (DictType, HomogeneousSetType, HomogeneousListType)):
+        if isinstance(iterable, (VariableIterator, DictItems)) and \
+                isinstance(iterable.variable.class_type, (DictType, HomogeneousSetType, HomogeneousListType)):
+            var = iterable.variable
+            iterable_type = var.class_type
             counter = Variable(IteratorType(iterable_type), indices[0].name)
             c_type = self.get_c_type(iterable_type)
-            iterable_code = self._print(iterable.iterable)
+            iterable_code = self._print(var)
             for_code = f'c_foreach ({self._print(counter)}, {c_type}, {iterable_code})'
-            additional_assign = CodeBlock([Assign(expr.target, DottedVariable(VoidType(), 'ref',
-                memory_handling='alias', lhs = counter))])
+            tmp_ref = DottedVariable(VoidType(), 'ref', memory_handling='alias', lhs = counter)
+            if isinstance(iterable, DictItems):
+                assigns = [Assign(expr.target[0], DottedVariable(VoidType(), 'first', lhs = tmp_ref)),
+                           Assign(expr.target[1], DottedVariable(VoidType(), 'second', lhs = tmp_ref))]
+            else:
+                assigns = [Assign(expr.target[0], tmp_ref)]
+            additional_assign = CodeBlock(assigns)
         else:
-            index = indices[0] if indices else expr.target
-            if iterable.num_loop_counters_required:
-                self.scope.insert_variable(index)
+            range_iterable = iterable.get_range()
+            if indices:
+                index = indices[0]
+                if iterable.num_loop_counters_required and index.is_temp:
+                    self.scope.insert_variable(index)
+            else:
+                index = expr.target[0]
 
-            target   = index
-            counter  = self._print(target)
-            iterable = expr.iterable.get_range()
-            additional_assign = CodeBlock(expr.iterable.get_assigns(expr.target))
+            targets = iterable.get_assign_targets()
+            additional_assign = CodeBlock([AliasAssign(i, t) if i.is_alias else Assign(i, t) \
+                                    for i,t in zip(expr.target[-len(targets):], targets)])
 
-            step = iterable.step
-            start_code = self._print(iterable.start)
-            stop_code  = self._print(iterable.stop )
-            step_code  = self._print(iterable.step )
+            index_code = self._print(index)
+            step = range_iterable.step
+            start_code = self._print(range_iterable.start)
+            stop_code  = self._print(range_iterable.stop )
+            step_code  = self._print(range_iterable.step )
 
             # testing if the step is a value or an expression
             if is_literal_integer(step):
                 op = '>' if int(step) < 0 else '<'
-                stop_condition = f'{counter} {op} {stop_code}'
+                stop_condition = f'{index_code} {op} {stop_code}'
             else:
-                stop_condition = f'({step_code} > 0) ? ({counter} < {stop_code}) : ({counter} > {stop_code})'
-            for_code = f'for ({counter} = {start_code}; {stop_condition}; {counter} += {step_code})\n'
+                stop_condition = f'({step_code} > 0) ? ({index_code} < {stop_code}) : ({index_code} > {stop_code})'
+            for_code = f'for ({index_code} = {start_code}; {stop_condition}; {index_code} += {step_code})\n'
+
+        if self._additional_code:
+            for_code = self._additional_code + for_code
+            self._additional_code = ''
 
         if self._additional_code:
             for_code = self._additional_code + for_code
diff --git a/pyccel/codegen/printing/fcode.py b/pyccel/codegen/printing/fcode.py
index 3675ed1b..9bc1cf98 100644
--- a/pyccel/codegen/printing/fcode.py
+++ b/pyccel/codegen/printing/fcode.py
@@ -22,7 +22,9 @@ from pyccel.ast.bind_c import BindCPointer, BindCFunctionDef, BindCFunctionDefAr
 
 from pyccel.ast.builtins import PythonInt, PythonType, PythonPrint, PythonRange
 from pyccel.ast.builtins import PythonTuple, DtypePrecisionToCastFunction
-from pyccel.ast.builtins import PythonBool, PythonList, PythonSet
+from pyccel.ast.builtins import PythonBool, PythonList, PythonSet, VariableIterator
+
+from pyccel.ast.builtin_methods.dict_methods import DictItems
 
 from pyccel.ast.builtin_methods.set_methods import SetUnion
 
@@ -1012,7 +1014,7 @@ class FCodePrinter(CodePrinter):
                     print_body.append(space_end)
 
                 for_body = [PythonPrint(print_body, file=expr.file)]
-                for_loop = For(for_index, for_range, for_body, scope=loop_scope)
+                for_loop = For((for_index,), for_range, for_body, scope=loop_scope)
                 for_end_char = LiteralString(']')
                 for_end = FunctionCallArgument(for_end_char,
                                                keyword='end')
@@ -2572,29 +2574,39 @@ class FCodePrinter(CodePrinter):
         self.set_scope(expr.scope)
 
         iterable = expr.iterable
-        iterable_type = iterable.iterable.class_type
-        indices = expr.iterable.loop_counters
-
-        if isinstance(iterable_type, (DictType, HomogeneousSetType)):
-            if isinstance(iterable.iterable, Variable):
-                suggested_name = iterable.iterable.name + '_'
+        indices = iterable.loop_counters
+
+        if isinstance(iterable, (VariableIterator, DictItems)) and \
+                isinstance(iterable.variable.class_type, (DictType, HomogeneousSetType)):
+            var = iterable.variable
+            iterable_type = var.class_type
+            if isinstance(var, Variable):
+                suggested_name = var.name + '_'
             else:
                 suggested_name = ''
                 errors.report("Iterating over a temporary object. This may cause compilation issues or cause calculations to be carried out twice",
                         severity='warning', symbol=expr)
-            iterable = self._print(iterable.iterable)
+            var_code = self._print(var)
             iterator = self.scope.get_temporary_variable(IteratorType(iterable_type),
                     name = suggested_name + 'iter')
             last = self.scope.get_temporary_variable(IteratorType(iterable_type),
                     name = suggested_name + 'last')
-            target = self._print(expr.target)
-            prolog = (f'{iterator} = {iterable} % begin()\n'
-                      f'{last} = {iterable} % end()\n'
-                      f'do while ({iterator} /= {last})\n'
-                      f'{target} = {iterator} % of()\n')
+            if isinstance(iterable, DictItems):
+                key = self._print(expr.target[0])
+                val = self._print(expr.target[1])
+                target_assign = (f'{key} = {iterator} % first()\n'
+                                 f'{val} = {iterator} % second()\n')
+            else:
+                target = self._print(expr.target[0])
+                target_assign = f'{target} = {iterator} % of()\n'
+
+            prolog = ''.join((f'{iterator} = {var_code} % begin()\n',
+                              f'{last} = {var_code} % end()\n',
+                              f'do while ({iterator} /= {last})\n',
+                              target_assign))
             epilog = f'call {iterator} % next()\nend do\n'
         else:
-            index = indices[0] if indices else expr.target
+            index = indices[0] if indices else expr.target[0]
             if iterable.num_loop_counters_required:
                 self.scope.insert_variable(index)
 
@@ -2606,7 +2618,10 @@ class FCodePrinter(CodePrinter):
             prolog = f'do {target} = {range_code}\n'
             epilog = 'end do\n'
 
-            prolog += self._print(CodeBlock(iterable.get_assigns(expr.target)))
+            targets = iterable.get_assign_targets()
+            additional_assign = CodeBlock([AliasAssign(i, t) if i.is_alias else Assign(i, t) \
+                                    for i,t in zip(expr.target[-len(targets):], targets)])
+            prolog += self._print(additional_assign)
 
         body = self._print(expr.body)
 
diff --git a/pyccel/codegen/printing/pycode.py b/pyccel/codegen/printing/pycode.py
index 29dbb205..26e449c6 100644
--- a/pyccel/codegen/printing/pycode.py
+++ b/pyccel/codegen/printing/pycode.py
@@ -445,8 +445,8 @@ class PythonCodePrinter(CodePrinter):
         else:
             return '{}({}+{}*1j)'.format(name, self._print(expr.real), self._print(expr.imag))
 
-    def _print_Iterable(self, expr):
-        return self._print(expr.iterable)
+    def _print_VariableIterator(self, expr):
+        return self._print(expr.variable)
 
     def _print_PythonRange(self, expr):
         return 'range({start}, {stop}, {step})'.format(
@@ -468,6 +468,10 @@ class PythonCodePrinter(CodePrinter):
                 func = self._print(expr.func.name),
                 args = self._print(expr.func_args))
 
+    def _print_PythonZip(self, expr):
+        args = ', '.join(self._print(a) for a in expr.args)
+        return f'zip({args})'
+
     def _print_PythonReal(self, expr):
         if isinstance(expr.internal_var, Variable):
             return '{}.real'.format(self._print(expr.internal_var))
@@ -855,6 +859,11 @@ class PythonCodePrinter(CodePrinter):
         else:
             return f"{dict_obj}.get({key})\n"
 
+    def _print_DictItems(self, expr):
+        dict_obj = self._print(expr.variable)
+
+        return f"{dict_obj}.items()"
+
     def _print_Slice(self, expr):
         start = self._print(expr.start) if expr.start else ''
         stop  = self._print(expr.stop)  if expr.stop  else ''
diff --git a/pyccel/codegen/wrapper/c_to_python_wrapper.py b/pyccel/codegen/wrapper/c_to_python_wrapper.py
index fd6ff0bb..fb82fce5 100644
--- a/pyccel/codegen/wrapper/c_to_python_wrapper.py
+++ b/pyccel/codegen/wrapper/c_to_python_wrapper.py
@@ -17,7 +17,7 @@ from pyccel.ast.core          import Interface, If, IfSection, Return, FunctionC
 from pyccel.ast.core          import FunctionDef, FunctionDefArgument, FunctionDefResult
 from pyccel.ast.core          import Assign, AliasAssign, Deallocate, Allocate
 from pyccel.ast.core          import Import, Module, AugAssign, CommentBlock, For
-from pyccel.ast.core          import FunctionAddress, Declare, ClassDef, AsName, Iterable
+from pyccel.ast.core          import FunctionAddress, Declare, ClassDef, AsName
 from pyccel.ast.cwrapper      import PyModule, PyccelPyObject, PyArgKeywords, PyModule_Create
 from pyccel.ast.cwrapper      import PyArg_ParseTupleNode, Py_None, PyClassDef, PyModInitFunc
 from pyccel.ast.cwrapper      import py_to_c_registry, check_type_registry, PyBuildValueNode
@@ -333,7 +333,7 @@ class CToPythonWrapper(Wrapper):
             for_body = [indexed_init]
             internal_type_check_condition, _ = self._get_type_check_condition(indexed_py_obj, arg[0], False, for_body)
             for_body.append(Assign(type_check_condition, PyccelAnd(type_check_condition, internal_type_check_condition)))
-            internal_type_check = For(idx, Iterable(PythonRange(size_var)), for_body, scope = for_scope)
+            internal_type_check = For((idx,), PythonRange(size_var), for_body, scope = for_scope)
 
             tuple_checks = IfSection(tuple_check, [size_assign, Assign(type_check_condition, LiteralTrue()), internal_type_check])
             default_value = IfSection(LiteralTrue(), [Assign(type_check_condition, LiteralFalse())])
@@ -2312,7 +2312,7 @@ class CToPythonWrapper(Wrapper):
                                     bound_argument, is_bind_c_argument, arg_var = indexed_arg_var)['body']
         self.exit_scope()
 
-        body.append(For(idx, Iterable(PythonRange(size_var)), for_body, scope = for_scope))
+        body.append(For((idx,), PythonRange(size_var), for_body, scope = for_scope))
 
 
         return {'body': body, 'args': arg_vars}
diff --git a/pyccel/errors/messages.py b/pyccel/errors/messages.py
index 79eccc1d..8fefbfa6 100644
--- a/pyccel/errors/messages.py
+++ b/pyccel/errors/messages.py
@@ -39,7 +39,6 @@ INCOMPATIBLE_TYPES_IN_STR_INTERPOLATION = 'Incompatible types in string interpol
 MUST_HAVE_NONE_RETURN_TYPE = 'The return type of "{}" must be None'
 INVALID_TUPLE_INDEX_TYPE = 'Invalid tuple index type'
 TUPLE_INDEX_OUT_OF_RANGE = 'Tuple index out of range'
-ITERABLE_EXPECTED = 'Iterable expected'
 INVALID_SLICE_INDEX = 'Slice index must be an integer or None'
 CANNOT_INFER_LAMBDA_TYPE = 'Cannot infer type of lambda'
 CANNOT_INFER_ITEM_TYPE = 'Cannot infer iterable item type'
diff --git a/pyccel/parser/scope.py b/pyccel/parser/scope.py
index 0e8b4b81..c3f9333a 100644
--- a/pyccel/parser/scope.py
+++ b/pyccel/parser/scope.py
@@ -322,10 +322,6 @@ class Scope(object):
         name : str, default=var.name
             The name of the variable in the Python code.
         """
-        if isinstance(var.class_type, InhomogeneousTupleType):
-            for v in var:
-                self.insert_variable(self.collect_tuple_element(v))
-
         if var.name == '_':
             raise ValueError("A temporary variable should have a name generated by Scope.get_new_name")
         if not isinstance(var, Variable):
@@ -337,6 +333,10 @@ class Scope(object):
         if not self.allow_loop_scoping and self.is_loop:
             self.parent_scope.insert_variable(var, name)
         else:
+            if isinstance(var.class_type, InhomogeneousTupleType):
+                for v in var:
+                    self.insert_variable(self.collect_tuple_element(v))
+
             if name in self._locals['variables']:
                 raise RuntimeError(f'New variable {name} already exists in scope')
             if name == '_':
@@ -532,12 +532,15 @@ class Scope(object):
         alias : pyccel.ast.basic.Basic
             The object which will be represented by the symbol.
         """
-        symbolic_aliases = self._locals['symbolic_alias']
-        if symbol in symbolic_aliases:
-            errors.report(f"{symbol} cannot represent multiple static concepts",
-                    symbol=symbol, severity='error')
+        if not self.allow_loop_scoping and self.is_loop:
+            self.parent_scope.insert_symbolic_alias(symbol, alias)
+        else:
+            symbolic_aliases = self._locals['symbolic_alias']
+            if symbol in symbolic_aliases:
+                errors.report(f"{symbol} cannot represent multiple static concepts",
+                        symbol=symbol, severity='error')
 
-        symbolic_aliases[symbol] = alias
+            symbolic_aliases[symbol] = alias
 
     def insert_symbols(self, symbols):
         """ Add multiple new symbols to the scope
diff --git a/pyccel/parser/semantic.py b/pyccel/parser/semantic.py
index b5fda035..e3b70b33 100644
--- a/pyccel/parser/semantic.py
+++ b/pyccel/parser/semantic.py
@@ -26,7 +26,7 @@ from pyccel.ast.bitwise_operators import PyccelBitOr, PyccelLShift, PyccelRShift
 from pyccel.ast.builtins import PythonPrint, PythonTupleFunction, PythonSetFunction
 from pyccel.ast.builtins import PythonComplex, PythonDict, PythonDictFunction, PythonListFunction
 from pyccel.ast.builtins import builtin_functions_dict, PythonImag, PythonReal
-from pyccel.ast.builtins import PythonList, PythonConjugate , PythonSet
+from pyccel.ast.builtins import PythonList, PythonConjugate , PythonSet, VariableIterator
 from pyccel.ast.builtins import (PythonRange, PythonZip, PythonEnumerate,
                                  PythonTuple, Lambda, PythonMap)
 
@@ -54,7 +54,6 @@ from pyccel.ast.core import AsName
 from pyccel.ast.core import With
 from pyccel.ast.core import Duplicate
 from pyccel.ast.core import StarredArguments
-from pyccel.ast.core import Iterable
 from pyccel.ast.core import Decorator
 from pyccel.ast.core import PyccelFunctionDef
 from pyccel.ast.core import Assert
@@ -74,6 +73,7 @@ from pyccel.ast.headers import FunctionHeader, MethodHeader, Header
 from pyccel.ast.headers import MacroFunction, MacroVariable
 
 from pyccel.ast.internals import PyccelFunction, Slice, PyccelSymbol
+from pyccel.ast.internals import Iterable
 from pyccel.ast.itertoolsext import Product
 
 from pyccel.ast.literals import LiteralTrue, LiteralFalse
@@ -1825,23 +1825,7 @@ class SemanticParser(BasicParser):
         new_expr = []
         while isinstance(loop, For):
             nlevels+=1
-            iterable = Iterable(self._visit(loop.iterable))
-            n_index = max(1, iterable.num_loop_counters_required)
-            # Set dummy indices to iterable object in order to be able to
-            # obtain a target with a deducible dtype
-            iterable.set_loop_counter(*[index]*n_index)
-
-            iterator = loop.target
-
-            # Collect a target with a deducible dtype
-            iterator_rhs = iterable.get_target_from_range()
-            # Use _visit_Assign to create the requested iterator with the correct type
-            # The result of this operation is not stored, it is just used to declare
-            # iterator with the correct dtype to allow correct dtype deductions later
-            pyccel_stage.set_stage('syntactic')
-            syntactic_assign = Assign(iterator, iterator_rhs, python_ast=expr.python_ast)
-            pyccel_stage.set_stage('semantic')
-            self._visit(syntactic_assign)
+            self._get_for_iterators(loop.iterable, loop.target, new_expr)
 
             loop_elem = loop.body.body[0]
 
@@ -1862,6 +1846,7 @@ class SemanticParser(BasicParser):
                     loop.substitute(gen, assign.lhs)
                     loop_elem = loop.body.body[0]
             loop = loop_elem
+
         # Remove the throw-away variable from the scope
         self.scope.remove_variable(index)
 
@@ -2103,6 +2088,125 @@ class SemanticParser(BasicParser):
 
         return lhs
 
+    def _get_iterable(self, syntactic_iterable):
+        """
+        Get an Iterable obect from a syntatic object that is used in an iterable context.
+
+        Get an Iterable obect from a syntatic object that is used in an iterable context.
+        A typical example of an iterable context is the iterable of a for loop.
+
+        Parameters
+        ----------
+        syntactic_iterable : PyccelAstNode
+            The syntatic object that should be usable as an iterable.
+
+        Returns
+        -------
+        Iterable
+            A semantic Iterable object.
+        """
+        iterable = self._visit(syntactic_iterable)
+        if isinstance(iterable, (Variable, IndexedElement)):
+            iterable = VariableIterator(iterable)
+        elif not isinstance(iterable, Iterable):
+            if isinstance(iterable, TypedAstNode):
+                pyccel_stage.set_stage('syntactic')
+                tmp_var = self.scope.get_new_name()
+                syntactic_assign = Assign(tmp_var, iterable, python_ast = iterable.python_ast)
+                pyccel_stage.set_stage('semantic')
+                assign = self._visit(syntactic_assign)
+                self._additional_exprs[-1].append(assign)
+                iterable = VariableIterator(self._visit(tmp_var))
+            else:
+                errors.report(f"{iterable} is not handled as the iterable of a for loop",
+                        symbol=syntactic_iterable, severity='fatal')
+
+        return iterable
+
+    def _get_for_iterators(self, syntactic_iterable, iterator, new_expr):
+        """
+        Get the semantic target and iterable of a for loop.
+
+        Get the semantic target and iterable of a for loop. This method can be used to
+        handle generators, comprehension expressions or basic for loops.
+
+        Parameters
+        ----------
+        syntactic_iterable : TypedAstNode
+            The iterable that the for loop iterates over.
+        iterator : TypedAstNode
+            The syntactic iterator that takes the value of the elements of the iterable.
+        new_expr : list[PyccelAstNode]
+            A list which allows collection of any additional expressions
+            resulting from this operation (e.g. Allocation).
+
+        Returns
+        -------
+        target : TypedAstNode
+            The semantic iterator that takes the value of the elements of the iterable.
+        iterable : TypedAstNode
+            The semantic iterable that the for loop iterates over.
+        """
+        iterable = self._get_iterable(syntactic_iterable)
+
+        if iterable.num_loop_counters_required:
+            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:
+            if isinstance(iterable, PythonEnumerate):
+                if isinstance(iterator, PythonTuple):
+                    syntactic_index = iterator[0]
+                else:
+                    pyccel_stage.set_stage('syntactic')
+                    syntactic_index = IndexedElement(iterator,0)
+                    pyccel_stage.set_stage('semantic')
+            else:
+                syntactic_index = iterator
+
+            index = self.check_for_variable(syntactic_index)
+            if index is None:
+                start = LiteralInteger(0)
+                d_var = self._infer_type(start)
+                if isinstance(syntactic_index, PyccelSymbol):
+                    index = self._assign_lhs_variable(syntactic_index, d_var,
+                                    rhs=start, new_expressions=new_expr)
+                else:
+                    index = self.scope.get_temporary_variable(PythonNativeInt())
+            iterable.set_loop_counter(index)
+
+        # Collect a target with a deducible dtype
+        iterator_rhs = iterable.get_python_iterable_item()
+
+        # Use _visit_Assign to create the requested iterator with the correct type
+        # The result of this operation is not stored, it is just used to declare
+        # iterator with the correct dtype to allow correct dtype deductions later
+        if isinstance(iterator, PyccelSymbol):
+            if len(iterator_rhs) != 1:
+                iterator_rhs = PythonTuple(*iterator_rhs, prefer_inhomogeneous=True)
+            else:
+                iterator_rhs = iterator_rhs[0]
+
+            iterator_d_var = self._infer_type(iterator_rhs)
+
+            target = self._assign_lhs_variable(iterator, iterator_d_var,
+                            rhs=iterator_rhs, new_expressions=new_expr)
+            if isinstance(target.class_type, InhomogeneousTupleType):
+                target = [self.scope.collect_tuple_element(v) for v in target]
+            else:
+                target = [target]
+
+        elif isinstance(iterator, PythonTuple):
+            target = [self._assign_lhs_variable(it, self._infer_type(rhs),
+                                rhs=rhs, new_expressions=new_expr)
+                        for it, rhs in zip(iterator, iterator_rhs)]
+        else:
+            raise errors.report(INVALID_FOR_ITERABLE, symbol=iterator,
+                   bounding_box=(self.current_ast_node.lineno, self.current_ast_node.col_offset),
+                   severity='error')
+
+        return target, iterable
+
     #====================================================
     #                 _visit functions
     #====================================================
@@ -3487,65 +3591,26 @@ class SemanticParser(BasicParser):
 
         scope = self.create_new_loop_scope()
 
-        # treatment of the index/indices
-        iterable = Iterable(self._visit(expr.iterable))
-
         new_expr = []
 
-        start = LiteralInteger(0)
-        iterator_d_var = self._infer_type(start)
-
-        iterator = expr.target
-
-        if iterable.num_loop_counters_required:
-            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:
-            if isinstance(iterable.iterable, PythonEnumerate):
-                syntactic_index = iterator[0]
-            else:
-                syntactic_index = iterator
-
-            index = self.check_for_variable(syntactic_index)
-            if index is None:
-                index = self._assign_lhs_variable(syntactic_index, iterator_d_var,
-                                rhs=start, new_expressions=new_expr)
-            iterable.set_loop_counter(index)
-
-        if isinstance(iterator, PyccelSymbol):
-            iterator_rhs = iterable.get_target_from_range()
-            iterator_d_var = self._infer_type(iterator_rhs)
-
-            target = self._assign_lhs_variable(iterator, iterator_d_var,
-                            rhs=iterator_rhs, new_expressions=new_expr)
-
-        elif isinstance(iterator, PythonTuple):
-            iterator_rhs = iterable.get_target_from_range()
-            target = [self._assign_lhs_variable(it, self._infer_type(rhs),
-                                rhs=rhs, new_expressions=new_expr)
-                        for it, rhs in zip(iterator, iterator_rhs)]
-        else:
-
-            errors.report(INVALID_FOR_ITERABLE, symbol=expr.target,
-                   bounding_box=(self.current_ast_node.lineno, self.current_ast_node.col_offset),
-                   severity='error')
+        # treatment of the index/indices
+        target, iterable = self._get_for_iterators(expr.iterable, expr.target, new_expr)
 
         body = self._visit(expr.body)
 
         self.exit_loop_scope()
 
-        if isinstance(iterable.iterable, Product):
+        if isinstance(iterable, Product):
             for_expr = body
             scopes = self.scope.create_product_loop_scope(scope, len(target))
 
-            for t, i, r, s in zip(target[::-1], iterable.loop_counters[::-1], iterable.get_target_from_range()[::-1], scopes[::-1]):
+            for t, i, r, s in zip(target[::-1], iterable.loop_counters[::-1], iterable.get_python_iterable_item()[::-1], scopes[::-1]):
                 # Create Variable iterable
-                loop_iter = Iterable(r.base)
+                loop_iter = VariableIterator(r.base)
                 loop_iter.set_loop_counter(i)
 
                 # Create a For loop for each level of the Product
-                for_expr = For(t, loop_iter, for_expr, scope=s)
+                for_expr = For((t,), loop_iter, for_expr, scope=s)
                 for_expr.end_annotation = expr.end_annotation
                 for_expr = [for_expr]
             for_expr = for_expr[0]
@@ -3742,8 +3807,8 @@ class SemanticParser(BasicParser):
             assert isinstance(l, For)
             # Sub in indices as defined here for coherent naming
             if idx.is_temp:
-                self.scope.remove_variable(l.target)
-                l.substitute(l.target, idx_subs[idx])
+                self.scope.remove_variable(l.target[0])
+                l.substitute(l.target[0], idx_subs[idx])
             l = l.body.body[-1]
 
         #self.exit_loop_scope()


Test patch:
diff --git a/tests/epyccel/modules/loops.py b/tests/epyccel/modules/loops.py
index 6ee2394f..7de5c370 100644
--- a/tests/epyccel/modules/loops.py
+++ b/tests/epyccel/modules/loops.py
@@ -129,6 +129,15 @@ def enumerate_on_1d_array_with_start(z : 'int[:]', k : 'int'):
 
     return res
 
+# ...
+def enumerate_on_1d_array_with_tuple(z : 'int[:]'):
+
+    res = 0
+    for v in enumerate( z ):
+        res += v[0]*v[1]
+
+    return res
+
 # ...
 def zip_prod(m : int):
 
@@ -254,4 +263,12 @@ def less_than_100(n : int):
             return True
     return False
 
+def for_expression():
+    import numpy as np
+    c = np.array([1,2,3,4,5])
+    d = np.array([2.0,3.0,1.0,9.0,11.0])
+    sum_e = 0.0
+    for ei in c+d:
+        sum_e = ei
 
+    return sum_e
diff --git a/tests/epyccel/test_epyccel_dicts.py b/tests/epyccel/test_epyccel_dicts.py
index 5f9d860a..36095a5b 100644
--- a/tests/epyccel/test_epyccel_dicts.py
+++ b/tests/epyccel/test_epyccel_dicts.py
@@ -239,4 +239,22 @@ def test_dict_copy_method(python_only_language):
     pyccel_result = epyc_dict_copy()
     python_result = dict_copy()
     assert python_result == pyccel_result
-    
\ No newline at end of file
+
+def test_dict_items(language):
+    def dict_items():
+        a = {1:1.0, 2:2.0, 3:3.0, 5:4.7}
+        key_sum = 0
+        val_sum = 0.0
+        for key, val in a.items():
+            key_sum += key
+            val_sum += val
+
+        return key_sum, val_sum
+
+    epyc_dict_items = epyccel(dict_items, language = language)
+    pyccel_result = epyc_dict_items()
+    python_result = dict_items()
+    assert python_result == pyccel_result
+    assert isinstance(python_result[0], type(pyccel_result[0]))
+    assert isinstance(python_result[1], type(pyccel_result[1]))
+
diff --git a/tests/epyccel/test_loops.py b/tests/epyccel/test_loops.py
index 3b24578f..c97d309c 100644
--- a/tests/epyccel/test_loops.py
+++ b/tests/epyccel/test_loops.py
@@ -150,6 +150,15 @@ def test_enumerate_on_1d_array_with_start(language):
     assert np.array_equal( f1(z, 5), f2(z, 5) )
     assert np.array_equal( f1(z,-2), f2(z,-2) )
 
+def test_enumerate_on_1d_array_with_tuple(language):
+
+    f1 = loops.enumerate_on_1d_array_with_tuple
+    f2 = epyccel(f1, language=language)
+
+    z = np.arange( 7 )
+
+    assert np.array_equal( f1(z), f2(z) )
+
 def test_zip_prod(language):
 
     f1 = loops.zip_prod
@@ -237,14 +246,8 @@ def test_less_than_100(language):
     assert f1(10) == f2(10)
     assert f1(101) == f2(101)
 
-##==============================================================================
-## CLEAN UP GENERATED FILES AFTER RUNNING TESTS
-##==============================================================================
-#
-#def teardown_module():
-#    import os, glob
-#    dirname  = os.path.dirname( loops.__file__ )
-#    pattern  = os.path.join( dirname, '__epyccel__*' )
-#    filelist = glob.glob( pattern )
-#    for f in filelist:
-#        os.remove( f )
+def test_for_expression(language):
+    f1 = loops.for_expression
+    f2 = epyccel( f1, language = language )
+
+    assert f1() == f2()