Skip to content

Arithmetic Operations

NumPy arrays support element-wise arithmetic operations.

Addition

1. Array Addition

import numpy as np

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

    c = a + b

    print(f"a = {a}")
    print(f"b = {b}")
    print(f"a + b = {c}")

if __name__ == "__main__":
    main()

Output:

a = [1 2 3]
b = [4 5 6]
a + b = [5 7 9]

2. Scalar Addition

import numpy as np

def main():
    a = np.array([1, 2, 3])
    b = 4

    c = a + b

    print(f"a = {a}")
    print(f"b = {b}")
    print(f"a + b = {c}")

if __name__ == "__main__":
    main()

3. 2D Array Addition

import numpy as np

def main():
    a = np.array([[1, 1], [2, 3]])
    b = np.array([[1, 1], [2, 3]])

    c = a + b

    print("a + b =")
    print(c)

if __name__ == "__main__":
    main()

Subtraction

1. Array Subtraction

import numpy as np

def main():
    a = np.array([[1, 2], [3, 4]])
    b = np.array([[1, 1], [2, 2]])

    c = a - b

    print("a =")
    print(a)
    print()
    print("b =")
    print(b)
    print()
    print("a - b =")
    print(c)

if __name__ == "__main__":
    main()

2. Negation

import numpy as np

def main():
    a = np.array([1, -2, 3, -4])

    print(f"a = {a}")
    print(f"-a = {-a}")

if __name__ == "__main__":
    main()

Multiplication

1. Element-wise Multiply

import numpy as np

def main():
    a = np.array([[1, 2], [3, 4]])
    b = np.array([[2, 2], [2, 2]])

    # Three equivalent ways
    c1 = a * b
    c2 = np.multiply(a, b)
    c3 = a.__mul__(b)

    print("a * b =")
    print(c1)
    print()
    print("np.multiply(a, b) =")
    print(c2)

if __name__ == "__main__":
    main()

2. Scalar Multiply

import numpy as np

def main():
    a = np.array([1, 2, 3])
    scalar = 3.5

    print(f"a = {a}")
    print(f"{scalar} * a = {scalar * a}")

if __name__ == "__main__":
    main()

Division

1. True Division

import numpy as np

def main():
    a = np.array([10, 20, 30])
    b = np.array([3, 4, 5])

    c = a / b

    print(f"a = {a}")
    print(f"b = {b}")
    print(f"a / b = {c}")

if __name__ == "__main__":
    main()

2. Floor Division

import numpy as np

def main():
    a = np.array([10, 20, 30])
    b = np.array([3, 4, 5])

    c = a // b

    print(f"a = {a}")
    print(f"b = {b}")
    print(f"a // b = {c}")

if __name__ == "__main__":
    main()

3. Modulo

import numpy as np

def main():
    a = np.array([[1, 2, 3, 4, 5],
                  [6, 7, 8, 9, 10]])

    b = a % 2

    print("a =")
    print(a)
    print()
    print("a % 2 =")
    print(b)

if __name__ == "__main__":
    main()

add vs iadd

1. Regular Addition

import numpy as np

def main():
    a = np.array([[1, 2], [3, 4]])
    original_id = id(a)

    a = a + 100  # __add__: creates new array

    print(f"ID changed: {id(a) != original_id}")
    print(a)

if __name__ == "__main__":
    main()

2. In-place Addition

import numpy as np

def main():
    a = np.array([[1, 2], [3, 4]])
    original_id = id(a)

    a += 100  # __iadd__: modifies in place

    print(f"ID changed: {id(a) != original_id}")
    print(a)

if __name__ == "__main__":
    main()

3. Performance Difference

import numpy as np
import time

def main():
    n = 10_000

    # __add__ (creates copy)
    a = np.random.randn(n, n)
    start = time.perf_counter()
    a = a + 100
    add_time = time.perf_counter() - start

    # __iadd__ (in-place)
    b = np.random.randn(n, n)
    start = time.perf_counter()
    b += 100
    iadd_time = time.perf_counter() - start

    print(f"a = a + 100: {add_time:.4f} sec")
    print(f"a += 100:    {iadd_time:.4f} sec")
    print(f"Speedup:     {add_time/iadd_time:.2f}x")

if __name__ == "__main__":
    main()

In-place Operators

1. All In-place Operators

import numpy as np

def main():
    a = np.array([10, 20, 30], dtype=float)

    print(f"Original: {a}")

    a += 5
    print(f"a += 5:   {a}")

    a -= 5
    print(f"a -= 5:   {a}")

    a *= 2
    print(f"a *= 2:   {a}")

    a /= 2
    print(f"a /= 2:   {a}")

    a //= 3
    print(f"a //= 3:  {a}")

    a %= 2
    print(f"a %= 2:   {a}")

if __name__ == "__main__":
    main()

2. When to Use In-place

  • Large arrays where memory matters
  • Repeated operations in loops
  • When original data is no longer needed

3. Caveats

import numpy as np

def main():
    # In-place cannot change dtype
    a = np.array([1, 2, 3], dtype=int)

    # This works (result fits in int)
    a *= 2
    print(f"a *= 2: {a}, dtype: {a.dtype}")

    # This may truncate (float -> int)
    a = np.array([1, 2, 3], dtype=int)
    a /= 2  # Becomes float division, but stored as int
    print(f"a /= 2: {a}, dtype: {a.dtype}")

if __name__ == "__main__":
    main()

Broadcasting in Arithmetic

1. Different Shapes

import numpy as np

def main():
    a = np.array([1, 2, 3])  # (3,)
    b = np.array([[1], [2], [3], [4]])  # (4, 1)

    c = a + b  # (4, 3)

    print(f"a shape: {a.shape}")
    print(f"b shape: {b.shape}")
    print(f"a + b shape: {c.shape}")
    print()
    print("a + b =")
    print(c)

