Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

The object system allows programmers to build and use abstract data representations. A final step in designing the behavior of user-defined classes is to specify how their instances can use built-in Python syntax, such as printing, displaying, adding, calling, and indexing into objects.

A central concept in object abstraction is a generic function, which is a function that can accept values of multiple different types. Python’s most common way of implementing generic functions is through shared interfaces.

String Conversion

To represent data effectively, an object value should behave like the kind of data it is meant to represent, including producing a string representation of itself. String representations of data values are especially important in an interactive language such as Python that automatically displays the string representation of the values of expressions in an interactive session.

String values provide a fundamental medium for communicating information among humans. Sequences of characters can be rendered on a screen, printed to paper, read aloud, converted to braille, or broadcast as Morse code. Strings are also fundamental to programming because they can represent Python expressions.

Python stipulates that all objects should produce two different string representations: one that is human-interpretable text and one that is a Python-interpretable expression. The constructor function for strings, str, returns a human-readable string. Where possible, the repr function returns a Python expression that evaluates to an equal object. The docstring for repr explains this property:

repr(object) -> string

Return the canonical string representation of the object.
For most object types, eval(repr(object)) == object.

The result of calling repr on the value of an expression is what Python prints in an interactive session.

>>> 12e12
12000000000000.0
>>> print(repr(12e12))
12000000000000.0

In cases where no representation exists that evaluates to the original value, Python typically produces a description surrounded by angled brackets.

>>> repr(min)
'<built-in function min>'

The str constructor often coincides with repr, but provides a more interpretable text representation in some cases. For instance, we see a difference between str and repr with dates.

>>> from datetime import date
>>> tues = date(2011, 9, 12)
>>> repr(tues)
'datetime.date(2011, 9, 12)'
>>> str(tues)
'2011-09-12'

Defining the repr function presents a new challenge: we would like it to apply correctly to all data types, even those that did not exist when repr was implemented. We would like it to be a generic or polymorphic function, one that can be applied to many (poly) different forms (morph) of data.

The object system provides an elegant solution in this case: the repr function always invokes a method called __repr__ on its argument.

>>> tues.__repr__()
'datetime.date(2011, 9, 12)'

By implementing this same method in user-defined classes, we can extend the applicability of repr to any class we create in the future. This example highlights another benefit of dot expressions in general, that they provide a mechanism for extending the domain of existing functions to new object types.

The str constructor is implemented in a similar manner: it invokes a method called __str__ on its argument.

>>> tues.__str__()
'2011-09-12'

These polymorphic functions are examples of a more general principle: certain functions should apply to multiple data types. Moreover, one way to create such a function is to use a shared method name with a different definition in each class.

Special Methods

In Python, certain special names are invoked by the Python interpreter in special circumstances. For instance, the __init__ method of a class is automatically invoked whenever an object is constructed. The __str__ method is invoked automatically when printing, and __repr__ is invoked in an interactive session to display values.

There are special names for many other behaviors in Python. Some of those used most commonly are described below.

True and false values. We saw previously that numbers in Python have a truth value; more specifically, 0 is a false value and all other numbers are true values. In fact, all objects in Python have a truth value. By default, objects of user-defined classes are considered to be true, but the special __bool__ method can be used to override this behavior. If an object defines the __bool__ method, then Python calls that method to determine its truth value.

As an example, suppose we want a bank account with 0 balance to be false. We can add a __bool__ method to the Account class to create this behavior.

>>> Account.__bool__ = lambda self: self.balance != 0

We can call the bool constructor to see the truth value of an object, and we can use any object in a boolean context.

>>> bool(Account('Spock'))
False
>>> if not Account('Spock'):
        print('Spock is broke')
Spock is broke

Sequence operations. We have seen that we can call the len function to determine the length of a sequence.

>>> len('Go Bears!')
9

The len function invokes the __len__ method of its argument to determine its length. All built-in sequence types implement this method.

>>> 'Go Bears!'.__len__()
9

Python uses a sequence’s length to determine its truth value, if it does not provide a __bool__ method. Empty sequences are false, while non-empty sequences are true.

>>> bool('')
False
>>> bool([])
False
>>> bool('Go Bears!')
True

The __getitem__ method is invoked by the item selection operator, but it can also be invoked directly.

>>> 'Go Bears!'[3]
'B'
>>> 'Go Bears!'.__getitem__(3)
'B'

Callable objects. In Python, functions are first-class objects, so they can be passed around as data and have attributes like any other object. Python also allows us to define objects that can be “called” like functions by including a __call__ method. With this method, we can define a class that behaves like a higher-order function.

As an example, consider the following higher-order function, which returns a function that adds a constant value to its argument.

>>> def make_adder(n):
        def adder(k):
            return n + k
        return adder

>>> add_three = make_adder(3)
>>> add_three(4)
7

We can create an Adder class that defines a __call__ method to provide the same functionality.

>>> class Adder:
        def __init__(self, n):
            self.n = n
        def __call__(self, k):
            return self.n + k

>>> add_three_obj = Adder(3)
>>> add_three_obj(4)
7

Here, the Adder class behaves like the make_adder higher-order function, and the add_three_obj object behaves like the add_three function. We have further blurred the line between data and functions.

Arithmetic. Special methods can also define the behavior of built-in operators such as + and * when applied to user-defined objects. In order to provide this generality, Python follows specific protocols to apply each operator. For example, to evaluate expressions that contain the + operator, Python checks for special methods on both the left and right operands of the expression. First, Python checks for an __add__ method on the value of the left operand, then checks for an __radd__ method on the value of the right operand. If either is found, that method is invoked with the value of the other operand as its argument. An example follows below. For readers interested in further details, the Python documentation describes the exhaustive set of method names for operators. Dive into Python 3 has a chapter on special method names that describes how many of these special method names are used.

Example: Ratios

A ratio of integers, also called a rational number, is an important kind of number. A rational number such as 1/3 or 17/29 is typically written as:

<numerator>/<denominator>

where both the <numerator> and <denominator> are placeholders for integer values. Both parts are needed to exactly characterize the value of the ratio. Actually dividing integers produces a float approximation, losing the exact precision of integers.

>>> 1/3
0.3333333333333333
>>> 1/3 == 0.33333333333333330123456789  # Dividing integers yields an approximation
True

However, we can create an exact representation for ratios using a class. The class defined below stores the ratio in lowest terms, dividing both the passed in numerator and denominator by their greatest common divisor. The special methods provide appropriate behavior when displaying, adding, and multiplying ratios.

>>> from __future__ import annotations
>>> import math
>>> class Ratio:
        "A ratio of integers: numer / denom"
        def __init__(self, n: int, d: int):
            g = math.gcd(n, d)
            self.numer = n // g
            self.denom = d // g
        def __str__(self):
            return f'{self.numer}/{self.denom}'
        def __repr__(self):
            return f'Ratio({self.numer}, {self.denom})'
        def __add__(self, other: Ratio):
            return Ratio(self.numer * other.denom + other.numer * self.denom, self.denom * other.denom)
        def __mul__(self, other: Ratio):
            return Ratio(self.numer * other.numer, self.denom * other.denom)
>>> half = Ratio(2, 4)
>>> half
Ratio(1, 2)
>>> print(half)
1/2
>>> fifth = Ratio(1, 5)
>>> print(half * fifth)
1/10
>>> print(fifth + fifth)
2/5

As the example above shows, our rational number implementation reduces ratios to lowest terms each time one is constructed, so fifth + fifth is two fifths, rather than ten twenty-fifths.