Repository: pyccel/pyccel (language: python)

Issue:
Create module to handle naming
We should have a module to handle names in pyccel. Currently there are multiple places where things are (or aren’t but should be) done to avoid name problems in fortran. It would be clearer if this was managed in one place. Possible problems include

- Case sensitivity:
    python is case sensitive but fortran isn't.
    x and X should be translated differently in fortran. What name should be used?
- Underscores:
    python names can start with underscores or even be underscores but fortran names can't.
    How should these variables be renamed?
- Name collisions:
    Sometimes names are generated with a random string to avoid name collisions.
    Name collisions could occur in different circumstances:
      - multiple functions epyccelized which have the same name
      - epyccelizing modules with the same name (ie. import a and import b.a)
      - renaming variables to manage "import module" syntax
- Fortran keywords
    Check that the names used in python are not fortran or c keywords
- Python keywords
    Builtin functions can be overridden in python but this is inadvisable. Therefore pyccel does not handle this information. An error should be raised if the user does this
- Lengthy names
    Names are limited to 96 characters in fortran. An error should be raised if the user has longer names
Clean up Scope
The class `Scope` contains the following objects:
- functions
- variables
- classes
- imports
- symbolic_functions
- macros
- templates
- headers
- decorators
- static_functions
- cls_construct

The class is not well documented it is important to add documentation especially for things that aren't obvious such as:
- symbolic_functions
- static_functions

(@saidctb can you say what these are?)

The scope is accessed via functions in the `SemanticParser`. But it is used in both the `SemanticParser` and the `FCodePrinter`. These functions should be consolidated into one place. This should also mean that it is no longer necessary to pass a  parser to the printer
Nested functional generators do not always work as expected
**Describe the bug**
Nested functional generators do not always work as expected

**To Reproduce**
Provide code to reproduce the behaviour:
```python
from numpy import ones

a=ones((5,5),float)

a8 = min(max(a[i,k] for i in range(5)) for k in range(5))
```

Provide the generated code, or the error message:
```bash/fortran/c
pyccel:
 |fatal [semantic]: tmp3.py| Cannot determine final dtype of 'max' call with arguments of different types (float(8), float(8)). Please cast arguments to the desired dtype (max(FunctionCallArgument('Dummy_0001'), FunctionCallArgument('a'['i','k'])))
```



