A 3 by 3 identity from NumPy prints with a decimal point on every entry, including the zeros.

Most readers reach for np.eye for that one job and never touch the offset argument, which moves the ones anywhere in the array, or the dtype default, which decides how much memory a label matrix costs. Both details are cheap to check once you know what the array holds.

What np.eye actually returns

The function allocates a zero array of the shape you asked for and then writes ones along one diagonal. In the square case you can reproduce it with two NumPy calls, and the comparison is the clearest way to see what the diagonal argument does.

print(np.eye(3).dtype, np.eye(3).shape, np.eye(3).flags["C_CONTIGUOUS"])
import numpy as np

m = np.zeros((3, 3))
np.fill_diagonal(m, 1)
print(np.array_equal(m, np.eye(3)))

That prints True. The built-in version is faster because it fills the diagonal while allocating, and it also accepts shapes the hand-built version cannot express, such as a rectangle or a diagonal away from the centre.

The k argument moves the ones and leaves the rest of the array at zero
  • The default dtype is float64, so the printed entries carry a decimal point.
  • The result is C-contiguous, which is what most downstream operations expect.
  • No argument is required beyond the row count, and the column count follows it when you leave it out.

Reading the printed array is also the fastest way to check your own offset decision, because a wrong sign puts the ones on the opposite side of the centre and the shape stays identical.

The three arguments that change the result

Every call takes a row count, an optional column count, and an optional diagonal index. The defaults turn a rectangular constructor into a square one, and the offset default puts the ones on the main diagonal.

print(np.eye(3, k=1))
print(np.eye(3, k=-1))
Call Shape Ones sit at
np.eye(3) (3, 3) Row index equals column index
np.eye(3, k=1) (3, 3) One column to the right
np.eye(3, k=-1) (3, 3) One row below the centre
np.eye(3, 5, k=1) (3, 5) Upper diagonal of a rectangle
np.eye(4, M=2) (4, 2) Main diagonal, clipped to the width

Column count and diagonal index can both be passed positionally, and the second one is easy to swap by accident because the two numbers sit next to each other in a call chain. Naming the offset costs four characters and removes the ambiguity.

print(np.eye(4, M=2))
print(np.eye(3, 5, k=1))
print(np.eye(3, 5, 1))

The pair above produces the same array, because the third positional argument is the offset. A reviewer reading a bare triple of numbers has to count the parameters, and a wrong count moves the diagonal instead of raising an error.

Turn class labels into one-hot rows

Indexing the identity matrix with an array of labels is the shortest one-hot encoder NumPy offers, because each label selects the row whose one sits at that position. The result is one row per sample with a single non-zero entry.

labels = np.array([0, 2, 1])
print(np.eye(3)[labels])
print(np.eye(3, dtype=bool)[labels])
Terminal output showing an integer label array converted into a float one-hot matrix and a boolean one-hot matrix with np.eye indexing
One indexing operation produces one row per label

The call allocates the full matrix before indexing, so the peak memory is the encoder array plus the result. Passing dtype=bool cuts the result by a factor of eight while keeping the same indexing behavior, which matters when the label count is large.

  • The number of columns is the number of classes, so np.eye(10)[labels] encodes ten classes.
  • A label equal to the class count raises IndexError rather than producing an all-zero row.
  • Machine learning frameworks accept the integer labels directly, so check whether you need the expanded form at all.
labels = np.array([0, 2, 1])
categorical = np.eye(3, dtype=np.int8)[labels]
print(categorical.nbytes)

A million samples across a hundred and twenty-eight classes occupies 128 MB as int8 and one gigabyte as the default float64. The dtype argument is the difference between a batch that fits and a batch that does not.

Use it as the identity matrix in linear algebra

A square eye array is the identity element for matrix multiplication. NumPy compares it numerically rather than symbolically, so a computed result lands near the identity instead of exactly on it.

rng = np.random.default_rng(0)
A = rng.normal(size=(4, 4))

print(np.round(A @ np.linalg.inv(A), 6))
residual = np.max(np.abs(A @ np.linalg.solve(A, np.eye(4)) - np.eye(4)))
print(residual)
Terminal output showing a matrix multiplied by its inverse rounding to the identity matrix and a solve residual of 2.220446049250313e-16
The identity is the reference the computation is measured against

The residual of 2.2e-16 is the floating point error floor for a 4 by 4 double precision solve, so any check against the identity should use a tolerance near that scale rather than an equality test. The rounded product also shows negative zeros, which are float values equal to zero in any comparison.

