Element-wise Operations & Universal Functions

Reviewed & published by Brayan K

A universal function (ufunc) is a NumPy function that applies the same operation to every element of an array at once, running in compiled C — element-wise operations like +, np.sqrt, and np.exp are all ufuncs.

Part of the free NumPy course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

In this lesson you'll use arithmetic ufuncs, math ufuncs like np.sqrt and np.log, see that np.add(a, b) equals a + b, run comparison ufuncs, and meet the out parameter.

What You'll Learn in This Lesson

1 What Is a Universal Function?

A universal function, or ufunc, is a function that runs element-by-element over an entire array in fast, compiled C code. Because the loop happens inside C rather than Python, ufuncs are dramatically faster than writing your own for loop.

You've already used ufuncs without knowing it: every arithmetic operator on an array is one.

import numpy as np

arr = np.array([1, 2, 3, 4])

# Each operator applies to every element at once
print(arr + 10)    # [11 12 13 14]
print(arr - 1)     # [0 1 2 3]
print(arr * 2)     # [2 4 6 8]
print(arr / 2)     # [0.5 1.  1.5 2. ]
print(arr ** 2)    # [ 1  4  9 16]

# ✅ Expected output:
# [11 12 13 14]
# [0 1 2 3]
# [2 4 6 8]
# [0.5 1.  1.5 2. ]
# [ 1  4  9 16]

Expected output: [11 12 13 14], [0 1 2 3], [2 4 6 8], [0.5 1. 1.5 2. ], and [ 1 4 9 16].

2 Math Ufuncs: sqrt, exp, log, sin

NumPy ships hundreds of math ufuncs. Each one applies its function to every element and returns a new array. The most common are np.sqrt (square root), np.exp (ex), np.log (natural log), and the trig functions like np.sin.

import numpy as np

arr = np.array([1, 4, 9, 16])

print(np.sqrt(arr))          # [1. 2. 3. 4.]

vals = np.array([0, 1, 2])
print(np.exp(vals))          # [1.         2.71828183 7.3890561 ]

print(np.log(np.array([1, np.e])))   # [0. 1.]

angles = np.array([0, np.pi / 2])
print(np.sin(angles))        # [0. 1.]

# ✅ Expected output:
# [1. 2. 3. 4.]
# [1.         2.71828183 7.3890561 ]
# [0. 1.]
# [0. 1.]

Expected output: [1. 2. 3. 4.], the exponentials [1. 2.718... 7.389...], [0. 1.] for the logs, and [0. 1.] for the sines.

3 np.add(a, b) Equals a + b

The operators are convenient aliases for named ufuncs. a + b calls np.add(a, b), a * b calls np.multiply(a, b), and so on. They give identical results — and the named form is what lets you pass extra options like out.

import numpy as np

a = np.array([1, 2, 3])
b = np.array([10, 20, 30])

# Operator form and ufunc form are equivalent
print(a + b)            # [11 22 33]
print(np.add(a, b))     # [11 22 33]

print(a * b)                  # [10 40 90]
print(np.multiply(a, b))      # [10 40 90]

# Confirm they really match
print(np.array_equal(a + b, np.add(a, b)))   # True

# ✅ Expected output:
# [11 22 33]
# [11 22 33]
# [10 40 90]
# [10 40 90]
# True

Expected output: [11 22 33] twice, [10 40 90] twice, and True.

4 Comparison Ufuncs and the out Parameter

Comparisons like a > b are ufuncs too — they return a boolean array, element by element. And most ufuncs accept an out argument to write the result into an existing array instead of allocating a new one, which saves memory on large data.

import numpy as np

a = np.array([1, 5, 3])
b = np.array([2, 4, 3])

# Comparison ufuncs return boolean arrays
print(a > b)             # [False  True False]
print(np.equal(a, b))    # [False False  True]

# The out parameter writes into an existing array (no new array made)
result = np.zeros(3)
np.add(a, b, out=result)
print(result)            # [3. 9. 6.]

# Scalar vs element-wise
print(a + 5)             # [ 6 10  8]  -> scalar added to each
print(a + b)             # [3 9 6]     -> matched position by position

# ✅ Expected output:
# [False  True False]
# [False False  True]
# [3. 9. 6.]
# [ 6 10  8]
# [3 9 6]

Expected output: [False True False], [False False True], [3. 9. 6.], then [ 6 10 8] and [3 9 6].

🎯 YOUR TURN: Fill in the Blanks

Replace each ___ so the program squares, square-roots, and adds two arrays.

import numpy as np

arr = np.array([1, 2, 3, 4])

print(arr ___ 2)              # square each element -> [ 1  4  9 16]

print(np.___(np.array([9, 16])))  # square root -> [3. 4.]

a = np.array([1, 2])
b = np.array([3, 4])
print(np.___(a, b))           # add the two arrays -> [4 6]

Expected output: [ 1 4 9 16], [3. 4.], and [4 6]. (Answers: **, sqrt, add.)

⚠️ Common Errors & Quick Reference

❌ TypeError: only size-1 arrays can be converted to Python scalars

You called math.sqrt(arr) from the standard library on a whole array.

