2025 Python Interview Prep: Key Technical Questions

Posts

When working with Python, particularly with mutable data types such as lists, dictionaries, or custom objects, developers often face scenarios where they need to copy these data structures. Copying is an important concept because modifying a data structure in one place might unintentionally affect other parts of a program if a reference is shared. This is where shallow and deep copies come into play. Understanding the distinction between the two is critical for writing bug-free and efficient Python code.

In Python, assignment does not copy an object; it merely creates a new reference to the same object. This means that changes made through one reference will reflect in all references to that object. To truly create a new object that is a copy of the original, you must either create a shallow copy or a deep copy.

This part of the discussion focuses on the foundational concepts behind copying in Python, including how memory and references work, what makes Python different from some other languages in this regard, and what scenarios typically call for copying rather than referencing.

Memory Management and Object References in Python

To fully grasp the implications of copying, one must understand how memory and object referencing works in Python. Python handles memory management through reference counting and automatic garbage collection. When an object is created, a reference is assigned to it. Multiple variables can point to the same object.

For instance, consider the example:

x = [1, 2, 3]
y = x

Here, both x and y point to the same list object in memory. Any change made through y will also reflect when accessing x, because both are just references to the same list. If you append a new element using y like so:

y.append(4)

Then printing x would also show [1, 2, 3, 4], indicating that the change made through one reference affects the original object. This behavior is useful in some cases, but in many scenarios—such as when working with functions or classes—you might want to prevent unintended side effects by working with a copy of the data.

The distinction between references and actual values becomes crucial in complex data structures such as lists of lists or dictionaries containing other dictionaries. A superficial or shallow copy of such structures might not be enough to create true independence between the original and the copy.

What Is a Shallow Copy?

A shallow copy of an object is a new object that is a copy of the original object, but it contains references to the same elements as the original. This means that the container (like a list or dictionary) is duplicated, but the contents themselves are not copied.

In other words, the top-level object is a new object, but the nested objects within it are still shared between the original and the copied object. This can lead to unexpected results if the internal data is modified, as these changes will be reflected in both the original and the copied structure.

You can create a shallow copy using several methods in Python. For example, with a list, you can use slicing:

original = [[1, 2], [3, 4]]
shallow_copy = original[:]

In this case, original and shallow_copy are different list objects, but they contain references to the same inner lists [1, 2] and [3, 4]. So modifying shallow_copy[0][0] = 100 will also modify original[0][0].

Alternatively, you can use the copy module’s copy() function to achieve the same result:

import copy
shallow_copy = copy.copy(original)

Again, this creates a new top-level list, but the elements inside the list are not independently duplicated.

This method of copying is efficient and fast, but it does not provide full isolation between the original and copied object when nested structures are involved. It is most suitable when working with flat data structures or when the inner objects are immutable.

When to Use Shallow Copies

Shallow copies are useful in scenarios where you need to duplicate the top-level structure but are okay with the inner objects being shared. This might be the case in performance-sensitive code where deep copying is too expensive, or when the inner elements are not going to be modified, thus eliminating the risk of unintended side effects.

For example, if you have a list of immutable elements such as integers, strings, or tuples, then a shallow copy is often sufficient. The immutability ensures that changes cannot be made to the inner elements, which means the shared references won’t be a problem.

Another valid use case is when working with configurations or templates that are partially static. You might want to copy the outer structure and selectively override only some fields without impacting the base template. In these situations, shallow copies strike a balance between performance and functionality.

However, developers must exercise caution. The assumption that inner elements won’t be changed can lead to hard-to-detect bugs if those elements are later modified in ways that affect multiple parts of the program. Therefore, shallow copies should only be used when you’re confident that the inner objects will remain untouched or are immutable by design.

What Is a Deep Copy?

A deep copy goes beyond simply copying the top-level object. It recursively copies all nested objects, creating entirely independent copies of every object within the structure. This means that modifications to the copied object, including its nested elements, have no effect on the original object.

Deep copies are particularly important when working with nested or complex data structures, such as dictionaries containing lists of dictionaries. Without a deep copy, any modification to an inner element would affect both the original and the copy, leading to unpredictable behavior and data corruption.

To create a deep copy in Python, you typically use the deepcopy() function from the copy module:

