← Python
ex01June 21, 2026 · 8 min read

2D Arrays: Shape, Axes & Slicing

How a 2D array is really a list of rows, what .shape actually measures, and how slicing lets you carve out sub-arrays without copying data element by element.

pythonnumpyslicing2d-array

Exercise 01 asks for a function that takes a 2D array, prints its shape, and returns a truncated slice. Behind that small task hide two ideas you will use every single day in data science: shape and slicing.

What "2D" really means

A 2D array is just a list of equal-length rows. The family table from the subject is four people, each with a height and a weight:

family = [[1.80, 78.4],
          [2.15, 102.7],
          [2.10, 98.5],
          [1.88, 75.2]]

The first axis (axis 0) runs down the rows; the second axis (axis 1) runs across the columns. That ordering — rows first, then columns — is the convention everywhere in NumPy, pandas and images.

.shape: the array's dimensions

shape is a tuple, one number per axis:

import numpy as np
arr = np.array(family)
arr.shape      # (4, 2)  -> 4 rows, 2 columns
arr.ndim       # 2       -> number of axes
arr.size       # 8       -> total elements

Reading a shape is a reflex worth building: (4, 2) means "4 along axis 0, 2 along axis 1". The expected output literally prints My shape is : (4, 2).

Slicing: start:stop

Python slicing selects a range with sequence[start:stop]. The start is included, the stop is excluded — the half-open convention — so list[0:2] gives the first two elements:

rows = family[0:2]   # first two people
rows = family[1:]    # everything from index 1 onward
rows = family[:-1]   # everything except the last row

That is exactly what the exercise wants from slice_me(family, start, end): print the original shape, slice the rows by start:end, print the new shape, and return the slice.

def slice_me(family, start, end):
    """Print the shape and return family truncated to [start:end]."""
    arr = np.array(family)
    print("My shape is :", arr.shape)
    truncated = arr[start:end]
    print("My new shape is :", truncated.shape)
    return truncated.tolist()

With slice_me(family, 0, 2) the new shape is (2, 2); with slice_me(family, 1, -2) it is (1, 2). Negative indices count from the end, so -2 stops two rows before the last.

The superpower: multi-axis slicing

A plain Python list only slices the outer dimension. A NumPy array slices every axis at once, separated by commas:

arr[0:2, :]    # first two rows, all columns
arr[:, 0]      # the height column only
arr[1:3, 0:1]  # rows 1-2, first column, kept 2D

This is why the subject insists you "use the slicing method" rather than building loops: arr[start:end] is one O(1) view operation, not an element-by-element rebuild.

Views vs copies (a crucial gotcha)

A basic NumPy slice returns a view — it shares memory with the original. Mutating the slice mutates the parent:

sub = arr[0:2]
sub[0, 0] = 99      # arr[0, 0] is now 99 too!

If you need independence, call .copy(). (Lists behave differently: a list slice is always a shallow copy.) Knowing whether you hold a view or a copy prevents some of the most confusing bugs in array code.

Error handling

The subject asks you to guard against bad input: a non-list argument, or rows of unequal length (a "ragged" array, which NumPy cannot turn into a clean 2D block). Check isinstance(family, list) and that every row has the same length before slicing, and raise a clear exception otherwise.

Takeaways

  • A 2D array is rows × columns; shape reports (rows, cols).
  • Slices are half-open (start in, stop out) and support negative indices.
  • NumPy slices across multiple axes with commas — lists cannot.
  • Basic slices are views that share memory; use .copy() to detach.