Contents
Chapter 7

Classes

Class definitions use minimal syntax. You start with the class keyword followed by the class name and a colon. Use def to create methods inside the indented class body:

# simple_class.py

class Simple:
    def __init__(self, text):
        print("Inside the Simple constructor")
        self.s = text
    # Two methods:
    def show(self, msg=""):
        if msg:
            print(f"{msg}:", self.s)
        else:
            print(self.s)
    def show_twice(self):
        self.show()  # Calling another method
        self.show()
# demo_simple_class.py
from simple_class import Simple

x = Simple("Constructor argument")  # Create an object
#: Inside the Simple constructor
x.show()
#: Constructor argument
x.show("A message")
#: A message: Constructor argument
x.show_twice()
#: Constructor argument
#: Constructor argument

Ordinary methods require a reference to the current object. When you define a method you must explicitly specify the reference as the first parameter. Python programmers traditionally name the reference self, but you can use any identifier (though anything else probably confuses people). To refer to the object’s attributes or its other methods, you must go through self.

# forgot_self.py

class Oops:
    def show():  # Missing the self parameter
        print("never runs")

try:
    Oops().show()  # type: ignore
except TypeError as e:
    print(e)
#: Oops.show() takes 0 positional arguments but 1 was given

When you call a method for an object, as in x.show(), Python passes the object reference automatically. The “1” in the error message counts that reference, and a method defined without self has no parameter to receive it. A type checker sees the mistake before anything runs, so the call carries a # type: ignore to say the mistake is deliberate.

The first method, __init__(), is the initializer. The double underscores on both ends make it a dunder, Python’s name for a method the language itself calls. The __new__() method is the constructor, which you rarely use (Singleton shows a case that needs it). Most programmers call __init__() the constructor, since it does the job of constructors in other OOP languages. This book follows that practice.

Python calls the constructor automatically during object creation. In the demo, creating an object looks like calling a function named after the class.

In C++ or Java you declare object-level fields inside the class body but outside the methods. In Python an object attribute comes into being when a method assigns to it through self (typically in the constructor, but not always). The assignment creates space for that attribute when the method runs. If you assign to a name in the class body, C++/Java style, that name becomes a class-level attribute instead (similar to a static field in C++/Java). Class Attributes shows what that shared storage does when you assign to it. A bare annotation with no value looks most like a C++ or Java field declaration, yet it creates neither kind of attribute. It records the type. Class Attributes and Data Classes as Types use bare annotations.

display_object() is a small inspection helper built in Metaprogramming. The helper shows the shape of an object by printing its attributes and methods:

# display_simple.py
from display import display_object
from simple_class import Simple

x = Simple("Constructor argument")
#: Inside the Simple constructor
display_object(x)
#: [Attributes]
#:   • s = 'Constructor argument'
#: [Methods]
#:   • show(self, msg='')
#:   • show_twice(self)

The instance carries one attribute, s, while the two methods belong to the class. The constructor __init__() is a dunder, so display_object() hides it by default.

Inheritance

Because Python is dynamically typed, it applies an operation to an object and never checks an interface first. With inheritance in C++ or Java, you often inherit only to establish a common interface. Python is different. You inherit an implementation, to reuse the code from the base class. Python can still name an interface without inheritance: a Protocol (Static Types) describes the shape a function needs, with no base class to inherit.

First import the base class the same way you import any name from a module (see Modules and Packages). Then inherit by listing the base class in parentheses after the name of the inheriting class. Python supports multiple inheritance, so you can list several classes, though Rethinking Objects argues against it in favor of protocols. simple_subclass.py imports and subclasses Simple from the simple_class module. Ignore the @override decorator for now. Marking Overrides with @override explains it:

# simple_subclass.py
from typing import override
from simple_class import Simple

class Derived(Simple):  # Derived inherits Simple
    def __init__(self, text):
        print("Inside Derived constructor")
        # Call the base-class constructor with super():
        super().__init__(text)
    def display(self):
        self.show("Called from display()")
    @override
    def show(self, msg=""):
        print("Overridden show() method")
        # Call the base-class method from inside
        # the overridden method:
        super().show(msg)

