Repository: pyccel/pyccel (language: python)

Issue:
Rename `NumpyArraySize`
As discussed in https://github.com/pyccel/pyccel/pull/1326#discussion_r1112041778, the name `NumpyArraySize` is misleading as it does not return the same thing as NumPy's `size` function. We should therefore rename the classes `NumpyArraySize` and `PyccelArraySize` as follows:

`NumpyArraySize`->`NumpyArrayShapeElement`
`PyccelArraySize`->`NumpyArraySize`

Rename Numpy AST nodes
This change were suggested in issue #1329 and [comment](https://github.com/pyccel/pyccel/pull/1326#discussion_r1112041778)
1. Renaming the classes and their occurrences:
  `NumpyArraySize` =>  `NumpyArrayShapeElement`
  `PyccelArraySize` => `NumpyArraySize`
2. Updating the developer_docs/codegen_stage.md.
3. Renaming the printing functions. ([doc](https://github.com/pyccel/pyccel/blob/devel/developer_docs/codegen_stage.md#_print))

**TODO:**
- [x] run tests.
- [x] fixing docs and coverage


Discussion / hints:
This would be a good first issue, but it is currently likely to trigger lots of documentation errors which will require slightly deeper knowledge to fix
Hello again! Thank you for this new pull request :star_struck:.

Don't forget to let me know when it is complete with the command `/bot mark as ready`.

Here is your checklist:
- [x] Write a clear PR description
- [ ] Add tests to check your code works as expected
- [ ] Update documentation if necessary
- [ ] Update Changelog
- [ ] Ensure any relevant issues are linked
- [ ] Ensure tests are passing

/bot run linux
Ran tests on commit fc172e0fb4ac00c068e32552a7e2499ac5ef22ab, for more details see [here](https://github.com/pyccel/pyccel/actions/runs/4871983359)
- :heavy_check_mark: linux / Unit tests

/bot command
This bot reacts to all comments which begin with `/bot`. This phrase can be followed by any of these commands:
- `show tests` : Lists the tests which can be triggered
- `run X` : Triggers the test X (acceptable values for X can be seen using `show tests`). Multiple tests can be specified separated by spaces.
- `try V X` : Triggers the test X (acceptable values for X can be seen using `show tests`) using Python version V. Multiple tests can be specified separated by spaces, but all will use the same Python version.
- `mark as ready` : Runs the PR tests. If they pass then it adds the appropriate review flag and requests reviews. This command should be used when the PR is first ready for review, or when a review has been answered.
- `commands` : Shows this list detailing all the commands which are understood.
- `trust user X` : Tells the bot that a new user X is trusted to run workflows (prevents misuse of GitHub actions for mining etc). This command can only be used by a trusted reviewer.
- `accept coverage` : Tells the bot that the coverage errors have been investigated and cannot be fixed in this PR.

Beware: if you have never contributed to this repository and you are not a member of the Pyccel organisation, the bot will ignore all requests to run tests until permitted by a trusted reviewer.

/bot show tests
The following is a list of keywords which can be used to run tests. Tests in bold are run by pull requests when they are marked as ready for review or pushed to while not in draft:
- **linux** : Runs the unit tests on a Linux system.
- **windows** : Runs the unit tests on a Windows system.
- **macosx** : Runs the unit tests on a MacOS X system.
- **coverage** : Runs the unit tests on a Linux system and checks the coverage of the tests.
- **docs** : Checks if the documentation follows the numpydoc format.
- **pylint** : Runs pylint on files which are too big to be handled by codacy.
- **lint** : Runs a linter to check that Pyccel's best practices are followed.
- **spelling** : Checks if everything in the documentation is spelled (and capitalised) correctly.
- pr_tests : Runs all the tests marked in bold.
- pickle : Checks that .pyccel files have been correctly generated and installed by the installation process.
- editable_pickle : Checks that .pyccel files have been correctly generated and installed by the editable installation process.
- pickle_wheel : Checks that .pyccel files have been correctly generated and packaged into the wheel.
- linux_anaconda : Runs the unit tests on a linux system using anaconda for python.
- windows_anaconda : Runs the unit tests on a windows system using anaconda for python.

These tests can be run with the command `/bot run X` (multiple tests can be specified separated by spaces), or with `try V X` to test on Python version V.

/bot run docs
Ran tests on commit fc172e0fb4ac00c068e32552a7e2499ac5ef22ab, for more details see [here](https://github.com/pyccel/pyccel/actions/runs/4872297611)
- :x: docs / Documentation Format

@mkaddani it looks like there is a minor problem with the doc check. These functions do not require doc strings. I am going to investigate and possibly push a commit to your branch
/bot run docs
Ran tests on commit 1407f53a9184e375f44c7c6f26b41bde1baf6b56, for more details see [here](https://github.com/pyccel/pyccel/actions/runs/4880074644)
- :heavy_check_mark: docs / Documentation Format

@EmilyBourne thank you.
/bot run pr_tests
Ran tests on commit 1407f53a9184e375f44c7c6f26b41bde1baf6b56, for more details see [here](https://github.com/pyccel/pyccel/actions/runs/4881238105)
- :heavy_check_mark: linux / Unit tests
- :heavy_check_mark: lint / Best practices
- :heavy_check_mark: docs / Documentation Format
- :heavy_check_mark: macosx / Unit tests
- :heavy_check_mark: pylint / Python best practices
- :heavy_check_mark: spelling / Documentation spellcheck
- :heavy_check_mark: windows / Unit tests
- :x: coverage / Unit tests

/bot run coverage
Ran tests on commit 6ff261f7c7a5bfdc630402eed676bad01c559d71, for more details see [here](https://github.com/pyccel/pyccel/actions/runs/4894933076)
- :heavy_check_mark: linux / Unit tests
- :x: coverage / Unit tests

/bot run coverage


Ran tests on commit dc05982e7790b0a22e9b0b213501ac91d5896dae, for more details see [here](https://github.com/pyccel/pyccel/actions/runs/4896365341)
- :heavy_check_mark: linux / Unit tests
- :heavy_check_mark: coverage / Unit tests

/bot run pr_tests
Ran tests on commit dc05982e7790b0a22e9b0b213501ac91d5896dae, for more details see [here](https://github.com/pyccel/pyccel/actions/runs/4898311408)
- :heavy_check_mark: docs / Documentation Format
- :heavy_check_mark: linux / Unit tests
- :heavy_check_mark: lint / Best practices
- :heavy_check_mark: pylint / Python best practices
- :heavy_check_mark: macosx / Unit tests
- :heavy_check_mark: spelling / Documentation spellcheck
- :heavy_check_mark: windows / Unit tests
- :heavy_check_mark: coverage / Unit tests

/bot run coverage
Ran tests on commit 004f7a5893152c3f565dc5c68133c43e9e37881e, for more details see [here](https://github.com/pyccel/pyccel/actions/runs/4903306299)
- :heavy_check_mark: linux / Unit tests
- :heavy_check_mark: coverage / Unit tests

/bot mark as ready
Ran tests on commit 004f7a5893152c3f565dc5c68133c43e9e37881e, for more details see [here](https://github.com/pyccel/pyccel/actions/runs/4903417809)
- :heavy_check_mark: linux / Unit tests
- :heavy_check_mark: docs / Documentation Format
- :heavy_check_mark: lint / Best practices
- :heavy_check_mark: pylint / Python best practices
- :heavy_check_mark: macosx / Unit tests
- :heavy_check_mark: spelling / Documentation spellcheck
- :heavy_check_mark: windows / Unit tests
- :heavy_check_mark: coverage / Unit tests

Hey @pyccel/pyccel-dev ! @mkaddani has just created this great new pull request! Check it out and let me know what you think!

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

Hi @mkaddani, according to the PR description there are still two TODO items which need addressing. Maybe you have forgotten to add tick marks?
@yguclu yes, I did forgot to check them.
Thank you 🙏 
@mkaddani, @yguclu has a few questions/comments about your code. Can you go through and see if you agree with them. If not go ahead and explain why. Once you've adressed all the comments let me know with `/bot mark as ready` and we will see if we can get approval.

> @yguclu yes, I did forgot to check them.
> Thank you 🙏 

I agree with Yaman. Most of the boxes are just missing check marks but the CHANGELOG needs updating before it can be checked off

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index cb7a58b9b2..371f71363e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -19,6 +19,9 @@ All notable changes to this project will be documented in this file.
 -   `pyccel_clean` tool now deletes folders **starting with** `__pyccel__` and `__epyccel__`.
 -   Pyccel-generated folder names are dependent on `PYTEST_XDIST_WORKER` when running with `pytest-xdist`.
 -   \[INTERNALS\] Add class object to class function call arguments.
+-   \[INTERNALS\] In `ast.numpyext` rename `Shape` as `NumpyShape`, `NumpyArraySize` as `NumpySize`
+-   \[INTERNALS\] In `ast.internals` rename `NumpyArraySize` as `NumpySize`, create new `NumpyArraySize` w/out `index` argument
+-   \[INTERNALS\] Make `NumpySize` a factory class (which cannot be instantiated)
 
 ### Deprecated
 
diff --git a/pyccel/ast/class_defs.py b/pyccel/ast/class_defs.py
index c2f2a9ccaf..957b6b6c6b 100644
--- a/pyccel/ast/class_defs.py
+++ b/pyccel/ast/class_defs.py
@@ -9,9 +9,9 @@
 from .core      import ClassDef, PyccelFunctionDef
 from .datatypes import (NativeBool, NativeInteger, NativeFloat,
                         NativeComplex, NativeString, NativeNumeric)
-from .numpyext  import (Shape, NumpySum, NumpyAmin, NumpyAmax,
+from .numpyext  import (NumpyShape, NumpySum, NumpyAmin, NumpyAmax,
                         NumpyImag, NumpyReal, NumpyTranspose,
-                        NumpyConjugate, NumpyArraySize)
+                        NumpyConjugate, NumpySize)
 
 __all__ = ('BooleanClass',
         'IntegerClass',
@@ -137,28 +137,30 @@
 
 NumpyArrayClass = ClassDef('numpy.ndarray',
         methods=[
-            PyccelFunctionDef('shape', func_class = Shape,
-                decorators={'property':'property', 'numpy_wrapper':'numpy_wrapper'}),
-            PyccelFunctionDef('size', func_class = NumpyArraySize,
-                decorators={'property':'property', 'numpy_wrapper': 'numpy_wrapper'}),
+            PyccelFunctionDef('shape', func_class = NumpyShape,
+                decorators = {'property': 'property', 'numpy_wrapper': 'numpy_wrapper'}),
+            PyccelFunctionDef('size', func_class = NumpySize,
+                decorators = {'property': 'property', 'numpy_wrapper': 'numpy_wrapper'}),
             PyccelFunctionDef('T', func_class = NumpyTranspose,
-                decorators={'property':'property', 'numpy_wrapper': 'numpy_wrapper'}),
+                decorators = {'property': 'property', 'numpy_wrapper': 'numpy_wrapper'}),
             PyccelFunctionDef('transpose', func_class = NumpyTranspose,
-                decorators={'numpy_wrapper': 'numpy_wrapper'}),
+                decorators = {'numpy_wrapper': 'numpy_wrapper'}),
             PyccelFunctionDef('sum', func_class = NumpySum,
-                decorators={'numpy_wrapper': 'numpy_wrapper'}),
+                decorators = {'numpy_wrapper': 'numpy_wrapper'}),
             PyccelFunctionDef('min', func_class = NumpyAmin,
-                decorators={'numpy_wrapper': 'numpy_wrapper'}),
+                decorators = {'numpy_wrapper': 'numpy_wrapper'}),
             PyccelFunctionDef('max', func_class = NumpyAmax,
-                decorators={'numpy_wrapper': 'numpy_wrapper'}),
+                decorators = {'numpy_wrapper': 'numpy_wrapper'}),
             PyccelFunctionDef('imag', func_class = NumpyImag,
-                decorators={'property':'property', 'numpy_wrapper': 'numpy_wrapper'}),
+                decorators = {'property': 'property', 'numpy_wrapper': 'numpy_wrapper'}),
             PyccelFunctionDef('real', func_class = NumpyReal,
-                decorators={'property':'property', 'numpy_wrapper': 'numpy_wrapper'}),
+                decorators = {'property': 'property', 'numpy_wrapper': 'numpy_wrapper'}),
             PyccelFunctionDef('conj', func_class = NumpyConjugate,
-                decorators={'numpy_wrapper': 'numpy_wrapper'}),
+                decorators = {'numpy_wrapper': 'numpy_wrapper'}),
             PyccelFunctionDef('conjugate', func_class = NumpyConjugate,
-                decorators={'numpy_wrapper': 'numpy_wrapper'})])
+                decorators = {'numpy_wrapper': 'numpy_wrapper'})
+        ]
+)
 
 #=======================================================================================
 
diff --git a/pyccel/ast/internals.py b/pyccel/ast/internals.py
index 6c255ae136..e0b80eb868 100644
--- a/pyccel/ast/internals.py
+++ b/pyccel/ast/internals.py
@@ -20,6 +20,7 @@
 __all__ = (
     'PrecomputedCode',
     'PyccelArraySize',
+    'PyccelArrayShapeElement',
     'PyccelInternalFunction',
     'PyccelSymbol',
     'Slice',
@@ -29,46 +30,108 @@
 
 
 class PyccelInternalFunction(PyccelAstNode):
-    """ Abstract class used by function calls
-    which are translated to Pyccel objects
+    """
+    Abstract class for function calls translated to Pyccel objects.
+
+    A subclass of this base class represents calls to a specific internal
+    function of Pyccel, which may be simplified at a later stage, or made
+    available in the target language when printing the generated code.
+
+    Parameters
+    ----------
+    *args : iterable
+        The arguments passed to the function call.
     """
     __slots__ = ('_args',)
     _attribute_nodes = ('_args',)
     name = None
+
     def __init__(self, *args):
-        self._args   = tuple(args)
+        self._args = tuple(args)
         super().__init__()
 
     @property
     def args(self):
-        """ The arguments passed to the function
+        """
+        The arguments passed to the function.
+
+        Tuple containing all the arguments passed to the function call.
         """
         return self._args
 
     @property
     def is_elemental(self):
-        """ Indicates whether the function should be
-        called elementwise for an array argument
+        """
+        Whether the function acts elementwise on an array argument.
+
+        Boolean indicating whether the (scalar) function should be called
+        elementwise on an array argument. Here we set the default to False.
         """
         return False
 
 
 class PyccelArraySize(PyccelInternalFunction):
     """
-    Class representing a call to a function which would
-    return the shape of an object in a given dimension
+    Gets the total number of elements in an array.
+
+    Class representing a call to a function which would return
+    the total number of elements in a multi-dimensional array.
+
+    Parameters
+    ----------
+    arg : PyccelAstNode
+        An array of unknown size.
+    """
+    __slots__ = ()
+    name = 'size'
+
+    _dtype = NativeInteger()
+    _precision = -1
+    _rank  = 0
+    _shape = None
+    _order = None
+
+    def __init__(self, arg):
+        super().__init__(arg)
+
+    @property
+    def arg(self):
+        """
+        Object whose size is investigated.
+
+        The argument of the function call, i.e. the object whose size is
+        investigated.
+        """
+        return self._args[0]
+
+    def __str__(self):
+        return f'Size({self.arg})'
+
+    def __eq__(self, other):
+        if isinstance(other, PyccelArraySize):
+            return self.arg == other.arg
+        else:
+            return False
+
+
+class PyccelArrayShapeElement(PyccelInternalFunction):
+    """
+    Gets the number of elements in a given dimension of an array.
+
+    Class representing a call to a function which would return
+    the shape of a multi-dimensional array in a given dimension.
 
     Parameters
-    ==========
-    arg   : PyccelAstNode
-            A PyccelAstNode of unknown shape
+    ----------
+    arg : PyccelAstNode
+        An array of unknown shape.
+
     index : int
-            The dimension along which the shape is
-            provided
+        The dimension along which the shape should be provided.
     """
-    __slots__ = ('_arg','_index')
-    name   = 'shape'
-    _attribute_nodes = ('_arg', '_index')
+    __slots__ = ()
+    name = 'shape'
+
     _dtype = NativeInteger()
     _precision = -1
     _rank  = 0
@@ -76,53 +139,73 @@ class PyccelArraySize(PyccelInternalFunction):
     _order = None
 
     def __init__(self, arg, index):
-        if not isinstance(arg, (list,
-                                tuple,
-                                PyccelAstNode)):
-            raise TypeError('Unknown type of  %s.' % type(arg))
+        if not isinstance(arg, PyccelAstNode):
+            raise TypeError(f'Unknown type {type(arg)} of {arg}.')
+
         if isinstance(index, int):
             index = LiteralInteger(index)
         elif not isinstance(index, PyccelAstNode):
-            raise TypeError('Unknown type of  %s.' % type(index))
+            raise TypeError(f'Unknown type {type(index)} of {index}.')
 
-        self._arg   = arg
-        self._index = index
-        super().__init__()
+        super().__init__(arg, index)
 
     @property
     def arg(self):
-        """ Object whose size is investigated
         """
-        return self._arg
+        Object whose size is investigated.
+
+        The first argument of the function call, i.e. the array whose size is
+        investigated.
+        """
+        return self._args[0]
 
     @property
     def index(self):
-        """ Dimension along which the size is calculated
         """
-        return self._index
+        Dimension along which the size is calculated.
+
+        The second argument of the function call, i.e. the dimension along
+        which the array size is calculated.
+        """
+        return self._args[1]
 
     def __str__(self):
-        return 'Shape({},{})'.format(str(self.arg), str(self.index))
+        return f'Shape({self.arg}, {self.index})'
 
     def __eq__(self, other):
-        if isinstance(other, PyccelArraySize):
+        if isinstance(other, PyccelArrayShapeElement):
             return self.arg == other.arg and self.index == other.index
         else:
             return False
 
+
 class Slice(Basic):
+    """
+    Represents a slice in the code.
 
-    """Represents a slice in the code.
+    An object of this class represents the slicing of a Numpy array along one of
+    its dimensions. In most cases this corresponds to a Python slice in the user
+    code, where it is represented by a `python.ast.Slice` object.
+
+    In addition, at the wrapper and code generation stages, an integer index
+    `i` used to create a view of a Numpy array is converted to an object
+    `Slice(i, i+1, 1, slice_type = Slice.Element)`. This allows using C
+    variadic arguments in the function `array_slicing` (in file
+    pyccel/stdlib/ndarrays/ndarrays.c).
 
     Parameters
     ----------
     start : PyccelSymbol or int
-        starting index
+        Starting index.
 
     stop : PyccelSymbol or int
-        ending index
+        Ending index.
+
+    step : PyccelSymbol or int, default=None
+        The step between indices.
 
-    step : PyccelSymbol or int default None
+    slice_type : LiteralInteger
+        The type of the slice. Either Slice.Range or Slice.Element.
 
     Examples
     --------
@@ -198,6 +281,7 @@ def __str__(self):
             stop = str(self.stop)
         return '{0} : {1}'.format(start, stop)
 
+
 class PyccelSymbol(str, Immutable):
     """Symbolic placeholder for a Python variable, which has a name but no type yet.
     This is very generic, and it can also represent a function or a module.
@@ -230,6 +314,7 @@ def is_temp(self):
         """
         return self._is_temp
 
+
 class PrecomputedCode(Basic):
     """
     Internal helper class for storing code which must be defined by the printer
@@ -260,6 +345,7 @@ def code(self):
         """
         return self._code
 
+
 def symbols(names):
     """
     Transform strings into instances of PyccelSymbol class.
@@ -287,6 +373,7 @@ def symbols(names):
     symbols = [PyccelSymbol(name.strip()) for name in names]
     return tuple(symbols)
 
+
 def max_precision(objs : list, dtype = None, allow_native = True):
     """
     Returns the largest precision of an object in the list
@@ -312,6 +399,7 @@ def max_precision(objs : list, dtype = None, allow_native = True):
             return get_final_precision(max(ndarray_list, key=attrgetter('precision')))
         return max(get_final_precision(o) for o in objs)
 
+
 def get_final_precision(obj):
     """
     Get the the usable precision of an object. Ie. the precision that you
diff --git a/pyccel/ast/numpyext.py b/pyccel/ast/numpyext.py
index e4dc4d214a..25567d7d5c 100644
--- a/pyccel/ast/numpyext.py
+++ b/pyccel/ast/numpyext.py
@@ -7,9 +7,6 @@
 """ Module containing objects from the numpy module understood by pyccel
 """
 
-from functools import reduce
-import operator
-
 import numpy
 
 from pyccel.errors.errors import Errors
@@ -30,14 +27,14 @@
                              NativeNumeric)
 
 from .internals      import PyccelInternalFunction, Slice, max_precision, get_final_precision
-from .internals      import PyccelArraySize
+from .internals      import PyccelArraySize, PyccelArrayShapeElement
 
 from .literals       import LiteralInteger, LiteralFloat, LiteralComplex, LiteralString, convert_to_literal
 from .literals       import LiteralTrue, LiteralFalse
 from .literals       import Nil
 from .mathext        import MathCeil
 from .operators      import broadcast, PyccelMinus, PyccelDiv, PyccelMul, PyccelAdd
-from .variable       import (Variable, Constant, HomogeneousTupleVariable)
+from .variable       import Variable, Constant, HomogeneousTupleVariable
 
 errors = Errors()
 pyccel_stage = PyccelStage()
@@ -75,7 +72,7 @@
     'NumpyAmin',
     'NumpyArange',
     'NumpyArray',
-    'NumpyArraySize',
+    'NumpySize',
     'NumpyBool',
     'NumpyCountNonZero',
     'NumpyComplex',
@@ -115,7 +112,7 @@
     'NumpyWhere',
     'NumpyZeros',
     'NumpyZerosLike',
-    'Shape',
+    'NumpyShape',
 )
 
 #=======================================================================================
