Data types as objects

By now you have learned the basic Python data types and know how to create your own data types using Classes. Note that Python is dynamically typed, which means that the types are determined at runtime, not compile time. This is one of the reasons why Python is so easy to use. You can simply try the following:

>>> type(3)
<class 'int'>
>>> type('Hello')
<class 'str'>
>>> type(['Hello', 'Pythonistas'])
<class 'list'>

In these examples you can see the built-in type function in Python. It can be applied to any Python object and returns the type of the object. In this example, the function tells you that 3 is an int (integer), that 'Hello' is a str (string) and that ['Hello', 'Pythonistas'] is a list.

Of greater interest, however, may be the fact that Python returns objects in response to calls to type; <<class 'int'>, <<class 'str'> and <<class 'list'> are the screen representations of the returned objects. So you can compare these Python objects with each other:

>>> type('Hello') == type('Pythonistas!')
True
>>> type('Hello') == type('Pythonistas!') == type(['Hello', 'Pythonistas'])
False

With this technique you can, among other things, perform a type check in your function and method definitions. However, the most common question about the types of objects is whether a particular object is an instance of a class. An example with a simple inheritance hierarchy makes this clearer:

  1. First, we define two classes with an inheritance hierarchy:

    >>> class Form:
    ...     pass
    ...
    >>> class Square(Form):
    ...     pass
    ...
    >>> class Circle(Form):
    ...     pass
    
  2. Now you can create an instance c1 of the class Circle:

    >>> c1 = Circle()
    
  3. As expected, the type function on c1 outputs that c1 is an instance of the class Circle defined in your current __main__ namespace:

    >>> type(c1)
    <class '__main__.Circle'>
    
  4. You can also get exactly the same information by accessing the __class__ attribute of the instance:

    >>> c1.__class__
    <class '__main__.Circle'>
    
  5. You can also explicitly check whether the two class objects are identical:

    >>> c1.__class__ == Circle
    True
    
  6. However, two built-in functions provide a more user-friendly way of obtaining most of the information normally required:

    isinstance()

    determines whether, for example, a class passed to a function or method is of the expected type.

    issubclass()

    determines whether one class is the subclass of another.

    >>> issubclass(Circle, Form)
    True
    >>> issubclass(Square, Form)
    True
    >>> isinstance(c1, Form)
    True
    >>> isinstance(c1, Square)
    False
    >>> isinstance(c1, Circle)
    True
    >>> issubclass(c1.__class__, Form)
    True
    >>> issubclass(c1.__class__, Square)
    False
    >>> issubclass(c1.__class__, Circle)
    True
    

Duck typing

The use of type, isinstance() and issubclass() makes it fairly easy to correctly determine the inheritance hierarchy of an object or class. However, Python also has a feature that makes using objects even easier: duck typing – „If it walks like a duck and it quacks like a duck, then it must be a duck“. This refers to Python’s way of determining whether an object is the required type for an operation, focusing on the interface of an object. In short, in Python you don’t have to worry about type-checking function or method arguments and the like, but instead rely on readable and documented code in conjunction with tests to ensure that an object „quacks like a duck when needed.“

Duck typing can increase the flexibility of well-written code and, in combination with advanced object-oriented functions, gives you the ability to create classes and objects that cover almost any situation. Such special methods are attributes of a class with special meaning for Python. While they are defined as methods, they are not intended to be called directly; instead, they are called automatically by Python in response to a request to an object of that class.

One of the simplest examples of a special method is object.__str__(). When defined in a class, the __str__ method attribute is called whenever an instance of that class is used and Python requires a user-readable string representation of that instance. To see this attribute in action, we again use our Form class with the standard __init__ method to initialise instances of the class, but also a __str__ method to return strings representing instances in a readable format:

>>> class Form:
...     def __init__(self, x, y):
...         self.x = x
...         self.y = y
...     def __str__(self):
...         return "Position: x={0}, y={1}".format (self.x, self.y)
...
>>> f = Form(2,3)
>>> print(f)
Position: x=2, y=3

Even though our special __str__ method attribute was not explicitly called by our code, it could still be used by Python because Python knows that the __str__ attribute, if present, defines a method for converting objects into user-readable strings. And this is exactly what distinguishes the special method attributes. For example, it is often a good idea to define the __str__ attribute for a class so that you can call print(instance) in debugging code and get an informative statement about your object.

Conversely, however, it may be surprising that an object type reacts differently to special method attributes. Therefore, I usually use special method attributes only in one of the following two cases:

  • in a commonly used class, usually for sequences, that behaves similarly to a Python built-in type, and which is made more useful by special method attributes.

  • in a class that behaves almost identically to a built-in class, for example lists implemented as balanced trees to speed up insertion, I can define the special method attributes.