if __name__ == "__main__":
    main()

2. Incompatible Shapes

import numpy as np

def main():
    a = np.array([1, 2, 3])  # (3,)
    b = np.array([4, 5, 6, 7])  # (4,)

    try:
        c = a + b
    except ValueError as e:
        print(f"Error: {e}")

if __name__ == "__main__":
    main()

3. Shape Requirements

Arrays broadcast when trailing dimensions match or one is 1.

Function Equivalents

1. np.add and np.subtract

import numpy as np

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

    print(f"np.add(a, b) = {np.add(a, b)}")
    print(f"np.subtract(a, b) = {np.subtract(a, b)}")

if __name__ == "__main__":
    main()

2. np.multiply and np.divide

import numpy as np

def main():
    a = np.array([10, 20, 30])
    b = np.array([2, 4, 5])

    print(f"np.multiply(a, b) = {np.multiply(a, b)}")
    print(f"np.divide(a, b) = {np.divide(a, b)}")
    print(f"np.floor_divide(a, b) = {np.floor_divide(a, b)}")
    print(f"np.mod(a, b) = {np.mod(a, b)}")

if __name__ == "__main__":
    main()

3. Using out Parameter

import numpy as np

def main():
    a = np.array([1, 2, 3])
    b = np.array([4, 5, 6])
    result = np.empty(3)

    np.add(a, b, out=result)

    print(f"Result: {result}")

if __name__ == "__main__":
    main()

Runnable Example: basic_operations_tutorial.py

"""
04_basic_operations.py - Vectorized Operations

Key concept: Eliminate Python loops with vectorization!
"""

import numpy as np

if __name__ == "__main__":

    print("="*80)
    print("VECTORIZED OPERATIONS - No Loops Needed!")
    print("="*80)

    # ============================================================================
    # Element-wise Arithmetic
    # ============================================================================

    print("\nElement-wise Arithmetic (Vectorization)")
    print("="*80)

    arr = np.array([1, 2, 3, 4, 5])
    print(f"Array: {arr}")
    print(f"  arr + 10 = {arr + 10}  ← Add to all")
    print(f"  arr * 2 = {arr * 2}   ← Multiply all")
    print(f"  arr ** 2 = {arr ** 2}  ← Square all")
    print(f"  1 / arr = {1 / arr}  ← Reciprocal")

    print("""
    \nCompare to Python lists:
      # Python (SLOW - explicit loop):
      result = [x * 2 for x in my_list]

      # NumPy (FAST - vectorized):
      result = arr * 2

    Vectorization = C-speed loop hidden from Python!
    """)

    # Two arrays
    arr1 = np.array([1, 2, 3])
    arr2 = np.array([10, 20, 30])
    print(f"\narr1 = {arr1}")
    print(f"arr2 = {arr2}")
    print(f"  arr1 + arr2 = {arr1 + arr2}  ← Element-wise")
    print(f"  arr1 * arr2 = {arr1 * arr2}  ← Element-wise")

    # ============================================================================
    # Universal Functions (ufuncs)
    # ============================================================================

    print("\n" + "="*80)
    print("Universal Functions (ufuncs)")
    print("="*80)

    arr = np.array([1, 2, 3, 4])
    print(f"Array: {arr}")
    print(f"  np.sqrt(arr) = {np.sqrt(arr)}")
    print(f"  np.exp(arr) = {np.exp(arr)}")
    print(f"  np.log(arr) = {np.log(arr)}")
    print(f"  np.sin(arr) = {np.sin(arr)}")

    # ============================================================================
    # Comparison Operations
    # ============================================================================

    print("\n" + "="*80)
    print("Comparison Operations")
    print("="*80)

    arr = np.array([10, 15, 20, 25, 30])
    print(f"Array: {arr}")
    print(f"  arr > 18 = {arr > 18}  ← Boolean array")
    print(f"  arr == 20 = {arr == 20}")
    print(f"  (arr >= 15) & (arr <= 25) = {(arr >= 15) & (arr <= 25)}")

    # ============================================================================
    # Aggregation Functions
    # ============================================================================

    print("\n" + "="*80)
    print("Aggregation Functions")
    print("="*80)

    arr = np.array([10, 20, 30, 40, 50])
    print(f"Array: {arr}")
    print(f"  arr.sum() = {arr.sum()}")
    print(f"  arr.mean() = {arr.mean()}")
    print(f"  arr.std() = {arr.std():.2f}")
    print(f"  arr.min() = {arr.min()}")
    print(f"  arr.max() = {arr.max()}")
    print(f"  arr.argmin() = {arr.argmin()} ← Index of min")
    print(f"  arr.argmax() = {arr.argmax()} ← Index of max")

    # 2D aggregations
    matrix = np.array([[1, 2, 3], [4, 5, 6]])
    print(f"\nMatrix:\n{matrix}")
    print(f"  matrix.sum() = {matrix.sum()} ← Total sum")
    print(f"  matrix.sum(axis=0) = {matrix.sum(axis=0)} ← Sum columns")
    print(f"  matrix.sum(axis=1) = {matrix.sum(axis=1)} ← Sum rows")

    print("""
    \n🎯 KEY TAKEAWAYS:
    1. Vectorization eliminates loops (50-200x faster!)
    2. Arithmetic works element-wise
    3. Universal functions (ufuncs) for math
    4. Aggregations: sum, mean, std, min, max
    5. axis parameter: 0=columns, 1=rows

    🔜 NEXT: Intermediate tutorials on broadcasting!
    """)