import copy
deep_copy = copy.deepcopy(original)

This approach ensures that every object, from the top-level container down to the deepest nested object, is duplicated. All references are broken, and each part of the new object occupies its own memory space.

Deep copies are more expensive in terms of time and memory compared to shallow copies, especially for large data structures. However, the trade-off is worth it when full isolation is required between the original and the copy.

Understanding when to use deep copying is critical for applications where data integrity is paramount. This includes simulations, testing, recursive algorithms, and scenarios involving state management, where preserving the original state is necessary while experimenting with or modifying a copy.

Practical Examples: Deep vs. Shallow Copy

To illustrate the difference between deep and shallow copies, consider the following example using a list of dictionaries:

original = [{‘a’: 1}, {‘b’: 2}]
shallow_copy = copy.copy(original)
deep_copy = copy.deepcopy(original)

If you modify an inner dictionary like this:

shallow_copy[0][‘a’] = 99

Then the change will be reflected in original as well, because shallow_copy and original still share the same inner dictionaries. However, making the same change in deep_copy will not affect the original, because deep_copy has its own copies of all nested dictionaries.

This example highlights the core difference between the two approaches: shallow copies duplicate the outer structure but not the contents, while deep copies duplicate everything. This distinction becomes increasingly important as the complexity and depth of your data structures increase.

Understanding this behavior also has implications for debugging and testing. In many cases, bugs that arise due to shared references are difficult to trace because they occur outside the immediate scope of the code. By using deep copies where appropriate, you can eliminate an entire class of potential errors.

Performance Considerations

While deep copying offers clear advantages in terms of independence and isolation, it comes at a performance cost. Each object in the hierarchy must be copied recursively, which can be computationally intensive. For large data structures, this can significantly affect the performance of your program.

In contrast, shallow copying is much faster, as it involves duplicating only the top-level object and referencing the existing inner objects. For applications where performance is a key consideration and the data structure does not require deep isolation, shallow copying may be the better choice.

Python developers must weigh these trade-offs based on the specific requirements of their applications. Profiling tools and memory analyzers can help determine the impact of different copying strategies on overall performance. In some cases, redesigning the data structure to minimize the need for copying may be more effective than trying to optimize the copying itself.

Copying is a fundamental concept in Python programming that can have far-reaching effects on how data is managed and manipulated. The difference between shallow and deep copies lies in how much of the original object’s structure and content is duplicated.

Shallow copies are suitable for simple, flat data structures or cases where inner objects are immutable and do not require isolation. They offer speed and efficiency, but they do not prevent side effects from shared references. Deep copies, on the other hand, provide complete independence by duplicating every part of the data structure, making them ideal for complex, nested objects where modifications must not affect the original.

Understanding when and how to use each type of copying is essential for writing robust, maintainable, and efficient Python code. Whether you’re working on a small script or a large-scale system, mastering these concepts will help you avoid common pitfalls and ensure that your programs behave as expected.

Real-World Scenarios Where Copying Matters

Understanding the difference between shallow and deep copies becomes even more important when working on real-world projects. In practice, developers regularly deal with data models, nested structures, configurations, user sessions, and complex object graphs that can introduce subtle bugs if not handled carefully.

When designing or maintaining code that manipulates such structures, deciding whether to copy, reference, or transform data can have a significant impact on the correctness, maintainability, and performance of the system.

Use Case: Cloning Configuration Templates

One common real-world example is cloning configuration dictionaries. Imagine a scenario where an application loads a base configuration template and applies user-specific overrides. If the base template is modified directly, it could impact all users sharing that reference.

python

CopyEdit

import copy

base_config = {

    “theme”: “light”,

    “features”: {

        “chat”: True,

        “notifications”: True

    }

}

# Shallow copy

user_config = copy.copy(base_config)

# Modify nested element

user_config[“features”][“chat”] = False

In this case, changing the chat feature for user_config will also affect base_config, because features is still a shared reference. The fix would be to use a deep copy:

python

CopyEdit

user_config = copy.deepcopy(base_config)

user_config[“features”][“chat”] = False

Now the modification is isolated to user_config, and base_config remains unchanged.

Use Case: Copying in Class Instances

