Embercode
✕
Object-Oriented ProgrammingLesson 12 of 17

45 min read

Object copying, the object base class and type checks in Python

Why this matters

When you write a class in Python, you inherit some methods without writing them. Every class is a child of a built-in class called object, and that class provides methods such as __str__, __eq__ and __hash__. If you do not know what those default methods do, a program can print a useless line, compare two objects and get the wrong answer, or crash on a value that is None.

This lesson also covers a trap from C++, another language with classes, called slicing. Programmers who learned C++ first often expect slicing in Python too, and they write copy code that loses data. Knowing why Python does not slice, and what happens when you copy by hand, prevents that mistake.

The rules for inheritance and base classes were covered in the inheritance lesson of this module. The __eq__ and __hash__ methods are covered in the operator overloading lesson, so this lesson only shows what the defaults do.

Part 1: slicing in C++

In C++, a variable declared with a plain value type, not a pointer or reference, has a fixed amount of room. If the type is Animal, the room holds the Animal fields. Suppose you copy a Dog into that variable. The Dog has extra fields, such as a favourite toy, but the room is only big enough for the Animal part. The extra part is cut off and thrown away. The name comes from the idea of slicing off a piece of something.

The loss is more than fields. The Dog version of a method is also lost, because the copy is now only an Animal. A call to a method that the Dog class changed runs the Animal version instead.

This is a C++ rule. Python does not have it, as the next part shows.

Part 2: Python does not slice on assignment

In Python, a name does not hold an object. It points to one. Assigning one name to another copies the pointer, not the object. The next program shows the effect.

Python · editable

The output is:

Rex barks
True

The name pet points to the same Dog object as rex. The check pet is rex asks whether the two names point to the same object, and the answer is True. The call pet.speak() runs the Dog version of speak, because the object really is a Dog. Python does not look at the name to choose the method. The object decides, as the polymorphism lesson showed.

Python objects are never sliced by an assignment. The data is still there, and so is the right behaviour. The problem the C++ programmer fears does not happen.

Part 3: copying by hand can still lose data

Python does not slice on assignment, but a copy that you write yourself can lose data. A copy usually comes from an initializer that takes an existing object and copies its values. Here is a base class and a subclass. Each copy is built by passing the values to the constructor.

Python · editable

The output is:

sliced: Rex makes a sound
copy: Rex barks toy rope

The line Animal(rex.name) builds an Animal. It copies only the name, because an Animal has no toy field. The result is a plain Animal, so speak runs the Animal version, and the toy is gone. This is slicing, done on purpose by the programmer. It is the same loss that C++ does automatically.

The line Dog(rex.name, rex.favorite_toy) builds a full Dog, and it keeps the toy. The copy has the right behaviour.

Python's copy module has a tool that avoids this mistake. The function copy.copy makes a new object of the same class, with the same attribute values. A subclass stays a subclass.

Python · editable

The output is:

Dog Rex barks rope False

The copy is a Dog, it barks, it keeps the toy, and it is a different object from rex. When you copy by hand, make sure the copy has the same type as the original. If the goal is a full copy, build the same class. If the goal is a smaller view, such as only the name, say so in the name of the method, so a reader knows the data is intentionally left out.

Part 4: the object base class

Every class in Python inherits from object, even when you do not write it. Because of this, every object has the methods of object. The most common ones are:

  • __str__ returns the text that print shows for the object.
  • __repr__ returns the text that the interactive prompt and lists show.
  • __eq__ says whether two objects are equal. Its default checks identity, as the operator overloading lesson explained.
  • __hash__ returns a whole number that a set or dictionary uses to find the object quickly.
  • __class__ gives the class of the object. The name of the class is available as __class__.__name__.

Here is a small class that does not write any of these methods. The output shows what the defaults do.

Python · editable

The output is:

Point
equals: False
same object: False
is an object: True
(<class '__main__.Point'>, <class 'object'>)
True

The default text of an object is <__main__.Point object at 0x...>. The part after 0x is the address in memory, which is different on each run and on each machine. Print that text only to see that an object exists. Write a __str__ method for a real class, such as return f"({self.x}, {self.y})".

The default == checks identity. Two objects are equal only if they are the same object. Here p and q hold the same numbers, but they are two separate objects, so == returns False. If you want Point objects with the same coordinates to be equal, write your own __eq__, following the rules in the operator overloading lesson.

The last line of the output shows the search order of the class, which ends at object. Every class ends its chain there.

isinstance and exact type are not the same check

The two checks give different answers for a subclass. isinstance accepts the class itself and every class below it. Comparing type(value) with a class name accepts only that exact class. This difference matters when you write __eq__. If __eq__ uses isinstance, an Animal and a Dog with the same name can count as equal. If it compares types exactly, they cannot.

Python · editable

The output is:

isinstance Animal: True
type is Animal: False
type is Dog: True

The value d is a Dog, so isinstance(d, Animal) is true, because a Dog is an Animal. The exact type check is false for Animal, because the object's exact type is Dog.

Part 5: object as a type that holds anything

Every class is an object, so a value can be a string, a number, a list, or your own class. A list in Python can hold mixed values, which is useful when a collection or a function must accept several kinds. The price is that you must check what the value is before you use it. Python has a match statement for this check. A pattern such as int(i) tests that the value is an int, and it gives the value a new name, i, in one step.

Python · editable

The output is:

integer 42
negative integer -7
text of length 5
list of 3
other: float
nothing