@@ -355,7 +352,7 @@ def process_dtype(dtype):
 
     Parameters
     ----------
-    dtype : PythonType | PyccelFunctionDef | String
+    dtype : PythonType, PyccelFunctionDef, LiteralString, str
         The actual dtype passed to the NumPy function.
 
     Returns
@@ -674,14 +671,31 @@ def a(self):
     def b(self):
         return self._args[1]
 
-
 #==============================================================================
+class NumpyShape(PyccelInternalFunction):
+    """
+    Represents a call to numpy.shape for code generation.
+
+    This wrapper class represents calls to the function `numpy.shape` in the
+    user code, or equivalently to the `shape` property of a `numpy.ndarray`.
 
-class Shape(PyccelInternalFunction):
-    """ Represents a call to numpy.shape for code generation
+    Objects of this class are never present in the Pyccel AST, because the
+    class constructor always returns a PythonTuple with the required shape.
+
+    Parameters
+    ----------
+    arg : PyccelAstNode
+        The Numpy array whose shape is being investigated.
+
+    Returns
+    -------
+    PythonTuple
+        The shape of the Numpy array, i.e. its size along each dimension.
     """
+
     __slots__ = ()
     name = 'shape'
+
     def __new__(cls, arg):
         if isinstance(arg.shape, PythonTuple):
             return arg.shape
@@ -962,24 +976,28 @@ def low(self):
 #==============================================================================
 class NumpyFull(NumpyNewArray):
     """