print(np.array_equal(np.eye(4), np.identity(4)))
print(np.allclose(A @ np.linalg.inv(A), np.eye(4)))

The first line prints True because identity calls eye with a square shape and the main diagonal. The second uses allclose, which is the comparison to reach for whenever the matrix came from arithmetic rather than from a literal.

Watch the dtype before the array gets large

A default eye array is float64, and the size grows with the square of the row count, which is easy to ignore on a 3 by 3 example and impossible to ignore once the matrix holds a hundred million entries.

print(np.eye(10_000).nbytes)
print(np.eye(10_000, dtype=np.float32).nbytes)
print(np.eye(10_000, dtype=np.int8).nbytes)
dtype Bytes for 10000 by 10000 Typical use
float64 800,000,000 Linear algebra, no dtype argument
float32 400,000,000 Graphics and single precision pipelines
int8 100,000,000 One-hot labels and masks
bool 100,000,000 Boolean masking and selection
python -c "import numpy as np; print(np.eye(10_000, dtype=np.int8).nbytes)"

Bool and int8 report the same byte count because both occupy one byte per entry. Reducing the dtype also changes what arithmetic you can run afterwards, since an int8 matrix overflows in the same operations a float64 matrix handles without comment.

Edge inputs and the errors they raise

The function is tolerant about the diagonal index and strict about the shape, so an offset beyond the array returns zeros instead of failing while a negative dimension stops the call. That exception names the problem directly.

print(np.eye(3, k=5))
print(np.eye(0).shape)
print(np.eye(3, order="F").flags["F_CONTIGUOUS"])

np.eye(-1)

An offset past the edge returns an all-zero array of the requested shape, and a zero row count returns an array with shape (0, 0) and no error. Order F returns a Fortran-contiguous array that some compiled libraries read faster, while a negative row count raises ValueError with the text negative dimensions are not allowed.

The silent all-zero result is the one worth guarding. A loop that computes an offset and drifts out of range produces a matrix of zeros that multiplies to zeros, and the failure surfaces much later as a model that predicts nothing.

k = 5
m = np.eye(3, k=k)
assert m.any(), f"no diagonal written for k={k}"

That assertion costs one line and turns the quiet case into a failure at the point where the offset was wrong rather than three steps later.

eye, identity, and diag compared

Three functions produce overlapping results, and the differences come down to which arguments each one accepts and which shapes it will build. Knowing the boundaries keeps you from working around a function that already does the job.

Function Accepts Best for
np.eye Rows, columns, offset, dtype Identity and offset diagonals, any shape
np.identity One size only Square identity when the argument list is fixed
np.diag A 1-D or 2-D array Placing known values on a diagonal
print(np.diag([2, 3]))
print(np.eye(2) * np.array([2, 3]))

Both calls put 2 and 3 on a diagonal, and they differ in how they get there. np.diag reads the values from a list, while eye builds ones and multiplication scales each row afterwards, which is the shape you want when the scale factors come from a separate computation.

When a dense eye array is the wrong tool

Every eye array stores all of its zeros, and for a large identity that is almost the whole allocation.

When the matrix only ever appears in a multiplication, a diagonal vector does the same work against a fraction of the memory.

big = np.eye(10_000, dtype=np.float32)
print(big.nbytes)

diagonal = np.ones(10_000, dtype=np.float32)
print(diagonal.nbytes)

The dense array needs 400 MB while the diagonal vector needs 40 KB, and the multiplication result is identical when the other operand is a vector. Keep eye for the small cases where the explicit matrix reads more clearly, and switch to a diagonal vector once the row count climbs into the tens of thousands.

What does the k argument do in np.eye?

k selects which diagonal receives the ones. The default 0 is the main diagonal, a positive value moves the ones above it toward the right, and a negative value moves them below it. An offset larger than the array produces an all-zero array rather than an error.

Is np.eye the same as np.identity?

For a square matrix with the default dtype they return equal arrays, and identity calls eye internally. eye is the more general function because it also takes a column count, a diagonal offset, a dtype, and a memory order.

What dtype does np.eye return by default?

float64. Pass dtype=int, dtype=np.int8, or dtype=bool when the matrix stores labels or masks, because a 10000 by 10000 float64 identity costs 800 MB while the int8 version costs 100 MB.

Why does np.eye(-1) raise an error while np.eye(3, k=5) does not?

The row count sets the array shape, so a negative value is rejected with ValueError negative dimensions are not allowed. The diagonal offset only decides which entries are written, so an out-of-range offset still returns a valid array full of zeros.

Share.
Leave A Reply