Discussion / hints:
Related : issue #225 
If functions are renamed to avoid case sensitivity name collisions then the correct name can be used in a c wrapper if this is used instead of f2py (see issue #370)
> If functions are renamed to avoid case sensitivity name collisions then the correct name can be used in a c wrapper if this is used instead of f2py (see issue #370)

Do you mean that after moving away from f2py we could print to Fortran the original Python names, which may contain capital letters, whenever we don't have name clashes?
> Do you mean that after moving away from f2py we could print to Fortran the original Python names, which may contain capital letters, whenever we don't have name clashes?
No. We can already do that. Let me provide an example. The following python code:
```
def littleTest():
    return 2
```
is translated to:
```
module test

implicit none

contains

!........................................
function littleTest() result(Out_0001)

  implicit none

  integer(kind=8) :: Out_0001

  Out_0001 = 2_8
  return

end function littleTest
!........................................

end module test
```
However as fortran is not case sensitive to call this function in C, you have to call `littletest()` in c. If we write our own wrapper this would be written:
```
static PyObject * littleTest_wrapper(PyObject *self, PyObject *args)
{

    long Out_0001;
    if (!PyArg_ParseTuple(args, "" ))
        return NULL;
    Out_0001 = littletest(); // call to fortran function
    return Py_BuildValue("l", Out_0001);
}

static PyMethodDef test_methods[] = {
    { "littleTest", littleTest_wrapper, METH_VARARGS, "" }, // name exposed to python
    { NULL, NULL, 0, NULL }
};
```
and as C is case sensitive we can then call test.littleTest from python. f2py however uses the same name for the call in the wrapper and for the name exposed to python. As a result currently when we translate the example we must convert the name to lower case when we want to use it.
```
In [1]: import test

In [2]: dir(test)
Out[2]: 
['__doc__',
 '__file__',
 '__loader__',
 '__name__',
 '__package__',
 '__spec__',
 '__version__',
 '_test_error',
 'littletest']
```



Fix patch:
diff --git a/developer_docs/overview.md b/developer_docs/overview.md
index 748516a838..1b8a7f171e 100644
--- a/developer_docs/overview.md
+++ b/developer_docs/overview.md
@@ -20,15 +20,17 @@ The role of this stage has decreased significantly since we moved from [redbaron
 
 This is the most important stage in pyccel. It is here that all the information about types is calculated. This stage strives to be **language-agnostic**; this means for example, that additional variables required to handle problems appearing in one specific language should not be created here.
 
-When adding functions to this stage the aim is often to create a `PyccelAstNode` (see [ast/basic.py](../pyccel/ast/basic.py)) and correctly define all of its parameters. This information is sometimes readily available (e.g. the type of a `PyccelAdd` can be derived from the type of the variables passed to it), but sometimes the information must be collected from elsewhere (e.g. when creating a `Variable` from a `PyccelSymbol` (roughly equivalent to a string). In this case information is needed from a `Scope` instance which is stored in the `namespace`.
+When adding functions to this stage the aim is often to create a `PyccelAstNode` (see [ast/basic.py](../pyccel/ast/basic.py)) and correctly define all of its parameters. This information is sometimes readily available (e.g. the type of a `PyccelAdd` can be derived from the type of the variables passed to it), but sometimes the information must be collected from elsewhere (e.g. when creating a `Variable` from a `PyccelSymbol` (roughly equivalent to a string). In this case information is needed from a `Scope` instance which is stored in the `scope`.
 
 In computer science, the _scope_ is the area of a program where an item (e.g. variable, function, etc.) is recognised. For example a variable defined in a function will not be recognised outside of that function, therefore the function defines its scope.
 
-In Pyccel, a `Scope` is an object defined in [parser/base.py](../pyccel/parser/base.py) which represents this concept. It includes all the functions, imports, variables, and classes which are available at a given point in the code. It also contains pointers to nested and parent scopes. The `namespace` in the `SemanticParser` (`SemanticParser._namespace`) stores the Scope relevant to the line of code being treated. It must be updated whenever the scope changes (e.g. through the `create_new_function_scope` function when entering into a function body).
+In Pyccel, a `Scope` is an object defined in [parser/base.py](../pyccel/parser/base.py) which represents this concept. It includes all the functions, imports, variables, and classes which are available at a given point in the code. It also contains pointers to nested and parent scopes. The `scope` in the `SemanticParser` (`SemanticParser._scope`) stores the Scope relevant to the line of code being treated. It must be updated whenever the scope changes (e.g. through the `create_new_function_scope` function when entering into a function body). The Scope is also used to avoid naming collisions. See [Scope documentation](./scope.md) for details.
 
 ### Code Generation Stage
 
-In this stage the Pyccel nodes are converted into a string which contains the translation into the requested language. Each language has its own printer. The printers are found in the folder [codegen/printing](../pyccel/codegen/printing)
+In this stage the Pyccel nodes are converted into a string which contains the translation into the requested language. Each language has its own printer. The printers are found in the folder [codegen/printing](../pyccel/codegen/printing).
+
+As in the Semantic stage, the Code Generation stage also stores the current Scope in a `scope` variable which must be updated whenever the scope changes.
 
 ### Compilation Stage
 
diff --git a/developer_docs/scope.md b/developer_docs/scope.md
new file mode 100644
index 0000000000..23d2031ab8
--- /dev/null
+++ b/developer_docs/scope.md
@@ -0,0 +1,26 @@
+## Scope
+
+In computer science, the _scope_ is the area of a program where an item (e.g. variable, function, etc.) is recognised. For example a variable defined in a function will not be recognised outside of that function, therefore the function defines its scope.
+
+In Pyccel, a `Scope` is an object defined in [parser/scope.py](../pyccel/parser/scope.py) which represents this concept. It includes all the functions, imports, variables, and classes which are available at a given point in the code. It also contains pointers to nested and parent scopes.
+
+Each of these objects must be inserted into the scope using and insert function.
+
+## ScopedNode
+
+Each scope is associated with a class, e.g. `FunctionDef`, `For`, `Module`. These classes inherit from the `ScopedNode` class. The scope associated with the class instance, is saved within the class. This makes the scope available when the class instance is available. This is important so as to correctly set the `scope` variable in the `SemanticParser` and the different `CodePrinter`s.
+
+## Name collisions
+
+The `Scope` object keeps track of all names used in the described scope. This means that it can be used to prevent name collisions. In order to do so a few steps must be respected:
+
+1.  In the syntactic stage all symbols in the code must be added to the correct scope. This is done via the function `insert_symbol`
+
+2.  All names of variables created by pyccel must be created using one of the following functions defined in the Scope class:
+    -   `get_expected_name` : Collect the name which will be used to create the Variable referenced in the argument. In most cases this operation will be the identity operation, but it ensures that name collisions are handled and that the Symbol has been correctly inserted into the Scope
+    -   `get_new_name` : Get a new name with no collisions. A name can be requested and will be used if available
+    -   `get_new_incremented_symbol` : Get a new name with no collisions following a pattern. This function keeps track of the index appended to the incremented string so it is most useful when creating multiple names with the same prefix
+
+3.  All newly created variables must be inserted into the scope using the `insert_variable` function.
+
+Temporary variables can also be created using the `get_temporary_variable` function. In this case it is not necessary to use the `insert_variable` function.
diff --git a/pyccel/ast/bind_c.py b/pyccel/ast/bind_c.py
index 56e918bb7c..9814cbe4af 100644
--- a/pyccel/ast/bind_c.py
+++ b/pyccel/ast/bind_c.py
@@ -11,6 +11,7 @@
 from pyccel.ast.core import Import
 from pyccel.ast.core import AsName
 from pyccel.ast.variable import Variable
+from pyccel.parser.scope import Scope
 
 __all__ = (
     'as_static_module',
@@ -32,7 +33,20 @@ def sanitize_arguments(args):
     return _args
 
 #=======================================================================================
-def as_static_function(func, name=None):
+def as_static_function(func, *, mod_scope, name=None):
+    """ Translate a FunctionDef to a BindCFunctionDef by altering the
+    arguments to allow the function to be called from c.
+    E.g. the size of each dimension of an array is provided
+
+    Parameters
+    ==========
+    func     : FunctionDef
+               The function to be translated
+    mod_scope: Scope
+               The scope of the module which contains func
+    name     : str
+               The new name of the function
+    """
 
     assert(isinstance(func, FunctionDef))
 
@@ -44,6 +58,8 @@ def as_static_function(func, name=None):
     _results = []
     interfaces = func.interfaces
 
+    scope = mod_scope.new_child_scope(func.name)
+
     # Convert array results to inout arguments
     for r in results:
         if r.rank > 0 and r not in args:
@@ -105,7 +121,6 @@ def as_static_function(func, name=None):
     arguments_inout = _arguments_inout
     # ...
     return BindCFunctionDef( name, list(args), results, body,
-                        local_vars = func.local_vars,
                         is_static = True,
                         arguments_inout = arguments_inout,
                         functions = functions,
@@ -113,6 +128,7 @@ def as_static_function(func, name=None):
                         imports = func.imports,
                         original_function = func,
                         doc_string = func.doc_string,
+                        scope = scope
                         )
 
 #=======================================================================================
@@ -132,13 +148,14 @@ def as_static_module(funcs, original_module, name = None):
     """
     funcs = [f for f in funcs if not f.is_private]
     imports = []
-    bind_c_funcs = [as_static_function_call(f, original_module, imports = imports) for f in funcs]
+    scope = Scope()
+    bind_c_funcs = [as_static_function_call(f, original_module, scope, imports = imports) for f in funcs]
     if name is None:
         name = 'bind_c_{}'.format(original_module)
-    return Module(name, (), bind_c_funcs, imports = imports)
+    return Module(name, (), bind_c_funcs, imports = imports, scope=scope)
 
 #=======================================================================================
-def as_static_function_call(func, mod_name, name=None, imports = None):
+def as_static_function_call(func, mod_name, mod_scope, name=None, imports = None):
     """ Translate a FunctionDef to a BindCFunctionDef which calls the
     original function. A BindCFunctionDef is a FunctionDef where the
     arguments are altered to allow the function to be called from c.
@@ -182,10 +199,10 @@ def as_static_function_call(func, mod_name, name=None, imports = None):
                        functions = func.functions,
                        interfaces = func.interfaces,
                        imports = local_imports,
-                       doc_string = func.doc_string,
+                       doc_string = func.doc_string
                        )
 
     # make it compatible with c
-    static_func = as_static_function(new_func, name)
+    static_func = as_static_function(new_func, name=name, mod_scope=mod_scope)
 
     return static_func
diff --git a/pyccel/ast/core.py b/pyccel/ast/core.py
index 5e3d73d072..053ecd43cf 100644
--- a/pyccel/ast/core.py
+++ b/pyccel/ast/core.py
@@ -8,10 +8,11 @@
 from sympy.logic.boolalg      import And as sp_And
 
 
-from pyccel.errors.errors import Errors
-from pyccel.errors.messages import RECURSIVE_RESULTS_REQUIRED
+from pyccel.errors.errors     import Errors
+from pyccel.errors.messages   import RECURSIVE_RESULTS_REQUIRED
 
-from pyccel.utilities.stage import PyccelStage
+from pyccel.utilities.stage   import PyccelStage
+from pyccel.utilities.strings import create_incremented_string
 
 from .basic     import Basic, PyccelAstNode, iterable, ScopedNode
 from .builtins  import (PythonEnumerate, PythonLen, PythonMap, PythonTuple,
@@ -161,49 +162,6 @@ def inline(func, args):
     body = subs(body, zip(func.arguments, args))
     return Block(str(func.name), local_vars, body)
 
-def create_incremented_string(forbidden_exprs, prefix = 'Dummy', counter = 1):
-    """This function takes a prefix and a counter and uses them to construct
-    a new name of the form:
-            prefix_counter
-    Where counter is formatted to fill 4 characters
-    The new name is checked against a list of forbidden expressions. If the
-    constructed name is forbidden then the counter is incremented until a valid
-    name is found
-
-      Parameters
-      ----------
-      forbidden_exprs : Set
-                        A set of all the values which are not valid solutions to this problem
-      prefix          : str
-                        The prefix used to begin the string
-      counter         : int
-                        The expected value of the next name
-
-      Returns
-      ----------
-      name            : str
-                        The incremented string name
-      counter         : int
-                        The expected value of the next name
-
-    """
-    assert(isinstance(forbidden_exprs, set))
-    nDigits = 4
-
-    if prefix is None:
-        prefix = 'Dummy'
-
-    name_format = "{prefix}_{counter:0="+str(nDigits)+"d}"
-    name = name_format.format(prefix=prefix, counter = counter)
-    counter += 1
-    while name in forbidden_exprs:
-        name = name_format.format(prefix=prefix, counter = counter)
-        counter += 1
-
-    forbidden_exprs.add(name)
-
-    return name, counter
-
 def create_variable(forbidden_names, prefix = None, counter = 1):
     """This function takes a prefix and a counter and uses them to construct
     a PyccelSymbol with a name of the form:
@@ -932,8 +890,6 @@ class While(ScopedNode):
         test condition given as an expression
     body : list of Pyccel objects
         list of statements representing the body of the While statement.
-    local_vars : list of Variables
-        list of Variables created inside the While
     scope : Scope
         The scope for the loop
 
@@ -945,10 +901,10 @@ class While(ScopedNode):
     >>> While((n>1), [Assign(n,n-1)])
     While(n > 1, (n := n - 1,))
     """
-    __slots__ = ('_body','_test','_local_vars')
-    _attribute_nodes = ('_body','_test','_local_vars')
+    __slots__ = ('_body','_test')
+    _attribute_nodes = ('_body','_test')
 
-    def __init__(self, test, body, local_vars=(), scope = None):
+    def __init__(self, test, body, scope = None):
 
         if pyccel_stage == 'semantic':
             if test.dtype is not NativeBool():
@@ -961,7 +917,6 @@ def __init__(self, test, body, local_vars=(), scope = None):
 
         self._test = test
         self._body = body
-        self._local_vars = local_vars
         super().__init__(scope)
 
     @property
@@ -974,7 +929,8 @@ def body(self):
 
     @property
     def local_vars(self):
-        return self._local_vars
+        """ List of variables defined in the loop """
+        return tuple(self.scope.variables.values())
 
 
 class With(ScopedNode):
@@ -1005,6 +961,7 @@ def __init__(
         self,
         test,
         body,
+        scope = None
         ):
 
         if iterable(body):
@@ -1014,7 +971,7 @@ def __init__(
 
         self._test = test
         self._body = body
-        super().__init__()
+        super().__init__(scope)
 
     @property
     def test(self):
@@ -1040,7 +997,7 @@ def block(self):
         body = start.body.body
         body += self.body.body
         body +=  end.body.body
-        return Block('with', [], body)
+        return Block('with', [], body, scope=self.scope)
 
 
 # TODO add a name to a block?
@@ -1075,7 +1032,8 @@ def __init__(
         self,
         name,
         variables,
-        body):
+        body,
+        scope = None):
         if not isinstance(name, str):
             raise TypeError('name must be of type str')
         if not iterable(variables):
@@ -1090,7 +1048,7 @@ def __init__(
         self._name = name
         self._variables = variables
         self._body = body
-        super().__init__()
+        super().__init__(scope)
 
     @property
     def name(self):
@@ -1422,9 +1380,6 @@ class Program(ScopedNode):
     variables: list
         list of the variables that appear in the block.
 
-    declarations: list
-        list of declarations of the variables that appear in the block.
-
     body: list
         a list of statements
 
@@ -1494,12 +1449,6 @@ def imports(self):
         """
         return self._imports
 
-    @property
-    def declarations(self):
-        """ Returns the declarations of the variables
-        """
-        return [Declare(i.dtype, i) for i in self.variables]
-
     def remove_import(self, name):
         """ Remove an import with the given source name from the list
         of imports
@@ -1664,15 +1613,14 @@ class For(ScopedNode):
     >>> For(i, (b,e,s), [Assign(x, i), Assign(A[0, 1], x)])
     For(i, (b, e, s), (x := i, IndexedElement(A, 0, 1) := x))
     """
-    __slots__ = ('_target','_iterable','_body','_local_vars','_end_annotation')
-    _attribute_nodes = ('_target','_iterable','_body','_local_vars')
+    __slots__ = ('_target','_iterable','_body','_end_annotation')
+    _attribute_nodes = ('_target','_iterable','_body')
 
     def __init__(
         self,
         target,
         iter_obj,
         body,
-        local_vars = (),
         scope = None
         ):
         if pyccel_stage != "syntactic":
@@ -1689,7 +1637,6 @@ def __init__(
         self._target = target
         self._iterable = iter_obj
         self._body = body
-        self._local_vars = local_vars
         self._end_annotation = None
         super().__init__(scope)
 
@@ -1715,7 +1662,8 @@ def body(self):
 
     @property
     def local_vars(self):
-        return self._local_vars
+        """ List of variables defined in the loop """
+        return tuple(self.scope.variables.values())
 
     def insert2body(self, stmt):
         stmt.set_current_user_node(self)
@@ -2198,9 +2146,6 @@ class FunctionDef(ScopedNode):
     body : iterable
         The body of the function.
 
-    local_vars : list of Symbols
-        These are used internally by the routine.
-
     global_vars : list of Symbols
         Variables which will not be passed into the function.
 
@@ -2275,12 +2220,12 @@ class FunctionDef(ScopedNode):
     >>> FunctionDef('incr', args, results, body)
     FunctionDef(incr, (x, n=4), (y,), [y := 1 + x], [], [], None, False, function, [])
     """
-    __slots__ = ('_name','_arguments','_results','_body','_local_vars',
+    __slots__ = ('_name','_arguments','_results','_body',
                  '_global_vars','_cls_name','_is_static','_imports',
                  '_decorators','_headers','_is_recursive','_is_pure',
                  '_is_elemental','_is_private','_is_header','_arguments_inout',
                  '_functions','_interfaces','_doc_string', '_is_external')
-    _attribute_nodes = ('_arguments','_results','_body','_local_vars',
+    _attribute_nodes = ('_arguments','_results','_body',
                  '_global_vars','_imports','_functions','_interfaces')
 
     def __init__(
@@ -2289,7 +2234,6 @@ def __init__(
         arguments,
         results,
         body,
-        local_vars=(),
         global_vars=(),
         cls_name=None,
         is_static=False,
@@ -2399,7 +2343,6 @@ def __init__(
         self._arguments       = arguments
         self._results         = results
         self._body            = body
-        self._local_vars      = local_vars
         self._global_vars     = global_vars
         self._cls_name        = cls_name
         self._is_static       = is_static
@@ -2441,7 +2384,9 @@ def body(self):
     @property
     def local_vars(self):
         """ List of variables defined in the function """
-        return self._local_vars
+        local_vars = self.scope.variables.values()
+        argument_vars = [a.var for a in self.arguments]
+        return tuple(l for l in local_vars if l not in self.results and l not in argument_vars)
 
     @property
     def global_vars(self):
@@ -2590,18 +2535,6 @@ def rename(self, newname):
 
         self._name = newname
 
-    def add_local_vars(self, *variables):
-        """
-        Add (a) new variable(s) to the local variables tuple
-
-        Parameters
-        ----------
-        var : Variable
-              The new local variable
-        """
-        _ = [v.set_current_user_node(self) for v in variables]
-        self._local_vars += variables
-
     def __getnewargs__(self):
         """
           This method returns the positional and keyword arguments
@@ -2614,7 +2547,6 @@ def __getnewargs__(self):
         self._body)
 
         kwargs = {
-        'local_vars':self._local_vars,
         'global_vars':self._global_vars,
         'cls_name':self._cls_name,
         'is_static':self._is_static,
@@ -3081,6 +3013,7 @@ def __init__(
         imports=(),
         superclass=(),
         interfaces=(),
+        scope = None
         ):
 
         # name
@@ -3161,7 +3094,7 @@ def __init__(
         self._superclass  = superclass
         self._interfaces = interfaces
 
-        super().__init__()
+        super().__init__(scope = scope)
 
     @property
     def name(self):
@@ -3294,6 +3227,10 @@ class Import(Basic):
         the module from which we import
     target : str, AsName, list, tuple
         targets to import
+    ignore_at_print : bool
+        indicates whether the import should be printed
+    mod : Module
+        The module describing the source
 
     Examples
     --------
@@ -3309,16 +3246,17 @@ class Import(Basic):
     >>> Import('foo', 'bar')
     from foo import bar
     """
-    __slots__ = ('_source','_target','_ignore_at_print')
+    __slots__ = ('_source','_target','_ignore_at_print','_source_mod')
     _attribute_nodes = ()
 
-    def __init__(self, source, target = None, ignore_at_print = False):
+    def __init__(self, source, target = None, ignore_at_print = False, mod = None):
 
         if not source is None:
             source = Import._format(source)
 
         self._source = source
         self._target = set()
+        self._source_mod      = mod
         self._ignore_at_print = ignore_at_print
         if target is None:
             if pyccel_stage == "syntactic":
@@ -3406,6 +3344,12 @@ def find_module_target(self, new_target):
                 return t
         return None
 
+    @property
+    def source_module(self):
+        """ The module describing the Import source
+        """
+        return self._source_mod
+
 
 # TODO: Should Declare have an optional init value for each var?
 
diff --git a/pyccel/ast/headers.py b/pyccel/ast/headers.py
index a6798cc06c..c5b9ca4726 100644
--- a/pyccel/ast/headers.py
+++ b/pyccel/ast/headers.py
@@ -4,12 +4,12 @@
 # go to https://github.com/pyccel/pyccel/blob/master/LICENSE for full license details.     #
 #------------------------------------------------------------------------------------------#
 
+from pyccel.utilities.strings import create_incremented_string
 from ..errors.errors    import Errors
 from ..errors.messages  import TEMPLATE_IN_UNIONTYPE
 from .basic             import Basic, iterable
 from .core              import Assign, FunctionCallArgument
 from .core              import FunctionDef, FunctionCall, FunctionAddress
-from .core              import create_incremented_string
 from .datatypes         import datatype, DataTypeFactory, UnionType
 from .internals         import PyccelSymbol, Slice
 from .macros            import Macro, MacroShape, construct_macro
@@ -372,7 +372,6 @@ def find_templates(signature, templates):
                 d_var['is_func'] = is_func
 
             func= FunctionDef(name, args, results, body,
-                              local_vars=[],
                               global_vars=[],
                               cls_name=cls_name,
                               is_static=is_static,
diff --git a/pyccel/ast/sympy_helper.py b/pyccel/ast/sympy_helper.py
index 66d1dab951..6b13bf6ef2 100644
--- a/pyccel/ast/sympy_helper.py
+++ b/pyccel/ast/sympy_helper.py
@@ -11,9 +11,11 @@
 import sympy as sp
 from sympy.core.numbers import One, NegativeOne, Zero, Half
 
+from pyccel.utilities.strings import create_incremented_string
+
 from .operators import PyccelAdd, PyccelMul, PyccelPow, PyccelUnarySub
 from .operators import PyccelDiv, PyccelMinus, PyccelAssociativeParenthesis
-from .core      import create_incremented_string, Iterable
+from .core      import Iterable
 
 from .builtins  import PythonRange, PythonTuple
 
diff --git a/pyccel/ast/utilities.py b/pyccel/ast/utilities.py
index 283374d464..55c6ad504a 100644
--- a/pyccel/ast/utilities.py
+++ b/pyccel/ast/utilities.py
@@ -315,7 +315,7 @@ def insert_index(expr, pos, index_var):
 LoopCollection = namedtuple('LoopCollection', ['body', 'length', 'modified_vars'])
 
 #==============================================================================
-def collect_loops(block, indices, new_index_name, tmp_vars, language_has_vectors = False, result = None):
+def collect_loops(block, indices, new_index, language_has_vectors = False, result = None):
     """
     Run through a code block and split it into lists of tuples of lists where
     each inner list represents a code block and the tuples contain the lists
@@ -338,11 +338,9 @@ def collect_loops(block, indices, new_index_name, tmp_vars, language_has_vectors
                             The expressions to be modified
     indices               : list
                             An empty list to be filled with the temporary variables created
-    new_index_name        : function
-                            A function which provides a new variable name from a base name,
-                            avoiding name collisions
-    tmp_vars              : list
-                            A list to which any temporary variables created can be appended
+    new_index             : function (class method of a Scope)
+                            A function which provides a new variable from a base name,
+                            avoiding name collisions.
     language_has_vectors  : bool
                             Indicates if the language has support for vector
                             operations of the same shape
@@ -431,7 +429,7 @@ def collect_loops(block, indices, new_index_name, tmp_vars, language_has_vectors
             # This ensures that the function is only called once and stops problems
             # for expressions such as:
             # c += b*np.sum(c)
-            func_vars1 = [Variable(f.dtype, new_index_name('tmp')) for f in internal_funcs]
+            func_vars1 = [new_index(f.dtype, 'tmp') for f in internal_funcs]
             _          = [v.copy_attributes(f) for v,f in zip(func_vars1, internal_funcs)]
             assigns    = [Assign(v, f) for v,f in zip(func_vars1, internal_funcs)]
 
@@ -441,7 +439,8 @@ def collect_loops(block, indices, new_index_name, tmp_vars, language_has_vectors
                         "which return tuples or None",
                         symbol=line, severity='fatal')
 
-            func_vars2 = [f.funcdef.results[0].clone(new_index_name('tmp')) for f in funcs]
+            func_results = [f.funcdef.results[0] for f in funcs]
+            func_vars2 = [new_index(r.dtype, r) for r in func_results]
             assigns   += [Assign(v, f) for v,f in zip(func_vars2, funcs)]
 
             if assigns:
@@ -453,8 +452,6 @@ def collect_loops(block, indices, new_index_name, tmp_vars, language_has_vectors
                             symbol=line, severity='fatal')
                 line.substitute(internal_funcs, func_vars1, excluded_nodes=(FunctionCall))
                 line.substitute(funcs, func_vars2)
-                tmp_vars.extend(func_vars1)
-                tmp_vars.extend(func_vars2)
                 result.extend(assigns)
                 current_level = 0
 
@@ -469,7 +466,7 @@ def collect_loops(block, indices, new_index_name, tmp_vars, language_has_vectors
                 new_level += 1
                 # If an index exists at the same depth, reuse it if not create one
                 if rank+index >= len(indices):
-                    indices.append(Variable('int',new_index_name('i')))
+                    indices.append(new_index('int','i'))
                 index_var = indices[rank+index]
                 new_vars = [insert_index(v, index, index_var) for v in new_vars]
                 new_vars_t = [insert_index(v, index, index_var) for v in new_vars_t]
@@ -525,7 +522,7 @@ def collect_loops(block, indices, new_index_name, tmp_vars, language_has_vectors
                                 index_var = LiteralInteger(j)),
                             rj) # lhs[j] = rhs[j]
                           for j, rj in enumerate(rhs)]
-            collect_loops(new_assigns, indices, new_index_name, tmp_vars, language_has_vectors, result = result)
+            collect_loops(new_assigns, indices, new_index, language_has_vectors, result = result)
 
         elif isinstance(line, Assign) and isinstance(line.rhs, Concatenate):
             lhs = line.lhs
@@ -533,7 +530,7 @@ def collect_loops(block, indices, new_index_name, tmp_vars, language_has_vectors
             arg1, arg2 = rhs.args
             assign1 = Assign(lhs[Slice(LiteralInteger(0), arg1.shape[0])], arg1)
             assign2 = Assign(lhs[Slice(arg1.shape[0], PyccelAdd(arg1.shape[0], arg2.shape[0], simplify=True))], arg2)
-            collect_loops([assign1, assign2], indices, new_index_name, tmp_vars, language_has_vectors, result = result)
+            collect_loops([assign1, assign2], indices, new_index, language_has_vectors, result = result)
 
         elif isinstance(line, Assign) and isinstance(line.rhs, Duplicate):
             lhs = line.lhs
@@ -541,7 +538,7 @@ def collect_loops(block, indices, new_index_name, tmp_vars, language_has_vectors
 
             if not isinstance(rhs.length, LiteralInteger):
                 if len(indices) == 0:
-                    indices.append(Variable('int',new_index_name('i')))
+                    indices.append(new_index('int', 'i'))
                 idx = indices[0]
 
                 assign = Assign(lhs[Slice(PyccelMul(rhs.val.shape[0], idx, simplify=True),
@@ -552,7 +549,7 @@ def collect_loops(block, indices, new_index_name, tmp_vars, language_has_vectors
 
                 tmp_indices = indices[1:]
 
-                block = collect_loops([assign], tmp_indices, new_index_name, tmp_vars, language_has_vectors)
+                block = collect_loops([assign], tmp_indices, new_index, language_has_vectors)
                 if len(tmp_indices)>len(indices)-1:
                     indices.extend(tmp_indices[len(indices)-1:])
 
@@ -564,7 +561,7 @@ def collect_loops(block, indices, new_index_name, tmp_vars, language_has_vectors
                                               PyccelAdd(LiteralInteger(idx), LiteralInteger(1), simplify=True),
                                               simplify=True))],
                                 rhs.val) for idx in range(rhs.length)]
-                collect_loops(assigns, indices, new_index_name, tmp_vars, language_has_vectors, result = result)
+                collect_loops(assigns, indices, new_index, language_has_vectors, result = result)
 
         else:
             # Save line in top level (no for loop)
@@ -575,7 +572,7 @@ def collect_loops(block, indices, new_index_name, tmp_vars, language_has_vectors
 
 #==============================================================================
 
-def insert_fors(blocks, indices, level = 0):
+def insert_fors(blocks, indices, scope, level = 0):
     """
     Run through the output of collect_loops and create For loops of the
     requested sizes
@@ -596,14 +593,20 @@ def insert_fors(blocks, indices, level = 0):
     if all(not isinstance(b, LoopCollection) for b in blocks.body):
         body = blocks.body
     else:
-        body = [insert_fors(b, indices, level+1) if isinstance(b, LoopCollection) else [b] \
+        loop_scope = scope.create_new_loop_scope()
+        body = [insert_fors(b, indices, loop_scope, level+1) if isinstance(b, LoopCollection) else [b] \
                 for b in blocks.body]
         body = [bi for b in body for bi in b]
+
     if blocks.length == 1:
         return body
     else:
         body = CodeBlock(body, unravelled = True)
-        return [For(indices[level], PythonRange(0,blocks.length), body)]
+        loop_scope = scope.create_new_loop_scope()
+        return [For(indices[level],
+                    PythonRange(0,blocks.length),
+                    body,
+                    scope = loop_scope)]
 
 #==============================================================================
 def expand_inhomog_tuple_assignments(block, language_has_vectors = False):
@@ -654,7 +657,7 @@ def expand_inhomog_tuple_assignments(block, language_has_vectors = False):
         expand_inhomog_tuple_assignments(block)
 
 #==============================================================================
-def expand_to_loops(block, new_index_name, language_has_vectors = False):
+def expand_to_loops(block, new_index, scope, language_has_vectors = False):
     """
     Re-write a list of expressions to include explicit loops where necessary
 
@@ -662,8 +665,8 @@ def expand_to_loops(block, new_index_name, language_has_vectors = False):
     ==========
     block          : CodeBlock
                      The expressions to be modified
-    new_index_name : function
-                     A function which provides a new variable name from a base name,
+    new_index      : function
+                     A function which provides a new variable from a base name,
                      avoiding name collisions
     language_has_vectors : bool
                      Indicates if the language has support for vector
@@ -691,10 +694,9 @@ def expand_to_loops(block, new_index_name, language_has_vectors = False):
     expand_inhomog_tuple_assignments(block)
 
     indices = []
-    tmp_vars = []
-    res = collect_loops(block.body, indices, new_index_name, tmp_vars, language_has_vectors)
+    res = collect_loops(block.body, indices, new_index, language_has_vectors)
 
-    body = [insert_fors(b, indices) if isinstance(b, tuple) else [b] for b in res]
+    body = [insert_fors(b, indices, scope) if isinstance(b, tuple) else [b] for b in res]
     body = [bi for b in body for bi in b]
 
-    return body, indices+tmp_vars
+    return body
diff --git a/pyccel/ast/variable.py b/pyccel/ast/variable.py
index 2d1b6461e8..4ee79d397c 100644
--- a/pyccel/ast/variable.py
+++ b/pyccel/ast/variable.py
@@ -524,6 +524,14 @@ def invalidate_node(self):
         # Don't invalidate Variables
         pass
 
+    @is_temp.setter
+    def is_temp(self, is_temp):
+        if not isinstance(is_temp, bool):
+            raise TypeError("is_temp must be a boolean")
+        elif is_temp:
+            raise ValueError("Variables cannot become temporary")
+        self._is_temp = is_temp
+
 class DottedName(Basic):
 
     """
diff --git a/pyccel/codegen/codegen.py b/pyccel/codegen/codegen.py
index 638dfc84ae..1a5568bf96 100644
--- a/pyccel/codegen/codegen.py
+++ b/pyccel/codegen/codegen.py
@@ -149,7 +149,7 @@ def set_printer(self, **settings):
         errors = Errors()
         errors.set_parser_stage('codegen')
         # set the code printer
-        self._printer = code_printer(self.parser, **settings)
+        self._printer = code_printer(self.parser.filename, **settings)
 
     def get_printer_imports(self):
         """return the imports of the current codeprinter"""
@@ -158,22 +158,22 @@ def get_printer_imports(self):
     def _collect_statements(self):
         """Collects statements and split them into routines, classes, etc."""
 
-        namespace  = self.parser.namespace
+        scope  = self.parser.scope
 
         funcs      = []
         interfaces = []
 
 
-        for i in namespace.functions.values():
+        for i in scope.functions.values():
             if isinstance(i, FunctionDef) and not i.is_header:
                 funcs.append(i)
             elif isinstance(i, Interface):
                 interfaces.append(i)
 
-        self._stmts['imports'   ] = list(namespace.imports['imports'].values())
-        self._stmts['variables' ] = list(self.parser.get_variables(namespace))
+        self._stmts['imports'   ] = list(scope.imports['imports'].values())
+        self._stmts['variables' ] = list(self.parser.get_variables(scope))
         self._stmts['routines'  ] = funcs
-        self._stmts['classes'   ] = list(namespace.classes.values())
+        self._stmts['classes'   ] = list(scope.classes.values())
         self._stmts['interfaces'] = interfaces
         self._stmts['body']       = self.ast
 
diff --git a/pyccel/codegen/printing/ccode.py b/pyccel/codegen/printing/ccode.py
index 727a87eb25..a6b9f151c3 100644
--- a/pyccel/codegen/printing/ccode.py
+++ b/pyccel/codegen/printing/ccode.py
@@ -6,8 +6,11 @@
 # pylint: disable=R0201
 # pylint: disable=missing-function-docstring
 import functools
+from itertools import chain
 import re
 
+from pyccel.ast.basic     import ScopedNode
+
 from pyccel.ast.builtins  import PythonRange, PythonComplex
 from pyccel.ast.builtins  import PythonPrint, PythonType
 from pyccel.ast.builtins  import PythonList, PythonTuple
@@ -19,7 +22,6 @@
 from pyccel.ast.core      import Assign, Import, AugAssign, AliasAssign
 from pyccel.ast.core      import SeparatorComment
 from pyccel.ast.core      import Module, AsName
-from pyccel.ast.core      import create_incremented_string
 
 from pyccel.ast.operators import PyccelAdd, PyccelMul, PyccelMinus, PyccelLt, PyccelGt
 from pyccel.ast.operators import PyccelAssociativeParenthesis, PyccelMod
@@ -243,17 +245,14 @@ class CCodePrinter(CodePrinter):
         'tabwidth': 4,
     }
 
-    def __init__(self, parser, prefix_module = None):
+    def __init__(self, filename, prefix_module = None):
 
-        if parser.filename:
-            errors.set_target(parser.filename, 'file')
+        errors.set_target(filename, 'file')
 
         super().__init__()
         self.prefix_module = prefix_module
         self._additional_imports = {'stdlib':c_imports['stdlib']}
-        self._parser = parser
         self._additional_code = ''
-        self._additional_declare = []
         self._additional_args = []
         self._temporary_args = []
         self._current_module = None
@@ -306,7 +305,7 @@ def copy_NumpyArray_Data(self, expr):
         lhs = expr.lhs
         if rhs.rank == 0:
             raise NotImplementedError(str(expr))
-        dummy_array_name, _ = create_incremented_string(self._parser.used_names, prefix = 'array_dummy')
+        dummy_array_name = self.scope.get_new_name('array_dummy')
         declare_dtype = self.find_in_dtype_registry(self._print(rhs.dtype), rhs.precision)
         dtype = self.find_in_ndarray_type_registry(self._print(rhs.dtype), rhs.precision)
         arg = rhs.arg if isinstance(rhs, NumpyArray) else rhs
@@ -373,7 +372,7 @@ def _init_stack_array(self, expr):
             lambda x,y: PyccelMul(x,y,simplify=True), var.alloc_shape))
         declare_dtype = self.find_in_dtype_registry('int', 8)
 
-        dummy_array_name, _ = create_incremented_string(self._parser.used_names, prefix = 'array_dummy')
+        dummy_array_name = self.scope.get_new_name('array_dummy')
         buffer_array = "{dtype} {name}[{size}];\n".format(
                 dtype = dtype,
                 name  = dummy_array_name,
@@ -407,10 +406,9 @@ def fill_NumpyArange(self, expr, lhs):
         step   = self._print(expr.step)
         dtype  = self.find_in_ndarray_type_registry(self._print(expr.dtype), expr.precision)
 
-        target = Variable(expr.dtype, name =  self._parser.get_new_name('s'))
-        index  = Variable(NativeInteger(), name = self._parser.get_new_name('i'))
+        target = self.scope.get_temporary_variable(expr.dtype)
+        index  = self.scope.get_temporary_variable(NativeInteger())
 
-        self._additional_declare += [index, target]
         self._additional_code += self._print(Assign(index, LiteralInteger(0)))
 
         code = 'for({target} = {start}; {target} {op} {stop}; {target} += {step})'
@@ -432,6 +430,10 @@ def _handle_inline_func_call(self, expr):
         func = expr.funcdef
         body = func.body
 
+        for b in body.body:
+            if isinstance(b, ScopedNode):
+                b.scope.update_parent_scope(self.scope, is_loop=True)
+
         # Print any arguments using the same inline function
         # As the function definition is modified directly this function
         # cannot be called recursively with the same FunctionDef
@@ -443,9 +445,8 @@ def _handle_inline_func_call(self, expr):
             else:
                 args.append(a.value)
 
-        new_local_vars = [v.clone(self._parser.get_new_name(v.name)) \
+        new_local_vars = [self.scope.get_temporary_variable(v) \
                             for v in func.local_vars]
-        self._additional_declare.extend(new_local_vars)
 
         parent_assign = expr.get_direct_user_nodes(lambda x: isinstance(x, Assign))
         if parent_assign:
@@ -493,7 +494,11 @@ def _handle_inline_func_call(self, expr):
             self.add_import(Import(mod.name, [AsName(v, v.name) \
                 for v in (*func.global_vars, *func.global_funcs)]))
             for v in (*func.global_vars, *func.global_funcs):
-                self._parser.used_names.add(v.name)
+                self.scope.insert_symbol(v.name)
+
+        for b in body.body:
+            if isinstance(b, ScopedNode):
+                b.scope.update_parent_scope(func.scope, is_loop=True)
 
         return code
 
@@ -581,6 +586,7 @@ def _print_Header(self, expr):
         return ''
 
     def _print_ModuleHeader(self, expr):
+        self.set_scope(expr.module.scope)
         self._in_header = True
         name = expr.module.name
         # TODO: Add classes and interfaces
@@ -593,6 +599,7 @@ def _print_ModuleHeader(self, expr):
         imports = ''.join(self._print(i) for i in imports)
 
         self._in_header = False
+        self.exit_scope()
         return ('#ifndef {name}_H\n'
                 '#define {name}_H\n\n'
                 '{imports}\n'
@@ -607,6 +614,7 @@ def _print_ModuleHeader(self, expr):
                         funcs   = funcs)
 
     def _print_Module(self, expr):
+        self.set_scope(expr.scope)
         self._current_module = expr.name
         body    = ''.join(self._print(i) for i in expr.body)
 
@@ -623,6 +631,7 @@ def _print_Module(self, expr):
                         variables = global_variables,
                         body      = body)
 
+        self.exit_scope()
         return code
 
     def _print_Break(self, expr):
@@ -632,7 +641,9 @@ def _print_Continue(self, expr):
         return 'continue;\n'
 
     def _print_While(self, expr):
+        self.set_scope(expr.scope)
         body = self._print(expr.body)
+        self.exit_scope()
         cond = self._print(expr.test)
         return 'while({condi})\n{{\n{body}}}\n'.format(condi = cond, body = body)
 
@@ -799,7 +810,7 @@ def get_print_format_and_arg(self, var):
         return arg_format, arg
 
     def extract_function_call_results(self, expr):
-        tmp_list = [self.create_tmp_var(a) for a in expr.funcdef.results]
+        tmp_list = [self.scope.get_temporary_variable(a.dtype) for a in expr.funcdef.results]
         return tmp_list
 
     def _print_PythonPrint(self, expr):
@@ -847,8 +858,7 @@ def formatted_args_to_printf(args_format, args, end):
                     code += formatted_args_to_printf(args_format, args, sep)
                     args_format = []
                     args = []
-                for_index = Variable(NativeInteger(), name = self._parser.get_new_name('i'))
-                self._additional_declare.append(for_index)
+                for_index = self.scope.get_temporary_variable(NativeInteger(), name = 'i')
                 max_index = PyccelMinus(f.shape[0], LiteralInteger(1), simplify = True)
                 for_range = PythonRange(max_index)
                 print_body = [ FunctionCallArgument(f[for_index]) ]
@@ -856,7 +866,8 @@ def formatted_args_to_printf(args_format, args, end):
                     print_body.append(space_end)
 
                 for_body  = [PythonPrint(print_body)]
-                for_loop  = For(for_index, for_range, for_body)
+                for_scope = self.scope.create_new_loop_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]),
@@ -1344,6 +1355,7 @@ def _print_Interface(self, expr):
     def _print_FunctionDef(self, expr):
         if expr.is_inline:
             return ''
+        self.set_scope(expr.scope)
 
         if len(expr.results) > 1:
             self._additional_args.append(expr.results)
@@ -1355,9 +1367,10 @@ def _print_FunctionDef(self, expr):
                     decs += [Declare(i.dtype, i)]
                 elif not isinstance(i, Variable):
                     decs += [FuncAddressDeclare(i)]
-        decs += [Declare(i.dtype, i) for i in self._additional_declare]
+        arguments = [a.var for a in expr.arguments]
+        decs += [Declare(v.dtype,v) for v in self.scope.variables.values() \
+                if v not in chain(expr.local_vars, expr.results, arguments)]
         decs  = ''.join(self._print(i) for i in decs)
-        self._additional_declare.clear()
 
         sep = self._print(SeparatorComment(40))
         if self._additional_args :
@@ -1374,6 +1387,8 @@ def _print_FunctionDef(self, expr):
                  '}\n',
                  sep]
 
+        self.exit_scope()
+
         return ''.join(p for p in parts if p)
 
     def stored_in_c_pointer(self, a):
@@ -1399,12 +1414,6 @@ def stored_in_c_pointer(self, a):
         return (a.is_pointer and not a.is_ndarray) or a.is_optional or \
                 any(a is bi for b in self._additional_args for bi in b)
 
-    def create_tmp_var(self, match_var):
-        tmp_var_name = self._parser.get_new_name('tmp')
-        tmp_var = Variable(name = tmp_var_name, dtype = match_var.dtype)
-        self._additional_declare.append(tmp_var)
-        return tmp_var
-
     def _print_FunctionCall(self, expr):
         func = expr.funcdef
         if func.is_inline:
@@ -1418,7 +1427,7 @@ def _print_FunctionCall(self, expr):
                 if isinstance(a, Variable):
                     args.append(VariableAddress(a))
                 elif not self.stored_in_c_pointer(a):
-                    tmp_var = self.create_tmp_var(f)
+                    tmp_var = self.scope.get_temporary_variable(f.dtype)
                     assign = Assign(tmp_var, a)
                     self._additional_code += self._print(assign)
                     args.append(VariableAddress(tmp_var))
@@ -1478,6 +1487,9 @@ def _print_Return(self, expr):
             # the user assigns a variable to an object contains IndexedElement object.
             if not last_assign:
                 code = ''+self._print(expr.stmt)
+            elif isinstance(last_assign[-1], AugAssign):
+                last_assign[-1].lhs.is_temp = False
+                code = ''+self._print(expr.stmt)
             else:
                 # make sure that stmt contains one assign node.
                 last_assign = last_assign[-1]
@@ -1488,7 +1500,7 @@ def _print_Return(self, expr):
                     return code + 'return {};\n'.format(self._print(last_assign.rhs))
                 else:
                     code = ''+self._print(expr.stmt)
-                    self._additional_declare.append(last_assign.lhs)
+                    last_assign.lhs.is_temp = False
 
         return code + 'return {0};\n'.format(self._print(args[0]))
 
@@ -1586,9 +1598,8 @@ def _print_Assign(self, expr):
                 # Create temporary variable to provide allocated
                 # memory space before assigning to the pointer value
                 # (may be NULL)
-                tmp_var_name = self._parser.get_new_name()
-                tmp_var = lhs.clone(tmp_var_name, is_optional=False)
-                self._additional_declare.append(tmp_var)
+                tmp_var = self.scope.get_temporary_variable(lhs,
+                        is_optional = False)
                 self._optional_partners[lhs] = tmp_var
             # Point optional variable at an allocated memory space
             prefix_code = self._print(AliasAssign(lhs, tmp_var))
@@ -1639,11 +1650,12 @@ def _print_AliasAssign(self, expr):
             return '{} = {};\n'.format(lhs, rhs)
 
     def _print_For(self, expr):
+        self.set_scope(expr.scope)
 
         indices = expr.iterable.loop_counters
         index = indices[0] if indices else expr.target
         if expr.iterable.num_loop_counters_required:
-            self._additional_declare.append(index)
+            self.scope.insert_variable(index)
 
         target   = index
         iterable = expr.iterable.get_range()
@@ -1667,6 +1679,7 @@ def _print_For(self, expr):
         if isinstance(test_step, PyccelUnarySub):
             test_step = iterable.step.args[0]
 
+        self.exit_scope()
         # testing if the step is a value or an expression
         if isinstance(test_step, Literal):
             op = '>' if isinstance(iterable.step, PyccelUnarySub) else '<'
@@ -1685,8 +1698,9 @@ def _print_FunctionalFor(self, expr):
 
     def _print_CodeBlock(self, expr):
         if not expr.unravelled:
-            body_exprs, new_vars = expand_to_loops(expr, self._parser.get_new_variable, language_has_vectors = False)
-            self._additional_declare.extend(new_vars)
+            body_exprs = expand_to_loops(expr,
+                    self.scope.get_temporary_variable, self.scope,
+                    language_has_vectors = False)
         else:
             body_exprs = expr.body
         body_stmts = []
@@ -1864,15 +1878,15 @@ def _print_Omp_End_Clause(self, expr):
     #=====================================
 
     def _print_Program(self, expr):
+        self.set_scope(expr.scope)
         body  = self._print(expr.body)
-        decs     = [self._print(i) for i in expr.declarations]
-        decs    += [self._print(Declare(i.dtype, i)) for i in self._additional_declare]
-        decs    = ''.join(self._print(i) for i in decs)
-        self._additional_declare.clear()
+        variables = self.scope.variables.values()
+        decs = ''.join(self._print(Declare(v.dtype, v)) for v in variables)
 
         imports = [*expr.imports, *self._additional_imports.values()]
         imports = ''.join(self._print(i) for i in imports)
 
+        self.exit_scope()
         return ('{imports}'
                 'int main()\n{{\n'
                 '{decs}'
@@ -1944,13 +1958,13 @@ def indent_code(self, code):
             level += increase[n]
         return pretty
 
-def ccode(expr, parser, assign_to=None, **settings):
+def ccode(expr, filename, assign_to=None, **settings):
     """Converts an expr to a string of c code
 
     expr : Expr
         A pyccel expression to be converted.
-    parser : Parser
-        The parser used to collect the expression
+    filename : str
+        The name of the file being translated. Used in error printing
     assign_to : optional
         When given, the argument is used as the name of the variable to which
         the expression is assigned. Can be a string, ``Symbol``,
@@ -1969,4 +1983,4 @@ def ccode(expr, parser, assign_to=None, **settings):
         For example, if ``dereference=[a]``, the resulting code would print
         ``(*a)`` instead of ``a``.
     """
-    return CCodePrinter(parser, **settings).doprint(expr, assign_to)
+    return CCodePrinter(filename, **settings).doprint(expr, assign_to)
diff --git a/pyccel/codegen/printing/codeprinter.py b/pyccel/codegen/printing/codeprinter.py
index ca3c47bf6d..9d40a7f41d 100644
--- a/pyccel/codegen/printing/codeprinter.py
+++ b/pyccel/codegen/printing/codeprinter.py
@@ -6,7 +6,6 @@
 # pylint: disable=R0201
 
 
-
 from pyccel.ast.basic import Basic
 
 from pyccel.ast.core      import Assign
@@ -26,6 +25,8 @@ class CodePrinter:
     The base class for code-printing subclasses.
     """
     language = None
+    def __init__(self):
+        self._scope = None
 
     def doprint(self, expr, assign_to=None):
         """
@@ -54,6 +55,23 @@ def doprint(self, expr, assign_to=None):
         # Format the output
         return ''.join(self._format_code(lines))
 
+    @property
+    def scope(self):
+        """ Return the scope associated with the object being printed
+        """
+        return self._scope
+
+    def set_scope(self, scope):
+        """ Change the current scope
+        """
+        assert scope is not None
+        self._scope = scope
+
+    def exit_scope(self):
+        """ Exit the current scope and return to the enclosing scope
+        """
+        self._scope = self._scope.parent_scope
+
     def _print(self, expr):
         """Print the AST node in the printer language
 
diff --git a/pyccel/codegen/printing/cwrappercode.py b/pyccel/codegen/printing/cwrappercode.py
index 5ce36d020b..d2d1351f74 100644
--- a/pyccel/codegen/printing/cwrappercode.py
+++ b/pyccel/codegen/printing/cwrappercode.py
@@ -13,7 +13,7 @@
 
 from pyccel.ast.core import Assign, AliasAssign, FunctionDef, FunctionAddress
 from pyccel.ast.core import If, IfSection, Return, FunctionCall, Deallocate
-from pyccel.ast.core import create_incremented_string, SeparatorComment
+from pyccel.ast.core import SeparatorComment
 from pyccel.ast.core import Import, Module, Declare
 from pyccel.ast.core import AugAssign, CodeBlock
 
@@ -41,6 +41,8 @@
 
 from pyccel.ast.variable  import VariableAddress, Variable
 
+from pyccel.parser.scope  import Scope
+
 from pyccel.errors.errors   import Errors
 
 errors = Errors()
@@ -59,13 +61,12 @@
 class CWrapperCodePrinter(CCodePrinter):
     """A printer to convert a python module to strings of c code creating
     an interface between python and an implementation of the module in c"""
-    def __init__(self, parser, target_language, **settings):
-        CCodePrinter.__init__(self, parser, **settings)
+    def __init__(self, filename, target_language, **settings):
+        CCodePrinter.__init__(self, filename, **settings)
         self._target_language = target_language
         self._cast_functions_dict = OrderedDict()
         self._to_free_PyObject_list = []
         self._function_wrapper_names = dict()
-        self._global_names = set()
         self._module_name = None
 
 
@@ -87,30 +88,6 @@ def stored_in_c_pointer(self, a):
         else:
             return CCodePrinter.stored_in_c_pointer(self,a)
 
-    def get_new_name(self, used_names, requested_name):
-        """
-        Generate a new name, return the requested_name if it's not in
-        used_names set  or generate new one based on the requested_name.
-        The generated name is appended to the used_names set
-
-        Parameters
-        ----------
-        used_names     : set of strings
-            Set of variable and function names to avoid name collisions
-        requested_name : String
-            The desired name
-
-        Returns
-        ----------
-        name  : String
-        """
-        if requested_name not in used_names:
-            used_names.add(requested_name)
-            return requested_name
-        else:
-            incremented_name, _ = create_incremented_string(used_names, prefix=requested_name)
-            return incremented_name
-
     def function_signature(self, expr, print_arg_names = True):
         args = list(expr.arguments)
         if any([isinstance(a.var, FunctionAddress) for a in args]):
@@ -149,7 +126,7 @@ def get_declare_type(self, expr):
         else:
             return '{0} '.format(dtype)
 
-    def get_new_PyObject(self, name, used_names):
+    def get_new_PyObject(self, name):
         """
         Create new PyccelPyObject Variable with the desired name
 
@@ -157,14 +134,12 @@ def get_new_PyObject(self, name, used_names):
         -----------
         name       : String
             The desired name
-        used_names : Set of strings
-            Set of variable and function names to avoid name collisions
 
         Returns: Variable
         -------
         """
         return Variable(dtype=PyccelPyObject(),
-                        name=self.get_new_name(used_names, name),
+                        name=self.scope.get_new_name(name),
                         is_pointer=True)
 
     def find_in_dtype_registry(self, dtype, prec):
@@ -220,7 +195,7 @@ def get_static_function(self, function):
         static_func : FunctionDef
         """
         if self._target_language == 'fortran':
-            static_func = as_static_function(function)
+            static_func = as_static_function(function, mod_scope = self.scope)
         else:
             static_func = function
 
@@ -363,14 +338,12 @@ def _get_check_type_statement(self, variable, collect_var, compulsory):
 
         return check
 
-    def _get_wrapper_name(self, used_names, func):
+    def _get_wrapper_name(self, func):
         """
         create wrapper function name
 
         Parameters
         -----------
-        used_names: list of strings
-            List of variable and function names to avoid name collisions
         func      : FunctionDef or Interface
 
         Returns
@@ -378,29 +351,26 @@ def _get_wrapper_name(self, used_names, func):
         wrapper_name : string
         """
         name         = func.name
-        wrapper_name = self.get_new_name(used_names.union(self._global_names), name+"_wrapper")
+        wrapper_name = self.scope.get_new_name(name+"_wrapper")
 
         self._function_wrapper_names[func.name] = wrapper_name
-        self._global_names.add(wrapper_name)
 
         return wrapper_name
 
-    def get_wrapper_arguments(self, used_names):
+    def get_wrapper_arguments(self):
         """
         Create wrapper arguments
 
-        Parameters
-        ----------
-        used_names : Set of strings
-            Set of variable and function names to avoid name collisions
-
         Returns
         -------
         List of variables
         """
-        python_func_args    = self.get_new_PyObject("args"  , used_names)
-        python_func_kwargs  = self.get_new_PyObject("kwargs", used_names)
-        python_func_selfarg = self.get_new_PyObject("self"  , used_names)
+        python_func_args    = self.get_new_PyObject("args")
+        python_func_kwargs  = self.get_new_PyObject("kwargs")
+        python_func_selfarg = self.get_new_PyObject("self"  )
+        self.scope.insert_variable(python_func_args)
+        self.scope.insert_variable(python_func_kwargs)
+        self.scope.insert_variable(python_func_selfarg)
 
         return [python_func_selfarg, python_func_args, python_func_kwargs]
 
@@ -531,14 +501,12 @@ def _body_array(self, variable, collect_var, check_type = False) :
 
         return body
 
-    def _body_management(self, used_names, variable, collect_var, default_value = None, check_type = False):
+    def _body_management(self, variable, collect_var, default_value = None, check_type = False):
         """
         Responsible for calling functions that take care of body creation
 
         Parameters
         ----------
-        used_names    : set of strings
-                        Set of variable and function names to avoid name collisions
         variable      : Variable
                         The Variable (which may be optional)
         collect_var   : Variable
@@ -566,7 +534,8 @@ def _body_management(self, used_names, variable, collect_var, default_value = No
         else:
             if variable.is_optional:
                 tmp_variable = Variable(dtype=variable.dtype, precision = variable.precision,
-                                        name = self.get_new_name(used_names, variable.name+"_tmp"))
+                                        name = self.scope.get_new_name(variable.name+"_tmp"))
+                self.scope.insert_variable(tmp_variable)
 
             body = [self._body_scalar(variable, collect_var, default_value, check_type, tmp_variable)]
 
@@ -599,6 +568,7 @@ def untranslatable_function(self, wrapper_name, wrapper_args, wrapper_results, e
         code : string
             returns the string containing the printed FunctionDef
         """
+        current_namespace = self.scope
         wrapper_func = FunctionDef(
                 name      = wrapper_name,
                 arguments = wrapper_args,
@@ -608,23 +578,23 @@ def untranslatable_function(self, wrapper_name, wrapper_args, wrapper_results, e
                                             '"{}"'.format(error_msg)),
                                 AliasAssign(wrapper_results[0], Nil()),
                                 Return(wrapper_results)
-                            ])
+                            ],
+                scope     = Scope())
 
-        return CCodePrinter._print_FunctionDef(self, wrapper_func)
+        code = CCodePrinter._print_FunctionDef(self, wrapper_func)
+        self.set_scope(current_namespace)
+        return code
 
     # -------------------------------------------------------------------
     # Parsing arguments and building values Types functions
     # -------------------------------------------------------------------
-    def get_PyArgParseType(self, used_names, variable):
+    def get_PyArgParseType(self, variable):
         """
         Responsible for creating any necessary intermediate variables which are used
         to collect the result of PyArgParse, and collecting the required cast function
 
         Parameters
         ----------
-        used_names : list of strings
-            List of variable and function names to avoid name collisions
-
         variable : Variable
             The variable which will be passed to the translated function
 
@@ -636,27 +606,27 @@ def get_PyArgParseType(self, used_names, variable):
 
         if variable.rank > 0:
             collect_type = PyccelPyArrayObject()
-            collect_var  = Variable(dtype= collect_type, is_pointer = True, rank = variable.rank,
-                                   order= variable.order,
-                                   name=self.get_new_name(used_names, variable.name+"_tmp"))
+            collect_var  = Variable(dtype = collect_type,
+                                is_pointer = True, rank = variable.rank,
+                                order= variable.order,
+                                name=self.scope.get_new_name(variable.name+"_tmp"))
 
         else:
             collect_type = PyccelPyObject()
-            collect_var  = Variable(dtype=collect_type, is_pointer=True,
-                                   name = self.get_new_name(used_names, variable.name+"_tmp"))
+            collect_var  = Variable(dtype = collect_type,
+                                is_pointer = True,
+                                name=self.scope.get_new_name(variable.name+"_tmp"))
+        self.scope.insert_variable(collect_var)
 
         return collect_var
 
-    def get_PyBuildValue(self, used_names, variable):
+    def get_PyBuildValue(self, variable):
         """
         Responsible for collecting the variable required to build the result
         and the necessary cast function
 
         Parameters
         ----------
-        used_names : list of strings
-            List of variable and function names to avoid name collisions
-
         variable : Variable
             The variable returned by the translated function
 
@@ -675,8 +645,9 @@ def get_PyBuildValue(self, used_names, variable):
         cast_function = FunctionCall(C_to_Python(variable), [VariableAddress(variable)])
 
         collect_type = PyccelPyObject()
-        collect_var = Variable(dtype=collect_type, is_pointer=True,
-            name = self.get_new_name(used_names, variable.name+"_tmp"))
+        collect_var = Variable(dtype = collect_type, is_pointer=True,
+            name = self.scope.get_new_name(variable.name+"_tmp"))
+        self.scope.insert_variable(collect_var)
         self._to_free_PyObject_list.append(collect_var) #TODO remove in next PR
 
         return collect_var, cast_function
@@ -740,9 +711,8 @@ def get_module_exec_function(self, expr, exec_func_name):
         ------
         str
         """
-        used_names = set([exec_func_name] + [v.name for v in expr.variables])
-        mod_var_name = self.get_new_name(used_names, 'm')
-        tmp_var_name = self.get_new_name(used_names, 'tmp')
+        mod_var_name = self.scope.get_new_name('m')
+        tmp_var_name = self.scope.get_new_name('tmp')
         tmp_var = Variable(dtype = PyccelPyObject(),
                       name       = tmp_var_name,
                       is_pointer = True)
@@ -787,25 +757,31 @@ def get_module_exec_function(self, expr, exec_func_name):
 
     def _print_Interface(self, expr):
 
+        # Find a name for the wrapper function
+        wrapper_name = self._get_wrapper_name(expr)
+
+        mod_scope = self.scope
+        scope = self.scope.new_child_scope(wrapper_name)
+        self.set_scope(scope)
+
         # Collecting all functions
         funcs = expr.functions
-        # Save all used names
-        used_names = set(n.name for n in funcs)
 
-        # Find a name for the wrapper function
-        wrapper_name = self._get_wrapper_name(used_names, expr)
-        used_names.add(wrapper_name)
+        # Save all used names
+        for n in funcs:
+            self.scope.insert_symbol(n.name)
 
         # Collect arguments and results
-        wrapper_args    = self.get_wrapper_arguments(used_names)
-        wrapper_results = [self.get_new_PyObject("result", used_names)]
+        wrapper_args    = self.get_wrapper_arguments()
+        wrapper_results = [self.get_new_PyObject("result")]
+        self.scope.insert_variable(wrapper_results[0])
 
         # Collect parser arguments
         wrapper_vars = {}
 
         # Collect argument names for PyArgParse
         arg_names         = [a.name for a in funcs[0].arguments]
-        keyword_list_name = self.get_new_name(used_names,'kwlist')
+        keyword_list_name = self.scope.get_new_name('kwlist')
         keyword_list      = PyArgKeywords(keyword_list_name, arg_names)
         wrapper_body      = [keyword_list]
 
@@ -815,31 +791,41 @@ def _print_Interface(self, expr):
         # To store the mini function responsible for collecting value and calling interfaces functions and return the builded value
         funcs_def = []
         default_value = {} # dict to collect all initialisation needed in the wrapper
-        check_var = Variable(dtype = NativeInteger(), name = self.get_new_name(used_names , "check"))
+        check_var = Variable(dtype = NativeInteger(), name = self.scope.get_new_name("check"))
         wrapper_vars[check_var.name] = check_var
+        scope.insert_variable(check_var, check_var.name)
         types_dict = OrderedDict((a.var, set()) for a in funcs[0].arguments) #dict to collect each variable possible type and the corresponding flags
         # collect parse arg
-        parse_args = [self.get_PyArgParseType(used_names,a.var) for a in funcs[0].arguments]
+        parse_args = [self.get_PyArgParseType(a.var) for a in funcs[0].arguments]
 
         # Managing the body of wrapper
         for func in funcs :
             mini_wrapper_func_body = []
             res_args = []
             static_func_args  = []
+
+            mini_wrapper_func_name = self.scope.get_new_name(func.name + '_mini_wrapper')
+            mini_scope = mod_scope.new_child_scope(mini_wrapper_func_name)
+            self.set_scope(mini_scope)
+
             # update ndarray local variables properties
             arg_vars = {a.var: a for a in func.arguments}
             local_arg_vars = {(v.clone(v.name, is_pointer=True, allocatable=False)
                               if isinstance(v, Variable) and v.rank > 0 or v.is_optional \
                               else v) : a for v,a in arg_vars.items()}
+            for a in local_arg_vars:
+                mini_scope.insert_variable(a)
+            for r in func.results:
+                mini_scope.insert_variable(r)
             mini_wrapper_func_vars = {a.name : a for a in local_arg_vars}
             flags = 0
             collect_vars = {}
 
             # Loop for all args in every functions and create the corresponding condition and body
             for p_arg, (f_var, f_arg) in zip(parse_args, local_arg_vars.items()):
-                collect_var  = self.get_PyArgParseType(used_names, f_var)
+                collect_var  = self.get_PyArgParseType(f_var)
                 collect_vars[f_var] = collect_var
-                body, tmp_variable = self._body_management(used_names, f_var, p_arg, f_arg.value)
+                body, tmp_variable = self._body_management(f_var, p_arg, f_arg.value)
                 if tmp_variable :
                     mini_wrapper_func_vars[tmp_variable.name] = tmp_variable
 
@@ -875,13 +861,15 @@ def _print_Interface(self, expr):
 
             mini_wrapper_func_body.append(func_call)
 
+
             # Loop for all res in every functions and create the corresponding body and cast
             for r in func.results :
-                collect_var, cast_func = self.get_PyBuildValue(used_names, r)
+                collect_var, cast_func = self.get_PyBuildValue(r)
                 mini_wrapper_func_vars[collect_var.name] = collect_var
                 if cast_func is not None:
                     mini_wrapper_func_vars[r.name] = r
                     mini_wrapper_func_body.append(AliasAssign(collect_var, cast_func))
+
                 res_args.append(VariableAddress(collect_var) if collect_var.is_pointer else collect_var)
 
             # Building PybuildValue and freeing the allocated variable after.
@@ -894,15 +882,14 @@ def _print_Interface(self, expr):
             mini_wrapper_func_body.append(Return(wrapper_results))
             self._to_free_PyObject_list.clear()
 
-            # Building Mini wrapper function
-            mini_wrapper_func_name = self.get_new_name(used_names.union(self._global_names), func.name + '_mini_wrapper')
-            self._global_names.add(mini_wrapper_func_name)
+            self.set_scope(scope)
 
+            # Building Mini wrapper function
             mini_wrapper_func_def = FunctionDef(name = mini_wrapper_func_name,
                 arguments = parse_args,
                 results = wrapper_results,
                 body = mini_wrapper_func_body,
-                local_vars = mini_wrapper_func_vars.values())
+                scope = mini_scope)
             funcs_def.append(mini_wrapper_func_def)
 
             # append check condition to the functioncall
@@ -911,7 +898,7 @@ def _print_Interface(self, expr):
 
         # Errors / Types management
         # Creating check_type function
-        check_func_def = self._create_wrapper_check(check_var, parse_args, types_dict, used_names, funcs[0].name)
+        check_func_def = self._create_wrapper_check(check_var, parse_args, types_dict, funcs[0].name)
         funcs_def.append(check_func_def)
 
         # Create the wrapper body with collected informations
@@ -939,13 +926,15 @@ def _print_Interface(self, expr):
             arguments = wrapper_args,
             results = wrapper_results,
             body = wrapper_body,
-            local_vars = wrapper_vars.values()))
+            scope = scope))
 
         sep = self._print(SeparatorComment(40))
 
+        self.exit_scope()
+
         return sep + '\n'.join(CCodePrinter._print_FunctionDef(self, f) for f in funcs_def)
 
-    def _create_wrapper_check(self, check_var, parse_args, types_dict, used_names, func_name):
+    def _create_wrapper_check(self, check_var, parse_args, types_dict, func_name):
         check_func_body = []
         flags = (len(types_dict) - 1) * 4
         for arg in types_dict:
@@ -970,13 +959,12 @@ def _create_wrapper_check(self, check_var, parse_args, types_dict, used_names, f
         check_func_body = [Assign(check_var, LiteralInteger(0))] + check_func_body
         check_func_body.append(Return([check_var]))
         # Creating check function definition
-        check_func_name = self.get_new_name(used_names.union(self._global_names), 'type_check')
-        self._global_names.add(check_func_name)
+        check_func_name = self.scope.parent_scope.get_new_name('type_check')
         check_func_def = FunctionDef(name = check_func_name,
             arguments = parse_args,
             results = [check_var],
             body = check_func_body,
-            local_vars = [])
+            scope = self.scope.new_child_scope(check_func_name))
         return check_func_def
 
     def _print_PyccelPyObject(self, expr):
@@ -1035,39 +1023,47 @@ def _print_PyModule_AddObject(self, expr):
                 self._print(expr.variable))
 
     def _print_FunctionDef(self, expr):
-        # Save all used names
-        used_names = set([a.name for a in expr.arguments]
-                        + [r.name for r in expr.results]
-                        + [expr.name])
+        self.set_scope(self.scope.new_child_scope(expr.name))
 
         arg_vars = {a.var: a for a in expr.arguments}
+
+        for a in expr.arguments:
+            v = a.var
+            if isinstance(v, Variable):
+                self.scope.insert_variable(v)
+            else:
+                self.scope.functions[v.name] = v
+        for r in expr.results:
+            self.scope.insert_variable(r)
         # update ndarray and optional local variables properties
         local_arg_vars = {(v.clone(v.name, is_pointer=True, allocatable=False)
                           if isinstance(v, Variable) and (v.rank > 0 or v.is_optional) \
                           else v) : a for v,a in arg_vars.items()}
 
         # Find a name for the wrapper function
-        wrapper_name = self._get_wrapper_name(used_names, expr)
-        used_names.add(wrapper_name)
+        wrapper_name = self._get_wrapper_name(expr)
         # Collect local variables
         wrapper_vars        = {a.name : a for a in local_arg_vars}
         wrapper_vars.update({r.name : r for r in expr.results})
 
         # Collect arguments and results
-        wrapper_args    = self.get_wrapper_arguments(used_names)
-        wrapper_results = [self.get_new_PyObject("result", used_names)]
+        wrapper_args    = self.get_wrapper_arguments()
+        wrapper_results = [self.get_new_PyObject("result")]
+        self.scope.insert_variable(wrapper_results[0])
 
         # Collect argument names for PyArgParse
         arg_names         = [a.name for a in local_arg_vars]
-        keyword_list_name = self.get_new_name(used_names,'kwlist')
+        keyword_list_name = self.scope.get_new_name('kwlist')
         keyword_list      = PyArgKeywords(keyword_list_name, arg_names)
 
         if expr.is_private:
+            self.exit_scope()
             return self.untranslatable_function(wrapper_name,
                         wrapper_args, wrapper_results,
                         "Private functions are not accessible from python")
 
         if any(isinstance(arg, FunctionAddress) for arg in local_arg_vars):
+            self.exit_scope()
             return self.untranslatable_function(wrapper_name,
                         wrapper_args, wrapper_results,
                         "Cannot pass a function as an argument")
@@ -1079,10 +1075,10 @@ def _print_FunctionDef(self, expr):
         parse_args = []
         collect_vars = {}
         for var, arg in local_arg_vars.items():
-            collect_var  = self.get_PyArgParseType(used_names, var)
+            collect_var  = self.get_PyArgParseType(var)
             collect_vars[var] = collect_var
 
-            body, tmp_variable = self._body_management(used_names, var, collect_var, arg.value, True)
+            body, tmp_variable = self._body_management(var, collect_var, arg.value, True)
             if tmp_variable :
                 wrapper_vars[tmp_variable.name] = tmp_variable
 
@@ -1124,7 +1120,7 @@ def _print_FunctionDef(self, expr):
         # Loop over results to carry out necessary casts and collect Py_BuildValue type string
         res_args = []
         for a in expr.results :
-            collect_var, cast_func = self.get_PyBuildValue(used_names, a)
+            collect_var, cast_func = self.get_PyBuildValue(a)
             if cast_func is not None:
                 wrapper_vars[collect_var.name] = collect_var
                 wrapper_body.append(AliasAssign(collect_var, cast_func))
@@ -1150,11 +1146,14 @@ def _print_FunctionDef(self, expr):
             arguments = wrapper_args,
             results = wrapper_results,
             body = wrapper_body,
-            local_vars = tuple(wrapper_vars.values()))
+            scope = self.scope)
+
+        self.exit_scope()
 
         return CCodePrinter._print_FunctionDef(self, wrapper_func)
 
     def _print_Module(self, expr):
+        self.set_scope(Scope())
         # The initialisation and deallocation shouldn't be exposed to python
         funcs_to_wrap = [f for f in expr.funcs if f not in (expr.init_func, expr.free_func)]
 
@@ -1165,7 +1164,6 @@ def _print_Module(self, expr):
             vars_to_wrap_decs = [Declare(v.dtype, v, module_variable=True) \
                                     for v in expr.variables if not v.is_private]
 
-        self._global_names = set(f.name for f in expr.funcs)
         self._module_name  = expr.name
         sep = self._print(SeparatorComment(40))
 
@@ -1197,21 +1195,21 @@ def _print_Module(self, expr):
                                                         if f.doc_string else '""')
                                      for f in funcs if f is not expr.init_func)
 
-        slots_name = self.get_new_name(self._global_names, '{}_slots'.format(expr.name))
-        exec_func_name = self.get_new_name(self._global_names, 'exec_func')
+        slots_name = self.scope.get_new_name('{}_slots'.format(expr.name))
+        exec_func_name = self.scope.get_new_name('exec_func')
         slots_def = ('static PyModuleDef_Slot {name}[] = {{\n'
                      '{{Py_mod_exec, {exec_func}}},\n'
                      '{{0, NULL}},\n'
                      '}};\n').format(name = slots_name,
                              exec_func = exec_func_name)
 
-        method_def_name = self.get_new_name(self._global_names, '{}_methods'.format(expr.name))
+        method_def_name = self.scope.get_new_name('{}_methods'.format(expr.name))
         method_def = ('static PyMethodDef {method_def_name}[] = {{\n'
                         '{method_def_func}'
                         '{{ NULL, NULL, 0, NULL}}\n'
                         '}};\n'.format(method_def_name = method_def_name ,method_def_func = method_def_func))
 
-        module_def_name = self.get_new_name(self._global_names, '{}_module'.format(expr.name))
+        module_def_name = self.scope.get_new_name('{}_module'.format(expr.name))
         module_def = ('static struct PyModuleDef {module_def_name} = {{\n'
                 'PyModuleDef_HEAD_INIT,\n'
                 '/* name of module */\n'
@@ -1240,6 +1238,8 @@ def _print_Module(self, expr):
         imports += self._additional_imports.values()
         imports  = ''.join(self._print(i) for i in imports)
 
+        self.exit_scope()
+
         return ('#define PY_ARRAY_UNIQUE_SYMBOL CWRAPPER_ARRAY_API\n'
                 '{imports}\n'
                 '{variable_declarations}\n'
@@ -1269,13 +1269,13 @@ def _print_Module(self, expr):
                     module_def = module_def,
                     init_func = init_func))
 
-def cwrappercode(expr, parser, target_language, assign_to=None, **settings):
+def cwrappercode(expr, filename, target_language, assign_to=None, **settings):
     """Converts an expr to a string of c wrapper code
 
     expr : Expr
         A pyccel expression to be converted.
-    parser : Parser
-        The parser used to collect the expression
+    filename : str
+        The name of the file being translated. Used in error printing
     assign_to : optional
         When given, the argument is used as the name of the variable to which
         the expression is assigned. Can be a string, ``Symbol``,
@@ -1295,4 +1295,4 @@ def cwrappercode(expr, parser, target_language, assign_to=None, **settings):
         ``(*a)`` instead of ``a``.
     """
 
-    return CWrapperCodePrinter(parser, target_language, **settings).doprint(expr, assign_to)
+    return CWrapperCodePrinter(filename, target_language, **settings).doprint(expr, assign_to)
diff --git a/pyccel/codegen/printing/fcode.py b/pyccel/codegen/printing/fcode.py
index 041abd11b4..68faa12295 100644
--- a/pyccel/codegen/printing/fcode.py
+++ b/pyccel/codegen/printing/fcode.py
@@ -194,16 +194,12 @@ class FCodePrinter(CodePrinter):
     }
 
 
-    def __init__(self, parser, prefix_module = None):
+    def __init__(self, filename, prefix_module = None):
 
-        if parser.filename:
-            errors.set_target(parser.filename, 'file')
+        errors.set_target(filename, 'file')
 
         super().__init__()
-        self.parser = parser
-        self._namespace = self.parser.namespace
         self._constantImports = set()
-        self._current_function = None
         self._current_class    = None
 
         self._additional_code = None
@@ -211,12 +207,6 @@ def __init__(self, parser, prefix_module = None):
 
         self.prefix_module = prefix_module
 
-    def change_to_program_scope(self):
-        self._namespace = self.parser.program_namespace
-
-    def change_to_module_scope(self):
-        self._namespace = self.parser.namespace
-
     def print_constant_imports(self):
         """Prints the use line for the constant imports used"""
         macro = "use, intrinsic :: ISO_C_Binding, only : "
@@ -234,27 +224,8 @@ def set_current_class(self, name):
 
         self._current_class = name
 
-    def set_current_function(self, name):
-
-        if name:
-            self._namespace = self._namespace.sons_scopes[name]
-            if self._current_function:
-                name = DottedName(self._current_function, name)
-        else:
-            self._namespace = self._namespace.parent_scope
-            if isinstance(self._current_function, DottedName):
-
-                # case of a function inside a function
-
-                name = self._current_function.name[:-1]
-                if len(name) > 1:
-                    name = DottedName(*name)
-                else:
-                    name = name[0]
-        self._current_function = name
-
     def get_method(self, cls_name, method_name):
-        container = self._namespace
+        container = self.scope
         while container:
             if cls_name in container.classes:
                 cls = container.classes[cls_name]
@@ -274,7 +245,7 @@ def get_method(self, cls_name, method_name):
             severity='fatal')
 
     def get_function(self, name):
-        container = self._namespace
+        container = self.scope
         while container:
             if name in container.functions:
                 return container.functions[name]
@@ -284,17 +255,6 @@ def get_function(self, name):
         errors.report(UNDEFINED_FUNCTION, symbol=name,
             severity='fatal')
 
-    def add_vars_to_namespace(self, *new_vars):
-        if self._current_function:
-            if self._current_class:
-                func = self.get_method(self._current_class, self._current_function)
-            else:
-                func = self.get_function(self._current_function)
-            func.add_local_vars(*new_vars)
-        else:
-            for var in new_vars:
-                self._namespace.variables[var.name] = var
-
     def _get_statement(self, codestring):
         return codestring
 
@@ -308,7 +268,7 @@ def print_kind(self, expr):
         precision = get_final_precision(expr)
         constant_name = iso_c_binding[self._print(expr.dtype)][precision]
         constant_shortcut = iso_c_binding_shortcut_mapping[constant_name]
-        if constant_shortcut not in self.parser.used_names and constant_name != constant_shortcut:
+        if constant_shortcut not in self.scope.all_used_symbols and constant_name != constant_shortcut:
             self._constantImports.add((constant_shortcut, constant_name))
             constant_name = constant_shortcut
         else:
@@ -318,6 +278,7 @@ def print_kind(self, expr):
     def _handle_inline_func_call(self, expr, provided_args, assign_lhs = None):
         """ Print a function call to an inline function
         """
+        scope = self.scope
         func = expr.funcdef
 
         # Print any arguments using the same inline function
@@ -332,14 +293,14 @@ def _handle_inline_func_call(self, expr, provided_args, assign_lhs = None):
                 args.append(a.value)
 
         # Create new local variables to ensure there are no name collisions
-        new_local_vars = [v.clone(self.parser.get_new_name(v.name)) \
+        new_local_vars = [v.clone(self.scope.get_new_name(v.name)) \
                             for v in func.local_vars]
         for v in new_local_vars:
-            self.add_vars_to_namespace(v)
+            self.scope.insert_variable(v)
 
-        # Put functions into current namespace
+        # Put functions into current scope
         for entry in ['variables', 'classes', 'functions']:
-            self._namespace.imports[entry].update(func.namespace_imports[entry])
+            self.scope.imports[entry].update(func.namespace_imports[entry])
 
         func.swap_in_args(args, new_local_vars)
 
@@ -400,8 +361,9 @@ def _handle_inline_func_call(self, expr, provided_args, assign_lhs = None):
             self._additional_imports.add(Import(mod.name, [AsName(v, v.name) \
                 for v in (*func.global_vars, *func.global_funcs)]))
             for v in (*func.global_vars, *func.global_funcs):
-                self.parser.used_names.add(v.name)
+                self.scope.insert_symbol(v.name)
 
+        self.set_scope(scope)
         return code
 
     def _get_external_declarations(self):
@@ -409,7 +371,7 @@ def _get_external_declarations(self):
         Look for external functions and declare their result type
         """
         decs = {}
-        for key,f in self._namespace.imports['functions'].items():
+        for key,f in self.scope.imports['functions'].items():
             if isinstance(f, FunctionDef) and f.is_external:
                 i = Variable(f.results[0].dtype, name=str(key))
                 dec = Declare(i.dtype, i, external=True)
@@ -422,6 +384,7 @@ def _print_PyccelSymbol(self, expr):
         return expr
 
     def _print_Module(self, expr):
+        self.set_scope(expr.scope)
         name = self._print(expr.name)
         name = name.replace('.', '_')
         if not name.startswith('mod_') and self.prefix_module:
@@ -480,20 +443,21 @@ def _print_Module(self, expr):
                  body,
                  'end module {}\n'.format(name)]
 
+        self.exit_scope()
+
         return '\n'.join([a for a in parts if a])
 
     def _print_Program(self, expr):
-        module_namespace = self._namespace
-        self.change_to_program_scope()
+        self.set_scope(expr.scope)
 
         name    = 'prog_{0}'.format(self._print(expr.name)).replace('.', '_')
         imports = ''.join(self._print(i) for i in expr.imports)
         body    = self._print(expr.body)
 
-        # Print the declarations of all variables in the namespace, which include:
+        # Print the declarations of all variables in the scope, which include:
         #  - user-defined variables (available in Program.variables)
         #  - pyccel-generated variables added to Scope when printing 'expr.body'
-        variables = self.parser.get_variables(self._namespace)
+        variables = self.scope.variables.values()
         decs = ''.join(self._print_Declare(Declare(v.dtype, v)) for v in variables)
 
         # Detect if we are using mpi4py
@@ -520,8 +484,7 @@ def _print_Program(self, expr):
                  body,
                 'end program {}\n'.format(name)]
 
-        self.change_to_module_scope()
-        self._namespace = module_namespace
+        self.exit_scope()
 
         return '\n'.join(a for a in parts if a)
 
@@ -696,10 +659,10 @@ def _print_DottedVariable(self, expr):
             base = expr.lhs.funcdef.results[0]
             if (not self._additional_code):
                 self._additional_code = ''
-            var_name = self.parser.get_new_name()
+            var_name = self.scope.get_new_name()
             var = base.clone(var_name)
 
-            self.add_vars_to_namespace(var)
+            self.scope.insert_variable(var)
 
             self._additional_code = self._additional_code + self._print(Assign(var,expr.lhs)) + '\n'
             return self._print(var) + '%' +self._print(expr.name)
@@ -827,7 +790,7 @@ def _print_NumpyLinspace(self, expr):
         else:
             template = '[(({start} + {index}*{step}), {index} = {zero},{end})]'
             var = Variable('int', 'linspace_index')
-            self.add_vars_to_namespace(var)
+            self.scope.insert_variable(var)
 
         init_value = template.format(
             start = self._print(expr.start),
@@ -908,9 +871,9 @@ def _print_NumpyArange(self, expr):
         start  = self._print(expr.start)
         step   = self._print(expr.step)
         shape  = PyccelMinus(expr.shape[0], LiteralInteger(1), simplify = True)
-        index  = Variable(NativeInteger(), name =  self.parser.get_new_name('i'))
+        index  = Variable(NativeInteger(), name =  self.scope.get_new_name('i'))
 
-        self.add_vars_to_namespace(index)
+        self.scope.insert_variable(index)
 
         code = '[({start} + {step} * {index}, {index} = {0}, {shape}, {1})]'
         code = code.format(self._print(LiteralInteger(0)),
@@ -996,12 +959,12 @@ def _print_NumpyRand(self, expr):
 
         if (not self._additional_code):
             self._additional_code = ''
-        var_name = self.parser.get_new_name()
+        var_name = self.scope.get_new_name()
         var = Variable(expr.dtype, var_name, is_stack_array = all([s.is_constant for s in expr.shape]),
                 shape = expr.shape, precision = expr.precision,
                 order = expr.order, rank = expr.rank)
 
-        self.add_vars_to_namespace(var)
+        self.scope.insert_variable(var)
 
         self._additional_code = self._additional_code + self._print(Assign(var,expr)) + '\n'
         return self._print(var)
@@ -1288,8 +1251,9 @@ def _print_AliasAssign(self, expr):
 
     def _print_CodeBlock(self, expr):
         if not expr.unravelled:
-            body_exprs, new_vars = expand_to_loops(expr, self.parser.get_new_variable, language_has_vectors = True)
-            self.add_vars_to_namespace(*new_vars)
+            body_exprs = expand_to_loops(expr,
+                    self.scope.get_temporary_variable, self.scope,
+                    language_has_vectors = True)
         else:
             body_exprs = expr.body
         body_stmts = []
@@ -1530,6 +1494,7 @@ def _print_Interface(self, expr):
 
 
    # def _print_With(self, expr):
+   #     self.set_scope(expr)
    #     test = 'call '+self._print(expr.test) + '%__enter__()'
    #     body = self._print(expr.body)
    #     end = 'call '+self._print(expr.test) + '%__exit__()'
@@ -1538,9 +1503,11 @@ def _print_Interface(self, expr):
    #            '{end}').format(test=test, body=body, end=end)
         #TODO return code later
   #      expr.block
+  #      self.exit_scope()
   #      return ''
 
     def _print_Block(self, expr):
+        self.set_scope(expr.scope)
 
         decs=[]
         for i in expr.variables:
@@ -1551,6 +1518,7 @@ def _print_Block(self, expr):
         body_code = self._print(body)
         prelude   = ''.join(self._print(i) for i in decs)
 
+        self.exit_scope()
 
         #case of no local variables
         if len(decs) == 0:
@@ -1570,10 +1538,8 @@ def _print_BindCFunctionDef(self, expr):
             # exposed to python so there is no need to print their signature
             return ''
 
-        self.parser.create_new_function_scope(name, {})
-        self.parser.exit_function_scope()
-        self.parser.insert_function(expr)
-        self.set_current_function(name)
+        self.set_scope(expr.scope)
+        self.scope.functions[expr.name] = expr
 
         body = self._print(expr.body)
 
@@ -1597,7 +1563,7 @@ def _print_BindCFunctionDef(self, expr):
         for i in expr.local_vars:
             dec = Declare(i.dtype, i)
             decs[i] = dec
-        vars_to_print = self.parser.get_variables(self._namespace)
+        vars_to_print = self.scope.variables.values()
         for v in vars_to_print:
             if (v not in expr.local_vars) and (v not in expr.results) and (v not in arguments):
                 decs[v] = Declare(v.dtype,v)
@@ -1612,7 +1578,6 @@ def _print_BindCFunctionDef(self, expr):
             dec = Declare(result.dtype, result, static=True)
             decs[result] = dec
 
-        self.set_current_function(None)
         # ...
 
         interfaces = '\n'.join(self._print(i) for i in expr.interfaces)
@@ -1622,6 +1587,8 @@ def _print_BindCFunctionDef(self, expr):
         body_code = body
         doc_string = self._print(expr.doc_string) if expr.doc_string else ''
 
+        self.exit_scope()
+
         parts = [doc_string,
                 '{0} {1}({2}) bind(c) {3}\n'.format(func_type, name, arg_code, func_end),
                  imports,
@@ -1708,9 +1675,9 @@ def function_signature(self, expr, name):
     def _print_FunctionDef(self, expr):
         if expr.is_inline:
             return ''
+        self.set_scope(expr.scope)
 
         name = self._print(expr.name)
-        self.set_current_function(name)
 
         if expr.cls_name:
             for k, m in list(_default_methods.items()):
@@ -1740,7 +1707,7 @@ def _print_FunctionDef(self, expr):
         decs.update(self._get_external_declarations())
 
         arguments = [a.var for a in expr.arguments]
-        vars_to_print = self.parser.get_variables(self._namespace)
+        vars_to_print = self.scope.variables.values()
         for v in vars_to_print:
             if (v not in expr.local_vars) and (v not in expr.results) and (v not in arguments):
                 decs[v] = Declare(v.dtype,v)
@@ -1751,8 +1718,6 @@ def _print_FunctionDef(self, expr):
 
         imports = ''.join(self._print(i) for i in expr.imports)
 
-        self.set_current_function(None)
-
         parts = [doc_string,
                 "{}({}) {}\n".format(sig_parts['sig'], sig_parts['arg_code'], sig_parts['func_end']),
                 imports,
@@ -1762,6 +1727,8 @@ def _print_FunctionDef(self, expr):
                 body_code,
                 'end {} {}\n'.format(sig_parts['func_type'], name)]
 
+        self.exit_scope()
+
         return '\n'.join(a for a in parts if a)
 
     def _print_Pass(self, expr):
@@ -1802,6 +1769,7 @@ def _print_ClassDef(self, expr):
         if expr.hide:
             return '', ''
         # ...
+        self.set_scope(expr.scope)
 
         name = self._print(expr.name)
         self.set_current_class(name)
@@ -1856,6 +1824,8 @@ def _print_ClassDef(self, expr):
 
         self.set_current_class(None)
 
+        self.exit_scope()
+
         return decs, methods
 
     def _print_Break(self, expr):
@@ -1895,11 +1865,12 @@ def _print_FunctionalFor(self, expr):
         return loops
 
     def _print_For(self, expr):
+        self.set_scope(expr.scope)
 
         indices = expr.iterable.loop_counters
         index = indices[0] if indices else expr.target
         if expr.iterable.num_loop_counters_required:
-            self.add_vars_to_namespace(index)
+            self.scope.insert_variable(index)
 
         target   = index
         my_range = expr.iterable.get_range()
@@ -1924,6 +1895,8 @@ def _print_For(self, expr):
             end_annotation = expr.end_annotation.replace("for", "do")
             epilog += end_annotation
 
+        self.exit_scope()
+
         return ('{prolog}'
                 '{body}'
                 '{epilog}').format(prolog=prolog, body=body, epilog=epilog)
@@ -2180,7 +2153,9 @@ def _do_range(target, iterable, prolog, epilog):
     #    return prelude, body
 
     def _print_While(self,expr):
+        self.set_scope(expr.scope)
         body = self._print(expr.body)
+        self.exit_scope()
         return ('do while ({test})\n'
                 '{body}'
                 'end do\n').format(test=self._print(expr.test), body=body)
@@ -2583,12 +2558,12 @@ def _print_IndexedElement(self, expr):
             else:
                 if (not self._additional_code):
                     self._additional_code = ''
-                var_name = self.parser.get_new_name()
+                var_name = self.scope.get_new_name()
                 var = Variable(base.dtype, var_name, is_stack_array = True,
                         shape=base.shape,precision=base.precision,
                         order=base.order,rank=base.rank)
 
-                self.add_vars_to_namespace(var)
+                self.scope.insert_variable(var)
 
                 self._additional_code = self._additional_code + self._print(Assign(var,base)) + '\n'
                 return self._print(var[expr.indices])
@@ -2717,8 +2692,8 @@ def _print_FunctionCall(self, expr):
                 key = a.keyword
                 arg = a.value
                 if arg in lhs_vars.values():
-                    var = arg.clone(self.parser.get_new_name())
-                    self.add_vars_to_namespace(var)
+                    var = arg.clone(self.scope.get_new_name())
+                    self.scope.insert_variable(var)
                     self._additional_code += self._print(Assign(var,arg))
                     newarg = var
                 else:
@@ -2732,10 +2707,10 @@ def _print_FunctionCall(self, expr):
                                 for n,r in lhs_vars.items()]
 
         elif len(func_results)>1:
-            results = [r.clone(name = self.parser.get_new_name()) \
+            results = [r.clone(name = self.scope.get_new_name()) \
                         for r in func_results]
             for var in results:
-                self.add_vars_to_namespace(var)
+                self.scope.insert_variable(var)
 
             if len(func_results) == 1:
                 results_strs = []
@@ -2777,10 +2752,10 @@ def _print_DottedFunctionCall(self, expr):
             base = expr.prefix.funcdef.results[0]
             if (not self._additional_code):
                 self._additional_code = ''
-            var_name = self.parser.get_new_name()
+            var_name = self.scope.get_new_name()
             var = base.clone(var_name)
 
-            self.add_vars_to_namespace(var)
+            self.scope.insert_variable(var)
 
             self._additional_code = self._additional_code + self._print(Assign(var,expr.prefix)) + '\n'
             user_node = expr.current_user_node
@@ -2936,13 +2911,13 @@ def indent_code(self, code):
         return new_code
 
 
-def fcode(expr, parser, assign_to=None, **settings):
+def fcode(expr, filename, assign_to=None, **settings):
     """Converts an expr to a string of Fortran code
 
     expr : Expr
         A pyccel expression to be converted.
-    parser : Parser
-        The parser used to collect the expression
+    filename : str
+        The name of the file being translated. Used in error printing
     assign_to : optional
         When given, the argument is used as the name of the variable to which
         the expression is assigned. Can be a string, ``Symbol``,
@@ -2957,4 +2932,4 @@ def fcode(expr, parser, assign_to=None, **settings):
         for examples.
     """
 
-    return FCodePrinter(parser, **settings).doprint(expr, assign_to)
+    return FCodePrinter(filename, **settings).doprint(expr, assign_to)
diff --git a/pyccel/codegen/printing/pycode.py b/pyccel/codegen/printing/pycode.py
index b547b194a9..e8683a2811 100644
--- a/pyccel/codegen/printing/pycode.py
+++ b/pyccel/codegen/printing/pycode.py
@@ -63,8 +63,8 @@ class PythonCodePrinter(CodePrinter):
         'tabwidth': 4,
     }
 
-    def __init__(self, parser=None):
-        self._parser = parser
+    def __init__(self, filename):
+        errors.set_target(filename, 'file')
         super().__init__()
         self._additional_imports = {}
         self._aliases = {}
@@ -211,6 +211,7 @@ def _print_Interface(self, expr):
         return self._print(func)
 
     def _print_FunctionDef(self, expr):
+        self.set_scope(expr.scope)
         name       = self._print(expr.name)
         imports    = ''.join(self._print(i) for i in expr.imports)
         interfaces = ''.join(self._print(i) for i in expr.interfaces if not i.is_argument)
@@ -270,6 +271,7 @@ def _print_FunctionDef(self, expr):
             headers = self._print(headers)
             code = '{header}\n{code}'.format(header=headers, code=code)
 
+        self.exit_scope()
         return code
 
     def _print_FunctionAddress(self, expr):
@@ -288,6 +290,8 @@ def _print_Return(self, expr):
         return prelude+'return {}\n'.format(','.join(self._print(i) for i in expr_return_vars))
 
     def _print_Program(self, expr):
+        mod_scope = self.scope
+        self.set_scope(expr.scope)
         imports  = ''.join(self._print(i) for i in expr.imports)
         body     = self._print(expr.body)
         imports += ''.join(self._print(i) for i in self.get_additional_imports())
@@ -295,6 +299,9 @@ def _print_Program(self, expr):
         body = imports+body
         body = self._indent_codestring(body)
 
+        self.exit_scope()
+        if mod_scope:
+            self.set_scope(mod_scope)
         return ('if __name__ == "__main__":\n'
                 '{body}\n').format(body=body)
 
@@ -423,7 +430,7 @@ def _print_DottedName(self, expr):
         return '.'.join(self._print(n) for n in expr.name)
 
     def _print_FunctionCall(self, expr):
-        if expr.funcdef.name in self._ignore_funcs:
+        if expr.funcdef in self._ignore_funcs:
             return ''
         if expr.interface:
             func_name = expr.interface_name
@@ -437,14 +444,14 @@ def _print_FunctionCall(self, expr):
             return code+'\n'
 
     def _print_Import(self, expr):
-        p       = self._parser.d_parsers.get(expr.source, None)
+        mod = expr.source_module
         init_func_name = ''
         free_func_name = ''
-        if p:
-            init_func = p.semantic_parser.ast.init_func
+        if mod:
+            init_func = mod.init_func
             if init_func:
                 init_func_name = init_func.name
-            free_func = p.semantic_parser.ast.free_func
+            free_func = mod.free_func
             if free_func:
                 free_func_name = free_func.name
 
@@ -474,10 +481,11 @@ def _print_Import(self, expr):
                 self._aliases.update([(pyccel_builtin_import_registery[source][t.name].cls_name, t.target) for t in target if t.name != t.target])
 
             target_names = {t.name:t.target for t in target}
-            if init_func_name in target_names:
-                self._ignore_funcs.append(target_names[init_func_name])
-            if free_func_name in target_names:
-                self._ignore_funcs.append(target_names[free_func_name])
+            if expr.source_module:
+                if expr.source_module.init_func:
+                    self._ignore_funcs.append(expr.source_module.init_func)
+                if expr.source_module.free_func:
+                    self._ignore_funcs.append(expr.source_module.free_func)
             target = [self._print(t) for t in target if t.name not in (init_func_name, free_func_name)]
             target = ', '.join(target)
             return 'from {source} import {target}\n'.format(source=source, target=target)
@@ -490,6 +498,7 @@ def _print_CodeBlock(self, expr):
             return code
 
     def _print_For(self, expr):
+        self.set_scope(expr.scope)
         iterable = self._print(expr.iterable)
         target   = expr.target
         if not isinstance(target,(list, tuple)):
@@ -500,6 +509,7 @@ def _print_For(self, expr):
         code   = ('for {0} in {1}:\n'
                 '{2}').format(target,iterable,body)
 
+        self.exit_scope()
         return code
 
     def _print_FunctionalFor(self, expr):
@@ -536,7 +546,9 @@ def _print_GeneratorComprehension(self, expr):
 
     def _print_While(self, expr):
         cond = self._print(expr.test)
+        self.set_scope(expr.scope)
         body = self._indent_codestring(self._print(expr.body))
+        self.exit_scope()
         return 'while {cond}:\n{body}'.format(
                 cond = cond,
                 body = body)
@@ -786,6 +798,7 @@ def _print_Print(self, expr):
         return 'print({0})\n'.format(fs)
 
     def _print_Module(self, expr):
+        self.set_scope(expr.scope)
         # Print interface functions (one function with multiple decorators describes the problem)
         imports  = ''.join(self._print(i) for i in expr.imports)
         interfaces = ''.join(self._print(i) for i in expr.interfaces)
@@ -797,7 +810,7 @@ def _print_Module(self, expr):
 
         init_func = expr.init_func
         if init_func:
-            self._ignore_funcs.append(init_func.name)
+            self._ignore_funcs.append(init_func)
             # Collect initialisation body
             init_if = init_func.get_attribute_nodes(IfSection)[0]
             # Remove boolean from init_body
@@ -808,7 +821,7 @@ def _print_Module(self, expr):
 
         free_func = expr.free_func
         if free_func:
-            self._ignore_funcs.append(free_func.name)
+            self._ignore_funcs.append(free_func)
 
         imports += ''.join(self._print(i) for i in self.get_additional_imports())
 
@@ -820,6 +833,7 @@ def _print_Module(self, expr):
         else:
             prog = ''
 
+        self.exit_scope()
         return ('{imports}\n'
                 '{body}'
                 '{prog}').format(
diff --git a/pyccel/codegen/python_wrapper.py b/pyccel/codegen/python_wrapper.py
index 620720d544..8e0cb4bce9 100644
--- a/pyccel/codegen/python_wrapper.py
+++ b/pyccel/codegen/python_wrapper.py
@@ -55,7 +55,7 @@ def create_shared_library(codegen,
         # Construct static interface for passing array shapes and write it to file bind_c_MOD.f90
         new_module_name = 'bind_c_{}'.format(module_name)
         bind_c_mod = as_static_module(codegen.routines, module_name, new_module_name)
-        bind_c_code = fcode(bind_c_mod, codegen.parser)
+        bind_c_code = fcode(bind_c_mod, new_module_name)
         bind_c_filename = '{}.f90'.format(new_module_name)
 
         with open(bind_c_filename, 'w') as f:
@@ -93,7 +93,7 @@ def create_shared_library(codegen,
     #---------------------------------------
     module_old_name = codegen.ast.name
     codegen.ast.set_name(sharedlib_modname)
-    wrapper_codegen = CWrapperCodePrinter(codegen.parser, language)
+    wrapper_codegen = CWrapperCodePrinter(codegen.parser.filename, language)
     wrapper_code = wrapper_codegen.doprint(codegen.ast)
     if errors.has_errors():
         return
diff --git a/pyccel/parser/base.py b/pyccel/parser/base.py
index 2ab944dbbe..9c0debaf4b 100644
--- a/pyccel/parser/base.py
+++ b/pyccel/parser/base.py
@@ -23,17 +23,18 @@
 from pyccel.ast.core import FunctionDef, Interface, FunctionAddress
 from pyccel.ast.core import SympyFunction
 from pyccel.ast.core import Import, AsName
-from pyccel.ast.core import create_incremented_string, create_variable
+from pyccel.ast.core import create_variable
 
 from pyccel.ast.utilities import recognised_source
 
+from pyccel.ast.variable import DottedName
+
 from pyccel.parser.scope     import Scope
 from pyccel.parser.utilities import is_valid_filename_pyh, is_valid_filename_py
 
 from pyccel.errors.errors   import Errors
 from pyccel.errors.messages import PYCCEL_UNFOUND_IMPORTED_MODULE
 
-
 #==============================================================================
 
 errors = Errors()
@@ -125,7 +126,7 @@ class BasicParser(object):
             True if in debug mode.
 
         headers: list, tuple
-            list of headers to append to the namespace
+            list of headers to append to the scope
 
         show_traceback: bool
             prints Traceback exception if True
@@ -141,13 +142,13 @@ def __init__(self,
         self._fst  = None
         self._ast  = None
 
-        self._filename  = None
-        self._metavars  = {}
-        self._namespace = Scope()
+        self._filename = None
+        self._metavars = {}
+        self._scope    = Scope()
 
         self._used_names = None
 
-        # represent the namespace of a function
+        # represent the scope of a function
 
         self._current_class    = None
         self._current_function = None
@@ -178,18 +179,20 @@ def __init__(self,
                 raise TypeError('Expecting a dict of headers')
 
 
-            self.namespace.headers.update(headers)
+            self.scope.headers.update(headers)
 
         self._created_from_pickle = False
 
     @property
-    def namespace(self):
-        return self._namespace
+    def scope(self):
+        """ The Scope object containing all objects defined within the current scope
+        """
+        return self._scope
 
-    @namespace.setter
-    def namespace(self, namespace):
-        assert isinstance(namespace, Scope)
-        self._namespace = namespace
+    @scope.setter
+    def scope(self, scope):
+        assert isinstance(scope, Scope)
+        self._scope = scope
 
     @property
     def filename(self):
@@ -255,40 +258,6 @@ def blocking(self):
     def show_traceback(self):
         return self._show_traceback
 
-    @property
-    def used_names(self):
-        """Returns a set of all names used in the current file.
-        The set is used to prevent name collisions when creating new variables
-        """
-        return self._used_names
-
-    def get_new_name(self, current_name = None):
-        """
-        Creates a new name. A current_name can be provided indicating the name the
-        user would like to use if possible. If this name is not available then it
-        will be used as a prefix for the new name.
-        If no current_name is provided, then the standard prefix is used, and the
-        dummy counter is used and updated to facilitate finding the next value of
-        this common case
-
-          Parameters
-          ----------
-          current_name : str
-
-          Returns
-          -------
-          new_name     : str
-        """
-        if current_name is not None and current_name not in self.used_names:
-            self.used_names.add(current_name)
-            return current_name
-
-        if current_name is None:
-            new_name, self._dummy_counter = create_incremented_string(self.used_names, prefix = current_name, counter = self._dummy_counter)
-        else:
-            new_name,_ = create_incremented_string(self.used_names, prefix = current_name)
-        return new_name
-
     def get_new_variable(self, prefix = None):
         """
         Creates a new PyccelSymbol using the prefix provided. If this prefix is None,
@@ -318,7 +287,7 @@ def insert_function(self, func):
         if isinstance(func, SympyFunction):
             self.insert_symbolic_function(func)
         elif isinstance(func, (FunctionDef, Interface, FunctionAddress)):
-            container = self.namespace.functions
+            container = self.scope.functions
             container[func.name] = func
         else:
             raise TypeError('Expected a Function definition')
@@ -326,7 +295,7 @@ def insert_function(self, func):
     def insert_symbolic_function(self, func):
         """."""
 
-        container = self.namespace.symbolic_functions
+        container = self.scope.symbolic_functions
         if isinstance(func, SympyFunction):
             container[func.name] = func
         elif isinstance(func, SymbolicAssign) and isinstance(func.rhs,
@@ -342,7 +311,7 @@ def insert_import(self, expr):
 
         if not isinstance(expr, Import):
             raise TypeError('Expecting Import expression')
-        container = self.namespace.imports['imports']
+        container = self.scope.imports['imports']
 
         # if source is not specified, imported things are treated as sources
         if len(expr.target) == 0:
@@ -362,6 +331,89 @@ def insert_import(self, expr):
                     container[source] = []
                 container[source] += expr.target
 
+    def create_new_function_scope(self, name, **kwargs):
+        """
+        Create a new Scope object for a Python function with the given name,
+        and attach any decorators' information to the scope. The new scope is
+        a child of the current one, and can be accessed from the dictionary of
+        its children using the function name as key.
+
+        Before returning control to the caller, the current scope (stored in
+        self._scope) is changed to the one just created, and the function's
+        name is stored in self._current_function.
+
+        Parameters
+        ----------
+        name : str
+            Function's name, used as a key to retrieve the new scope.
+
+        decorators : dict
+            Decorators attached to FunctionDef object at syntactic stage.
+
+        """
+        child = self.scope.new_child_scope(name, **kwargs)
+
+        self._scope = child
+        if self._current_function:
+            name = DottedName(self._current_function, name)
+        self._current_function = name
+
+        return child
+
+    def exit_function_scope(self):
+        """ Exit the function scope and return to the encasing scope
+        """
+
+        self._scope = self._scope.parent_scope
+        if isinstance(self._current_function, DottedName):
+
+            name = self._current_function.name[:-1]
+            if len(name)>1:
+                name = DottedName(*name)
+            else:
+                name = name[0]
+        else:
+            name = None
+        self._current_function = name
+
+    def create_new_loop_scope(self):
+        """ Create a new scope describing a loop
+        """
+        self._scope = self._scope.create_new_loop_scope()
+        return self._scope
+
+    def exit_loop_scope(self):
+        """ Exit the loop scope and return to the encasing scope
+        """
+        self._scope = self._scope.parent_scope
+
+    def create_new_class_scope(self, name, **kwargs):
+        """
+        Create a new Scope object for a Python class with the given name,
+        and attach any decorators' information to the scope. The new scope is
+        a child of the current one, and can be accessed from the dictionary of
+        its children using the function name as key.
+
+        Before returning control to the caller, the current scope (stored in
+        self._scope) is changed to the one just created, and the function's
+        name is stored in self._current_function.
+
+        Parameters
+        ----------
+        name : str
+            Function's name, used as a key to retrieve the new scope.
+
+        """
+        child = self.scope.new_child_scope(name, **kwargs)
+        self._scope = child
+
+        return child
+
+    def exit_class_scope(self):
+        """ Exit the class scope and return to the encasing scope
+        """
+        self._scope = self._scope.parent_scope
+
     def dump(self, filename=None):
         """
         Dump the current ast using Pickle.
@@ -480,10 +532,8 @@ def copy(self, parser):
         self._fst = parser.fst
         self._ast = parser.ast
 
-        self._metavars  = parser.metavars
-        self._namespace = parser.namespace
-
-        self._used_names = parser.used_names
+        self._metavars = parser.metavars
+        self._scope    = parser.scope
 
         # the following flags give us a status on the parsing stage
         self._syntax_done   = parser.syntax_done
diff --git a/pyccel/parser/parser.py b/pyccel/parser/parser.py
index 6634e88d77..7567dea1e4 100644
--- a/pyccel/parser/parser.py
+++ b/pyccel/parser/parser.py
@@ -101,18 +101,15 @@ def metavars(self):
             return self._syntax_parser.metavars
 
     @property
-    def namespace(self):
+    def scope(self):
         if self._semantic_parser:
-            return self._semantic_parser.namespace
+            return self._semantic_parser.scope
         else:
-            return self._syntax_parser.namespace
+            return self._syntax_parser.scope
 
     @property
     def imports(self):
-        if self._semantic_parser:
-            return self._semantic_parser.namespace.imports['imports']
-        else:
-            return self._syntax_parser.namespace.imports['imports']
+        return self.scope.collect_all_imports()
 
     @property
     def fst(self):
@@ -204,7 +201,7 @@ def parse_sons(self, d_parsers, verbose=False):
 
         """
 
-        imports     = self.imports.keys()
+        imports     = self.imports
         treated     = d_parsers.keys()
         not_treated = [i for i in imports if i not in treated]
         for source in not_treated:
diff --git a/pyccel/parser/scope.py b/pyccel/parser/scope.py
index 49e4cf9ef3..2399e75e4f 100644
--- a/pyccel/parser/scope.py
+++ b/pyccel/parser/scope.py
@@ -5,14 +5,18 @@
 #------------------------------------------------------------------------------------------#
 """ Module containing the Scope class
 """
-from pyccel.ast.core import ClassDef
+
+from pyccel.ast.core      import ClassDef
 from pyccel.ast.datatypes import DataTypeFactory
-from pyccel.ast.headers import MacroFunction, MacroVariable
-from pyccel.ast.headers import FunctionHeader, ClassHeader, MethodHeader
-from pyccel.ast.variable import Variable, DottedName
+from pyccel.ast.headers   import MacroFunction, MacroVariable
+from pyccel.ast.headers   import FunctionHeader, ClassHeader, MethodHeader
+from pyccel.ast.internals import PyccelSymbol
+from pyccel.ast.variable  import Variable, DottedName
 
 from pyccel.errors.errors import Errors
 
+from pyccel.utilities.strings import create_incremented_string
+
 errors = Errors()
 
 class Scope(object):
@@ -25,8 +29,10 @@ class Scope(object):
                  A dictionary of any decorators which operate on
                  objects in this scope
     """
-    __slots__ = ('_imports','_locals','parent_scope','_sons_scopes',
-            '_is_loop','_loops','_temporary_variables')
+    allow_loop_scoping = False
+    __slots__ = ('_imports','_locals','_parent_scope','_sons_scopes',
+            '_is_loop','_loops','_temporary_variables', '_used_symbols',
+            '_dummy_counter')
 
     categories = ('functions','variables','classes',
             'imports','symbolic_functions',
@@ -34,7 +40,7 @@ class Scope(object):
             'cls_constructs')
 
     def __init__(self, *, decorators=None, is_loop = False,
-                    parent_scope = None):
+                    parent_scope = None, used_symbols = None):
 
         self._imports = {k:{} for k in self.categories}
 
@@ -42,12 +48,16 @@ def __init__(self, *, decorators=None, is_loop = False,
 
         self._temporary_variables = []
 
+        self._used_symbols = used_symbols or set()
+
+        self._dummy_counter = 0
+
         if decorators:
             self._locals['decorators'].update(decorators)
 
         # TODO use another name for headers
         #      => reserved keyword, or use __
-        self.parent_scope        = parent_scope
+        self._parent_scope       = parent_scope
         self._sons_scopes        = {}
 
 
@@ -77,11 +87,13 @@ def new_child_scope(self, name, **kwargs):
             New child scope, which has the current object as parent.
 
         """
+        ps = kwargs.pop('parent_scope', self)
+        if ps is not self:
+            raise ValueError("A child of {} cannot have a parent {}".format(self, ps))
 
         child = Scope(**kwargs, parent_scope = self)
 
-        self._sons_scopes[name] = child
-        child.parent_scope = self
+        self.add_son(name, child)
 
         return child
 
@@ -215,10 +227,10 @@ def create_new_loop_scope(self):
         """
         new_scope = Scope(decorators=self.decorators, is_loop = True,
                         parent_scope = self)
-        self._loops.append(new_scope)
+        self.add_loop(new_scope)
         return new_scope
 
-    def insert_variable(self, var, name = None, allow_loop_scoping = False):
+    def insert_variable(self, var, name = None):
         """ Add a variable to the current scope
 
         Parameters
@@ -235,19 +247,24 @@ def insert_variable(self, var, name = None, allow_loop_scoping = False):
                 to loops
                 Default : True
         """
+        if var.name == '_':
+            raise ValueError("A temporary variable should have a name generated by Scope.get_new_name")
         if not isinstance(var, Variable):
             raise TypeError('variable must be of type Variable')
 
         if name is None:
             name = var.name
 
-        if not allow_loop_scoping and self.is_loop:
-            self.parent_scope.insert_variable(var, name, allow_loop_scoping)
+        if not self.allow_loop_scoping and self.is_loop:
+            self.parent_scope.insert_variable(var, name)
         else:
             if name in self._locals['variables']:
                 raise RuntimeError('New variable {} already exists in scope'.format(name))
-            self._locals['variables'][name] = var
-            self._temporary_variables.append(var)
+            if name == '_':
+                self._temporary_variables.append(var)
+            else:
+                self._locals['variables'][name] = var
+            self.insert_symbol(name)
 
     def remove_variable(self, var, name = None):
         """ Remove a variable from anywhere in scope
@@ -330,3 +347,195 @@ def insert_header(self, expr):
             msg = 'header of type{0} is not supported'
             msg = msg.format(str(type(expr)))
             raise TypeError(msg)
+
+    def insert_symbol(self, symbol):
+        """ Add a new symbol to the scope
+        """
+        if not self.allow_loop_scoping and self.is_loop:
+            self.parent_scope.insert_symbol(symbol)
+        else:
+            self._used_symbols.add(symbol)
+
+
+    def insert_symbols(self, symbols):
+        """ Add multiple new symbols to the scope
+        """
+        if not self.allow_loop_scoping and self.is_loop:
+            self.parent_scope.insert_symbols(symbols)
+        else:
+            self._used_symbols.update(symbols)
+
+    @property
+    def all_used_symbols(self):
+        """ Get all symbols which already exist in this scope
+        """
+        if self.parent_scope:
+            symbols = self.parent_scope.all_used_symbols
+        else:
+            symbols = set()
+        symbols.update(self._used_symbols)
+        return symbols
+
+    @property
+    def local_used_symbols(self):
+        """ Get all symbols which already exist in this scope
+        excluding enclosing scopes
+        """
+        return self._used_symbols
+
+    def get_new_incremented_symbol(self, prefix, counter):
+        """
+        Creates a new name by adding a numbered suffix to the provided prefix.
+
+          Parameters
+          ----------
+          prefix : str
+
+          Returns
+          -------
+          new_name     : str
+        """
+
+        new_name, counter = create_incremented_string(self.local_used_symbols, prefix = prefix)
+
+        self.insert_symbol(new_name)
+
+        return PyccelSymbol(new_name, is_temp=True), counter
+
+    def get_new_name(self, current_name = None):
+        """
+        Creates a new name. A current_name can be provided indicating the name the
+        user would like to use if possible. If this name is not available then it
+        will be used as a prefix for the new name.
+        If no current_name is provided, then the standard prefix is used, and the
+        dummy counter is used and updated to facilitate finding the next value of
+        this common case
+
+          Parameters
+          ----------
+          current_name : str
+
+          Returns
+          -------
+          new_name     : str
+        """
+        if current_name is not None and current_name not in self.all_used_symbols:
+            self.insert_symbol(current_name)
+            return PyccelSymbol(current_name)
+
+        if current_name is None:
+            # Avoid confusing names by also searching in parent scopes
+            new_name, self._dummy_counter = create_incremented_string(self.all_used_symbols,
+                                                prefix = current_name,
+                                                counter = self._dummy_counter)
+        else:
+            # When a name is suggested, try to stick to it
+            new_name,_ = create_incremented_string(self.all_used_symbols, prefix = current_name)
+
+        self.insert_symbol(new_name)
+
+        return PyccelSymbol(new_name, is_temp = True)
+
+    def get_temporary_variable(self, dtype_or_var, name = None, **kwargs):
+        """
+        Get a temporary variable
+
+        Parameters
+        ----------
+        dtype_or_var : str, DataType, Variable
+            In the case of a string of DataType: The type of the Variable to be created
+            In the case of a Variable: a Variable which will be cloned to set all the Variable properties
+        name : str
+            The requested name for the new variable
+        kwargs : dict
+            See Variable keyword arguments
+        """
+        name = self.get_new_name(name)
+        if isinstance(dtype_or_var, Variable):
+            var = dtype_or_var.clone(name, **kwargs)
+        else:
+            var = Variable(dtype_or_var, name, **kwargs)
+        self.insert_variable(var)
+        return var
+
+    def get_expected_name(self, start_name):
+        """ Get a name with no collisions, ideally the provided name.
+        The provided name should already exist in the symbols
+        """
+        if start_name == '_':
+            return self.get_new_name()
+        elif start_name in self.local_used_symbols:
+            return start_name
+        elif self.parent_scope:
+            return self.parent_scope.get_expected_name(start_name)
+        else:
+            raise RuntimeError("{} does not exist in scope".format(start_name))
+
+    def create_product_loop_scope(self, inner_scope, n_loops):
+        """ Create a n_loops loop scopes such that the innermost loop
+        has the scope inner_scope
+
+        Parameters
+        ----------
+        inner_scope : Namespace
+                      Namespace describing the innermost scope
+        n_loops     : The number of loop scopes required
+        """
+        assert inner_scope == self._loops[-1]
+        scopes = [self.create_new_loop_scope()]
+        for _ in range(n_loops-2):
+            scopes.append(scopes[-1].create_new_loop_scope())
+        inner_scope.update_parent_scope(scopes[-1], is_loop = True)
+        scopes.append(inner_scope)
+        return scopes
+
+    def collect_all_imports(self):
+        """ Collect the names of all modules necessary to understand this scope
+        """
+        imports = list(self._imports['imports'].keys())
+        imports.extend([i for s in self._sons_scopes.values() for i in s.collect_all_imports()])
+        return imports
+
+    def update_parent_scope(self, new_parent, is_loop, name = None):
+        """ Change the parent scope
+        """
+        if is_loop:
+            if self.parent_scope:
+                self.parent_scope.remove_loop(self)
+            self._parent_scope = new_parent
+            self.parent_scope.add_loop(self)
+        else:
+            if self.parent_scope:
+                name = self.parent_scope.remove_son(self)
+            self._parent_scope = new_parent
+            self.parent_scope.add_son(name, self)
+
+    @property
+    def parent_scope(self):
+        """ Return the enclosing scope
+        """
+        return self._parent_scope
+
+    def remove_loop(self, loop):
+        """ Remove a loop from the scope
+        """
+        self._loops.remove(loop)
+
+    def remove_son(self, son):
+        """ Remove a sub-scope from the scope
+        """
+        name = [k for k,v in self._sons_scopes.items() if v is son]
+        assert len(name) == 1
+        self._sons_scopes.pop(name[0])
+
+    def add_loop(self, loop):
+        """ Make parent aware of new child loop
+        """
+        assert loop.parent_scope is self
+        self._loops.append(loop)
+
+    def add_son(self, name, son):
+        """ Make parent aware of new child
+        """
+        assert son.parent_scope is self
+        self._sons_scopes[name] = son
diff --git a/pyccel/parser/semantic.py b/pyccel/parser/semantic.py
index 7fe6268594..567c5bbb2f 100644
--- a/pyccel/parser/semantic.py
+++ b/pyccel/parser/semantic.py
@@ -18,7 +18,7 @@
 
 #==============================================================================
 
-from pyccel.ast.basic import Basic, PyccelAstNode
+from pyccel.ast.basic import Basic, PyccelAstNode, ScopedNode
 
 from pyccel.ast.builtins import PythonPrint
 from pyccel.ast.builtins import PythonInt, PythonBool, PythonFloat, PythonComplex
@@ -178,11 +178,10 @@ def __init__(self, inputs, **kwargs):
         self._filename  = parser._filename
         self._mod_name  = ''
         self._metavars  = parser._metavars
-        self._namespace = parser._namespace
-        self._namespace.imports['imports'] = {}
-        self._program_namespace = Scope()
-        self._module_namespace  = self._namespace
-        self._used_names = parser.used_names
+        self.scope = parser.scope
+        self.scope.imports['imports'] = {}
+        self._module_namespace  = self.scope
+        self._program_namespace = self.scope.new_child_scope('__main__')
         self._dummy_counter = parser._dummy_counter
 
         # used to store the local variables of a code block needed for garbage collecting
@@ -259,16 +258,16 @@ def annotate(self, **settings):
 
     def change_to_program_scope(self):
         self._allocs.append([])
-        self._module_namespace = self._namespace
-        self._namespace = self._program_namespace
+        self._module_namespace = self.scope
+        self.scope = self._program_namespace
 
     def change_to_module_scope(self):
-        self._program_namespace = self._namespace
-        self._namespace = self._module_namespace
+        self._program_namespace = self.scope
+        self.scope = self._module_namespace
 
     def check_for_variable(self, name):
         """
-        Search for a Variable object with the given name in the current namespace,
+        Search for a Variable object with the given name in the current scope,
         defined by the local and global Python scopes. Return None if not found.
         """
 
@@ -279,7 +278,7 @@ def check_for_variable(self, name):
                     return var
             return None
         else:
-            return self.namespace.find(name, 'variables')
+            return self.scope.find(name, 'variables')
 
     def get_variable(self, name):
         """ Like 'check_for_variable', but raise Pyccel error if Variable is not found.
@@ -305,7 +304,7 @@ def get_class_construct(self, name):
         """Returns the class datatype for name if it exists.
         Raises an error otherwise
         """
-        result = self.namespace.find(name, 'cls_constructs')
+        result = self.scope.find(name, 'cls_constructs')
 
         if result is None:
             msg = 'class construct {} not found'.format(name)
@@ -315,30 +314,9 @@ def get_class_construct(self, name):
         else:
             return result
 
-
-    def get_import(self, name):
-        """
-        Search for an import with the given name in the current namespace.
-        Return None if not found.
-        """
-
-        imp = None
-
-        container = self.namespace
-        while container:
-
-            if name in container.imports['imports']:
-                imp =  container.imports['imports'][name]
-                break
-            container = container.parent_scope
-
-
-        return imp
-
-
     def insert_import(self, name, target, storage_name = None):
         """
-        Create and insert a new import in namespace if it's not defined
+        Create and insert a new import in scope if it's not defined
         otherwise append target to existing import.
 
         Parameters
@@ -354,9 +332,9 @@ def insert_import(self, name, target, storage_name = None):
         source = _get_name(name)
         if storage_name is None:
             storage_name = source
-        imp = self.get_import(source)
+        imp = self.scope.find(source, 'imports')
         if imp is None:
-            imp = self.get_import(storage_name)
+            imp = self.scope.find(storage_name, 'imports')
 
         if imp is not None:
             imp_source = imp.source
@@ -368,13 +346,15 @@ def insert_import(self, name, target, storage_name = None):
                               bounding_box=(self._current_fst_node.lineno, self._current_fst_node.col_offset),
                               severity='fatal')
         else:
-            container = self.namespace.imports
+            container = self.scope.imports
             container['imports'][storage_name] = Import(source, target, True)
 
 
-    def get_header(self, name):
-        """."""
-        container = self.namespace
+    def get_headers(self, name):
+        """ Get all headers in the scope which reference the
+        requested name
+        """
+        container = self.scope
         headers = []
         while container:
             if name in container.headers:
@@ -386,61 +366,6 @@ def get_header(self, name):
         return headers
 
 
-    def create_new_function_scope(self, name, decorators):
-        """
-        Create a new Scope object for a Python function with the given name,
-        and attach any decorators' information to the scope. The new scope is
-        a child of the current one, and can be accessed from the dictionary of
-        its children using the function name as key.
-
-        Before returning control to the caller, the current scope (stored in
-        self._namespace) is changed to the one just created, and the function's
-        name is stored in self._current_function.
-
-        Parameters
-        ----------
-        name : str
-            Function's name, used as a key to retrieve the new scope.
-
-        decorators : dict
-            Decorators attached to FunctionDef object at syntactic stage.
-
-        Returns
-        -------
-        child : Scope
-            The newly created scope
-
-        """
-        child = self.namespace.new_child_scope(name, decorators=decorators)
-
-        self._namespace = child
-        if self._current_function:
-            name = DottedName(self._current_function, name)
-        self._current_function = name
-
-        return child
-
-    def exit_function_scope(self):
-
-        self._namespace = self._namespace.parent_scope
-        if isinstance(self._current_function, DottedName):
-
-            name = self._current_function.name[:-1]
-            if len(name)>1:
-                name = DottedName(*name)
-            else:
-                name = name[0]
-        else:
-            name = None
-        self._current_function = name
-
-    def create_new_loop_scope(self):
-        self._namespace = self._namespace.create_new_loop_scope()
-        return self._namespace
-
-    def exit_loop_scope(self):
-        self._namespace = self._namespace.parent_scope
-
     #=======================================================
     #              Utility functions
     #=======================================================
@@ -594,7 +519,7 @@ def _infere_type(self, expr, **settings):
 
         elif isinstance(expr, ConstructorCall):
             cls_name = expr.func.cls_name
-            cls = self.namespace.find(cls_name, 'classes')
+            cls = self.scope.find(cls_name, 'classes')
 
             dtype = self.get_class_construct(cls_name)()
 
@@ -643,7 +568,7 @@ def is_literal_index(a):
                 else:
                     return self._visit(var[indices[0]][indices[1:]])
             else:
-                tmp_var = PyccelSymbol(self.get_new_name())
+                tmp_var = PyccelSymbol(self.scope.get_new_name())
                 assign = Assign(tmp_var, var)
                 assign.set_fst(expr.fst)
                 self._additional_exprs[-1].append(self._visit(assign))
@@ -923,7 +848,7 @@ def _create_variable(self, name, dtype, rhs, d_lhs):
             is_homogeneous = True
             elem_d_lhs_ref = None
             for i,r in enumerate(iterable):
-                elem_name = self.get_new_name( name + '_' + str(i) )
+                elem_name = self.scope.get_new_name( name + '_' + str(i) )
                 elem_d_lhs = self._infere_type( r )
 
                 self._ensure_target( r, elem_d_lhs )
@@ -1013,6 +938,8 @@ def _assign_lhs_variable(self, lhs, d_var, rhs, new_expressions, is_augassign, *
         elif isinstance(lhs, PyccelSymbol):
 
             name = lhs
+            if lhs == '_':
+                name = self.scope.get_new_name()
             dtype = d_var.pop('datatype')
 
             d_lhs = d_var.copy()
@@ -1025,7 +952,7 @@ def _assign_lhs_variable(self, lhs, d_var, rhs, new_expressions, is_augassign, *
             if var is None:
 
                 # Update variable's dictionary with information from function decorators
-                decorators = self._namespace.decorators
+                decorators = self.scope.decorators
                 if decorators:
                     if 'stack_array' in decorators:
                         if name in decorators['stack_array']:
@@ -1035,16 +962,18 @@ def _assign_lhs_variable(self, lhs, d_var, rhs, new_expressions, is_augassign, *
                         if lhs in decorators['allow_negative_index']:
                             d_lhs.update(allows_negative_indexes=True)
 
+                new_name = self.scope.get_expected_name(name)
+
                 # Create new variable
-                lhs = self._create_variable(name, dtype, rhs, d_lhs)
+                lhs = self._create_variable(new_name, dtype, rhs, d_lhs)
 
                 # Add variable to scope
-                self.namespace.insert_variable(lhs, name)
+                self.scope.insert_variable(lhs, name)
 
                 # ...
                 # Add memory allocation if needed
                 if lhs.allocatable:
-                    if self._namespace.is_loop:
+                    if self.scope.is_loop:
                         # Array defined in a loop may need reallocation at every cycle
                         errors.report(ARRAY_DEFINITION_IN_LOOP, symbol=name,
                             severity='warning', blocker=False,
@@ -1081,7 +1010,7 @@ def _assign_lhs_variable(self, lhs, d_var, rhs, new_expressions, is_augassign, *
                 # ...
 
                 # We cannot allow the definition of a stack array in a loop
-                if lhs.is_stack_array and self._namespace.is_loop:
+                if lhs.is_stack_array and self.scope.is_loop:
                     errors.report(STACK_ARRAY_DEFINITION_IN_LOOP, symbol=name,
                         severity='error', blocker=False,
                         bounding_box=(self._current_fst_node.lineno,
@@ -1100,27 +1029,11 @@ def _assign_lhs_variable(self, lhs, d_var, rhs, new_expressions, is_augassign, *
 
             # Variable already exists
             else:
-                precision = d_var.get('precision',None)
-                internal_precision = default_precision[str(dtype)] if precision == -1 else precision
-
-                # TODO improve check type compatibility
-                if not hasattr(var, 'dtype'):
-                    errors.report(INCOMPATIBLE_TYPES_IN_ASSIGNMENT.format('<module>', dtype),
-                            symbol='{}={}'.format(name, str(rhs)),
-                            bounding_box=(self._current_fst_node.lineno, self._current_fst_node.col_offset),
-                            severity='fatal', blocker=False)
-
-                elif not is_augassign and var.is_ndarray and isinstance(rhs, (Variable, IndexedElement)) and var.allocatable:
+                if not is_augassign and var.is_ndarray and isinstance(rhs, (Variable, IndexedElement)) and var.allocatable:
                     errors.report(ASSIGN_ARRAYS_ONE_ANOTHER,
                         bounding_box=(self._current_fst_node.lineno,
                             self._current_fst_node.col_offset),
-                                severity='error', symbol=lhs)
-
-                elif not is_augassign and var.is_ndarray and var.is_target:
-                    errors.report(ARRAY_ALREADY_IN_USE,
-                        bounding_box=(self._current_fst_node.lineno,
-                            self._current_fst_node.col_offset),
-                                severity='error', symbol=var.name)
+                                severity='error', symbol=var)
 
                 elif var.is_ndarray and var.is_pointer and isinstance(rhs, NumpyNewArray):
                     errors.report(INVALID_POINTER_REASSIGN,
@@ -1128,109 +1041,8 @@ def _assign_lhs_variable(self, lhs, d_var, rhs, new_expressions, is_augassign, *
                             self._current_fst_node.col_offset),
                                 severity='error', symbol=var.name)
 
-                elif var.is_ndarray and var.is_pointer:
-                    # we allow pointers to be reassigned multiple times
-                    # pointers reassigning need to call free_pointer func
-                    # to remove memory leaks
-                    new_expressions.append(Deallocate(var))
-
-                elif not is_augassign and (str(dtype) != str(var.dtype) or \
-                        internal_precision != get_final_precision(var)):
-                    # Get type name from cast function (handles precision implicitly)
-                    try:
-                        d1 = DtypePrecisionToCastFunction[var.dtype.name][var.precision].name
-                    except KeyError:
-                        d1 = str(var.dtype)
-                    try:
-                        d2 = DtypePrecisionToCastFunction[dtype.name][precision].name
-                    except KeyError:
-                        d2 = str(var.dtype)
-
-                    errors.report(INCOMPATIBLE_TYPES_IN_ASSIGNMENT.format(d1, d2),
-                        symbol='{}={}'.format(name, str(rhs)),
-                        bounding_box=(self._current_fst_node.lineno, self._current_fst_node.col_offset),
-                        severity='error', blocker=False)
-
-                elif not is_augassign:
-
-                    rank  = getattr(var, 'rank' , 'None')
-                    order = getattr(var, 'order', 'None')
-                    shape = getattr(var, 'shape', 'None')
-
-                    if (d_var['rank'] != rank) or (rank > 1 and d_var['order'] != order):
-
-                        txt = '|{name}| {dtype}{old} <-> {dtype}{new}'
-                        format_shape = lambda s: "" if len(s)==0 else s
-                        txt = txt.format(name=name, dtype=dtype, old=format_shape(var.shape),
-                            new=format_shape(d_var['shape']))
-                        errors.report(INCOMPATIBLE_REDEFINITION, symbol=txt,
-                            bounding_box=(self._current_fst_node.lineno, self._current_fst_node.col_offset),
-                            severity='error', blocker=False)
-
-                    elif d_var['shape'] != shape:
-
-                        if var.is_argument:
-                            errors.report(ARRAY_IS_ARG, symbol=var,
-                                severity='error', blocker=False,
-                                bounding_box=(self._current_fst_node.lineno,
-                                    self._current_fst_node.col_offset))
-
-                        elif var.is_stack_array:
-                            errors.report(INCOMPATIBLE_REDEFINITION_STACK_ARRAY, symbol=name,
-                                severity='error', blocker=False,
-                                bounding_box=(self._current_fst_node.lineno,
-                                    self._current_fst_node.col_offset))
-
-                        else:
-                            var.set_changeable_shape()
-                            previous_allocations = var.get_direct_user_nodes(lambda p: isinstance(p, Allocate))
-                            if not previous_allocations:
-                                errors.report("PYCCEL INTERNAL ERROR : Variable exists already, but it has never been allocated",
-                                        symbol=var, severity='fatal')
-
-                            last_allocation = previous_allocations[-1]
-
-                            # Find outermost IfSection of last allocation
-                            last_alloc_ifsection = last_allocation.get_user_nodes(IfSection)
-                            alloc_ifsection = last_alloc_ifsection[-1] if last_alloc_ifsection else None
-                            while len(last_alloc_ifsection)>0:
-                                alloc_ifsection = last_alloc_ifsection[-1]
-                                last_alloc_ifsection = alloc_ifsection.get_user_nodes(IfSection)
-
-                            ifsection_has_if = len(alloc_ifsection.get_direct_user_nodes(
-                                                                lambda x: isinstance(x,If))) == 1 \
-                                            if alloc_ifsection else False
-
-                            if alloc_ifsection and not ifsection_has_if:
-                                status = last_allocation.status
-                            elif last_allocation.get_user_nodes((If, For, While)):
-                                status='unknown'
-                            else:
-                                status='allocated'
-                            new_expressions.append(Allocate(var,
-                                shape=d_var['shape'], order=d_var['order'],
-                                status=status))
-
-                            if status != 'unallocated':
-                                errors.report(ARRAY_REALLOCATION, symbol=name,
-                                    severity='warning', blocker=False,
-                                    bounding_box=(self._current_fst_node.lineno,
-                                        self._current_fst_node.col_offset))
-                    else:
-                        # Same shape as before
-                        previous_allocations = var.get_direct_user_nodes(lambda p: isinstance(p, Allocate))
-
-                        if previous_allocations and previous_allocations[-1].get_user_nodes(IfSection) \
-                                and not previous_allocations[-1].get_user_nodes((If)):
-                            # If previously allocated in If still under construction
-                            status = previous_allocations[-1].status
-
-                            new_expressions.append(Allocate(var,
-                                shape=d_var['shape'], order=d_var['order'],
-                                status=status))
-
-                if var.precision == -1 and precision != var.precision:
-                    var.use_exact_precision()
+                else:
+                    self._ensure_infered_type_matches_existing(dtype, d_var, var, is_augassign, new_expressions)
 
                 # in the case of elemental, lhs is not of the same dtype as
                 # var.
@@ -1242,12 +1054,12 @@ def _assign_lhs_variable(self, lhs, d_var, rhs, new_expressions, is_augassign, *
         elif isinstance(lhs, DottedName):
 
             dtype = d_var.pop('datatype')
-            name = lhs.name[:-1]
+            name = self.scope.get_expected_name(lhs.name[-1])
             if self._current_function == '__init__':
 
                 cls      = self.get_variable('self')
                 cls_name = str(cls.cls_base.name)
-                cls      = self.namespace.find(cls_name, 'classes')
+                cls      = self.scope.find(cls_name, 'classes')
 
                 attributes = cls.attributes
                 parent     = cls.superclass
@@ -1266,10 +1078,10 @@ def _assign_lhs_variable(self, lhs, d_var, rhs, new_expressions, is_augassign, *
                 member = self._create_variable(n_name, dtype, rhs, d_lhs)
                 lhs    = member.clone(member.name, new_class = DottedVariable, lhs = var)
 
-                # update the attributes of the class and push it to the namespace
+                # update the attributes of the class and push it to the scope
                 attributes += [member]
                 new_cls = ClassDef(cls_name, attributes, [], superclass=parent)
-                self.namespace.parent_scope.insert_class(new_cls)
+                self.scope.parent_scope.insert_class(new_cls)
             else:
                 lhs = self._visit(lhs, **settings)
         else:
@@ -1277,13 +1089,156 @@ def _assign_lhs_variable(self, lhs, d_var, rhs, new_expressions, is_augassign, *
 
         return lhs
 
-    def _assign_GeneratorComprehension(self, expr, **settings):
+    def _ensure_infered_type_matches_existing(self, dtype, d_var, var, is_augassign, new_expressions):
+        """
+        Ensure that the inferred type of the new variable, matches the existing variable (which has the
+        same name). If this is not the case then errors are raised preventing pyccel reaching the codegen
+        stage.
+        This function also handles any reallocations caused by differing shapes between the two objects.
+        These allocations/deallocations are saved in the list new_expressions
+
+        Parameters
+        ----------
+        dtype : DataType
+                The inferred DataType
+        d_var : dict
+                The inferred information about the variable. Usually created by the _infere_type function
+        var   : Variable
+                The existing variable
+        is_augassign : bool
+                A boolean indicating if the assign statement is an augassign (tests are less strict)
+        new_expressions : list
+                A list to which any new expressions created are appended
+        """
+        precision = d_var.get('precision',None)
+        internal_precision = default_precision[str(dtype)] if precision == -1 else precision
+
+        # TODO improve check type compatibility
+        if not hasattr(var, 'dtype'):
+            errors.report(INCOMPATIBLE_TYPES_IN_ASSIGNMENT.format('<module>', dtype),
+                    symbol='{}={}'.format(var.name, dtype),
+                    bounding_box=(self._current_fst_node.lineno, self._current_fst_node.col_offset),
+                    severity='fatal', blocker=False)
+
+        elif not is_augassign and var.is_ndarray and var.is_target:
+            errors.report(ARRAY_ALREADY_IN_USE,
+                bounding_box=(self._current_fst_node.lineno,
+                    self._current_fst_node.col_offset),
+                        severity='error', symbol=var.name)
+
+        elif var.is_ndarray and var.is_pointer:
+            # we allow pointers to be reassigned multiple times
+            # pointers reassigning need to call free_pointer func
+            # to remove memory leaks
+            new_expressions.append(Deallocate(var))
+
+        elif not is_augassign and (str(dtype) != str(var.dtype) or \
+                internal_precision != get_final_precision(var)):
+            # Get type name from cast function (handles precision implicitly)
+            try:
+                d1 = DtypePrecisionToCastFunction[var.dtype.name][var.precision].name
+            except KeyError:
+                d1 = str(var.dtype)
+            try:
+                d2 = DtypePrecisionToCastFunction[dtype.name][precision].name
+            except KeyError:
+                d2 = str(var.dtype)
+
+            errors.report(INCOMPATIBLE_TYPES_IN_ASSIGNMENT.format(d1, d2),
+                symbol=var,
+                bounding_box=(self._current_fst_node.lineno, self._current_fst_node.col_offset),
+                severity='error', blocker=False)
+
+        elif not is_augassign:
+
+            rank  = getattr(var, 'rank' , 'None')
+            order = getattr(var, 'order', 'None')
+            shape = getattr(var, 'shape', 'None')
+
+            if (d_var['rank'] != rank) or (rank > 1 and d_var['order'] != order):
+
+                txt = '|{name}| {dtype}{old} <-> {dtype}{new}'
+                format_shape = lambda s: "" if len(s)==0 else s
+                txt = txt.format(name=var.name, dtype=dtype, old=format_shape(var.shape),
+                    new=format_shape(d_var['shape']))
+                errors.report(INCOMPATIBLE_REDEFINITION, symbol=txt,
+                    bounding_box=(self._current_fst_node.lineno, self._current_fst_node.col_offset),
+                    severity='error', blocker=False)
+
+            elif d_var['shape'] != shape:
+
+                if var.is_argument:
+                    errors.report(ARRAY_IS_ARG, symbol=var,
+                        severity='error', blocker=False,
+                        bounding_box=(self._current_fst_node.lineno,
+                            self._current_fst_node.col_offset))
+
+                elif var.is_stack_array:
+                    errors.report(INCOMPATIBLE_REDEFINITION_STACK_ARRAY, symbol=var.name,
+                        severity='error', blocker=False,
+                        bounding_box=(self._current_fst_node.lineno,
+                            self._current_fst_node.col_offset))
+
+                else:
+                    var.set_changeable_shape()
+                    previous_allocations = var.get_direct_user_nodes(lambda p: isinstance(p, Allocate))
+                    if not previous_allocations:
+                        errors.report("PYCCEL INTERNAL ERROR : Variable exists already, but it has never been allocated",
+                                symbol=var, severity='fatal')
+
+                    last_allocation = previous_allocations[-1]
+
+                    # Find outermost IfSection of last allocation
+                    last_alloc_ifsection = last_allocation.get_user_nodes(IfSection)
+                    alloc_ifsection = last_alloc_ifsection[-1] if last_alloc_ifsection else None
+                    while len(last_alloc_ifsection)>0:
+                        alloc_ifsection = last_alloc_ifsection[-1]
+                        last_alloc_ifsection = alloc_ifsection.get_user_nodes(IfSection)
+
+                    ifsection_has_if = len(alloc_ifsection.get_direct_user_nodes(
+                                                        lambda x: isinstance(x,If))) == 1 \
+                                    if alloc_ifsection else False
+
+                    if alloc_ifsection and not ifsection_has_if:
+                        status = last_allocation.status
+                    elif last_allocation.get_user_nodes((If, For, While)):
+                        status='unknown'
+                    else:
+                        status='allocated'
+                    new_expressions.append(Allocate(var,
+                        shape=d_var['shape'], order=d_var['order'],
+                        status=status))
+
+                    if status != 'unallocated':
+                        errors.report(ARRAY_REALLOCATION, symbol=var.name,
+                            severity='warning', blocker=False,
+                            bounding_box=(self._current_fst_node.lineno,
+                                self._current_fst_node.col_offset))
+            else:
+                # Same shape as before
+                previous_allocations = var.get_direct_user_nodes(lambda p: isinstance(p, Allocate))
+
+                if previous_allocations and previous_allocations[-1].get_user_nodes(IfSection) \
+                        and not previous_allocations[-1].get_user_nodes((If)):
+                    # If previously allocated in If still under construction
+                    status = previous_allocations[-1].status
+
+                    new_expressions.append(Allocate(var,
+                        shape=d_var['shape'], order=d_var['order'],
+                        status=status))
+
+        if var.precision == -1 and precision != var.precision:
+            var.use_exact_precision()
+
+    def _assign_GeneratorComprehension(self, lhs_name, expr, **settings):
         """
         Visit the GeneratorComprehension node creating all necessary expressions
         for its definition
 
         Parameters
         ----------
+        lhs_name : str
+                    The name to which the expression is assigned
         expr : GeneratorComprehension
 
         Results
@@ -1291,16 +1246,13 @@ def _assign_GeneratorComprehension(self, expr, **settings):
         new_expr : CodeBlock
                    CodeBlock containing the semantic version of the GeneratorComprehension node
         """
-
         result   = expr.expr
-        lhs_name = _get_name(expr.lhs)
-        lhs  = self.check_for_variable(lhs_name)
 
         loop = expr.loops
         nlevels = 0
         # Create throw-away variable to help obtain result type
-        index   = Variable('int',self.get_new_name('to_delete'), is_temp=True)
-        self.namespace.insert_variable(index)
+        index   = Variable('int',self.scope.get_new_name('to_delete'), is_temp=True)
+        self.scope.insert_variable(index)
         new_expr = []
         while isinstance(loop, For):
             nlevels+=1
@@ -1327,13 +1279,15 @@ def _assign_GeneratorComprehension(self, expr, **settings):
                 gens = set(loop_elem.get_attribute_nodes(GeneratorComprehension))
                 if len(gens)==1:
                     gen = gens.pop()
-                    assign = self._visit(Assign(gen.lhs, gen, fst=gen.fst))
+                    assert isinstance(gen.lhs, PyccelSymbol) and gen.lhs.is_temp
+                    gen_lhs = self.scope.get_new_name() if gen.lhs.is_temp else gen.lhs
+                    assign = self._visit(Assign(gen_lhs, gen, fst=gen.fst))
                     new_expr.append(assign)
                     loop.substitute(gen, assign.lhs)
                     loop_elem = loop.body.body[0]
             loop = loop_elem
-        # Remove the throw-away variable from the namespace
-        self.namespace.remove_variable(index)
+        # Remove the throw-away variable from the scope
+        self.scope.remove_variable(index)
 
         # Visit result expression (correctly defined as iterator
         # objects exist in the scope despite not being defined)
@@ -1345,8 +1299,14 @@ def _assign_GeneratorComprehension(self, expr, **settings):
         d_var = self._infere_type(result, **settings)
         dtype = d_var.pop('datatype')
         d_var['is_temp'] = expr.lhs.is_temp
-        lhs = Variable(dtype, lhs_name, **d_var)
-        self.namespace.insert_variable(lhs)
+
+        lhs  = self.check_for_variable(lhs_name)
+        if lhs:
+            self._ensure_infered_type_matches_existing(dtype, d_var, lhs, False, new_expr)
+        else:
+            lhs_name = self.scope.get_expected_name(lhs_name)
+            lhs = Variable(dtype, lhs_name, **d_var)
+            self.scope.insert_variable(lhs)
 
         # Iterate over the loops
         # This provides the definitions of iterators as well
@@ -1359,7 +1319,9 @@ def _assign_GeneratorComprehension(self, expr, **settings):
             loop = loops[0]
             for _ in range(nlevels-1):
                 loop = loop.body.body[0]
-            _ = [loop.body.insert2body(e, back=False) for e in new_expr]
+            for e in new_expr:
+                loop.body.insert2body(e, back=False)
+                e.loops[-1].scope.update_parent_scope(loop.scope, is_loop = True)
 
         if isinstance(expr, FunctionalSum):
             val = LiteralInteger(0)
@@ -1430,7 +1392,7 @@ def _visit_Module(self, expr, **settings):
         init_func_body    = []
         mod_name = expr.name
         self._mod_name = mod_name
-        prog_name = self.get_new_name('prog_'+expr.name)
+        prog_name = self.scope.get_new_name('prog_'+expr.name)
 
         for b in body:
             if isinstance(b, If):
@@ -1447,7 +1409,7 @@ def _visit_Module(self, expr, **settings):
             else:
                 init_func_body.append(b)
 
-        variables = self.get_variables(self.namespace)
+        variables = self.get_variables(self.scope)
         init_func = None
         free_func = None
         program   = None
@@ -1456,22 +1418,25 @@ def _visit_Module(self, expr, **settings):
 
         if not all(isinstance(l, comment_types) for l in init_func_body):
             # If there are any initialisation statements then create an initialisation function
-            init_var = Variable(NativeBool(), self.get_new_name('initialised'),
+            init_var = Variable(NativeBool(), self.scope.get_new_name('initialised'),
                                 is_private=True)
-            init_func_name = self.get_new_name(expr.name+'__init')
+            init_func_name = self.scope.get_new_name(expr.name+'__init')
+            # Ensure that the function is correctly defined within the namespaces
+            init_scope = self.create_new_function_scope(init_func_name)
+            for b in init_func_body:
+                if isinstance(b, ScopedNode):
+                    b.scope.update_parent_scope(init_scope, is_loop = True)
             init_func_body = If(IfSection(PyccelNot(init_var),
                                 init_func_body+[Assign(init_var, LiteralTrue())]))
-            # Ensure that the function is correctly defined within the namespaces
-            scope = self.create_new_function_scope(init_func_name, [])
             init_func = FunctionDef(init_func_name, [], [], [init_func_body],
-                    global_vars = variables, scope=scope)
+                    global_vars = variables, scope=init_scope)
             self.exit_function_scope()
             self.insert_function(init_func)
 
         if init_func:
-            free_func_name = self.get_new_name(expr.name+'__free')
+            free_func_name = self.scope.get_new_name(expr.name+'__free')
             deallocs = self._garbage_collector(init_func.body)
-            pyccelised_imports = [imp for imp_name, imp in self._namespace.imports['imports'].items() \
+            pyccelised_imports = [imp for imp_name, imp in self.scope.imports['imports'].items() \
                              if imp_name in self.d_parsers]
 
             import_frees = [self.d_parsers[imp.source].semantic_parser.ast.free_func for imp in pyccelised_imports \
@@ -1488,7 +1453,7 @@ def _visit_Module(self, expr, **settings):
                 free_func_body = If(IfSection(init_var,
                     import_free_calls+deallocs+[Assign(init_var, LiteralFalse())]))
                 # Ensure that the function is correctly defined within the namespaces
-                scope = self.create_new_function_scope(free_func_name, [])
+                scope = self.create_new_function_scope(free_func_name)
                 free_func = FunctionDef(free_func_name, [], [], [free_func_body],
                                     global_vars = variables, scope = scope)
                 self.exit_function_scope()
@@ -1496,7 +1461,7 @@ def _visit_Module(self, expr, **settings):
 
         funcs = []
         interfaces = []
-        for f in self.namespace.functions.values():
+        for f in self.scope.functions.values():
             if isinstance(f, FunctionDef):
                 funcs.append(f)
             elif isinstance(f, Interface):
@@ -1508,10 +1473,10 @@ def _visit_Module(self, expr, **settings):
         if self.is_header_file:
             # ARA : issue-999
             is_external = self.metavars.get('external', False)
-            for name, headers in self.namespace.headers.items():
+            for name, headers in self.scope.headers.items():
                 if all(isinstance(v, FunctionHeader) and \
                         not isinstance(v, MethodHeader) for v in headers):
-                    F = self.namespace.find(name, 'functions')
+                    F = self.scope.find(name, 'functions')
                     if F is None:
                         func_defs = [vi for v in headers for vi in v.create_definition(is_external=is_external)]
                         if len(func_defs) == 1:
@@ -1532,9 +1497,9 @@ def _visit_Module(self, expr, **settings):
                     init_func = init_func,
                     free_func = free_func,
                     interfaces=interfaces,
-                    classes=self.namespace.classes.values(),
-                    imports=self._namespace.imports['imports'].values(),
-                    scope=self.namespace)
+                    classes=self.scope.classes.values(),
+                    imports=self.scope.imports['imports'].values(),
+                    scope=self.scope)
         container = self._program_namespace.imports
         container['imports'][mod_name] = Import(mod_name, mod)
 
@@ -1725,9 +1690,9 @@ def _visit_PyccelSymbol(self, expr, **settings):
         var = self.check_for_variable(name)
 
         if var is None:
-            var = self.namespace.find(name, 'functions')
+            var = self.scope.find(name, 'functions')
         if var is None:
-            var = self.namespace.find(name, 'symbolic_functions')
+            var = self.scope.find(name, 'symbolic_functions')
         if var is None:
             var = python_builtin_datatype(name)
 
@@ -1766,22 +1731,22 @@ def _visit_DottedName(self, expr, **settings):
         if isinstance(first, Module):
 
             if rhs_name in first:
-                imp = self.get_import(_get_name(lhs))
+                imp = self.scope.find(_get_name(lhs), 'imports')
 
                 new_name = rhs_name
                 if imp is not None:
                     new_name = imp.find_module_target(rhs_name)
                     if new_name is None:
-                        new_name = self.get_new_name(rhs_name)
+                        new_name = self.scope.get_new_name(rhs_name)
 
                         # Save the import target that has been used
                         imp.define_target(AsName(first[rhs_name], PyccelSymbol(new_name)))
                 elif isinstance(rhs, FunctionCall):
-                    self.namespace.imports['functions'][new_name] = first[rhs_name]
+                    self.scope.imports['functions'][new_name] = first[rhs_name]
                 elif isinstance(rhs, ConstructorCall):
-                    self.namespace.imports['classes'][new_name] = first[rhs_name]
+                    self.scope.imports['classes'][new_name] = first[rhs_name]
                 elif isinstance(rhs, Variable):
-                    self.namespace.imports['variables'][new_name] = rhs
+                    self.scope.imports['variables'][new_name] = rhs
 
                 if isinstance(rhs, FunctionCall):
                     # If object is a function
@@ -1827,7 +1792,7 @@ def _visit_DottedName(self, expr, **settings):
                         bounding_box=(self._current_fst_node.lineno,
                             self._current_fst_node.col_offset),
                         severity='fatal')
-            macro = self.namespace.find(rhs_name, 'macros')
+            macro = self.scope.find(rhs_name, 'macros')
             if macro is not None:
                 master = macro.master
                 args = rhs.args
@@ -1881,7 +1846,7 @@ def _visit_DottedName(self, expr, **settings):
         # look for a macro
         else:
 
-            macro = self.namespace.find(rhs_name, 'macros')
+            macro = self.scope.find(rhs_name, 'macros')
 
             # Macro
             if isinstance(macro, MacroVariable):
@@ -1947,7 +1912,7 @@ def _visit_Lambda(self, expr, **settings):
         funcs = expr.expr.get_attribute_nodes(FunctionCall)
         for func in funcs:
             name = _get_name(func)
-            f = self.namespace.find(name, 'symbolic_functions')
+            f = self.scope.find(name, 'symbolic_functions')
             if f is None:
                 errors.report(UNDEFINED_LAMBDA_FUNCTION, symbol=name,
                     bounding_box=(self._current_fst_node.lineno, self._current_fst_node.col_offset),
@@ -1967,23 +1932,23 @@ def _visit_FunctionCall(self, expr, **settings):
         if hasattr(self, annotation_method):
             return getattr(self, annotation_method)(expr, **settings)
 
-        func     = self.namespace.find(name, 'functions')
+        func     = self.scope.find(name, 'functions')
 
         args = self._handle_function_args(expr.args, **settings)
 
         if name == 'lambdify':
-            args = self.namespace.find(str(expr.args[0]), 'symbolic_functions')
+            args = self.scope.find(str(expr.args[0]), 'symbolic_functions')
         F = pyccel_builtin_function(expr, args)
 
         if F is not None:
             return F
 
-        elif self.namespace.find(name, 'cls_constructs'):
+        elif self.scope.find(name, 'cls_constructs'):
 
             # TODO improve the test
-            # we must not invoke the namespace like this
+            # we must not invoke the scope like this
 
-            cls = self.namespace.find(name, 'classes')
+            cls = self.scope.find(name, 'classes')
             d_methods = cls.methods_as_dict
             method = d_methods.pop('__init__', None)
 
@@ -2009,13 +1974,13 @@ def _visit_FunctionCall(self, expr, **settings):
             # first we check if it is a macro, in this case, we will create
             # an appropriate FunctionCall
 
-            macro = self.namespace.find(name, 'macros')
+            macro = self.scope.find(name, 'macros')
             if macro is not None:
                 func = macro.master.funcdef
                 name = _get_name(func.name)
                 args = macro.apply(args)
             else:
-                func = self.namespace.find(name, 'functions')
+                func = self.scope.find(name, 'functions')
             if func is None:
                 return errors.report(UNDEFINED_FUNCTION, symbol=name,
                         bounding_box=(self._current_fst_node.lineno, self._current_fst_node.col_offset),
@@ -2040,11 +2005,12 @@ def _visit_Assign(self, expr, **settings):
 
         # Steps before visiting
         if isinstance(rhs, GeneratorComprehension):
-            genexp = self._assign_GeneratorComprehension(rhs, **settings)
+            rhs.substitute(rhs.lhs, lhs)
+            genexp = self._assign_GeneratorComprehension(_get_name(lhs), rhs, **settings)
             if isinstance(expr, AugAssign):
                 new_expressions.append(genexp)
                 rhs = genexp.lhs
-            elif rhs.lhs == lhs:
+            elif genexp.lhs.name == lhs:
                 return genexp
             else:
                 new_expressions.append(genexp)
@@ -2063,15 +2029,12 @@ def _visit_Assign(self, expr, **settings):
                 false_section = IfSection(LiteralTrue(), [self._visit(assign_false)])
                 return If(true_section, false_section)
 
-
         # Visit object
         if isinstance(rhs, FunctionCall):
             name = rhs.funcdef
-            macro = self.namespace.find(name, 'macros')
+            macro = self.scope.find(name, 'macros')
             if macro is None:
                 rhs = self._visit(rhs, **settings)
-            elif isinstance(lhs, PyccelSymbol) and lhs.is_temp:
-                return self._visit(rhs, **settings)
             else:
 
                 # TODO check types from FunctionDef
@@ -2119,7 +2082,7 @@ def _visit_Assign(self, expr, **settings):
         elif isinstance(rhs, DottedVariable):
             var = rhs.rhs
             name = _get_name(var)
-            macro = self.namespace.find(name, 'macros')
+            macro = self.scope.find(name, 'macros')
             if macro is None:
                 rhs = self._visit(rhs, **settings)
             else:
@@ -2134,7 +2097,7 @@ def _visit_Assign(self, expr, **settings):
                         lhs = Variable(dtype, lhs.name, **d_var)
                         var = self.check_for_variable(lhs.name)
                         if var is None:
-                            self.namespace.insert_variable(lhs)
+                            self.scope.insert_variable(lhs)
 
                     name = macro.name
                     if not sympy_iterable(lhs):
@@ -2186,7 +2149,7 @@ def _visit_Assign(self, expr, **settings):
                     d_var = self._infere_type(stmt, **settings)
                     dtype = d_var.pop('datatype')
                     lhs = Variable(dtype, name , **d_var)
-                    self.namespace.insert_variable(lhs)
+                    self.scope.insert_variable(lhs)
 
             if isinstance(expr, Assign):
                 stmt = Assign(lhs, stmt)
@@ -2244,7 +2207,7 @@ def _visit_Assign(self, expr, **settings):
         elif isinstance(rhs, PythonMap):
 
             name = str(rhs.args[0])
-            func = self.namespace.find(name, 'functions')
+            func = self.scope.find(name, 'functions')
 
             if func is None:
                 errors.report(UNDEFINED_FUNCTION, symbol=name,
@@ -2271,7 +2234,7 @@ def _visit_Assign(self, expr, **settings):
 
                 if name.startswith('Pyccel'):
                     name = name[6:]
-                    d['cls_base'] = self.namespace.find(name, 'classes')
+                    d['cls_base'] = self.scope.find(name, 'classes')
                     #TODO: Avoid writing the default variables here
                     d['is_pointer'] = d_var.get('is_target',False) or d_var.get('is_pointer',False)
 
@@ -2351,7 +2314,7 @@ def _visit_Assign(self, expr, **settings):
             func  = _get_name(rhs.args[0])
             alloc = Assign(lhs, NumpyZeros(lhs.shape, lhs.dtype))
             alloc.set_fst(fst)
-            index_name = self.get_new_name(expr)
+            index_name = self.scope.get_new_name(expr)
             index = Variable('int',index_name, is_temp=True)
             range_ = FunctionCall('range', (FunctionCall('len', lhs,),))
             name  = _get_name(lhs)
@@ -2427,7 +2390,7 @@ def _visit_Assign(self, expr, **settings):
                 # in a symbolic assign, the rhs can be a lambda expression
                 # it is then treated as a def node
 
-                F = self.namespace.find(l, 'symbolic_functions')
+                F = self.scope.find(l, 'symbolic_functions')
                 if F is None:
                     self.insert_symbolic_function(new_expr)
                 else:
@@ -2457,7 +2420,8 @@ def _visit_For(self, expr, **settings):
         iterator_d_var = self._infere_type(start)
 
         if iterable.num_loop_counters_required:
-            indices = [Variable('int', self.get_new_name(), is_temp=True) for i in range(iterable.num_loop_counters_required)]
+            indices = [Variable('int', 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):
@@ -2495,26 +2459,30 @@ def _visit_For(self, expr, **settings):
 
         body = [self._visit(i, **settings) for i in body]
 
-        local_vars = list(self.namespace.variables.values())
         self.exit_loop_scope()
 
         if isinstance(iterable.iterable, Product):
             for_expr = body
-            for t, r in zip(target, iterable.get_range()):
-                for_expr = For(t, r, for_expr, local_vars=local_vars, scope=scope)
+            scopes = self.scope.create_product_loop_scope(scope, len(target))
+
+            for t, r, s in zip(target, iterable.get_range(), scopes[::-1]):
+                for_expr = For(t, r, for_expr, scope=s)
                 for_expr.end_annotation = expr.end_annotation
                 for_expr = [for_expr]
             for_expr = for_expr[0]
         else:
-            for_expr = For(target, iterable, body, local_vars=local_vars, scope=scope)
+            for_expr = For(target, iterable, body, scope=scope)
             for_expr.end_annotation = expr.end_annotation
         return for_expr
 
 
     def _visit_FunctionalFor(self, expr, **settings):
+        old_index   = expr.index
+        new_index   = self.scope.get_new_name()
+        expr.substitute(old_index, new_index)
 
         target  = expr.expr
-        index   = expr.index
+        index   = new_index
         indices = expr.indices
         dims    = []
         body    = expr.loops[1]
@@ -2524,7 +2492,7 @@ def _visit_FunctionalFor(self, expr, **settings):
         #scope = self.create_new_loop_scope()
 
         # The symbols created to represent unknown valued objects are temporary
-        tmp_used_names = self.used_names.copy()
+        tmp_used_names = self.scope.all_used_symbols.copy()
         while isinstance(body, For):
 
             stop  = None
@@ -2563,13 +2531,13 @@ def _visit_FunctionalFor(self, expr, **settings):
                 errors.report(PYCCEL_RESTRICTION_TODO,
                               bounding_box=(self._current_fst_node.lineno, self._current_fst_node.col_offset),
                               severity='fatal')
-            existing_var = self.namespace.find(var.name, 'variables')
+            existing_var = self.scope.find(var.name, 'variables')
             if existing_var:
                 if self._infere_type(existing_var, **settings) != dvar:
                     errors.report("Variable {} already exists with different type".format(var),
                             symbol = expr, severity='error')
             else:
-                self.namespace.insert_variable(var)
+                self.scope.insert_variable(var)
             step.invalidate_node()
             step  = pyccel_to_sympy(step , idx_subs, tmp_used_names)
             start.invalidate_node()
@@ -2690,9 +2658,14 @@ def _visit_FunctionalFor(self, expr, **settings):
     def _visit_GeneratorComprehension(self, expr, **settings):
         lhs = self.check_for_variable(expr.lhs)
         if lhs is None:
-            creation = self._visit(Assign(expr.lhs,expr, fst=expr.fst))
+            if expr.lhs.is_temp:
+                lhs = PyccelSymbol(self.scope.get_new_name(), is_temp=True)
+            else:
+                lhs = expr.lhs
+
+            creation = self._visit(Assign(lhs, expr, fst=expr.fst))
             self._additional_exprs[-1].append(creation)
-            return self.get_variable(expr.lhs)
+            return self.get_variable(lhs)
         else:
             return lhs
 
@@ -2701,10 +2674,9 @@ def _visit_While(self, expr, **settings):
         scope = self.create_new_loop_scope()
         test = self._visit(expr.test, **settings)
         body = self._visit(expr.body, **settings)
-        local_vars = list(self.namespace.variables.values())
         self.exit_loop_scope()
 
-        return While(test, body, local_vars=local_vars, scope=scope)
+        return While(test, body, scope=scope)
 
     def _visit_IfSection(self, expr, **settings):
         condition = expr.condition
@@ -2717,16 +2689,6 @@ def _visit_IfSection(self, expr, **settings):
         if prog_check:
             cond = InProgram()
             self.change_to_program_scope()
-
-            mod_container = self._module_namespace
-            prog_container = self._program_namespace
-            mod_imports = mod_container.imports
-            for k in mod_imports:
-                prog_container.imports[k].update(mod_container.imports[k])
-            prog_container.imports['variables'].update(mod_container.variables)
-            prog_container.imports['functions'].update(mod_container.functions)
-            prog_container.imports['classes'].update(mod_container.classes)
-            self._program_namespace.cls_constructs.update(self._module_namespace.cls_constructs)
         else:
             cond = self._visit(expr.condition)
         body = self._visit(expr.body)
@@ -2770,7 +2732,7 @@ def _visit_If(self, expr, **settings):
     def _visit_IfTernaryOperator(self, expr, **settings):
         value_true  = self._visit(expr.value_true, **settings)
         if value_true.rank > 0 or value_true.dtype is NativeString():
-            lhs = self.get_new_variable()
+            lhs = PyccelSymbol(self.scope.get_new_name(), is_temp=True)
             # Temporarily deactivate type checks to construct syntactic assigns
             pyccel_stage.set_stage('syntactic')
             assign_true  = Assign(lhs, expr.value_true, fst = expr.fst)
@@ -2799,24 +2761,24 @@ def _visit_VariableHeader(self, expr, **settings):
         d_var.pop('is_func')
 
         var = Variable(dtype, name, **d_var)
-        self.namespace.insert_variable(var)
+        self.scope.insert_variable(var)
         return expr
 
     def _visit_FunctionHeader(self, expr, **settings):
         # TODO should we return it and keep it in the AST?
         expr.clear_user_nodes()
-        self.namespace.insert_header(expr)
+        self.scope.insert_header(expr)
         return expr
 
     def _visit_Template(self, expr, **settings):
         expr.clear_user_nodes()
-        self.namespace.insert_template(expr)
+        self.scope.insert_template(expr)
         return expr
 
     def _visit_ClassHeader(self, expr, **settings):
         # TODO should we return it and keep it in the AST?
         expr.clear_user_nodes()
-        self.namespace.insert_header(expr)
+        self.scope.insert_header(expr)
         return expr
 
     def _visit_Return(self, expr, **settings):
@@ -2826,7 +2788,7 @@ def _visit_Return(self, expr, **settings):
         if isinstance(f_name, DottedName):
             f_name = f_name.name[-1]
 
-        return_vars = self.namespace.find(f_name, 'functions').results
+        return_vars = self.scope.find(f_name, 'functions').results
         assigns     = []
         for v,r in zip(return_vars, results):
             if not (isinstance(r, PyccelSymbol) and r == (v.name if isinstance(v, Variable) else v)):
@@ -2866,17 +2828,17 @@ def _visit_FunctionDef(self, expr, **settings):
             errors.report(UNUSED_DECORATORS, symbol=', '.join(not_used), severity='warning')
 
         args_number = len(expr.arguments)
-        templates = self.namespace.find_all('templates')
+        templates = self.scope.find_all('templates')
         if decorators['template']:
             # Load templates dict from decorators dict
             templates.update(decorators['template']['template_dict'])
 
         tmp_headers = expr.headers
         if cls_name:
-            tmp_headers += self.get_header(cls_name + '.' + name)
+            tmp_headers += self.get_headers(cls_name + '.' + name)
             args_number -= 1
         else:
-            tmp_headers += self.get_header(name)
+            tmp_headers += self.get_headers(name)
         for header in tmp_headers:
             if all(header.dtypes != hd.dtypes for hd in headers):
                 headers.append(header)
@@ -2896,7 +2858,7 @@ def _visit_FunctionDef(self, expr, **settings):
         if len(headers) == 0:
             # check if a header is imported from a header file
             # TODO improve in the case of multiple headers ( interface )
-            func       = self.namespace.find(name, 'functions')
+            func       = self.scope.find(name, 'functions')
             if func and func.is_header:
                 interfaces = [func]
 
@@ -2942,7 +2904,6 @@ def _visit_FunctionDef(self, expr, **settings):
         for i, m in enumerate(interfaces):
             args           = []
             results        = []
-            local_vars     = []
             global_vars    = []
             imports        = []
             arg            = None
@@ -2951,15 +2912,16 @@ def _visit_FunctionDef(self, expr, **settings):
 
             if len(interfaces) > 1:
                 name = interface_name + '_' + str(i).zfill(2)
-            scope = self.create_new_function_scope(name, decorators)
+            scope = self.create_new_function_scope(name, decorators = decorators,
+                    used_symbols = expr.scope.local_used_symbols.copy())
 
             if cls_name and str(arguments[0].name) == 'self':
                 arg       = arguments[0]
                 arguments = arguments[1:]
                 dt        = self.get_class_construct(cls_name)()
-                cls_base  = self.namespace.find(cls_name, 'classes')
+                cls_base  = self.scope.find(cls_name, 'classes')
                 var       = Variable(dt, 'self', cls_base=cls_base)
-                self.namespace.insert_variable(var)
+                self.scope.insert_variable(var)
 
             if arguments:
                 for (a, ah) in zip(arguments, m.arguments):
@@ -2984,7 +2946,7 @@ def _visit_FunctionDef(self, expr, **settings):
                         if d_var['rank']>0:
                             d_var['cls_base'] = NumpyArrayClass
 
-                        if 'allow_negative_index' in self._namespace.decorators:
+                        if 'allow_negative_index' in self.scope.decorators:
                             if a.name in decorators['allow_negative_index']:
                                 d_var.update(allows_negative_indexes=True)
                         if a.has_default:
@@ -3011,7 +2973,7 @@ def _visit_FunctionDef(self, expr, **settings):
                     if isinstance(a_new, FunctionAddress):
                         self.insert_function(a_new)
                     else:
-                        self.namespace.insert_variable(a_new)
+                        self.scope.insert_variable(a_new)
             results = expr.results
             if header_results:
                 new_results = []
@@ -3020,7 +2982,7 @@ def _visit_FunctionDef(self, expr, **settings):
                     d_var = self._infere_type(ah, **settings)
                     dtype = d_var.pop('datatype')
                     a_new = Variable(dtype, a, **d_var)
-                    self.namespace.insert_variable(a_new)
+                    self.scope.insert_variable(a_new)
                     new_results.append(a_new)
 
                 results = new_results
@@ -3046,36 +3008,35 @@ def _visit_FunctionDef(self, expr, **settings):
 
             if arg and cls_name:
                 dt       = self.get_class_construct(cls_name)()
-                cls_base = self.namespace.find(cls_name, 'classes')
+                cls_base = self.scope.find(cls_name, 'classes')
                 var      = Variable(dt, 'self', cls_base=cls_base)
                 args     = [FunctionDefArgument(var)] + args
 
             arg_vars = [a.var for a in args]
 
             # Determine local and global variables
-            local_vars  = [v for v in self.get_variables(self.namespace)              if v not in arg_vars + results]
-            global_vars = [v for v in self.get_variables(self.namespace.parent_scope) if v not in arg_vars + results + local_vars]
+            global_vars = list(self.get_variables(self.scope.parent_scope))
             global_vars = [g for g in global_vars if body.is_user_of(g)]
 
             # get the imports
-            imports   = self.namespace.imports['imports'].values()
+            imports   = self.scope.imports['imports'].values()
             imports   = list(set(imports))
 
             # remove the FunctionDef from the function scope
             # TODO improve func_ is None in the case of an interface
-            func_     = self.namespace.functions.pop(name, None)
+            func_     = self.scope.functions.pop(name, None)
             is_recursive = False
             # check if the function is recursive if it was called on the same scope
             if func_ and func_.is_recursive:
                 is_recursive = True
 
-            sub_funcs = [i for i in self.namespace.functions.values() if not i.is_header and not isinstance(i, FunctionAddress)]
+            sub_funcs = [i for i in self.scope.functions.values() if not i.is_header and not isinstance(i, FunctionAddress)]
 
-            func_args = [i for i in self.namespace.functions.values() if isinstance(i, FunctionAddress)]
+            func_args = [i for i in self.scope.functions.values() if isinstance(i, FunctionAddress)]
             if func_args:
                 func_interfaces.append(Interface('', func_args, is_argument = True))
 
-            namespace_imports = self._namespace.imports
+            namespace_imports = self.scope.imports
             self.exit_function_scope()
 
             # ... computing inout arguments
@@ -3118,7 +3079,7 @@ def _visit_FunctionDef(self, expr, **settings):
                     while not(intent) and i_fa < n_fa:
                         fa = d_apps[a][i_fa]
                         f_name = fa.funcdef.name
-                        func = self.namespace.find(f_name, 'functions')
+                        func = self.scope.find(f_name, 'functions')
 
                         j = list(fa.args).index(a)
                         intent = func.arguments_inout[j]
@@ -3149,7 +3110,6 @@ def _visit_FunctionDef(self, expr, **settings):
                     severity='fatal')
 
             func_kwargs = {
-                    'local_vars':local_vars,
                     'global_vars':global_vars,
                     'cls_name':cls_name,
                     'is_pure':is_pure,
@@ -3166,7 +3126,7 @@ def _visit_FunctionDef(self, expr, **settings):
                     }
             if is_inline:
                 func_kwargs['namespace_imports'] = namespace_imports
-                global_funcs = [f for f in body.get_attribute_nodes(FunctionDef) if self.namespace.find(f.name, 'functions')]
+                global_funcs = [f for f in body.get_attribute_nodes(FunctionDef) if self.scope.find(f.name, 'functions')]
                 func_kwargs['global_funcs'] = global_funcs
                 cls = InlineFunctionDef
             else:
@@ -3180,12 +3140,12 @@ def _visit_FunctionDef(self, expr, **settings):
                 recursive_func_obj.invalidate_node()
 
             if cls_name:
-                cls = self.namespace.find(cls_name, 'classes')
+                cls = self.scope.find(cls_name, 'classes')
                 methods = list(cls.methods) + [func]
 
                 # update the class methods
 
-                self.namespace.insert_class(ClassDef(cls_name, cls.attributes,
+                self.scope.insert_class(ClassDef(cls_name, cls.attributes,
                         methods, superclass=cls.superclass))
 
             funcs += [func]
@@ -3206,11 +3166,11 @@ def _visit_FunctionDef(self, expr, **settings):
 #        TODO move this to codegen
 #        if vec_func:
 #           self._visit_FunctionDef(vec_func, **settings)
-#           vec_func = self.namespace.functions.pop(vec_name)
+#           vec_func = self.scope.functions.pop(vec_name)
 #           if isinstance(funcs, Interface):
 #               funcs = list(funcs.funcs)+[vec_func]
 #           else:
-#               self.namespace.sons_scopes['sc_'+ name] = self.namespace.sons_scopes[name]
+#               self.scope.sons_scopes['sc_'+ name] = self.scope.sons_scopes[name]
 #               funcs = funcs.rename('sc_'+ name)
 #               funcs = [funcs, vec_func]
 #           funcs = Interface(name, funcs)
@@ -3235,7 +3195,7 @@ def _visit_PythonPrint(self, expr, **settings):
         if is_symbolic(args[0]):
             _args = []
             for a in args:
-                f = self.namespace.find(a.name, 'symbolic_functions')
+                f = self.scope.find(a.name, 'symbolic_functions')
                 if f is None:
                     _args.append(a)
                 else:
@@ -3254,13 +3214,14 @@ def _visit_ClassDef(self, expr, **settings):
 
         name = expr.name
         name = name.replace("'", '')
+        scope = self.create_new_class_scope(name, used_symbols=expr.scope.local_used_symbols)
         methods = list(expr.methods)
         parent = expr.superclass
         interfaces = []
 
         # remove quotes for str representation
         cls = ClassDef(name, [], [], superclass=parent)
-        self.namespace.insert_class(cls)
+        self.scope.insert_class(cls)
         const = None
 
         for (i, method) in enumerate(methods):
@@ -3269,7 +3230,7 @@ def _visit_ClassDef(self, expr, **settings):
             if m_name == '__init__':
                 self._visit_FunctionDef(method, **settings)
                 methods.pop(i)
-                const = self.namespace.functions.pop(m_name)
+                const = self.scope.functions.pop(m_name)
                 break
 
 
@@ -3283,27 +3244,29 @@ def _visit_ClassDef(self, expr, **settings):
         for i in methods:
             self._visit_FunctionDef(i, **settings)
             m_name = i.name.replace("'", '')
-            m = self.namespace.functions.pop(m_name)
+            m = self.scope.functions.pop(m_name)
             ms.append(m)
 
         methods = [const] + ms
-        header = self.get_header(name)
+        header = self.get_headers(name)
 
         if not header:
             errors.report(PYCCEL_MISSING_HEADER, symbol=name,
                    bounding_box=(self._current_fst_node.lineno, self._current_fst_node.col_offset),
                    severity='fatal', blocker=self.blocking)
 
-        attributes = self.namespace.find(name, 'classes').attributes
+        attributes = self.scope.find(name, 'classes').attributes
 
         for i in methods:
             if isinstance(i, Interface):
                 methods.remove(i)
                 interfaces += [i]
 
+        self.exit_class_scope()
+
         cls = ClassDef(name, attributes, methods,
-              interfaces=interfaces, superclass=parent)
-        self.namespace.insert_class(cls)
+              interfaces=interfaces, superclass=parent, scope=scope)
+        self.scope.insert_class(cls)
 
         return EmptyNode()
 
@@ -3363,9 +3326,9 @@ def _visit_Import(self, expr, **settings):
 
         # TODO - must have a dict where to store things that have been
         #        imported
-        #      - should not use namespace
+        #      - should not use scope
 
-        container = self.namespace.imports
+        container = self.scope.imports
 
         result = EmptyNode()
 
@@ -3380,7 +3343,7 @@ def _visit_Import(self, expr, **settings):
             imports = pyccel_builtin_import(expr)
 
             def _insert_obj(location, target, obj):
-                F = self.namespace.find(target)
+                F = self.scope.find(target)
 
                 if obj is F:
                     errors.report(FOUND_DUPLICATED_IMPORT,
@@ -3432,7 +3395,7 @@ def _insert_obj(location, target, obj):
                 targets = {i.target if isinstance(i,AsName) else i:None for i in expr.target}
                 names = [i.name if isinstance(i,AsName) else i for i in expr.target]
                 for entry in ['variables', 'classes', 'functions']:
-                    d_son = getattr(p.namespace, entry)
+                    d_son = getattr(p.scope, entry)
                     for t,n in zip(targets.keys(),names):
                         if n in d_son:
                             e = d_son[n]
@@ -3450,8 +3413,8 @@ def _insert_obj(location, target, obj):
                 container['variables'][source_target] = mod
                 targets = [AsName(mod, source_target)]
 
-            self.namespace.cls_constructs.update(p.namespace.cls_constructs)
-            self.namespace.macros.update(p.namespace.macros)
+            self.scope.cls_constructs.update(p.scope.cls_constructs)
+            self.scope.macros.update(p.scope.macros)
 
             # ... meta variables
 
@@ -3471,7 +3434,7 @@ def _insert_obj(location, target, obj):
 
             if import_init:
                 old_name = import_init.name
-                new_name = self.get_new_name(old_name)
+                new_name = self.scope.get_new_name(old_name)
                 targets.append(AsName(import_init, new_name))
 
                 if new_name != old_name:
@@ -3481,24 +3444,24 @@ def _insert_obj(location, target, obj):
 
             if import_free:
                 old_name = import_free.name
-                new_name = self.get_new_name(old_name)
+                new_name = self.scope.get_new_name(old_name)
                 targets.append(AsName(import_free, new_name))
 
                 if new_name != old_name:
                     import_free = import_free.clone(new_name)
 
-            expr = Import(source, targets)
+            mod = p.semantic_parser.ast
 
             if __import_all__:
-                mod = p.semantic_parser.ast
-                expr = Import(source_target, AsName(mod, __module_name__))
+                expr = Import(source_target, AsName(mod, __module_name__), mod=mod)
                 container['imports'][source_target] = expr
 
             elif __module_name__:
-                expr = Import(__module_name__, targets)
+                expr = Import(__module_name__, targets, mod=mod)
                 container['imports'][source_target] = expr
 
             elif not __ignore_at_import__:
+                expr = Import(source, targets, mod=mod)
                 container['imports'][source_target] = expr
 
         return result
@@ -3506,6 +3469,7 @@ def _insert_obj(location, target, obj):
 
 
     def _visit_With(self, expr, **settings):
+        scope = self.create_new_loop_scope()
 
         domaine = self._visit(expr.test, **settings)
         parent  = domaine.cls_base
@@ -3515,7 +3479,9 @@ def _visit_With(self, expr, **settings):
                    severity='fatal', blocker=self.blocking)
 
         body = self._visit(expr.body, **settings)
-        return With(domaine, body).block
+
+        self.exit_loop_scope()
+        return With(domaine, body, scope).block
 
 
 
@@ -3523,9 +3489,9 @@ def _visit_MacroFunction(self, expr, **settings):
         # we change here the master name to its FunctionDef
 
         f_name = expr.master
-        header = self.get_header(f_name)
+        header = self.get_headers(f_name)
         if not header:
-            func = self.namespace.find(f_name, 'functions')
+            func = self.scope.find(f_name, 'functions')
             if func is None:
                 errors.report(MACRO_MISSING_HEADER_OR_FUNC,
                 symbol=f_name,severity='error', blocker=self.blocking,
@@ -3553,7 +3519,7 @@ def get_arg(func_arg, master_arg):
         master = FunctionCall(func, master_args)
         macro   = MacroFunction(name, args, master, master_args,
                                 results=expr.results, results_shapes=expr.results_shapes)
-        self.namespace.insert_macro(macro)
+        self.scope.insert_macro(macro)
 
         return macro
 
@@ -3568,7 +3534,7 @@ def _visit_MacroVariable(self, expr, **settings):
             errors.report(PYCCEL_RESTRICTION_TODO,
                           bounding_box=(self._current_fst_node.lineno, self._current_fst_node.col_offset),
                           severity='fatal')
-        header = self.get_header(master)
+        header = self.get_headers(master)
         if header is None:
             var = self.get_variable(master)
         else:
@@ -3577,7 +3543,7 @@ def _visit_MacroVariable(self, expr, **settings):
                 # TODO -> Said: must handle interface
 
         expr = MacroVariable(expr.name, var)
-        self.namespace.insert_macro(expr)
+        self.scope.insert_macro(expr)
         return expr
 
     def _visit_StarredArguments(self, expr, **settings):
@@ -3604,7 +3570,7 @@ def _visit_NumpyMatmul(self, expr, **settings):
         raise ValueError('Expecting an argument for filename')
 
     parser = SyntaxParser(filename)
-#    print(parser.namespace)
+#    print(parser.scope)
     parser = SemanticParser(parser)
 #    print(parser.ast)
 #    parser.view_namespace('variables')
diff --git a/pyccel/parser/syntactic.py b/pyccel/parser/syntactic.py
index e2d4e2f4dd..cf319d1c88 100644
--- a/pyccel/parser/syntactic.py
+++ b/pyccel/parser/syntactic.py
@@ -43,7 +43,6 @@
 from pyccel.ast.core import StarredArguments
 from pyccel.ast.core import CodeBlock
 from pyccel.ast.core import IndexedElement
-from pyccel.ast.core import create_variable
 
 from pyccel.ast.bitwise_operators import PyccelRShift, PyccelLShift, PyccelBitXor, PyccelBitOr, PyccelBitAnd, PyccelInvert
 from pyccel.ast.operators import PyccelPow, PyccelAdd, PyccelMul, PyccelDiv, PyccelMod, PyccelFloorDiv
@@ -65,10 +64,10 @@
 
 from pyccel.ast.internals import Slice, PyccelSymbol, PyccelInternalFunction
 
+from pyccel.parser.base        import BasicParser
 from pyccel.parser.extend_tree import extend_tree
-from pyccel.parser.base import BasicParser
-from pyccel.parser.utilities import read_file
-from pyccel.parser.utilities import get_default_path
+from pyccel.parser.utilities   import read_file
+from pyccel.parser.utilities   import get_default_path
 
 from pyccel.parser.syntax.headers import parse as hdr_parse
 from pyccel.parser.syntax.openmp  import parse as omp_parse
@@ -127,8 +126,8 @@ def __init__(self, inputs, **kwargs):
 
             code = read_file(inputs)
 
-        self._code  = code
-        self._scope = []
+        self._code    = code
+        self._context = []
 
         self.load()
 
@@ -136,6 +135,7 @@ def __init__(self, inputs, **kwargs):
         self._fst           = tree
         self._used_names    = set(get_name(a) for a in ast.walk(self._fst) if isinstance(a, (ast.Name, ast.arg, ast.FunctionDef)))
         self._dummy_counter = 1
+        self._in_lhs_assign = False
 
         self.parse(verbose=True)
         self.dump()
@@ -205,11 +205,11 @@ def _visit(self, stmt):
         cls = type(stmt)
         syntax_method = '_visit_' + cls.__name__
         if hasattr(self, syntax_method):
-            self._scope.append(stmt)
+            self._context.append(stmt)
             result = getattr(self, syntax_method)(stmt)
             if isinstance(result, Basic) and result.fst is None and isinstance(stmt, ast.AST):
                 result.set_fst(stmt)
-            self._scope.pop()
+            self._context.pop()
             return result
 
         # Unknown object, we raise an error.
@@ -225,15 +225,14 @@ def _visit_Module(self, stmt):
         name = os.path.splitext(os.path.basename(self._filename))[0]
 
         body = [b for i in body for b in (i.body if isinstance(i, CodeBlock) else [i])]
-        return Module(name, [], [], program = CodeBlock(body))
+        return Module(name, [], [], program = CodeBlock(body), scope=self.scope)
 
     def _visit_Expr(self, stmt):
         val = self._visit(stmt.value)
         if not isinstance(val, (CommentBlock, PythonPrint)):
             # Collect any results of standalone expressions
             # into a variable to avoid errors in C/Fortran
-            tmp_var,_ = create_variable(self._used_names)
-            val = Assign(tmp_var, val)
+            val = Assign(PyccelSymbol('_', is_temp=True), val)
         return val
 
     def _visit_Tuple(self, stmt):
@@ -273,7 +272,7 @@ def _visit_str(self, stmt):
 
     def _visit_Str(self, stmt):
         val =  stmt.s
-        if isinstance(self._scope[-2], ast.Expr):
+        if isinstance(self._context[-2], ast.Expr):
             return CommentBlock(val)
         return LiteralString(val)
 
@@ -291,7 +290,9 @@ def _visit_Num(self, stmt):
 
     def _visit_Assign(self, stmt):
 
+        self._in_lhs_assign = True
         lhs = self._visit(stmt.targets)
+        self._in_lhs_assign = False
         if len(lhs)==1:
             lhs = lhs[0]
         else:
@@ -344,6 +345,7 @@ def _visit_arguments(self, stmt):
                                             value = self._visit(d))
                                         for a,d in zip(stmt.args[n_expl:],stmt.defaults)]
             arguments              = positional_args + valued_arguments
+            self.scope.insert_symbols(a.arg for a in stmt.args)
 
         if stmt.kwonlyargs:
             for a,d in zip(stmt.kwonlyargs,stmt.kw_defaults):
@@ -354,6 +356,7 @@ def _visit_arguments(self, stmt):
                             value=val, kwonly=True)
 
                 arguments.append(arg)
+                self.scope.insert_symbol(a.arg)
 
         return arguments
 
@@ -398,11 +401,14 @@ def _visit_NameConstant(self, stmt):
 
 
     def _visit_Name(self, stmt):
+        if self._in_lhs_assign:
+            self.scope.insert_symbol(stmt.id)
         return PyccelSymbol(stmt.id)
 
     def _treat_import_source(self, source, level):
         source = '.'*level + source
         if source.count('.') == 0:
+            self.scope.insert_symbol(source)
             source = PyccelSymbol(source)
         else:
             source = DottedName(*source.split('.'))
@@ -584,9 +590,10 @@ def _visit_FunctionDef(self, stmt):
         name = self._visit(stmt.name)
         name = name.replace("'", '')
 
+        scope = self.create_new_function_scope(name)
+
         arguments    = self._visit(stmt.args)
 
-        local_vars   = []
         global_vars  = []
         headers      = []
         template    = {}
@@ -789,18 +796,18 @@ def fill_types(ls):
             if pyccel_symbol and same_results and name_available:
                 result_name = r0
             else:
-                result_name, result_counter = create_variable(self._used_names, \
-                            prefix = 'Out', counter = result_counter)
+                result_name, result_counter = self.scope.get_new_incremented_symbol('Out', result_counter)
 
             results.append(result_name)
 
+        self.exit_function_scope()
+
         cls = InlineFunctionDef if is_inline else FunctionDef
         func = cls(
                name,
                arguments,
                results,
                body,
-               local_vars=local_vars,
                global_vars=global_vars,
                is_pure=is_pure,
                is_elemental=is_elemental,
@@ -808,20 +815,23 @@ def fill_types(ls):
                imports=imports,
                decorators=decorators,
                headers=headers,
-               doc_string=doc_string)
+               doc_string=doc_string,
+               scope=scope)
 
         return func
 
     def _visit_ClassDef(self, stmt):
 
         name = stmt.name
+        scope = self.create_new_class_scope(name)
         methods = [self._visit(i) for i in stmt.body if isinstance(i, ast.FunctionDef)]
         for i in methods:
             i.cls_name = name
         attributes = methods[0].arguments
         parent = [self._visit(i) for i in stmt.bases]
+        self.exit_class_scope()
         expr = ClassDef(name=name, attributes=attributes,
-                        methods=methods, superclass=parent)
+                        methods=methods, superclass=parent, scope=scope)
 
         # we set the fst to keep track of needed information for errors
 
@@ -859,6 +869,8 @@ def _visit_Index(self, stmt):
 
     def _visit_Attribute(self, stmt):
         val  = self._visit(stmt.value)
+        if self._in_lhs_assign:
+            self.scope.insert_symbol(stmt.attr)
         attr = PyccelSymbol(stmt.attr)
         return DottedName(val, attr)
 
@@ -900,37 +912,52 @@ def _visit_keyword(self, stmt):
 
     def _visit_For(self, stmt):
 
+        scope = self.create_new_loop_scope()
+
+        self._in_lhs_assign = True
         iterator = self._visit(stmt.target)
+        self._in_lhs_assign = False
         iterable = self._visit(stmt.iter)
         body = self._visit(stmt.body)
-        expr = For(iterator, iterable, body)
+
+        self.exit_loop_scope()
+
+        expr = For(iterator, iterable, body, scope=scope)
         return expr
 
     def _visit_comprehension(self, stmt):
 
+        scope = self.create_new_loop_scope()
+
+        self._in_lhs_assign = True
         iterator = self._visit(stmt.target)
+        self._in_lhs_assign = False
         iterable = self._visit(stmt.iter)
-        expr = For(iterator, iterable, [])
+
+        self.exit_loop_scope()
+
+        expr = For(iterator, iterable, [], scope=scope)
         return expr
 
     def _visit_ListComp(self, stmt):
 
         result = self._visit(stmt.elt)
+
         generators = list(self._visit(stmt.generators))
 
-        if not isinstance(self._scope[-2],ast.Assign):
+        if not isinstance(self._context[-2],ast.Assign):
             errors.report(PYCCEL_RESTRICTION_LIST_COMPREHENSION_ASSIGN,
                           symbol = stmt,
                           severity='error')
-            lhs = self.get_new_variable()
+            lhs = PyccelSymbol('_', is_temp=True)
         else:
-            lhs = self._visit(self._scope[-2].targets)
+            lhs = self._visit(self._context[-2].targets)
             if len(lhs)==1:
                 lhs = lhs[0]
             else:
                 raise NotImplementedError("A list comprehension cannot be unpacked")
 
-        index = self.get_new_variable()
+        index = PyccelSymbol('_', is_temp=True)
 
         args = [index]
         target = IndexedElement(lhs, *args)
@@ -949,6 +976,7 @@ def _visit_ListComp(self, stmt):
             generators[-1].insert2body(F)
             indices.append(generators[-1].target)
         indices = indices[::-1]
+
         return FunctionalFor([assign1, generators[-1]],target.rhs, target.lhs,
                              indices, index)
 
@@ -957,19 +985,19 @@ def _visit_GeneratorExp(self, stmt):
         result = self._visit(stmt.elt)
 
         generators = self._visit(stmt.generators)
-        parent = self._scope[-2]
+        parent = self._context[-2]
         if not isinstance(parent, ast.Call):
             raise NotImplementedError("GeneratorExp is not the argument of a function call")
 
         name = self._visit(parent.func)
 
-        grandparent = self._scope[-3]
+        grandparent = self._context[-3]
         if isinstance(grandparent, ast.Assign):
             if len(grandparent.targets) != 1:
                 raise NotImplementedError("Cannot unpack function with generator expression argument")
             lhs = self._visit(grandparent.targets[0])
         else:
-            lhs = self.get_new_variable()
+            lhs = PyccelSymbol('_', is_temp=True)
 
         body = result
         if name == 'sum':
@@ -985,6 +1013,7 @@ def _visit_GeneratorExp(self, stmt):
             indices.append(generators[-1].target)
             generators[-1].insert2body(body)
             body = generators.pop()
+
         indices = indices[::-1]
         if name == 'sum':
             expr = FunctionalSum(body, result, lhs, indices)
@@ -1026,9 +1055,14 @@ def _visit_IfExp(self, stmt):
 
     def _visit_While(self, stmt):
 
+        scope = self.create_new_loop_scope()
+
         test = self._visit(stmt.test)
         body = self._visit(stmt.body)
-        return While(test, body)
+
+        self.exit_loop_scope()
+
+        return While(test, body, scope=scope)
 
     def _visit_Assert(self, stmt):
         expr = self._visit(stmt.test)
diff --git a/pyccel/parser/syntax/openacc.py b/pyccel/parser/syntax/openacc.py
index 9f90299a43..d319f274a2 100644
--- a/pyccel/parser/syntax/openacc.py
+++ b/pyccel/parser/syntax/openacc.py
@@ -553,7 +553,7 @@ def expr(self):
         if DEBUG:
             print("> AccAsync: expr")
 
-        # TODO check if variable exist in namespace
+        # TODO check if variable exist in scope
         args = ', '.join(str(arg) for arg in self.args)
         return 'async({})'.format(args)
 
@@ -587,7 +587,7 @@ def expr(self):
         if DEBUG:
             print("> AccBind: expr")
 
-        # TODO check if variable exist in namespace
+        # TODO check if variable exist in scope
         arg = self.arg
         return 'bind({})'.format(str(arg))
 
@@ -606,7 +606,7 @@ def expr(self):
         if DEBUG:
             print("> AccCache: expr")
 
-        # TODO check if variable exist in namespace
+        # TODO check if variable exist in scope
         args = ', '.join(str(arg) for arg in self.args)
         return 'cache({})'.format(args)
 
@@ -642,7 +642,7 @@ def expr(self):
         if DEBUG:
             print("> AccCopy: expr")
 
-        # TODO check if variable exist in namespace
+        # TODO check if variable exist in scope
         args = ', '.join(str(arg) for arg in self.args)
         return 'copy({})'.format(args)
 
@@ -661,7 +661,7 @@ def expr(self):
         if DEBUG:
             print("> AccCopyin: expr")
 
-        # TODO check if variable exist in namespace
+        # TODO check if variable exist in scope
         args = ', '.join(str(arg) for arg in self.args)
         return 'copyin({})'.format(args)
 
@@ -680,7 +680,7 @@ def expr(self):
         if DEBUG:
             print("> AccCopyout: expr")
 
-        # TODO check if variable exist in namespace
+        # TODO check if variable exist in scope
         args = ', '.join(str(arg) for arg in self.args)
         return 'copyout({})'.format(args)
 
@@ -699,7 +699,7 @@ def expr(self):
         if DEBUG:
             print("> AccCreate: expr")
 
-        # TODO check if variable exist in namespace
+        # TODO check if variable exist in scope
         args = ', '.join(str(arg) for arg in self.args)
         return 'create({})'.format(args)
 
@@ -735,7 +735,7 @@ def expr(self):
         if DEBUG:
             print("> AccDefaultAsync: expr")
 
-        # TODO check if variable exist in namespace
+        # TODO check if variable exist in scope
         args = ', '.join(str(arg) for arg in self.args)
         return 'default_async({})'.format(args)
 
@@ -754,7 +754,7 @@ def expr(self):
         if DEBUG:
             print("> AccDelete: expr")
 
-        # TODO check if variable exist in namespace
+        # TODO check if variable exist in scope
         args = ', '.join(str(arg) for arg in self.args)
         return 'delete({})'.format(args)
 
@@ -773,7 +773,7 @@ def expr(self):
         if DEBUG:
             print("> AccDevice: expr")
 
-        # TODO check if variable exist in namespace
+        # TODO check if variable exist in scope
         args = ', '.join(str(arg) for arg in self.args)
         return 'device({})'.format(args)
 
@@ -809,7 +809,7 @@ def expr(self):
         if DEBUG:
             print("> AccDevicePtr: expr")
 
-        # TODO check if variable exist in namespace
+        # TODO check if variable exist in scope
         args = ', '.join(str(arg) for arg in self.args)
         return 'deviceptr({})'.format(args)
 
@@ -828,7 +828,7 @@ def expr(self):
         if DEBUG:
             print("> AccDeviceResident: expr")
 
-        # TODO check if variable exist in namespace
+        # TODO check if variable exist in scope
         args = ', '.join(str(arg) for arg in self.args)
         return 'device_resident({})'.format(args)
 
@@ -847,7 +847,7 @@ def expr(self):
         if DEBUG:
             print("> AccDeviceType: expr")
 
-        # TODO check if variable exist in namespace
+        # TODO check if variable exist in scope
         args = ', '.join(str(arg) for arg in self.args)
         return 'device_type({})'.format(args)
 
@@ -881,7 +881,7 @@ def expr(self):
         if DEBUG:
             print("> AccFirstPrivate: expr")
 
-        # TODO check if variable exist in namespace
+        # TODO check if variable exist in scope
         args = ', '.join(str(arg) for arg in self.args)
         return 'firstprivate({})'.format(args)
 
@@ -900,7 +900,7 @@ def expr(self):
         if DEBUG:
             print("> AccGang: expr")
 
-        # TODO check if variable exist in namespace
+        # TODO check if variable exist in scope
         args = ', '.join(str(a.arg) for a in self.args)
         return 'gang({})'.format(args)
 
@@ -919,7 +919,7 @@ def expr(self):
         if DEBUG:
             print("> AccHost: expr")
 
-        # TODO check if variable exist in namespace
+        # TODO check if variable exist in scope
         args = ', '.join(str(arg) for arg in self.args)
         return 'host({})'.format(args)
 
@@ -938,7 +938,7 @@ def expr(self):
         if DEBUG:
             print("> AccIf: expr")
 
-        # TODO check if variable exist in namespace
+        # TODO check if variable exist in scope
         cond = self.cond
         return 'if({})'.format(str(cond))
 
@@ -987,7 +987,7 @@ def expr(self):
         if DEBUG:
             print("> AccLink: expr")
 
-        # TODO check if variable exist in namespace
+        # TODO check if variable exist in scope
         args = ', '.join(str(arg) for arg in self.args)
         return 'link({})'.format(args)
 
@@ -1055,7 +1055,7 @@ def expr(self):
         if DEBUG:
             print("> AccPresent: expr")
 
-        # TODO check if variable exist in namespace
+        # TODO check if variable exist in scope
         args = ', '.join(str(arg) for arg in self.args)
         return 'present({})'.format(args)
 
@@ -1074,7 +1074,7 @@ def expr(self):
         if DEBUG:
             print("> AccPrivate: expr")
 
-        # TODO check if variable exist in namespace
+        # TODO check if variable exist in scope
         args = ', '.join(str(arg) for arg in self.args)
         return 'private({})'.format(args)
 
@@ -1094,7 +1094,7 @@ def expr(self):
         if DEBUG:
             print("> AccReduction: expr")
 
-        # TODO check if variable exist in namespace
+        # TODO check if variable exist in scope
         op   = self.op
         args = ', '.join(str(arg) for arg in self.args)
         return 'reduction({0}: {1})'.format(op, args)
@@ -1114,7 +1114,7 @@ def expr(self):
         if DEBUG:
             print("> AccSelf: expr")
 
-        # TODO check if variable exist in namespace
+        # TODO check if variable exist in scope
         args = ', '.join(str(arg) for arg in self.args)
         return 'self({})'.format(args)
 
@@ -1148,7 +1148,7 @@ def expr(self):
         if DEBUG:
             print("> AccTile: expr")
 
-        # TODO check if variable exist in namespace
+        # TODO check if variable exist in scope
         args = ', '.join(str(arg) for arg in self.args)
         return 'tile({})'.format(args)
 
@@ -1167,7 +1167,7 @@ def expr(self):
         if DEBUG:
             print("> AccUseDevice: expr")
 
-        # TODO check if variable exist in namespace
+        # TODO check if variable exist in scope
         args = ', '.join(str(arg) for arg in self.args)
         return 'use_device({})'.format(args)
 
@@ -1186,7 +1186,7 @@ def expr(self):
         if DEBUG:
             print("> AccVector: expr")
 
-        # TODO check if variable exist in namespace
+        # TODO check if variable exist in scope
         args = ', '.join(str(a.arg) for a in self.args)
         return 'vector({})'.format(args)
 
@@ -1222,7 +1222,7 @@ def expr(self):
         if DEBUG:
             print("> AccWait: expr")
 
-        # TODO check if variable exist in namespace
+        # TODO check if variable exist in scope
         args = ', '.join(str(arg) for arg in self.args)
         return 'wait({})'.format(args)
 
@@ -1241,7 +1241,7 @@ def expr(self):
         if DEBUG:
             print("> AccWorker: expr")
 
-        # TODO check if variable exist in namespace
+        # TODO check if variable exist in scope
         args = ', '.join(str(a.arg) for a in self.args)
         return 'worker({})'.format(args)
 #################################################
diff --git a/pyccel/parser/syntax/openmp.py b/pyccel/parser/syntax/openmp.py
index a31d5c41e8..e2ee4f1cdc 100644
--- a/pyccel/parser/syntax/openmp.py
+++ b/pyccel/parser/syntax/openmp.py
@@ -328,7 +328,7 @@ class OmpPrivate(OmpClauses):
     def __init__(self, **kwargs):
         args = kwargs.pop('args')
 
-        #TODO check for variables in namespace
+        #TODO check for variables in scope
         txt = ', '.join(str(arg) for arg in args)
         self._expr = 'private({})'.format(txt)
 
diff --git a/pyccel/utilities/strings.py b/pyccel/utilities/strings.py
new file mode 100644
index 0000000000..25509f1926
--- /dev/null
+++ b/pyccel/utilities/strings.py
@@ -0,0 +1,50 @@
+# -*- coding: utf-8 -*-
+#------------------------------------------------------------------------------------------#
+# This file is part of Pyccel which is released under MIT License. See the LICENSE file or #
+# go to https://github.com/pyccel/pyccel/blob/master/LICENSE for full license details.     #
+#------------------------------------------------------------------------------------------#
+""" Module containing helper functions for managing strings
+"""
+
+def create_incremented_string(forbidden_exprs, prefix = 'Dummy', counter = 1):
+    """This function takes a prefix and a counter and uses them to construct
+    a new name of the form:
+            prefix_counter
+    Where counter is formatted to fill 4 characters
+    The new name is checked against a list of forbidden expressions. If the
+    constructed name is forbidden then the counter is incremented until a valid
+    name is found
+
+      Parameters
+      ----------
+      forbidden_exprs : Set
+                        A set of all the values which are not valid solutions to this problem
+      prefix          : str
+                        The prefix used to begin the string
+      counter         : int
+                        The expected value of the next name
+
+      Returns
+      ----------
+      name            : str
+                        The incremented string name
+      counter         : int
+                        The expected value of the next name
+
+    """
+    assert(isinstance(forbidden_exprs, set))
+    nDigits = 4
+
+    if prefix is None:
+        prefix = 'Dummy'
+
+    name_format = "{prefix}_{counter:0="+str(nDigits)+"d}"
+    name = name_format.format(prefix=prefix, counter = counter)
+    counter += 1
+    while name in forbidden_exprs:
+        name = name_format.format(prefix=prefix, counter = counter)
+        counter += 1
+
+    forbidden_exprs.add(name)
+
+    return name, counter


Test patch:
diff --git a/tests/ast/test_basic.py b/tests/ast/test_basic.py
index 30b2cc8447..c9785fcd21 100644
--- a/tests/ast/test_basic.py
+++ b/tests/ast/test_basic.py
@@ -28,7 +28,7 @@ def get_functions(filename):
     # Assert semantic success
     assert(not errors.has_errors())
 
-    return list(ast.namespace.functions.values())
+    return list(ast.scope.functions.values())
 
 def test_get_attribute_nodes():
     filename = os.path.join(path_dir, "math.py")
diff --git a/tests/epyccel/test_arrays_multiple_assignments.py b/tests/epyccel/test_arrays_multiple_assignments.py
index c77febb485..1e9eba7385 100644
--- a/tests/epyccel/test_arrays_multiple_assignments.py
+++ b/tests/epyccel/test_arrays_multiple_assignments.py
@@ -218,7 +218,7 @@ def f():
 
     # Check that the error is correct
     error_info = [*errors.error_info_map.values()][0][0]
-    assert error_info.symbol  == 'x'
+    assert str(error_info.symbol)  == 'x'
     assert error_info.message == ASSIGN_ARRAYS_ONE_ANOTHER
 
 #==============================================================================