-    Represents a call to numpy.full for code generation.
+    Represents a call to `numpy.full` for code generation.
+
+    Represents a call to the NumPy function `full` which creates an array
+    of a specified size and shape filled with a specified value.
 
     Parameters
     ----------
-    shape : int or sequence of ints
+    shape : PyccelAstNode
         Shape of the new array, e.g., ``(2, 3)`` or ``2``.
+        For a 1D array this is either a `LiteralInteger` or an expression.
+        For a ND array this is a `PythonTuple` or a `HomogeneousTupleVariable`.
 
-    fill_value : scalar
+    fill_value : PyccelAstNode
         Fill value.
 
-    dtype: str, DataType
-        datatype for the constructed array
-        The default, `None`, means `np.array(fill_value).dtype`.
+    dtype : PythonType, PyccelFunctionDef, LiteralString, str, optional
+        Datatype for the constructed array.
+        If `None` the dtype of the fill value is used.
 
     order : {'C', 'F'}, optional
         Whether to store multidimensional data in C- or Fortran-contiguous
         (row- or column-wise) order in memory.
-
     """
     __slots__ = ('_fill_value','_dtype','_precision','_shape','_rank','_order')
     name = 'full'
@@ -1042,7 +1060,6 @@ def __init__(self, shape, dtype='float', order='C'):
     def fill_value(self):
         return None
 
-
 #==============================================================================
 class NumpyZeros(NumpyAutoFill):
     """ Represents a call to numpy.zeros for code generation.