Another common use case is duplicating objects of custom classes that contain mutable attributes. When copying class instances, it is important to ensure that mutable attributes such as lists or dictionaries are also copied correctly.

python

CopyEdit

class Product:

    def __init__(self, name, tags):

        self.name = name

        self.tags = tags

import copy

p1 = Product(“Laptop”, [“electronics”, “tech”])

p2 = copy.copy(p1)

p3 = copy.deepcopy(p1)

p2.tags.append(“new”)

p3.tags.append(“sale”)

Here, modifying p2.tags also changes p1.tags, because a shallow copy shares the same list reference. However, p3.tags remains independent due to the deep copy.

This distinction is critical in applications such as e-commerce, where product templates or variants must be cloned and independently customized.

Use Case: Cloning Trees and Graphs

When dealing with recursive data structures such as trees or graphs, a shallow copy is almost never sufficient. These structures often contain references that point back to earlier nodes, or contain deeply nested children that must be duplicated entirely to avoid recursion-related bugs.

python

CopyEdit

class Node:

    def __init__(self, value):

        self.value = value

        self.children = []

root = Node(1)

child = Node(2)

root.children.append(child)

# Deep copy is required to duplicate entire structure

import copy

new_root = copy.deepcopy(root)

Without a deep copy, any modification to child through new_root would also affect root, making the original structure unsafe for concurrent or independent modifications.

In machine learning, data preprocessing pipelines sometimes rely on tree-like structures. Failing to deep copy these objects during training and testing phases can lead to unpredictable behavior or data leakage between models.

Use Case: Function Argument Side Effects

Copying is also important when passing mutable objects to functions. Consider a function that modifies a list:

python

CopyEdit

def process_data(data):

    data.append(“processed”)

records = [“raw”]

process_data(records)

In this case, records is modified in place. If the goal is to preserve the original and work on a copy, you should pass a copy instead:

python

CopyEdit

def process_data(data):

    new_data = copy.deepcopy(data)

    new_data.append(“processed”)

    return new_data

This pattern is useful in pure functional programming styles, unit testing, and concurrency, where side effects should be avoided.

Nested Containers and the Pitfalls of Shallow Copying

A major pitfall when using shallow copies is the presence of nested containers. Consider the following structure:

python

CopyEdit

matrix = [[0] * 3 for _ in range(3)]

shallow_copy = copy.copy(matrix)

shallow_copy[0][0] = 1

Although shallow_copy is a new list, each row is still a shared reference to the original rows. As a result, changing one element in shallow_copy also modifies matrix. A deep copy is the correct approach if you want complete independence.

python

CopyEdit

deep_copy = copy.deepcopy(matrix)

deep_copy[0][0] = 1  # matrix remains unchanged

This problem becomes harder to detect in large codebases, especially when the nested objects span multiple layers and modules. To avoid such surprises, deep copies should be the default when duplicating unknown or recursive structures.

Customizing Copy Behavior with __copy__ and __deepcopy__

Python allows developers to customize how their objects are copied by implementing special methods in their classes: __copy__() and __deepcopy__(). This is especially useful when you want to control which attributes get copied, skip certain properties, or optimize the copying process.

Implementing __copy__

python

CopyEdit

class User:

    def __init__(self, name, preferences):

        self.name = name

        self.preferences = preferences

    def __copy__(self):

        print(“Using custom shallow copy”)

        return User(self.name, self.preferences)

Implementing __deepcopy__

python

CopyEdit

class User:

    def __deepcopy__(self, memo):

        print(“Using custom deep copy”)

        copied_prefs = copy.deepcopy(self.preferences, memo)

        return User(copy.deepcopy(self.name, memo), copied_prefs)

The memo dictionary prevents infinite recursion when copying recursive structures. It tracks already-copied objects and reuses them if encountered again.

Best Practices for Using Copies

Use Immutable Types When Possible

One of the simplest ways to avoid the need for copying is to design your data structures using immutable types (e.g., tuples instead of lists, frozensets instead of sets). These structures inherently prevent unintended side effects and reduce the risk of bugs related to shared references.

Be Explicit About Copying

Avoid implicit copying or assumptions about whether a function or operation returns a new object or a reference. Use the copy or deepcopy function to clearly communicate your intention, and document your code accordingly.