class Different:
    def show(self):
        print("Not derived from Simple")
# demo_subclass.py
from simple_subclass import Derived, Different

x = Derived("Derived constructor argument")
#: Inside Derived constructor
#: Inside the Simple constructor
x.display()
#: Overridden show() method
#: Called from display(): Derived constructor argument
x.show()
#: Overridden show() method
#: Derived constructor argument
x.show_twice()  # Inherited from Simple
#: Overridden show() method
#: Derived constructor argument
#: Overridden show() method
#: Derived constructor argument
def f(obj):  # Works on any obj with a show()
    obj.show()
f(x)
#: Overridden show() method
#: Derived constructor argument
f(Different())
#: Not derived from Simple

Derived inherits from Simple. In the constructor, super().__init__() calls the base-class constructor. In display(), you can call show() as a method of self. When you override a method but still want the base-class version, call it through super(), as the overridden show() does.

super() and ordinary attribute lookup both follow one list, the class’s method resolution order (MRO). The MRO names the classes Python searches for a name, starting with the class itself and ending at object. Derived.__mro__ is (Derived, Simple, object). With a single base class the order is obvious. When two base classes define the same name, the MRO decides which one supplies it. A and B below both define show(), and C inherits from both:

# mro_conflict.py

class A:
    def show(self):
        print("A.show")

class B:
    def show(self):
        print("B.show")

class C(A, B):
    pass  # Defines no show() of its own

print([c.__name__ for c in C.__mro__])
#: ['C', 'A', 'B', 'object']
C().show()  # A comes first in the MRO
#: A.show

C.__mro__ lists A before B, so C().show() runs A’s version, not B’s.

The base-class constructor runs because Derived’s constructor calls it. Unlike C++ and Java, Python never calls a base-class constructor on its own. If you remove the super().__init__(text) line, nothing creates self.s, so the first method that reads it raises an AttributeError. Dropping the call and then calling show() produces that error:

# missing_super.py
from exceptions import expect
from simple_class import Simple

class Broken(Simple):
    def __init__(self, text):
        pass  # Forgot super().__init__(text)

expect(AttributeError, Broken("ignored").show)
#: [AttributeError] 'Broken' object has no attribute 's'

A derived class that defines no constructor of its own inherits and runs the base version. Derived also inherits show_twice() unchanged.

The class Different also has a method named show(), but does not derive from Simple. f() in demo_subclass.py demonstrates dynamic typing. It requires one thing of obj, a show() it can call, so it accepts a Derived and a Different alike.

An import inside a class body binds the imported name like any other assignment, so importing a module-level function there attaches it to the class as a method, self and all. More than one unrelated class can pick up the same function this way, but that import trick is a curiosity more than a technique: a helper object or a plain module-level function is almost always clearer.

Marking Overrides with @override

When you override a method, nothing requires the name to match a method in the base class. If you misspell the name, you define a new method instead of overriding one, and Python reports nothing. Renaming or removing the base method later has the same effect, and that bug is easy to miss.

The @override decorator from the typing module catches it. A line starting with @ above a definition applies a decorator to it. Decorators shows how they work, and this chapter only applies existing ones. @override declares that a method replaces one from a base class:

# override_intro.py
from typing import override

class Base:
    def show(self):
        print("Base.show")

class Derived(Base):
    @override
    def show(self):
        print("Derived.show")

class Typo(Base):
    # @override  # "shwo" does not override anything
    def shwo(self):
        print("Typo.shwo")

Derived().show()
#: Derived.show

A type checker now verifies that claim. If a decorated method matches nothing in a base class, whether from a misspelling or from a base method that no longer exists, the checker reports an error. Uncomment the decorator on Typo.shwo, and the checker reports:

error[invalid-explicit-override]: Method `shwo` is decorated with
`@override` but does not override anything

Python runs the program either way. Catching the mistake takes a separate tool, and Static Types sets that tool up.

At run time @override returns the same function object it received, with no wrapper. Before returning the function, the decorator tries to set an __override__ attribute on it, so that code can find overrides by introspection. On some callables that assignment raises an exception, which the decorator catches and ignores.