@@ -1085,9 +1102,41 @@ def fill_value(self):
             raise TypeError('Unknown type')
         return value
 
-#=======================================================================================
+#==============================================================================
 class NumpyFullLike(PyccelInternalFunction):
-    """ Represents a call to numpy.full_like for code generation.
+    """
+    Represents a call to numpy.full_like for code generation.
+
+    This wrapper class represents calls to the function numpy.full_like.
+    Objects of this class are never present in the Pyccel AST, because the
+    class constructor always returns an object of type `NumpyFull`.
+
+    Parameters
+    ----------
+    a : Variable
+        Numpy array which is used as a template.
+
+    fill_value : PyccelAstNode
+        Scalar value which will be assigned to each entry of the new array.
+
+    dtype : PythonType, PyccelFunctionDef, LiteralString, str, optional
+        Type of the data contained in the new array. If None, a.dtype is used.
+
+    order : str, default='K'
+        Ordering used for the indices of a multi-dimensional array.
+
+    subok : bool, default=True
+        This parameter is currently ignored.
+
+    shape : PythonTuple of PyccelAstNode
+        Overrides the shape of the array.
+        For a 1D array this is either a `LiteralInteger` or an expression.
+        For a ND array this is a `PythonTuple` or a `HomogeneousTupleVariable`.
+
+    See Also
+    --------
+    numpy.full_like :
+        See documentation of `numpy.full_like`: <https://numpy.org/doc/stable/reference/generated/numpy.full_like.html>.
     """
     __slots__ = ()
     name = 'full_like'
@@ -1097,28 +1146,87 @@ def __new__(cls, a, fill_value, dtype=None, order='K', subok=True, shape=None):
         if dtype is None:
             dtype = DtypePrecisionToCastFunction[a.dtype.name][a.precision]
         order = a.order if str(order).strip('\'"') in ('K', 'A') else order
-        shape = Shape(a) if shape is None else shape
+        shape = NumpyShape(a) if shape is None else shape
         return NumpyFull(shape, fill_value, dtype, order)
 
-#=======================================================================================
+#==============================================================================
 class NumpyEmptyLike(PyccelInternalFunction):
-    """ Represents a call to numpy.empty_like for code generation.
+    """
+    Represents a call to numpy.empty_like for code generation.
+
+    This wrapper class represents calls to the function numpy.empty_like.
+    Objects of this class are never present in the Pyccel AST, because the
+    class constructor always returns an object of type `NumpyEmpty`.
+
+    Parameters
+    ----------
+    a : Variable
+        Numpy array which is used as a template.
+
+    dtype : PythonType, PyccelFunctionDef, LiteralString, str, optional
+        Type of the data contained in the new array. If None, a.dtype is used.
+
+    order : str, default='K'
+        Ordering used for the indices of a multi-dimensional array.
+
+    subok : bool, default=True
+        This parameter is currently ignored.
+
+    shape : PythonTuple of PyccelAstNode
+        Overrides the shape of the array.
+        For a 1D array this is either a `LiteralInteger` or an expression.
+        For a ND array this is a `PythonTuple` or a `HomogeneousTupleVariable`.
+
+    See Also
+    --------
+    numpy.empty_like :
+        See documentation of `numpy.empty_like`: <https://numpy.org/doc/stable/reference/generated/numpy.empty_like.html>.
     """
     __slots__ = ()
     name = 'empty_like'
+
     def __new__(cls, a, dtype=None, order='K', subok=True, shape=None):
 
         # NOTE: we ignore 'subok' argument
         if dtype is None:
             dtype = DtypePrecisionToCastFunction[a.dtype.name][a.precision]
         order = a.order if str(order).strip('\'"') in ('K', 'A') else order
-        shape = Shape(a) if shape is None else shape
+        shape = NumpyShape(a) if shape is None else shape
 
         return NumpyEmpty(shape, dtype, order)
 
-#=======================================================================================
+#==============================================================================
 class NumpyOnesLike(PyccelInternalFunction):
-    """ Represents a call to numpy.ones_like for code generation.
+    """
+    Represents a call to numpy.ones_like for code generation.
+
+    This wrapper class represents calls to the function numpy.ones_like.
+    Objects of this class are never present in the Pyccel AST, because the
+    class constructor always returns an object of type `NumpyOnes`.
+
+    Parameters
+    ----------
+    a : Variable
+        Numpy array which is used as a template.
+
+    dtype : PythonType, PyccelFunctionDef, LiteralString, str, optional
+        Type of the data contained in the new array. If None, a.dtype is used.
+
+    order : str, default='K'
+        Ordering used for the indices of a multi-dimensional array.
+
+    subok : bool, default=True
+        This parameter is currently ignored.
+
+    shape : PythonTuple of PyccelAstNode
+        Overrides the shape of the array.
+        For a 1D array this is either a `LiteralInteger` or an expression.
+        For a ND array this is a `PythonTuple` or a `HomogeneousTupleVariable`.
+
+    See Also
+    --------
+    numpy.ones_like :
+        See documentation of `numpy.ones_like`: <https://numpy.org/doc/stable/reference/generated/numpy.ones_like.html>.
     """
     __slots__ = ()
     name = 'ones_like'
@@ -1128,28 +1236,57 @@ def __new__(cls, a, dtype=None, order='K', subok=True, shape=None):
         if dtype is None:
             dtype = DtypePrecisionToCastFunction[a.dtype.name][a.precision]
         order = a.order if str(order).strip('\'"') in ('K', 'A') else order
-        shape = Shape(a) if shape is None else shape
+        shape = NumpyShape(a) if shape is None else shape
 
         return NumpyOnes(shape, dtype, order)
 
-#=======================================================================================
+#==============================================================================
 class NumpyZerosLike(PyccelInternalFunction):
-    """ Represents a call to numpy.zeros_like for code generation.
+    """
+    Represents a call to numpy.zeros_like for code generation.
+
+    This wrapper class represents calls to the function numpy.zeros_like.
+    Objects of this class are never present in the Pyccel AST, because the
+    class constructor always returns an object of type `NumpyZeros`.
+
+    Parameters
+    ----------
+    a : Variable
+        Numpy array which is used as a template.
+
+    dtype : PythonType, PyccelFunctionDef, LiteralString, str, optional
+        Type of the data contained in the new array. If None, a.dtype is used.
+
+    order : str, default='K'
+        Ordering used for the indices of a multi-dimensional array.
+
+    subok : bool, default=True
+        This parameter is currently ignored.
+
+    shape : PythonTuple of PyccelAstNode
+        Overrides the shape of the array.
+        For a 1D array this is either a `LiteralInteger` or an expression.
+        For a ND array this is a `PythonTuple` or a `HomogeneousTupleVariable`.
+
+    See Also
+    --------
+    numpy.zeros_like :
+        See documentation of `numpy.zeros_like`: <https://numpy.org/doc/stable/reference/generated/numpy.zeros_like.html>.
     """
     __slots__ = ()
     name = 'zeros_like'
