We select two rows from an array for a local transformation and subtract the mean of each column. The values look right. But when we return to the original features, the first two rows have changed there too. We meant to prepare a separate batch and ended up rewriting part of the source.

The array shape is correct, and the calculation raised no error. What we need to understand is who owns the data being changed. We'll work through four rows, make the copy boundary explicit, and check that behavior with tests.

A separate variable can still share the same data

Take two numeric features. From the first two rows, we want to subtract their column means: 15 and 150.

import numpy as np features = np.array( [[10.0, 100.0], [20.0, 200.0], [30.0, 300.0], [40.0, 400.0]] ) before = features.copy() batch = features[:2] batch -= batch.mean(axis=0, keepdims=True) print(features) 

Output:

[[ -5. -50.] [ 5. 50.] [ 30. 300.] [ 40. 400.]] 

The batch has the expected values. The surprise is that those values now appear in features. If the next step expected the original features, it is already working with different data.

A basic NumPy slice shares its data buffer with the source array. The documentation calls this result a view. The variable batch gives us another way to access the same elements. The -= operation writes in place, so the change is visible through both variables.

First, check whether the arrays share memory

In this small example, we can check for overlapping memory with numpy.shares_memory:

view = features[:2] copied = features[:2].copy() print(np.shares_memory(view, features)) # True print(np.shares_memory(copied, features)) # False 

This is a question about two specific arrays. Checking view is features does not answer it: the Python objects can differ while sharing their numeric data.

I'd use this check when investigating an unexpected mutation. For complex views with unusual strides, determining exact overlap can be expensive; our simple slice is a small diagnostic example.

We now know what happened. The next decision is where the program actually needs independent data.

Copy at the transformation boundary

If a function is supposed to return a transformed batch while preserving its input, I'd put that guarantee inside the function. Then the calling code does not have to remember to make a copy before every call.

Save this in preprocessing.py:

import numpy as np def center_columns(values): result = np.array(values, dtype=np.float64, copy=True) if result.ndim != 2 or result.size == 0: raise ValueError("Expected a nonempty two-dimensional array") result -= result.mean(axis=0, keepdims=True) return result 

The copy=True argument is deliberate. The result gets its own numeric buffer. Converting to float64 also lets us subtract the mean when the original values are integers.

Restore the initial data from our example and call the function:

from preprocessing import center_columns features = before.copy() centered = center_columns(features[:2]) print(centered) print(np.array_equal(features, before)) print(np.shares_memory(centered, features)) 

We get:

[[ -5. -50.] [ 5. 50.]] True False 

The result contains the same numbers, and the original features remain unchanged. Both parts matter: the calculation is correct, and it has no side effect on the input.

The function is intended for a nonempty, two-dimensional numeric array. This example uses finite values within an ordinary range. Missing values, overflow, and arrays of Python objects are outside its scope.

Why a list of indices behaves differently

Selecting with a list of indices creates a copy when reading. Assigning through those indices still changes the source array:

selected = features[[0, 1]] selected[0, 0] = -1.0 print(features[0, 0]) # 10.0  features[[0, 1]] = 0.0 print(features[0, 0]) # 0.0 

In the first case, we change the separate selection result. In the second, we explicitly write values into the selected rows of features. So I would avoid shortening the rule to "a list of indices makes mutation safe." We need to look at the target of the assignment.

Nor should we assume that reshape always creates independent data. Depending on how the elements are laid out, it can return a view or a copy. If a function promises to preserve its input, express the copying explicitly.

What the tests should establish

Save the tests in test_preprocessing.py. The first demonstrates a write through a slice. The second checks our input-preservation guarantee and the expected result. The third distinguishes reading with a list of indices from assigning through it. The last rejects shapes the function is not designed to handle.

import unittest import numpy as np from preprocessing import center_columns class ArrayOwnershipTests(unittest.TestCase): def setUp(self): self.features = np.array( [[10.0, 100.0], [20.0, 200.0], [30.0, 300.0], [40.0, 400.0]] ) def test_slice_writes_reach_the_source(self): batch = self.features[:2] self.assertTrue(np.shares_memory(batch, self.features)) batch[0, 0] = -1.0 self.assertEqual(self.features[0, 0], -1.0) def test_centering_returns_owned_data(self): before = self.features.copy() centered = center_columns(self.features[:2]) np.testing.assert_array_equal(self.features, before) np.testing.assert_array_equal( centered, [[-5.0, -50.0], [5.0, 50.0]] ) self.assertFalse(np.shares_memory(centered, self.features)) def test_advanced_read_and_assignment_are_different(self): selected = self.features[[0, 1]] self.assertFalse(np.shares_memory(selected, self.features)) selected[0, 0] = -1.0 self.assertEqual(self.features[0, 0], 10.0) self.features[[0, 1]] = 0.0 np.testing.assert_array_equal(self.features[:2], np.zeros((2, 2))) def test_centering_rejects_an_unsupported_shape(self): for values in [[], [1.0, 2.0], np.empty((0, 2)), np.empty((2, 0)), np.zeros((1, 1, 1))]: with self.subTest(values=values): with self.assertRaises(ValueError): center_columns(values) if __name__ == "__main__": unittest.main() 

Run: python -m unittest -v test_preprocessing.py. The four tests and the outputs shown here were checked on Python 3.12.14 and NumPy 2.3.5.

A copy costs memory and time. I wouldn't copy every intermediate array without a reason. If a step deliberately changes a shared buffer, the calling code should make that intent clear. If it promises to preserve the original data, that promise needs an explicit boundary and a check.

For our example, one copy at the start of the transformation is enough. The next step can once again rely on the original features. That agreement between parts of the program matters more than having a separate variable called batch.

References

  1. NumPy 2.3. Copies and views.
  2. NumPy 2.3. numpy.shares_memory.
  3. NumPy 2.3. numpy.array.