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.
The output is:
Rex barks
TrueThe 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.
The output is:
sliced: Rex makes a sound
copy: Rex barks toy ropeThe 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.
The output is:
Dog Rex barks rope FalseThe 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 thatprintshows 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.
The output is:
Point
equals: False
same object: False
is an object: True
(<class '__main__.Point'>, <class 'object'>)
TrueThe 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.
The output is:
isinstance Animal: True
type is Animal: False
type is Dog: TrueThe 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.
The output is:
integer 42
negative integer -7
text of length 5
list of 3
other: float
nothingEach 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:
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:
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:
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:
The output is:
TypeError: can only concatenate str (not "int") to strTo 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:
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
CircleThe 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
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
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.copyto 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
isinstanceor amatchpattern to check the type of a value before you use it. - Use
copy.copyfor a shallow copy andcopy.deepcopyfor a copy that includes the objects inside. - Calling a method on
Nonefails with anAttributeError, so check forNonebefore the call.