+
     def __new__(cls, a, dtype=None, order='K', subok=True, shape=None):
 
         # NOTE: we ignore 'subok' argument
         if dtype is None:
             dtype = DtypePrecisionToCastFunction[a.dtype.name][a.precision]
         order = a.order if str(order).strip('\'"') in ('K', 'A') else order
-        shape = Shape(a) if shape is None else shape
+        shape = NumpyShape(a) if shape is None else shape
 
         return NumpyZeros(shape, dtype, order)
 
-#=======================================================================================
-
+#==============================================================================
 class NumpyNorm(PyccelInternalFunction):
     """ Represents call to numpy.norm"""
     __slots__ = ('_shape','_rank','_order','_arg','_precision')
@@ -1193,9 +1330,6 @@ def axis(self):
         """
         return self._args[1]
 
-#====================================================
-
-
 #==============================================================================
 # Numpy universal functions
 # https://numpy.org/doc/stable/reference/ufuncs.html#available-ufuncs
@@ -1344,9 +1478,7 @@ class NumpyArctanh(NumpyUfuncUnary):
 #     __slots__ = ()
 #     name = 'rad2deg'
 
-#=======================================================================================
-
-
+#==============================================================================
 class NumpySign(NumpyUfuncUnary):
     """Represent a call to the sign function in the Numpy library"""
     __slots__ = ()
@@ -1679,52 +1811,44 @@ def keep_dims(self):
         """
         return self._keep_dims
 
-class NumpyArraySize(PyccelInternalFunction):
+
+class NumpySize(PyccelInternalFunction):
     """
-    Class representing a call to the numpy size function which
-    returns the shape of an object in a given dimension
+    Represent a call to numpy.size in the user code.
+
+    This wrapper class represents a call to the NumPy `size` function, which
+    returns the total number of elements in a multidimensional array, or the
+    number of elements along a given dimension.
+
+    Objects of this class are never present in the Pyccel AST, because the
+    class constructor returns objects of type `PyccelArraySize`, `LiteralInteger`, or
+    `PyccelArrayShapeElement`.
 
     Parameters
-    ==========
-    arg   : PyccelAstNode
-            A PyccelAstNode of unknown shape
-    axis  : int
-            The dimension along which the size is
-            requested
+    ----------
+    a : PyccelAstNode
+        An array of unknown size.
+
+    axis : PyccelAstNode, optional
+        The integer dimension along which the size is requested.
+
+    See Also
+    --------
+    numpy.size :
+        See NumPy docs : <https://numpy.org/doc/stable/reference/generated/numpy.ma.size.html>.
     """
-    __slots__ = ('_arg',)
-    _attribute_nodes = ('_arg',)
-    name   = 'size'
-    _dtype = NativeInteger()
-    _precision = -1
-    _rank  = 0
-    _shape = None
-    _order = None
+    __slots__ = ()
+    name = 'size'
 
     def __new__(cls, a, axis = None):
-        if axis is not None:
-            return PyccelArraySize(a, axis)
-        elif not isinstance(a, (list,
-                                    tuple,
-                                    PyccelAstNode)):
-            raise TypeError('Unknown type of  %s.' % type(a))
-        elif all(isinstance(s, LiteralInteger) for s in a.shape):
-            return LiteralInteger(reduce(operator.mul, [s.python_value for s in a.shape]))
-        else:
-            return super().__new__(cls)
 
-    def __init__(self, a, axis = None):
-        self._arg   = a
-        super().__init__(a)
+        if axis is None:
+            return PyccelArraySize(a)
 
-    @property
-    def arg(self):
-        """ Object whose size is investigated
-        """
-        return self._arg
+        if isinstance(axis, LiteralInteger) and a.shape is not None:
+            return a.shape[axis.python_value]
 
-    def __str__(self):
-        return 'Size({})'.format(str(self.arg))
+        return PyccelArrayShapeElement(a, axis)
 
 #==============================================================================
 # TODO split numpy_functions into multiple dictionaries following
@@ -1755,8 +1879,8 @@ def __str__(self):
     'array'     : PyccelFunctionDef('array'     , NumpyArray),
     'arange'    : PyccelFunctionDef('arange'    , NumpyArange),
     # ...
-    'shape'     : PyccelFunctionDef('shape'     , Shape),
-    'size'      : PyccelFunctionDef('size'      , NumpyArraySize),
+    'shape'     : PyccelFunctionDef('shape'     , NumpyShape),
+    'size'      : PyccelFunctionDef('size'      , NumpySize),
     'norm'      : PyccelFunctionDef('norm'      , NumpyNorm),
     'int'       : PyccelFunctionDef('int'       , NumpyInt),
     'real'      : PyccelFunctionDef('real'      , NumpyReal),
diff --git a/pyccel/ast/sympy_helper.py b/pyccel/ast/sympy_helper.py
index 8e3ea31992..01d35b038c 100644
--- a/pyccel/ast/sympy_helper.py
+++ b/pyccel/ast/sympy_helper.py
@@ -4,8 +4,8 @@
 #------------------------------------------------------------------------------------------#
 
 """
-Module containing functions which allow us to treat expressions expressed as pyccel nodes with sympy,
-by providing translations between the sympy representation and the pyccel nodes
+Module containing functions which allow us to treat expressions expressed as Pyccel nodes with SymPy,
+by providing translations between the SymPy representation and the Pyccel nodes
 """
 
 import sympy as sp
@@ -13,19 +13,15 @@
 
 from pyccel.utilities.strings import create_incremented_string
 
+from .internals import PyccelArrayShapeElement
 from .operators import PyccelAdd, PyccelMul, PyccelPow, PyccelUnarySub
 from .operators import PyccelDiv, PyccelMinus, PyccelAssociativeParenthesis
 from .core      import Iterable
-
 from .builtins  import PythonRange, PythonTuple
-
 from .mathext   import MathCeil
-
 from .literals  import LiteralInteger, LiteralFloat, LiteralComplex
-
 from .datatypes import NativeInteger
-
-from .variable  import Variable, PyccelArraySize
+from .variable  import Variable
 
 __all__ = ('sympy_to_pyccel',
            'pyccel_to_sympy')
@@ -33,19 +29,23 @@
 #==============================================================================
 def sympy_to_pyccel(expr, symbol_map):
     """
-    Convert a sympy expression to a pyccel expression replacing sympy symbols with
-    pyccel expressions provided in a symbol_map
-
-      Parameters
-      ----------
-      expr       : PyccelAstNode
-                   The pyccel node to be translated
-      symbol_map : dict
-                   Dictionary mapping sympy symbols to pyccel objects
-
-      Returns
-      ----------
-      expr       : pyccel Object
+    Convert a SymPy expression to a Pyccel expression.
+
+    Convert a SymPy expression to a Pyccel expression replacing SymPy symbols with
+    Pyccel expressions provided in a symbol_map
+
+    Parameters
+    ----------
+    expr : SymPy object
+        The SymPy expression to be translated.
+
+    symbol_map : dict
+        Dictionary mapping SymPy symbols to Pyccel objects.
+
+    Returns
+    -------
+    PyccelAstNode
+        The Pyccel equivalent of the SymPy object `expr`.
     """
 
     #Constants