✅ Fix: use the ufunc np.sqrt(arr), which handles every element.

❌ ValueError: operands could not be broadcast together

You added two arrays of mismatched shapes element-wise.

✅ Fix: make the shapes match, or add a scalar like arr + 5.

TaskCode
Element-wise adda + b or np.add(a, b)
Powerarr ** 2
Square rootnp.sqrt(arr)
Exponential / lognp.exp(arr) / np.log(arr)
Comparea > b
Write into a buffernp.add(a, b, out=res)

🏆 Mini Challenge: Distance from the Origin

Given x and y coordinates, compute each point's distance from the origin using ufuncs: sqrt(x² + y²).

import numpy as np

x = np.array([3, 6, 0])
y = np.array([4, 8, 5])

# Square, add, then square-root — all element-wise ufuncs
distances = np.sqrt(x ** 2 + y ** 2)
print(distances)   # [5. 10.  5.]

# Which points are farther than 6 away?
print(distances > 6)   # [False  True False]

# ✅ Expected output:
# [ 5. 10.  5.]
# [False  True False]

❓ Frequently Asked Questions

Lesson complete — math at C speed!

You now know what a ufunc is, use arithmetic and math ufuncs across whole arrays, understand that np.add(a, b) equals a + b, run comparison ufuncs, and can route results through the out parameter.

Practice quiz

What is a universal function (ufunc) in NumPy?

  • A function that only works on scalars
  • A function that runs element-by-element across an array in compiled C
  • A plotting helper
  • A way to read files

Answer: A function that runs element-by-element across an array in compiled C. A ufunc applies the same operation to every element at once, in fast C.

What does arr + 10 do for arr = [1, 2, 3, 4]?

  • Raises an error
  • Appends 10
  • Adds 10 to every element -> [11 12 13 14]
  • Returns 10

Answer: Adds 10 to every element -> [11 12 13 14]. A scalar is applied element-wise, adding 10 to each value.

Is a + b the same as np.add(a, b)?

  • Yes, the operator is a shortcut for the ufunc
  • No, results differ
  • Only for floats
  • Only for 1D arrays

Answer: Yes, the operator is a shortcut for the ufunc. The arithmetic operators are friendly aliases for the named ufuncs.

Why use np.sqrt(arr) instead of math.sqrt(arr)?

  • np.sqrt is slower
  • They are identical
  • math.sqrt is more accurate
  • math.sqrt only works on one number, np.sqrt works on a whole array

Answer: math.sqrt only works on one number, np.sqrt works on a whole array. np.sqrt handles every element; math.sqrt errors on an array.

What does a comparison ufunc like a > b return?

  • A single True/False
  • A boolean array, element by element
  • An integer count
  • The larger array

Answer: A boolean array, element by element. Comparisons are ufuncs that return a boolean array of the same shape.

What does the out parameter of a ufunc do?

  • Prints the result
  • Reverses the array
  • Writes the result into an existing array instead of allocating a new one
  • Sorts the output

Answer: Writes the result into an existing array instead of allocating a new one. out= reuses an existing buffer, saving memory on large arrays.

What does np.sqrt([1, 4, 9, 16]) return?

  • [1. 2. 3. 4.]
  • [1 4 9 16]
  • [1 2 3 4 5]
  • [2. 4. 6. 8.]

Answer: [1. 2. 3. 4.]. Element-wise square roots give [1. 2. 3. 4.].

What does 'element-wise' mean?

  • Operating on the whole array as one value
  • Sorting the elements
  • Removing duplicates
  • Operating on each matching pair of elements independently

Answer: Operating on each matching pair of elements independently. a + b adds a[0]+b[0], a[1]+b[1], and so on, position by position.

What does arr ** 2 produce for arr = [1, 2, 3, 4]?

  • [2 4 6 8]
  • [1 2 3 4]
  • [1 4 9 16]
  • 10

Answer: [1 4 9 16]. ** raises each element to the power 2 element-wise.

What does np.add(a, b, out=res) do?

  • Returns a new array only
  • Stores the element-wise sum into res
  • Subtracts b from a
  • Compares a and b

Answer: Stores the element-wise sum into res. It writes the sum a+b into the existing array res.

Continue this course

Frequently asked questions

What is a universal function (ufunc) in NumPy?

A ufunc is a function that operates element-by-element across a whole array, running in compiled C for speed. np.sqrt, np.exp, and even the + operator are ufuncs — they apply the same operation to every element at once without a Python loop.

Is a + b the same as np.add(a, b)?

Yes. The arithmetic operators are friendly shortcuts for the underlying ufuncs: a + b calls np.add, a * b calls np.multiply, and so on. They produce identical results, so use whichever reads more clearly.

What does element-wise mean?

Element-wise means the operation is applied to each pair of matching elements independently. For arrays a and b, a + b adds a[0]+b[0], a[1]+b[1], and so on, producing a new array of the same shape.

What is the 'out' parameter for?

Most ufuncs accept out= to write the result into an existing array instead of allocating a new one. This saves memory on large arrays. For example np.add(a, b, out=a) stores the sum back into a.