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;
shapereports(rows, cols). - Slices are half-open (
startin,stopout) and support negative indices. - NumPy slices across multiple axes with commas — lists cannot.
- Basic slices are views that share memory; use
.copy()to detach.