Minimize Deep Copying in Performance-Critical Code

Deep copying can be expensive. In performance-sensitive applications such as real-time systems, games, or large-scale data processing, consider using alternative strategies such as:

  • Object pooling
  • Lazy loading
  • Selective copying (only copying what’s needed)

Always Test Shared vs. Independent Behavior

When designing tests for code that involves copying, explicitly test whether objects are shared or independent after operations. This helps catch bugs early and ensures your logic works as expected.

Beware of Shared Default Arguments

Do not use mutable objects as default arguments in function definitions. This can lead to unexpected shared state between calls.

python

CopyEdit

def get_config(options={}):  # Bad

    options[“timeout”] = 10

    return options

Instead, use None and initialize inside the function:

python

CopyEdit

def get_config(options=None):

    if options is None:

        options = {}

    options[“timeout”] = 10

    return options

Interview-Style Python Exercises on Copying

In technical interviews, candidates are often asked questions that test their understanding of object copying in real Python programs. These questions are rarely theoretical — instead, they challenge your reasoning about how memory, references, and mutability work together.

This section presents coding problems that illustrate real-world applications and edge cases of shallow and deep copying, followed by explanations and sample answers.

Exercise 1: Shallow Copy Behavior with Nested Lists

Question

What will be the output of the following code, and why?

python

CopyEdit

import copy

a = [[1, 2], [3, 4]]

b = copy.copy(a)

b[0][0] = 99

print(“a:”, a)

print(“b:”, b)

Answer

Output:

lua

CopyEdit

a: [[99, 2], [3, 4]]

b: [[99, 2], [3, 4]]

Explanation:

copy.copy(a) creates a shallow copy of a. This means that the outer list b is a new object, but the inner lists [1, 2] and [3, 4] are shared between a and b. Modifying b[0][0] changes the inner list that is still referenced by both a and b.

Exercise 2: Deep Copy Independence

Question

Modify the code from Exercise 1 so that a remains unchanged when b is modified.

Answer

Use copy.deepcopy():

python

CopyEdit

import copy

a = [[1, 2], [3, 4]]

b = copy.deepcopy(a)

b[0][0] = 99

print(“a:”, a)

print(“b:”, b)

Output:

lua

CopyEdit

a: [[1, 2], [3, 4]]

b: [[99, 2], [3, 4]]

Explanation:

copy.deepcopy() recursively copies every nested object, so b contains entirely new inner lists. Changes to b do not affect a.

Exercise 3: Copying Dictionaries with Nested Lists

Question

What will the following code print?

python

CopyEdit

import copy

original = {‘key’: [1, 2, 3]}

shallow = copy.copy(original)

shallow[‘key’].append(4)

print(“original:”, original)

print(“shallow:”, shallow)

Answer

Output:

yaml

CopyEdit

original: {‘key’: [1, 2, 3, 4]}

shallow: {‘key’: [1, 2, 3, 4]}

Explanation:

The outer dictionary was copied, but the inner list is shared. Appending to the list inside shallow also affects original.

To avoid this, use a deep copy:

python

CopyEdit

deep = copy.deepcopy(original)

deep[‘key’].append(5)

Now original remains unchanged.

Exercise 4: Custom Class with Mutable Attributes

Question

What happens when you shallow copy a custom class containing a mutable attribute?

python

CopyEdit

class Item:

    def __init__(self, data):

        self.data = data

import copy

x = Item([1, 2])

y = copy.copy(x)

y.data.append(3)

print(“x.data:”, x.data)

print(“y.data:”, y.data)

Answer

Output:

kotlin

CopyEdit

x.data: [1, 2, 3]

y.data: [1, 2, 3]

Explanation:

The shallow copy of x creates a new Item instance y, but the list inside data is still shared. Any modification to the list affects both objects.

To avoid this, use:

python

CopyEdit

y = copy.deepcopy(x)

Now the data list will also be copied.

Exercise 5: Shared References in Function Arguments

Question

Given the following function, what will be printed?

python

CopyEdit

def add_entry(record, item):

    record.append(item)

data = [‘start’]

copy_data = data

add_entry(copy_data, ‘end’)

print(data)

Answer

Output:

css

CopyEdit