Apply @override to any method that replaces an inherited method. Two kinds stay undecorated by convention: constructors, and dunders such as __repr__() and __str__() that replace a default inherited from object.

Properties

With @property, you can start with a plain attribute and convert it to a computed one later, without changing the calling code:

# properties.py

class Circle:
    def __init__(self, radius):
        self.radius = radius  # A plain attribute

    @property
    def area(self):  # Used like an attribute, not a call
        return 3.14159 * self.radius ** 2

c = Circle(10)
print(c.radius)
#: 10
print(c.area)  # Properties don't use parentheses
#: 314.159

radius is a plain attribute here and area a computation, and the call site reads both the same way.

A @property with a getter alone rejects writes: assigning to it raises an AttributeError. A setter enables writing, and it is the place to validate the value before storing it:

# property_setter.py

class Circle:
    def __init__(self, radius):
        # Goes through the setter below
        self.radius = radius

    @property
    def radius(self):
        return self._radius

    @radius.setter
    def radius(self, value):
        if value < 0:
            raise ValueError("radius cannot be negative")
        self._radius = value

    @property
    def area(self):  # Unchanged from the version above
        return 3.14159 * self.radius ** 2

c = Circle(10)
print(c.radius)  # The same two lines as before
#: 10
print(c.area)
#: 314.159
c.radius = 5  # Now the setter validates, then stores
print(c.radius)
#: 5
try:
    Circle(-1)
except ValueError as e:
    print(f"Failed: {e}")
#: Failed: radius cannot be negative

property_setter.py completes the conversion: radius began as a plain attribute and is now a validated property. The two lines that read c.radius and c.area are the ones from properties.py, unchanged. Code outside the class reads both versions the same way, and that is why you can wait to add a setter until you need one.

The property owns the name radius on the class, so the value goes into a separate attribute. A single leading underscore marks _radius as internal to the class, a convention rather than a language rule. The separate name matters. Inside the getter, self.radius calls the getter again; inside the setter, self.radius = value calls the setter again. Either loop repeats until the interpreter raises a RecursionError. Naming both the property and the backing attribute radius reproduces it:

# property_recursion.py
from exceptions import expect

class Circle:
    def __init__(self, radius):
        self.radius = radius  # Calls the setter

    @property
    def radius(self):
        return self.radius  # Calls itself again

    @radius.setter
    def radius(self, value):
        self.radius = value  # Calls itself again

expect(RecursionError, Circle, 10)
#: [RecursionError] maximum recursion depth exceeded

The getter and setter are independent, so you choose the access you want by defining one or both. A write-only property is possible but rare; a plain method expresses that intent better.

A @property reruns its code on every access. When the computation is expensive and the answer cannot change, functools.cached_property runs it once, on first access, and stores the result:

# cached_property_demo.py
from functools import cached_property

class Numbers:
    def __init__(self, values):
        self.values = values

    @cached_property
    def total(self):
        print("summing", len(self.values), "values")
        return sum(self.values)

n = Numbers([5, 10, 15])
print(n.total)
#: summing 3 values
#: 30
# Second access: stored value, no recomputation
print(n.total)
#: 30
n.values.append(20)
print(n.total)  # Still the old sum: the cache is stale
#: 30
del n.total  # Discard the cached value
print(n.total)
#: summing 4 values
#: 50

The first access runs the method. The second access produces the same result from the stored value. The attribute is lazily initialized, created on first use, so it costs nothing until something reads it. The stored value lives in the instance’s __dict__. A class declared with slots=True (Performance uses it) has no __dict__, so cached_property has nowhere to store the value.

cached_property trades freshness for speed, so if n.values changes, total becomes stale, as the appended 20 in cached_property_demo.py shows. A plain @property recomputes every time, so its answer is always current. Cache only what cannot change.

String Representation

By default, printing an object shows its class and its address, as in <__main__.Point object at 0x7f2dd669cd70>, and that says nothing about the value the object holds. Two dunder methods control how an object displays. __str__() is the readable form for users, and __repr__() is the unambiguous form for developers:

# representation.py

class Point:
    def __init__(self, x, y):
        self.x, self.y = x, y

    def __repr__(self):
        return f"Point({self.x}, {self.y})"

