Avoid Common Python List Mistakes: [:] vs = Explained

Madhura AnandMadhura Anand
5 min read

In Python, the difference between nums = ... and nums[:] = ... can be subtle but very powerful when working with lists. This article breaks down how each works under the hood, why [:] makes in-place changes, and when to use each.


🧩 Problem Statement: Rotating an Array Left by One

Given an integer array nums, rotate the array to the left by one.

Note: There is no need to return anything — just modify the given array in-place.

Let’s say:

nums = [1, 2, 3, 4, 5]

After rotation, it should become:

[2, 3, 4, 5, 1]

❌ Common Mistake: Using Reassignment

Many beginners might write:

nums = nums[1:] + [nums[0]]

At first glance, this looks correct — it creates the right result.
But what does it really do? Try guessing:

nums = [1, 2, 3, 4, 5]
ref = nums
nums = nums[1:] + [nums[0]]
print(ref)  # ❓ What will this print?

Output:

[1, 2, 3, 4, 5]  # ❌ NOT modified!

Why? Because this code creates a new list and rebinds nums to it. The original list (and anything else pointing to it) remains unchanged.


✅ The Correct Way: Slice Assignment

To actually modify the original array, you should do:

nums[:] = nums[1:] + [nums[0]]

Now:

nums = [1, 2, 3, 4, 5]
ref = nums
nums[:] = nums[1:] + [nums[0]]
print(ref)  # ✅ [2, 3, 4, 5, 1]

Here, the contents of the list are replaced in-place, so any reference to nums sees the update.


🔁 The Two Approaches

1. nums = nums[1:] + [nums[0]]

  • Creates a new list by slicing and concatenating.

  • The variable nums is rebound to this new list.

  • The original list becomes unreferenced and is eligible for garbage collection.

2. nums[:] = nums[1:] + [nums[0]]

  • Uses slice assignment to replace the entire contents of the list nums.

  • The list object itself remains the same; only its contents are replaced.

  • No garbage is created; memory is reused.


🔍 Under the Hood: What's Really Happening

nums[:] = new_list

  • Translates to nums.__setitem__(slice(None), new_list).

  • This is slice assignment.

  • It modifies the existing object in-place, keeping the same id(nums).

nums = new_list

  • Just rebinds the name nums to a new object.

  • The old object is no longer referenced and becomes a candidate for garbage collection.


🔬 Elaborating on Slice Assignment Internals

Absolutely — let’s elaborate deeply on what happens when Python sees a slice on the left-hand side of an assignment like:

nums[:] = new_list

🔍 Step-by-Step: What Python Does Internally

💡 When you write nums[:] = new_list:

Python interprets this as:

nums.__setitem__(slice(None, None, None), new_list)

Let’s break this down.


🧩 1. nums[:]** becomes a slice object**

nums[:]  ➝ slice(None, None, None)
  • This is Python’s way of saying:
    ➤ "Select the full list — from beginning to end with step size 1."

It is syntactic sugar for:

slice_obj = slice(None, None, None)

🔧 2. Python calls __setitem__ on the list object

nums.__setitem__(slice_obj, new_list)

This triggers a special method inside Python’s list object that replaces the list’s contents for the specified slice.

It’s like saying:

“Hey list object — replace the part matching this slice with new_list.”


⚙️ Internals of __setitem__ for slice:

The method __setitem__(s: slice, iterable) will:

  • Compute the range of indexes the slice refers to

  • Remove those elements

  • Insert new elements from iterable in their place

  • All while modifying the same underlying list object

✅ No new object is created
✅ Same memory, same identity (id(nums) remains unchanged)

🔁 Example with __setitem__ manually:

nums = [1, 2, 3, 4, 5]
slice_obj = slice(None)
nums.__setitem__(slice_obj, [9, 9, 9])
print(nums)  # [9, 9, 9]

✅ This is exactly what nums[:] = [9,9,9] does.


🧠 Summary: Why [:] Triggers In-Place Change

  • [:] → Creates a slice object: slice(None, None, None)

  • nums[:] = ... → Triggers __setitem__ method on the list object

  • __setitem__ replaces elements inside the list’s memory

  • So the list’s identity (memory address) doesn’t change

  • No rebinding of nums, so aliases like other_ref = nums still see the changes


🔁 Example with __setitem__ manually:

nums = [1, 2, 3, 4, 5]
slice_obj = slice(None)
nums.__setitem__(slice_obj, [9, 9, 9])
print(nums)  # [9, 9, 9]

✅ This is exactly what nums[:] = [9,9,9] does.


🔬 Contrast: nums = new_list

When you write:

nums = new_list

Python does not call any list method. Instead, it performs variable rebinding in the local scope:

# In stack frame
nums ──▶ new_list   # Now points to a different object

The original list is no longer referenced by nums.

There’s no slicing, no method call, no mutation — just a pointer update.


🧠 Memory Behind the Scenes

  • nums[:] = ... → invokes __setitem__ → updates content of the same heap object

  • nums = ... → stack variable now points to a different heap object

Result:

nums[:] = ...    ➝ modifies existing object in-place
nums = ...       ➝ switches pointer to a new object

✅ Summary

ActionObject Mutated?Reference Changed?In-Place?
nums[:] = new_list✅ Yes❌ No✅ Yes
nums = new_list❌ No✅ Yes❌ No

Use [:] when:

  • You want to preserve references to the same list

  • You want to modify a list in-place inside a function or across multiple variables

Use = when:

  • You want to assign an entirely new object to the variable

  • You don’t care about preserving other references


0
Subscribe to my newsletter

Read articles from Madhura Anand directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Madhura Anand
Madhura Anand