Back to Blog
Python

Python List Aliasing: Why Two Names Can Share One List

python list aliasing: Understand how Python list aliasing works, why assigning a list to a new name shares the same object, and how to copy lists correctly.

list aliasingpython listsshallow copydeep copymutable objects
Two variable name labels pointing to a single shared Python list object, with a separate copy arrow leading to an independent list.

When you assign a list to a new variable in Python, you do not create a copy of the list. You create a second name that refers to the same list object. This behavior is called python list aliasing, and it is the source of a common class of bugs where mutating one variable unexpectedly changes another.

original = [1, 2, 3] alias = original alias.append(4) print(original) # [1, 2, 3, 4]

The variable alias does not hold a separate list. Both names point to the same object in memory. Any mutation through either name is visible through the other.

How Aliasing Happens

Aliasing is not limited to simple variable assignment. It occurs any time you pass a list to a function, store it in a data structure, or return it from a function, because Python passes and returns object references rather than copies.

def add_item(items, value): items.append(value) cart = ["apple"] add_item(cart, "banana") print(cart) # ['apple', 'banana']

The function receives a reference to the same list that cart refers to. Modifying items inside the function modifies the caller's list. This is intentional Python behavior, and it is useful when you want a function to update a collection in place. But when you expect the function to leave the original untouched, aliasing produces surprising results.

Why Aliasing Causes Bugs

The danger appears when code assumes that a new variable name means a new object. Consider a configuration list that is shared across several components:

default_ports = [80, 443] user_ports = default_ports user_ports.append(8080) print(default_ports) # [80, 443, 8080]

The default_ports list is now corrupted because user_ports was meant to be a separate list. The root cause is that user_ports = default_ports created an alias, not a copy.

The same problem appears when a list is stored as a default value in a function definition:

def register_user(roles=[]): roles.append("user") return roles

Every call that omits roles shares the same default list object, so roles accumulate across calls. This is a well-known Python gotcha, and it is a direct consequence of list aliasing combined with mutable default arguments.

Creating a Real Copy with copy() and Slicing

To break the alias, you need to create a new list object. The standard ways are the copy() method, the list() constructor, and the slice operator.

original = [1, 2, 3] copy_method = original.copy() copy_constructor = list(original) copy_slice = original[:] copy_method.append(4) print(original) # [1, 2, 3] print(copy_method) # [1, 2, 3, 4]

All three approaches create a new list object that contains references to the same elements. The new list is independent at the top level: appending, removing, or reordering elements in the copy does not affect the original.

The slice operator is the oldest idiom and still appears in legacy code. The copy() method is more explicit and is the clearest choice for new code. The list() constructor is useful when you want to convert any iterable into a list and also happens to copy.

Shallow Copy vs. Deep Copy

The copies above are shallow. The new list contains references to the same element objects. If the elements themselves are mutable, mutations to those elements are visible through both lists.

matrix = [[1, 2], [3, 4]] shallow = matrix.copy() shallow[0].append(99) print(matrix) # [[1, 2, 99], [3, 4]]

The inner lists are shared between matrix and shallow. To fully separate nested structures, you need a deep copy from the copy module.

import copy matrix = [[1, 2], [3, 4]] deep = copy.deepcopy(matrix) deep[0].append(99) print(matrix) # [[1, 2], [3, 4]] print(deep) # [[1, 2, 99], [3, 4]]

copy.deepcopy() recursively copies every object in the structure. It is slower than a shallow copy and can fail on objects that cannot be copied, such as open file handles or certain connection objects. Use it only when nested mutable elements must be independent.

Detecting Aliasing with the identity Operator

When you are unsure whether two names refer to the same list, use the is operator to compare object identity.

a = [1, 2, 3] b = a c = a.copy() print(a is b) # True print(a is c) # False

The is check tells you whether two variables reference the same object, which is exactly the condition that defines aliasing. This is useful in debugging and in tests that assert that a function did not accidentally mutate its input.

Choosing the Right Copying Strategy

The decision between aliasing, shallow copy, and deep copy depends on the structure of the data and the intent of the code.

ApproachNew top-level listNew nested objectsUse when
Direct assignmentNoNoYou intentionally share state
copy() / list() / sliceYesNoElements are immutable or sharing is fine
copy.deepcopy()YesYesNested mutable elements must be independent

For a flat list of immutable values such as integers, strings, or tuples, a shallow copy is sufficient. For a list of dictionaries or lists, a shallow copy leaves the inner objects shared, which is often the source of subtle bugs. Deep copy guarantees full independence but costs more and may fail on uncopyable objects.

When a function needs to modify a list without affecting the caller, the function should create a copy internally:

def with_item_added(items, value): result = items.copy() result.append(value) return result

This pattern is common in functional-style code where inputs are treated as immutable. It keeps the caller's data safe and makes the function's behavior predictable.

Aliasing in Function Arguments and Return Values

Function parameters are local names that alias the objects passed in. This is the basis of Python's pass-by-object-reference model. When you mutate a list parameter, you mutate the caller's list. When you reassign the parameter, you only change the local name.

def mutate(items): items.append("changed") def reassign(items): items = ["new", "list"] data = [1] mutate(data) print(data) # [1, 'changed'] reassign(data) print(data) # [1, 'changed']

The reassign function does not affect data because items = ["new", "list"] creates a new list and points the local name at it. The original list remains unchanged. Understanding this distinction is essential for writing functions that either update or preserve their inputs.

Return values can also create aliases. If a function returns an internal list, the caller gains direct access to that list and can mutate it. To protect internal state, return a copy instead:

class Inventory: def __init__(self): self._items = [] def items(self): return self._items.copy()

This prevents callers from modifying the internal _items list through the accessor. The tradeoff is that each call creates a new list, which is acceptable for most application workloads but worth considering when the accessor is called frequently in a hot path.

Aliasing in List Comprehensions and Loops

List comprehensions create new lists, so they do not alias the source list. However, the elements inside the new list are the same objects as in the source.

source = [[1], [2]] result = [row for row in source] result[0].append(99) print(source) # [[1, 99], [2]]

The comprehension copies the outer list structure but shares the inner list objects. If you need the result to be fully independent, apply a deep copy inside the comprehension:

import copy source = [[1], [2]] result = [copy.deepcopy(row) for row in source]

This pattern is useful when transforming nested data while preserving the original structure. It also makes the independence requirement explicit at the point where the copy is created.

Practical Rules for Avoiding Aliasing Bugs

The core rule is simple: if you want an independent list, create a new list explicitly. Relying on assignment to produce a copy is the most common mistake.

When you write a function that accepts a list, decide whether it should mutate the input or treat it as read-only. If it mutates, document that behavior. If it does not, copy the input at the start of the function or build a new list and return it.

When you store a list as a class attribute, decide whether external code should be able to modify it. Returning a copy from accessors protects internal state but adds allocation cost. For small lists in typical applications, the cost is negligible.

When you work with nested lists, remember that shallow copy is rarely what you want if the inner elements are modified after the copy. Use copy.deepcopy() when full independence is required, and be aware that it cannot copy every object type.

These decisions are not about avoiding aliasing entirely. Aliasing is a legitimate and efficient way to share large structures without copying. The goal is to know when you are aliasing and when you are copying, so that the behavior of your code matches your intent.

python list aliasing: Practical Usage and Code Examples | RYUSLOG DEV