The very first exercise of the Array piscine looks innocent — "compute a BMI" — but it is really an introduction to the single most important idea in scientific Python: the difference between a list and an array, and the power of vectorized operations.
The exercise
You must write two functions:
def give_bmi(height: list[int | float],
weight: list[int | float]) -> list[int | float]:
# returns the BMI for each (height, weight) pair
def apply_limit(bmi: list[int | float], limit: int) -> list[bool]:
# returns True where bmi is strictly above the limit
And you have to handle the error cases: the two lists must be the same length, and every element must be an int or a float.
A Python list is not an array
People coming from C, Java or NumPy often assume a Python list is a contiguous block of numbers. It is not. A list is a dynamic array of pointers to arbitrary objects. That flexibility (it can hold an int, a string and a function at once) comes at a cost:
- No math operators.
[1, 2] + [3, 4]does not add element-wise — it concatenates into[1, 2, 3, 4]. And[1, 2] * 3repeats, it does not scale. - Slow numerics. Every element is a boxed Python object, so looping over millions of them is heavy.
- No shape. A list has a length, not a multidimensional shape.
Enter NumPy
NumPy's ndarray is the real array: a single, typed, contiguous buffer with a known dtype and shape. The same expression now means the math you expect:
import numpy as np
a = np.array([1, 2, 3])
b = np.array([4, 5, 6])
a + b # array([5, 7, 9]) element-wise
a * 2 # array([2, 4, 6]) scalar broadcast
a / b # array([0.25, 0.4, 0.5])
This is vectorization: you express an operation over the whole array at once, and NumPy runs the loop in optimized C. You write less code and it runs orders of magnitude faster.
BMI, the vectorized way
The Body Mass Index formula is weight / height². With NumPy we never write an explicit loop:
import numpy as np
def give_bmi(height, weight):
"""Return the BMI for each height/weight pair."""
height = np.array(height, dtype=float)
weight = np.array(weight, dtype=float)
if height.shape != weight.shape:
raise ValueError("height and weight must have the same length")
return list(weight / (height ** 2))
The whole computation is the single expression weight / (height ** 2). Both ** and / are applied to every element, pair by pair.
Boolean masks: apply_limit
Comparing an array to a scalar produces a boolean array — one of NumPy's most useful features, the foundation of filtering and masking:
def apply_limit(bmi, limit):
"""Return True where bmi is strictly above limit."""
return list(np.array(bmi) > limit)
For [22.5, 29.0] with a limit of 26 you get [False, True] — exactly the expected output.
Error handling: the part that is actually graded
The subject is explicit: handle lists of different sizes and non-numeric content. Validation is what separates a script from a function:
def give_bmi(height, weight):
if len(height) != len(weight):
raise ValueError("lists must be the same size")
for value in height + weight:
if not isinstance(value, (int, float)):
raise TypeError("values must be int or float")
...
Two subtle points:
isinstance(value, (int, float))accepts both required types in one check. Beware that in Pythonboolis a subclass ofint, soTruewould sneak through — reject it explicitly if you care.- Raise a specific exception (
ValueError,TypeError) with a clear message. The piscine rule "any uncaught exception invalidates the exercise" means yourmain()wraps calls intry/exceptand prints the message.
Takeaways
- A Python
listis a container of objects; a NumPyndarrayis a typed numeric buffer. - Vectorized operations replace explicit loops — shorter, faster, clearer.
- Comparisons yield boolean arrays, the gateway to masking and filtering.
- Validate shape and dtype before you compute, and fail with a precise exception.