@@ -114,23 +114,28 @@ def sympy_to_pyccel(expr, symbol_map):
 #==============================================================================
 def pyccel_to_sympy(expr, symbol_map, used_names):
     """
-    Convert a pyccel expression to a sympy expression saving any pyccel objects
-    converted to sympy symbols in a dictionary to allow the reverse conversion
-    to be carried out later
-
-      Parameters
-      ----------
-      expr       : PyccelAstNode
-                   The pyccel node to be translated
-      symbol_map : dict
-                   Dictionary containing any pyccel objects converted to sympy symbols
-      used_names : Set
-                   A set of all the names which already exist and therefore cannot
-                   be used to create new symbols
-
-      Returns
-      ----------
-      expr       : sympy Object
+    Convert a Pyccel expression to a SymPy expression.
+
+    Convert a Pyccel expression to a SymPy expression saving any Pyccel objects
+    converted to SymPy symbols in a dictionary to allow the reverse conversion
+    to be carried out later.
+
+    Parameters
+    ----------
+    expr : PyccelAstNode
+        The Pyccel node to be translated.
+
+    symbol_map : dict
+        Dictionary containing any Pyccel objects converted to SymPy symbols.
+
+    used_names : Set
+        A set of all the names which already exist and therefore cannot
+        be used to create new symbols.
+
+    Returns
+    -------
+    SymPy Object
+        The SymPy equivalent of the `expr` argument.
     """
 
     #Constants
@@ -182,7 +187,7 @@ def pyccel_to_sympy(expr, symbol_map, used_names):
         symbol_map[sym] = expr
         return sym
 
-    elif isinstance(expr, PyccelArraySize):
+    elif isinstance(expr, PyccelArrayShapeElement):
         sym_name,_ = create_incremented_string(used_names, prefix = 'tmp_size')
         used_names.add(sym_name)
         sym = sp.Symbol(sym_name)
diff --git a/pyccel/ast/variable.py b/pyccel/ast/variable.py
index 33b263676e..bc6b2defc7 100644
--- a/pyccel/ast/variable.py
+++ b/pyccel/ast/variable.py
@@ -16,7 +16,7 @@
 from .datatypes import (datatype, DataType,
                         NativeInteger, NativeBool, NativeFloat,
                         NativeComplex)
-from .internals import PyccelArraySize, Slice, get_final_precision
+from .internals import PyccelArrayShapeElement, Slice, get_final_precision
 from .literals  import LiteralInteger, Nil
 from .operators import (PyccelMinus, PyccelDiv, PyccelMul,
                         PyccelUnarySub, PyccelAdd)
