A pure Python linear algebra and tensor library built entirely from scratch. No NumPy, no C extensions, no shortcuts β just potatoes all the way down.
It took me 3 weeks alone and 2 hours of Claude documentation refinement to make this.
PotatoNumPy is a learning project that answers three questions I kept asking myself:
-
How does linear algebra actually work under the hood? Every matrix multiplication here is three nested loops. Every determinant is recursive cofactor expansion. Nothing hides behind an opaque C function β you can read every line and follow along.
-
Why is NumPy so ridiculously fast? After watching PotatoNumPy chug through a 50x50 matrix multiply, you'll feel what NumPy's C/Fortran backends, SIMD vectorization, and contiguous memory layouts buy you. It's not subtle.
-
What does Python's interpreter overhead actually cost? Dynamic type checking, function call overhead, pointer-chasing through Python lists β it all adds up. This library makes that cost painfully tangible.
pip install potatonumpyOr clone and install locally:
git clone https://github.com/Madhavyamjala/potatonumpy.git
cd potatonumpy
pip install -e .No dependencies needed β that's the whole point.
import potatonumpy as pp
# Create arrays
a = pp.array([1, 2, 3])
b = pp.array([4, 5, 6])
# Elementwise math, just like you'd expect
print(a + b) # [5, 7, 9]
print(a * b) # [4, 10, 18]
print(a ** 2) # [1, 4, 9]
print(a + 10) # [11, 12, 13]
# Linear algebra
print(pp.dot(a, b)) # 32
print(pp.cross(a, b)) # [-3, 6, -3]
print(pp.magnitude(a)) # 3.7416...
print(pp.normalize(a)) # [0.2672, 0.5345, 0.8017]import potatonumpy as pp
A = pp.array([[1, 2], [3, 4]])
B = pp.array([[5, 6], [7, 8]])
print(pp.matmul(A, B)) # [[19, 22], [43, 50]]
print(pp.transpose(A)) # [[1, 3], [2, 4]]
print(pp.determinant(A)) # -2.0
print(pp.inverse(A)) # [[-2, 1], [1.5, -0.5]]
print(pp.trace(A)) # 5
print(pp.diagonal(A)) # [1, 4]
# Special matrices
print(pp.identity(3))
print(pp.zeros((2, 3)))You can inspect array metadata and modify structural dimensions.
import potatonumpy as pp
a = pp.array([[1, 2, 3], [4, 5, 6]])
print(a.ndim)
print(a.size)
print(a.shape)
flat = a.flatten()
print(flat)
reshaped = flat.reshape((3, 2))
print(reshaped)
raw_list = reshaped.tolist()
print(raw_list)import potatonumpy as pp
# 3D tensor β go wild
t = pp.array([[[1, 2], [3, 4]], [[5, 6], [7, 8]]])
print(t.shape) # (2, 2, 2)
# Reductions
print(pp.tensor_sum(t)) # 36
print(pp.tensor_sum(t, axis=0)) # [[6, 8], [10, 12]]
print(pp.tensor_mean(t)) # 4.5
print(pp.tensor_min(t)) # 1
print(pp.tensor_max(t)) # 8Works with int, float, and complex:
c = pp.array([1+2j, 3+4j])
print(c + 1) # [(2+2j), (4+4j)]PotatoNumPy doesn't silently do the wrong thing. It yells at you with specific exceptions:
from potatonumpy import (
ShapeMismatchError, # tried to add (2,) and (3,)?
InvalidTensorError, # ragged array? nope
SingularMatrixError, # can't invert that matrix, sorry
InvalidOperationError, # division by zero, etc.
)
pp.array([1, 2]) + pp.array([1, 2, 3]) # ShapeMismatchError
pp.array([[1, 2], [3, 4, 5]]) # InvalidTensorError
pp.inverse(pp.array([[1, 2], [2, 4]])) # SingularMatrixError
pp.array([1, 2]) / 0 # InvalidOperationErrorpython examples/benchmarking.pyHere's what you'll see (roughly):
| Operation | Pure Loops | PotatoNumPy | NumPy | Slowdown vs NumPy |
|---|---|---|---|---|
| Vector Add (10k) | ~2ms | ~5ms | ~0.01ms | ~500x |
| Matrix Mul (50x50) | ~50ms | ~80ms | ~0.05ms | ~1600x |
| Scalar Mul (10k) | ~1ms | ~3ms | ~0.005ms | ~600x |
Yeah. NumPy is that much faster. Here's why:
| What | PotatoNumPy | NumPy |
|---|---|---|
| Inner loops | Python bytecode | Compiled C/Fortran |
| Memory layout | Scattered Python objects | Contiguous typed arrays |
| Vectorization | Nope | SIMD instructions |
| Type checking | Every single operation | Once at array creation |
| Function calls | Python stack frames | Inlined C calls |
| Math backend | Hand-rolled loops | BLAS/LAPACK |
python -m unittest discover tests -v99 tests (One Short π₯²) covering all operations, edge cases, and error conditions.
.
βββ src/
β βββ potatonumpy/
β βββ __init__.py
β βββ core.py
β βββ linalg.py
β βββ tensor.py
β βββ exceptions.py
β βββ utils.py
β βββ benchmarks.py
βββ tests/
β βββ test_core.py
β βββ test_linalg.py
β βββ test_tensor.py
βββ examples/
β βββ vectors.py
β βββ matrices.py
β βββ benchmarking.py
βββ README.md
βββ pyproject.toml
βββ setup.py
- No magic. Every operation is explicit loops you can step through in a debugger. Want to understand how matrix inverse works? Read the code.
- Recursion over cleverness. Determinants use cofactor expansion. Shape validation walks the full tree. This is intentional β clarity over performance.
- The slowness is a feature. Seriously. The massive performance gap between PotatoNumPy and NumPy is the lesson.
- Zero dependencies. Standard library only. If Python doesn't ship it, we don't use it.
- Slow. Orders of magnitude slower than NumPy. That's the point.
- Scalar broadcasting only. No fancy NumPy-style shape broadcasting.
- Memory hungry. Python lists use ~8x more memory per element than NumPy arrays.
- Determinant is O(n!). Don't try matrices bigger than ~12x12 unless you have time to spare.
- No eigenvalues, SVD, FFT, or sparse matrices. This is an educational tool, not a production library.
MIT License. See LICENSE for details.
Built with love, frustration, and an unreasonable number of nested for loops. π₯