Each element squared in a first loop, then square-rooted in a second loop. The replay shows squares filling up completely before the second loop starts building roots, making the two-pass structure explicit.

By hand

With NumPy

a ** 2 raises every element to the power 2 in one call. np.sqrt(a) applies the square-root ufunc elementwise. Both operations return new arrays without an explicit loop. The snapshot shows both results in labeled blocks.

naive.py
import math
values = [1, 2, 3, 4, 5, 6]
squares = []
for v in values:
    squares.append(v * v)
roots = []
for v in values:
    roots.append(round(math.sqrt(v), 2))
print('RESULT:', (squares, roots))
library.py
import numpy as np

values = [1, 2, 3, 4, 5, 6]
a = np.array(values)
squares = a ** 2
roots = np.sqrt(a)
sq_vals = squares.tolist()
rt_vals = [round(v, 2) for v in roots.tolist()]
print(f'squares: shape: {squares.shape} dtype: {squares.dtype} values: {sq_vals}')
print(f'roots: shape: {roots.shape} dtype: {roots.dtype} values: {rt_vals}')
print('RESULT:', (sq_vals, rt_vals))
squares: shape: (6,) dtype: int64 values: [1, 4, 9, 16, 25, 36]
roots: shape: (6,) dtype: float64 values: [1.0, 1.41, 1.73, 2.0, 2.24, 2.45]
RESULT: ([1, 4, 9, 16, 25, 36], [1.0, 1.41, 1.73, 2.0, 2.24, 2.45])

Implementation notes

  • np.sqrt, np.sin, np.log, and similar functions are ufuncs (universal functions). A ufunc applies a C-level scalar kernel to every element of its input array, returning an array of the same shape — the NumPy equivalent of the hand-written loop here.
  • For this integer array, a ** 2 keeps the integer dtype (int64), while np.sqrt(a) promotes to float64. np.sqrt preserves an input's float dtype — e.g. a float32 input stays float32.
  • Shape, dtype, and values are shown explicitly here because ndarray.__repr__ output varies with NumPy version and print options.