Locked learning resources

Join us and get access to thousands of tutorials and a community of expert Pythonistas.

Unlock This Lesson

Locked learning resources

This lesson is for members only. Join us and get access to thousands of tutorials and a community of expert Pythonistas.

Unlock This Lesson

Applying Copying to User Defined Classes

00:00 This lesson and the next are going to be all about how the built-in copy module behaves when dealing with user-defined classes. One of the reasons the copy module is so versatile is that except for a handful of data types like files, network sockets, and modules, you can apply copy() and deepcopy() to pretty much anything, including classes you define.

00:20 In this lesson, you’ll examine their default behavior without writing any custom code for handling copying. We’re going to start by defining a class for representing rectangles.

00:30 Its instance attributes will be two objects of a point class, which you’ll also define that represent the x, y coordinates of the top left and bottom right corners of the rectangle. class Rectangle:

00:43 def __init__(self, top_left, bottom_right): and in the body of the function, self.top_left is set to the value of top_left, while self.bottom_right is set to the value of bottom_right.

00:59 Also define a __repr__ method that only takes the self parameter, which returns the f-string representation Rectangle( open parenthesis, the value of self. top_left, the value of self. bottom_right, close parenthesis. That’s the Rectangle class.

01:17 Now define the Point class. class Point: This class’s __init__ will take the parameters, self, x, and y. def __init__(self, x, y): self.x is set to x, and self.y is set to y.

01:34 Define a __repr__ method here as well. Again, only taking in the parameter self, return the f-string, Point(x= the value of self.x, y= the value of self.y). Close parenthesis.

01:50 Test out the classes by creating a variable bounding_box that is the result of calling Rectangle(, passing in two Point objects.

01:57 Point(10, 20) and Point(30, 40). Examine bounding_box. Cool! Based on the __repr__ methods you defined, you see that bounding_box is a Rectangle object defined by a top left Point with the coordinates 10, 20 and a bottom right Point with the coordinates 30, 40.

02:16 So how does this play with the copy module? import copy. Make a shallow copy.

02:24 shallow_copy = copy. copy(bounding_box) and a deep copy.

02:32 deep_copy = copy.deepcopy(bounding_box).

02:37 User-defined objects are mutable by default, so go ahead and modify bounding_box by setting its bottom_right attribute to a Point with the values 500, 700. bounding_box. bottom_right = Point(500, 700).

02:55 What do you think? Have either the shallow copy or the deep copy changed because of this? Examine bounding_box,

03:04 you’ll see the coordinates 10, 20, 500, 700. Examine shallow_copy

03:11 and the coordinates, 10, 20, 30, and 40 appear. And examine deep_copy, same as shallow copy, the coordinates 10, 20, 30, and 40. So far, no changes.

03:25 Did I trick you? Remember the expected behavior is that the top level object, bounding_box, is distinct from its copies, and that’s what we saw here.

03:34 But what happens if you modify one of its Point objects directly? bounding_box.top_left. x = 50. Now look at bounding_box.

03:47 It has the coordinates 50, 20, 500, 700. Check shallow_copy. It has the coordinates 50, 20, 30, 40, and check deep_copy.

04:01 The original coordinates remain 10, 20, 30, and 40. Shallow copy was mutated along with bounding_box, but deep copy remained distinct, and this is exactly how we would expect copy behavior to work for any composite object.

04:17 The shallow copy shares references while deep copies do not. With an object like this Rectangle class, default behavior is probably all you need.

04:26 In the next lesson, you’ll see a case where the default behavior isn’t enough and a custom solution is required.

Avatar image for Daniel Podobinski

Daniel Podobinski on Aug. 29, 2025

The behaviour described at 4:15 is not what I got when running this in Spyder. Both shallow and deep copies were unaffected.

Avatar image for Bartosz Zaczyński

Bartosz Zaczyński RP Team on Aug. 29, 2025

@Daniel Podobinski This should work as described in the video lesson regardless of your code editor. Did you mutate the bounding_box variable in-place or did you rebind it to a brand new value?

Avatar image for Daniel Podobinski

Daniel Podobinski on Aug. 29, 2025

@Bartosz Zaczynski What I noticed after posting was that, if I did exactly as shown in the video i.e:

bounding_box.top_left.x = 50

the shallow copy WAS affected. But out of curiosity, I also tried

bounding_box.bottom_right.x = 1000

and this did NOT change the shallow copy.

Avatar image for Bartosz Zaczyński

Bartosz Zaczyński RP Team on Aug. 29, 2025

@Daniel Podobinski Okay, so repeating the same steps exactly as shown in the video does actually produce the same results on your end 🙂

The reason why you observe a different behavior for the .bottom_right attribute is because you rebound it to a new value in an earlier step:

bounding_box.bottom_right = Point(500, 700)

This overwrites the original Point(30, 40), which is still referenced by shallow_copy. So, when you later mutate bounding_box.bottom_right.x, this doesn’t affect the original point. Instead, it modifies this new Point(500, 700) instance, which is only attached to bounding_box.

When you repeat the same steps but without the line of code listed above, then you’ll get the same result as for the .top_left attribute. To understand this better, it helps to draw the reference graph, or you can check the pitfalls of shallow copying.

Become a Member to join the conversation.