Each case line checks a type. The name in brackets, such as i or s, holds the value already matched to that type. Inside case str(s), you can call len(s) without a conversion. The if i < 0 part is a guard, a condition that must also be true for the case to match. The line case None handles the missing value. Without it, None would reach the last case and print other: NoneType.

The value 2.5 is a float, and no case matches it, so the last case runs. It calls type(value).__name__ to print the type name. This is a good use of the type information: the last case can still say what it received.

Use mixed values only when you need that mix. When one type is enough, make the function accept that type. Then the code is simpler, and you do not need the checks.

Common mistakes

Calling clone() on an object. Some languages have a clone method on every object. Python does not. The call fails with an AttributeError:

Python · editable

The program stops with an error. The last line of the output is:

AttributeError: 'Account' object has no attribute 'clone'

The Python tool for a copy is copy.copy, which makes a shallow copy, or copy.deepcopy, which copies the objects inside too. A shallow copy copies the outer object, but any list or object inside it is still shared with the original. The next example shows this:

Python · editable

The output is:

[[1, 9], [2]]
[[1, 9], [2]] [[1, 9], [2, 7]]

The shallow copy shares the inner list [1], so the append to shallow[0] shows in sheet. The deep copy has its own inner lists, so the append to deep[1] does not reach sheet.

Calling a method on None. A None value points to nothing, so there is no object to run a method on. Python stops the program and reports an AttributeError. This is one of the most common errors in Python programs:

Python · editable

The program stops with an error. The last line of the output is:

AttributeError: 'NoneType' object has no attribute 'strip'

The message names the method that was called and the type, NoneType, which is the type of None. The fix is to check for None before the call, with if value is not None:, or to make sure the value is set before you use it.

Treating a value as another type. Python never changes a value's type by itself. A value of type str stays a string. Adding it to a number fails:

Python · editable

The output is:

TypeError: can only concatenate str (not "int") to str

To use the text as a number, convert it with int(item). The conversion works for "42", and it fails for text that is not a number:

Python · editable

The output is:

43
ValueError: invalid literal for int() with base 10: 'abc'

Use isinstance or a match pattern when you have a value of an unknown type, and convert only when you know the text has the right form.

Expecting == to compare contents. For objects, == uses __eq__, and the default checks identity. Two Point objects with the same coordinates are not equal by default, as the output in Part 4 shows. Write __eq__ when you want contents to count.

Reading the address in a default string as data. The address after 0x in a default text is not stable across programs and not related to your data. Do not save it, compare it, or print it as if it means something. Write a __str__ method that prints the fields you care about.

Practice

Exercise 1

Predict the output of this program, then check it.

class Shape:
    def name(self):
        return "shape"
 
 
class Circle(Shape):
    def name(self):
        return "circle"
 
 
s = Circle()
print(s.name())
print(type(s).__name__)

Show the answer

The output is:

circle
Circle

The name s points to a Circle object, so name() runs the Circle version. The expression type(s).__name__ gives the class of the object, which is Circle, not the class that was named in the code.

Exercise 2

Write a copy for a class Cat that has the attributes name and lives. The copy must have the same name and the same number of lives. Use copy.copy rather than writing the copy by hand.

Show the answer

Python · editable

The output is Miso 9 False. The copy has the same values, and it is a different object. Both attributes are simple values, so no other object is shared.

Exercise 3

Write a function sum_integers(items) that adds up every value in the list that is an int, and ignores all other values. For the list [1, "two", 3, 4.0, 5] it must return 9. Note that True is also an int in Python, so exclude it if you want only numbers.

Show the answer

Python · editable

The output is 9. The isinstance check runs before the value is used, so the addition cannot fail. The string "two" and the float 4.0 are skipped. The result is 1 + 3 + 5 = 9. The extra check for bool is needed because True and False are subclasses of int in Python.

Exercise 4

Explain in one or two sentences why this code is wrong, and give the error it will raise:

value = None
code = value.__hash__ and value.bit_length()

Show the answer

The variable value is None, so there is no object for bit_length to belong to. Python raises an AttributeError, with the message 'NoneType' object has no attribute 'bit_length'. The fix is to make sure value is an integer before the call, or to check value is not None first.

Exercise 5

A Box class has an attribute items that is a list. A teacher says that a copy made with copy.copy gives an independent box. Is the teacher right? What happens if you add a string to the list of the copy?

Show the answer

The teacher is not right. copy.copy makes a shallow copy, so the copy and the original point to the same list. Adding a string through the copy also changes the list that the original box sees. To make an independent box, use copy.deepcopy, or write a method that builds a new list, for example list(self.items), and gives it to the new box.

Summary

  • Python does not slice objects on assignment. Assigning one name to another shares the object, and the object's own methods run.
  • A copy written by hand can still lose data, so build the copy from the same class, or use copy.copy to keep the subclass.
  • Every class inherits from object, which provides __str__, __repr__, __eq__ and __hash__. Their defaults are rarely what a real class needs.
  • Use isinstance or a match pattern to check the type of a value before you use it.
  • Use copy.copy for a shallow copy and copy.deepcopy for a copy that includes the objects inside.
  • Calling a method on None fails with an AttributeError, so check for None before the call.
Check yourself: Slicing and the object class (7 questions) →Practice this topic: coding problems for Object-Oriented Programming →

Stuck? Ask the tutor

Ask about anything in this lesson. Answers are short and use simple words.

Sign in to ask the tutor.