[‘start’, ‘end’]

Explanation:

copy_data = data does not copy the list; it simply creates another reference. The function modifies the list in place, and the change is visible through both names.

To prevent this, copy the list before passing:

python

CopyEdit

add_entry(data[:], ‘end’)  # Shallow copy for flat list

Debugging Tip 1: Identifying Shared References

Use the id() function or the is operator to check whether two variables point to the same object.

python

CopyEdit

a = [1, 2, 3]

b = copy.copy(a)

print(a is b)  # False (different objects)

print(a[0] is b[0])  # True (same elements, if immutable)

This helps detect hidden sharing issues in complex structures.

Debugging Tip 2: Logging Before and After Copy

When tracking down a bug caused by improper copying, insert print statements or use logging to inspect structures before and after copy operations:

python

CopyEdit

import logging

logging.basicConfig(level=logging.DEBUG)

original = {‘nums’: [1, 2, 3]}

copy_version = copy.copy(original)

logging.debug(f”Original ID: {id(original[‘nums’])}”)

logging.debug(f”Copy ID: {id(copy_version[‘nums’])}”)

This will show whether inner objects were actually duplicated.

Interview Question: Custom __deepcopy__ Implementation

Question

How would you implement a custom deep copy method for a class that contains a list of other class instances?

Sample Answer

python

CopyEdit

class Node:

    def __init__(self, value):

        self.value = value

        self.children = []

    def __deepcopy__(self, memo):

        import copy

        copied_node = Node(self.value)

        memo[id(self)] = copied_node

        copied_node.children = [copy.deepcopy(child, memo) for child in self.children]

        return copied_node

This method ensures that each node and its children are recursively copied, while also handling cyclic references using the memo dictionary.

Final Notes on Interview Preparation

Common Mistakes to Avoid

  • Assuming assignment copies an object (it doesn’t).
  • Using shallow copies with nested mutable elements.
  • Forgetting to copy function arguments that will be modified.
  • Misusing default mutable arguments in function definitions.

How to Practice

  • Use nested lists, dicts, and custom classes.
  • Write copy vs. deepcopy test cases.
  • Explain memory layout and references out loud.
  • Practice tracing reference chains on paper or a whiteboard.

Final Thoughts 

Understanding the difference between shallow copy and deep copy is a foundational skill in Python programming. These concepts affect how data is handled in memory, how changes to data propagate through your program, and how bugs related to shared references can be introduced — or avoided.

Why It Matters

In simple scripts, the impact of copying may seem minor, but in real-world applications — such as web frameworks, data processing pipelines, configuration systems, or recursive algorithms — incorrect copying can cause subtle and difficult-to-detect issues. One accidental shared reference in a deeply nested structure might lead to months of instability or hard-to-reproduce bugs.

Python gives you full control over how your objects are copied, but with that power comes responsibility. You must decide:

  • When to reference
  • When to shallow copy
  • When a full deep copy is essential
  • When to redesign your data structures to eliminate the need for copying altogether

What to Remember

  • Assignment never creates a copy. It only creates a new reference to the same object.
  • Shallow copy copies the container but not the contents. Use it only when inner objects don’t need to be isolated.
  • Deep copy copies everything recursively. It is safer for complex structures but more expensive in time and memory.
  • Immutable objects don’t need deep copies. If your data is read-only or made of immutable types, copying may be unnecessary.
  • Be cautious with mutable defaults. Always avoid using mutable types as default function arguments.
  • Use the copy module. For both shallow (copy.copy) and deep (copy.deepcopy) copying, the built-in copy module is the standard tool.

Key Advice

  1. Always think about data ownership. Ask yourself: who should be allowed to modify this data?
  2. Be intentional. Don’t assume copying happens just because you assigned a variable. Be explicit when duplication is needed.
  3. Favor simplicity. If your code requires constant deep copying, it might be a signal to refactor or redesign your data model.
  4. Test for independence. Especially when writing functions, classes, or libraries, verify that returned objects are isolated from inputs when needed.

The right understanding and use of shallow and deep copies will help you write cleaner, safer, and more predictable Python code. Whether you’re working solo or on a team, making intentional choices about copying ensures that your software behaves the way you expect — without hidden surprises.