@@ -219,15 +219,24 @@ def __init__(
             self._order = None
 
     def process_shape(self, shape):
-        """ Simplify the provided shape and ensure it
-        has the expected format
+        """
+        Simplify the provided shape and ensure it has the expected format.
+
+        The provided shape is the shape used to create the object, and it can
+        be a long expression. In most cases where the shape is required the
+        provided shape is inconvenient, or it might have become invalid. This
+        function therefore replaces those expressions with calls to the function
+        `PyccelArrayShapeElement`.
 
-        The provided shape is the shape used to create
-        the object. In most cases where the shape is
-        required we do not require this expression
-        (which can be quite long). This function therefore
-        replaces those expressions with calls to
-        PyccelArraySize
+        Parameters
+        ----------
+        shape : iterable of int
+            The array shape to be simplified.
+
+        Returns
+        -------
+        tuple
+            The simplified array shape.
         """
         if self.rank == 0:
             return None
@@ -235,16 +244,16 @@ def process_shape(self, shape):
             shape = [shape]
 
         new_shape = []
-        for i,s in enumerate(shape):
+        for i, s in enumerate(shape):
             if self.shape_can_change(i):
                 # Shape of a pointer can change
-                new_shape.append(PyccelArraySize(self, LiteralInteger(i)))
+                new_shape.append(PyccelArrayShapeElement(self, LiteralInteger(i)))
             elif isinstance(s, LiteralInteger):
                 new_shape.append(s)
             elif isinstance(s, int):
                 new_shape.append(LiteralInteger(s))
             elif s is None or isinstance(s, PyccelAstNode):
-                new_shape.append(PyccelArraySize(self, LiteralInteger(i)))
+                new_shape.append(PyccelArrayShapeElement(self, LiteralInteger(i)))
             else:
                 raise TypeError('shape elements cannot be '+str(type(s))+'. They must be one of the following types: LiteralInteger,'
                                 'Variable, Slice, PyccelAstNode, int, Function')
@@ -252,16 +261,31 @@ def process_shape(self, shape):
 
     def shape_can_change(self, i):
         """
-        Indicates if the shape can change in the i-th dimension
+        Indicate if the shape can change in the i-th dimension.
+
+        Indicate whether the Variable's shape can change in the i-th dimension
+        at run time.
+
+        Parameters
+        ----------
+        i : int
+            The dimension over which the shape can change at runtime.
+
+        Returns
+        -------
+        bool
+            Whether or not the variable shape can change in the i-th dimension.
         """
         return self.is_alias
 
     def set_changeable_shape(self):
         """
+        Indicate that the Variable's shape is unknown at compilation time.
+
         Indicate that the exact shape is unknown, e.g. if the allocate is done in
         an If block.
         """
-        self._shape = [PyccelArraySize(self, LiteralInteger(i)) for i in range(self.rank)]
+        self._shape = [PyccelArrayShapeElement(self, LiteralInteger(i)) for i in range(self.rank)]
 
     @property
     def name(self):
diff --git a/pyccel/codegen/printing/ccode.py b/pyccel/codegen/printing/ccode.py
index bbae800e1e..07cec25df8 100644
--- a/pyccel/codegen/printing/ccode.py
+++ b/pyccel/codegen/printing/ccode.py
@@ -28,7 +28,7 @@
 from pyccel.ast.datatypes import NativeInteger, NativeBool, NativeComplex, NativeVoid
 from pyccel.ast.datatypes import NativeFloat, NativeTuple, datatype, default_precision
 
-from pyccel.ast.internals import Slice, PrecomputedCode, get_final_precision
+from pyccel.ast.internals import Slice, PrecomputedCode, get_final_precision, PyccelArrayShapeElement
 
 from pyccel.ast.literals  import LiteralTrue, LiteralFalse, LiteralImaginaryUnit, LiteralFloat
 from pyccel.ast.literals  import LiteralString, LiteralInteger, Literal
@@ -42,7 +42,7 @@
 from pyccel.ast.utilities import expand_to_loops
 
 from pyccel.ast.variable import IndexedElement
-from pyccel.ast.variable import PyccelArraySize, Variable
+from pyccel.ast.variable import Variable
 from pyccel.ast.variable import DottedName
 from pyccel.ast.variable import DottedVariable
 from pyccel.ast.variable import InhomogeneousTupleVariable, HomogeneousTupleVariable
@@ -1215,7 +1215,7 @@ def _print_IndexedElement(self, expr):
                 #managing the Slice input
                 for i , ind in enumerate(inds):
                     if isinstance(ind, Slice):
-                        inds[i] = self._new_slice_with_processed_arguments(ind, PyccelArraySize(base, i),
+                        inds[i] = self._new_slice_with_processed_arguments(ind, PyccelArrayShapeElement(base, i),
                             allow_negative_indexes)
                     else:
                         inds[i] = Slice(ind, PyccelAdd(ind, LiteralInteger(1), simplify = True), LiteralInteger(1),
@@ -1266,19 +1266,28 @@ def _print_DottedVariable(self, expr):
 
     @staticmethod
     def _new_slice_with_processed_arguments(_slice, array_size, allow_negative_index):
-        """ Create new slice with informations collected from old slice and decorators
+        """
+        Create new slice with information collected from old slice and decorators.
+
+        Create a new slice where the original `start`, `stop`, and `step` have
+        been processed using basic simplifications, as well as additional rules
+        identified by the function decorators.
 
         Parameters
         ----------
-            _slice : Slice
-                slice needed to collect (start, stop, step)
-            array_size : PyccelArraySize
-                call to function size()
-            allow_negative_index : Bool
-                True when the decorator allow_negative_index is present
+        _slice : Slice
+            Slice needed to collect (start, stop, step).
+
+        array_size : PyccelArrayShapeElement
+            Call to function size().
+
+        allow_negative_index : bool
+            True when the decorator allow_negative_index is present.
+
         Returns
         -------
-            Slice
+        Slice
+            The new slice with processed arguments (start, stop, step).
         """
         start = LiteralInteger(0) if _slice.start is None else _slice.start
         stop = array_size if _slice.stop is None else _slice.stop
@@ -1286,13 +1295,13 @@ def _new_slice_with_processed_arguments(_slice, array_size, allow_negative_index
         # negative start and end in slice
         if isinstance(start, PyccelUnarySub) and isinstance(start.args[0], LiteralInteger):
             start = PyccelMinus(array_size, start.args[0], simplify = True)
-        elif allow_negative_index and not isinstance(start, (LiteralInteger, PyccelArraySize)):
+        elif allow_negative_index and not isinstance(start, (LiteralInteger, PyccelArrayShapeElement)):
             start = IfTernaryOperator(PyccelLt(start, LiteralInteger(0)),
                             PyccelMinus(array_size, start, simplify = True), start)
 
         if isinstance(stop, PyccelUnarySub) and isinstance(stop.args[0], LiteralInteger):
             stop = PyccelMinus(array_size, stop.args[0], simplify = True)
-        elif allow_negative_index and not isinstance(stop, (LiteralInteger, PyccelArraySize)):
+        elif allow_negative_index and not isinstance(stop, (LiteralInteger, PyccelArrayShapeElement)):
             stop = IfTernaryOperator(PyccelLt(stop, LiteralInteger(0)),
                             PyccelMinus(array_size, stop, simplify = True), stop)
 
@@ -1315,14 +1324,14 @@ def _new_slice_with_processed_arguments(_slice, array_size, allow_negative_index
 
         return Slice(start, stop, step)
 
-    def _print_NumpyArraySize(self, expr):
+    def _print_PyccelArraySize(self, expr):
         arg = expr.arg
         if self.is_c_pointer(arg):
             return '{}->length'.format(self._print(ObjectAddress(arg)))
         return '{}.length'.format(self._print(arg))
 
-    def _print_PyccelArraySize(self, expr):
-        arg    = expr.arg
+    def _print_PyccelArrayShapeElement(self, expr):
+        arg = expr.arg
         if self.is_c_pointer(arg):
             return '{}->shape[{}]'.format(self._print(ObjectAddress(arg)), self._print(expr.index))
         return '{}.shape[{}]'.format(self._print(arg), self._print(expr.index))
diff --git a/pyccel/codegen/printing/fcode.py b/pyccel/codegen/printing/fcode.py
index 2aef2bc956..1b83c51e85 100644
--- a/pyccel/codegen/printing/fcode.py
+++ b/pyccel/codegen/printing/fcode.py
@@ -24,7 +24,6 @@
 from pyccel.ast.core import ErrorExit, FunctionAddress
 from pyccel.ast.core import Return, Module
 from pyccel.ast.core import Import
-from pyccel.ast.internals    import PyccelInternalFunction, get_final_precision
 from pyccel.ast.itertoolsext import Product
 from pyccel.ast.core import (Assign, AliasAssign, Declare,
                              CodeBlock, AsName, EmptyNode,
@@ -33,7 +32,7 @@
 from pyccel.ast.variable  import (Variable,
                              IndexedElement,
                              InhomogeneousTupleVariable,
-                             DottedName, PyccelArraySize)
+                             DottedName, )
 
 from pyccel.ast.operators      import PyccelAdd, PyccelMul, PyccelMinus
 from pyccel.ast.operators      import PyccelMod
@@ -54,7 +53,8 @@
 from pyccel.ast.datatypes import NativeRange, NativeNumeric
 from pyccel.ast.datatypes import CustomDataType
 
-from pyccel.ast.internals import Slice, PrecomputedCode
+from pyccel.ast.internals import Slice, PrecomputedCode, PyccelArrayShapeElement
+from pyccel.ast.internals import PyccelInternalFunction, get_final_precision
 
 from pyccel.ast.literals  import LiteralInteger, LiteralFloat, Literal
 from pyccel.ast.literals  import LiteralTrue, LiteralFalse, LiteralString
@@ -69,7 +69,6 @@
 from pyccel.ast.numpyext import NumpyNewArray
 from pyccel.ast.numpyext import NumpyNonZero
 from pyccel.ast.numpyext import NumpySign
-from pyccel.ast.numpyext import Shape
 from pyccel.ast.numpyext import DtypePrecisionToCastFunction
 
 from pyccel.ast.utilities import builtin_import_registry as pyccel_builtin_import_registry
@@ -1145,16 +1144,19 @@ def _print_NumpyArange(self, expr):
     def _print_NumpyMod(self, expr):
         return self._print(PyccelMod(*expr.args))
 
-    def _print_NumpyArraySize(self, expr):
+    # ======================================================================= #
+    def _print_PyccelArraySize(self, expr):
         init_value = self._print(expr.arg)
         prec = self.print_kind(expr)
-        return 'size({0}, kind={1})'.format(init_value, prec)
+        return f'size({init_value}, kind={prec})'
 
-    # ======================================================================= #
-    def _print_PyccelArraySize(self, expr):
+    def _print_PyccelArrayShapeElement(self, expr):
         init_value = self._print(expr.arg)
         prec = self.print_kind(expr)
 
+        if expr.arg.rank == 1:
+            return f'size({init_value}, kind={prec})'
+
         if expr.arg.order == 'C':
             index = PyccelMinus(LiteralInteger(expr.arg.rank), expr.index, simplify = True)
             index = self._print(index)
@@ -1162,10 +1164,7 @@ def _print_PyccelArraySize(self, expr):
             index = PyccelAdd(expr.index, LiteralInteger(1), simplify = True)
             index = self._print(index)
 
-        if expr.arg.rank == 1:
-            return 'size({0}, kind={1})'.format(init_value, prec)
-
-        return 'size({0}, {1}, {2})'.format(init_value, index, prec)
+        return f'size({init_value}, {index}, {prec})'
 
     def _print_PythonInt(self, expr):
         value = self._print(expr.arg)
@@ -2845,11 +2844,10 @@ def _print_IndexedElement(self, expr):
         inds = list(expr.indices)
         if expr.base.order == 'C':
             inds = inds[::-1]
-        base_shape = Shape(expr.base)
         allow_negative_indexes = base.allows_negative_indexes
 
         for i, ind in enumerate(inds):
-            _shape = PyccelArraySize(base, i if expr.base.order != 'C' else len(inds) - i - 1)
+            _shape = PyccelArrayShapeElement(base, i if expr.base.order != 'C' else len(inds) - i - 1)
             if isinstance(ind, Slice):
                 inds[i] = self._new_slice_with_processed_arguments(ind, _shape, allow_negative_indexes)
             elif isinstance(ind, PyccelUnarySub) and isinstance(ind.args[0], LiteralInteger):
@@ -2868,19 +2866,28 @@ def _print_IndexedElement(self, expr):
 
     @staticmethod
     def _new_slice_with_processed_arguments(_slice, array_size, allow_negative_index):
-        """ Create new slice with informations collected from old slice and decorators
+        """
+        Create new slice with information collected from old slice and decorators.
+
+        Create a new slice where the original `start`, `stop`, and `step` have
+        been processed using basic simplifications, as well as additional rules
+        identified by the function decorators.
 
         Parameters
         ----------
-            _slice : Slice
-                slice needed to collect (start, stop, step)
-            array_size : PyccelArraySize
-                call to function size()
-            allow_negative_index : Bool
-                True when the decorator allow_negative_index is present
+        _slice : Slice
+            Slice needed to collect (start, stop, step).
+
+        array_size : PyccelArrayShapeElement
+            Call to function size().
+
+        allow_negative_index : bool
+            True when the decorator allow_negative_index is present.
+
         Returns
         -------
-            Slice
+        Slice
+            The new slice with processed arguments (start, stop, step).
         """
         start = _slice.start
         stop = _slice.stop
diff --git a/pyccel/codegen/printing/pycode.py b/pyccel/codegen/printing/pycode.py
index b0414e9a69..90e24e7a8d 100644
--- a/pyccel/codegen/printing/pycode.py
+++ b/pyccel/codegen/printing/pycode.py
@@ -13,7 +13,7 @@
 from pyccel.ast.functionalexpr import FunctionalFor
 from pyccel.ast.literals   import LiteralTrue, LiteralString
 from pyccel.ast.literals   import LiteralInteger, LiteralFloat, LiteralComplex
-from pyccel.ast.numpyext   import Shape as NumpyShape, numpy_target_swap
+from pyccel.ast.numpyext   import NumpyShape, NumpySize, numpy_target_swap
 from pyccel.ast.numpyext   import NumpyArray, NumpyNonZero
 from pyccel.ast.numpyext   import DtypePrecisionToCastFunction
 from pyccel.ast.variable   import DottedName, HomogeneousTupleVariable, Variable
@@ -429,15 +429,26 @@ def _print_PythonConjugate(self, expr):
     def _print_PythonPrint(self, expr):
         return 'print({})\n'.format(', '.join(self._print(a) for a in expr.expr))
 
-    def _print_PyccelArraySize(self, expr):
+    def _print_PyccelArrayShapeElement(self, expr):
         arg = self._print(expr.arg)
         index = self._print(expr.index)
-        name = self._aliases.get(NumpyShape, expr.name)
-        if name == expr.name:
+        expected_name = NumpyShape.name
+        name = self._aliases.get(NumpyShape, expected_name)
+        if name == expected_name:
+            self.insert_new_import(
+                    source = 'numpy',
+                    target = AsName(type(expr), expected_name))
+        return f'{name}({arg})[{index}]'
+
+    def _print_PyccelArraySize(self, expr):
+        arg = self._print(expr.arg)
+        expected_name = NumpySize.name
+        name = self._aliases.get(NumpySize, expected_name)
+        if name == expected_name:
             self.insert_new_import(
                     source = 'numpy',
-                    target = AsName(type(expr),expr.name))
-        return '{0}({1})[{2}]'.format(name, arg, index)
+                    target = AsName(type(expr), expected_name))
+        return f'{name}({arg})'
 
     def _print_Comment(self, expr):
         txt = self._print(expr.text)


Test patch:
diff --git a/tests/epyccel/recognised_functions/test_numpy_funcs.py b/tests/epyccel/recognised_functions/test_numpy_funcs.py
index 5b78719532..42963f044a 100644
--- a/tests/epyccel/recognised_functions/test_numpy_funcs.py
+++ b/tests/epyccel/recognised_functions/test_numpy_funcs.py
@@ -1196,61 +1196,80 @@ def create_full_arg_names(val):
 
     size = randint(10)
 
-    f_shape_1d  = epyccel(create_full_shape_1d, language = language)
-    assert(f_shape_1d(size) == create_full_shape_1d(size))
+    f_shape_1d = epyccel(create_full_shape_1d, language = language)
+    assert f_shape_1d(size) == create_full_shape_1d(size)
 
-    f_shape_2d  = epyccel(create_full_shape_2d, language = language)
-    assert(f_shape_2d(size) == create_full_shape_2d(size))
+    f_shape_2d = epyccel(create_full_shape_2d, language = language)
+    assert f_shape_2d(size) == create_full_shape_2d(size)
 
-    f_val       = epyccel(create_full_val, language = language)
-    assert(f_val(size)      == create_full_val(size))
+    f_val = epyccel(create_full_val, language = language)
+    assert f_val(size) == create_full_val(size)
     assert matching_types(f_val(size)[0], create_full_val(size)[0])
 
     f_arg_names = epyccel(create_full_arg_names, language = language)
-    assert(f_arg_names(size) == create_full_arg_names(size))
+    assert f_arg_names(size) == create_full_arg_names(size)
     assert matching_types(f_arg_names(size)[0], create_full_arg_names(size)[0])
 
 def test_size(language):
-    @types('int[:]')
-    def test_size_1d(f):
+    def test_size_1d(f: 'int[:]'):
         from numpy import size
         return size(f)
 
-    @types('int[:,:]')
-    def test_size_2d(f):
+    def test_size_2d(f: 'int[:,:]'):
         from numpy import size
         return size(f)
 
+    def test_size_axis_variable_2d(f: 'int[:,:]', axis :'int'):
+        from numpy import size
+        return size(f, axis)
+
+    def test_size_axis_literal_3d(f: 'int[:,:,:]'):
+        from numpy import size
+        return size(f, 2)
+
     from numpy import empty
     f1 = epyccel(test_size_1d, language = language)
     f2 = epyccel(test_size_2d, language = language)
+    f3 = epyccel(test_size_axis_variable_2d, language = language)
+    f4 = epyccel(test_size_axis_literal_3d, language = language)
     n1 = randint(20)
     n2 = randint(20)
     n3 = randint(20)
-    x1 = empty(n1,dtype = int)
-    x2 = empty((n2,n3), dtype = int)
+    axis = randint(2)
+    x1 = empty(n1, dtype = int)
+    x2 = empty((n1, n2), dtype = int)
+    x3 = empty((n1, n3), dtype = int)
+    x4 = empty((n1, n2, n3), dtype = int)
     assert f1(x1) == test_size_1d(x1)
     assert f2(x2) == test_size_2d(x2)
+    assert f3(x3, axis) == test_size_axis_variable_2d(x3, axis)
+    assert f4(x4) == test_size_axis_literal_3d(x4)
+
 
 def test_size_property(language):
-    @types('int[:]')
-    def test_size_1d(f):
+    def test_size_1d(f: 'int[:]'):
         return f.size
 
-    @types('int[:,:]')
-    def test_size_2d(f):
+    def test_size_2d(f: 'int[:,:]'):
+        return f.size
+
+    def test_size_3d(f: 'int[:,:,:]'):
         return f.size
 
     from numpy import empty
     f1 = epyccel(test_size_1d, language = language)
     f2 = epyccel(test_size_2d, language = language)
+    f3 = epyccel(test_size_3d, language = language)
     n1 = randint(20)
     n2 = randint(20)
     n3 = randint(20)
-    x1 = empty(n1,dtype = int)
-    x2 = empty((n2,n3), dtype = int)
+    x1 = empty(n1, dtype = int)
+    x2 = empty((n1, n2), dtype = int)
+    x3 = empty((n1, n2, n3), dtype = int)
     assert f1(x1) == test_size_1d(x1)
     assert f2(x2) == test_size_2d(x2)
+    assert f3(x3) == test_size_3d(x3)
+
 
 def test_full_basic_real(language):
     @types('int')
@@ -6328,3 +6347,4 @@ def nonzero_func(a):
     assert epyccel_func(fl) == nonzero_func(fl)
     assert epyccel_func(fl32) == nonzero_func(fl32)
     assert epyccel_func(fl64) == nonzero_func(fl64)
+