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
- What a ufunc is and why it's fast
- Arithmetic ufuncs: +, -, *, /, **
- Math ufuncs: np.sqrt, np.exp, np.log, np.sin
- np.add(a, b) is the same as a + b
- Comparison ufuncs returning boolean arrays
- The out parameter for in-place results
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]
# TrueExpected 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.
| Task | Code |
|---|---|
| Element-wise add | a + b or np.add(a, b) |
| Power | arr ** 2 |
| Square root | np.sqrt(arr) |
| Exponential / log | np.exp(arr) / np.log(arr) |
| Compare | a > b |
| Write into a buffer | np.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
- Previous: Reshaping and Flattening
- Next: Broadcasting Explained — Operate on arrays of different shapes without writing loops
- Quick reference: NumPy cheat sheet › Math & Aggregation
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.