p = Point(3, 4)
print(p)  # Falls back to __repr__
#: Point(3, 4)
print([p, p])
#: [Point(3, 4), Point(3, 4)]

A __str__() on the same class separates the two forms:

# representation_str.py

class Point:
    def __init__(self, x, y):
        self.x, self.y = x, y

    def __repr__(self):
        return f"Point({self.x}, {self.y})"

    def __str__(self):
        return f"({self.x}, {self.y})"

p = Point(3, 4)
print(p)  # print() prefers __str__
#: (3, 4)
print(repr(p))
#: Point(3, 4)
print([p])
#: [Point(3, 4)]

print() and str() use __str__() when it exists and fall back to __repr__() when it does not. The fallback runs in one direction: repr() never consults __str__(). A container builds its own display from the __repr__() of its elements, and that is why the list prints Point(3, 4) rather than (3, 4). In an f-string, {p} selects __str__() and {p!r} selects __repr__(). By convention __repr__() returns the call that would rebuild the object, so it reads Point(3, 4).

Define __repr__() on classes you debug, and add __str__() only when users see the output.

For classes that are primarily a bundle of typed data, Data Classes as Types shows how @dataclass writes the constructor and __repr__().

Static and Class Methods

A method that never touches self can be a @staticmethod. A method that needs the class rather than an instance can be a @classmethod. A class method receives the class as its first argument, conventionally named cls:

# class_methods.py

class Temperature:
    def __init__(self, celsius):
        self.celsius = celsius

    @classmethod
    # An alternative constructor
    def from_fahrenheit(cls, f):
        return cls((f - 32) * 5 / 9)

    @staticmethod
    def is_freezing(celsius):  # Needs no self or cls
        return celsius <= 0

class Reading(Temperature):
    pass  # Adds nothing; inherits from_fahrenheit()

t = Temperature.from_fahrenheit(212)
print(round(t.celsius))
#: 100
print(Temperature.is_freezing(-4))
#: True
r = Reading.from_fahrenheit(212)
print(type(r).__name__)
#: Reading

from_fahrenheit() builds its result with cls(...) rather than Temperature(...). Called on a subclass, from_fahrenheit() receives that subclass as cls, so the alternative constructor produces the right kind of object. The subclass inherits the method unchanged. Reading.from_fahrenheit(212) proves it: cls is Reading there, not Temperature, so type(r).__name__ reports 'Reading'. Naming the class directly, return Temperature(...), would hard-code Temperature into every subclass, including Reading.

is_freezing() would also work as a module-level function. Inside the class it sits where a reader looks for it, and a subclass can replace it the way it replaces any other method.

Exercises

  1. Add a method shrink(self, factor) to Circle in property_setter.py that sets self.radius = self.radius / factor, going through the existing setter. Confirm shrink(2) on a Circle(10) leaves the radius at 5.0. Then call shrink(-2) on that same circle, which would divide the radius down to -2.5, and confirm the setter raises its ValueError instead of silently storing a negative radius.
  2. In class_methods.py, add a second alternative constructor, from_kelvin(cls, k), using celsius = k - 273.15. Add a call that builds a Temperature both ways for the same physical temperature and confirms they agree, within rounding.
  3. In simple_subclass.py, add a third class, MoreDerived(Derived), that overrides show() again, printing its own message before calling super().show(msg). Predict, then confirm, the full chain of prints from MoreDerived("x").show_twice().
  4. Add a @cached_property called average to Numbers in cached_property_demo.py that returns self.total / len(self.values). Access n.total and then n.average, and confirm total is not recomputed when average uses it.
  5. Give Temperature in class_methods.py a __repr__() that returns Temperature(21.0) for a temperature of 21 degrees Celsius. Print a single Temperature and a list of two of them, and confirm the list shows the same form for each element. Then add a __str__() returning 21.0C and confirm which of the two print() uses for each case.
  6. In override_intro.py, misspell Derived’s method as shwo(), keeping the @override decorator. Run the program and confirm it still prints Base.show, then run the type checker (Static Types sets one up) and read what it says. Remove @override and confirm the type checker goes quiet while the program’s behavior does not change.