The isinstance() and issubclass() functions are foundational tools in Python’s type system, providing a robust mechanism for checking object and class relationships. They are essential for implementing polymorphism, validating function arguments, and writing flexible code that can handle a variety of types. Understanding their behavior, especially in the context of inheritance and multiple inheritance, is critical for any advanced Python developer.

The isinstance() Function

The isinstance(object, classinfo) function returns True if the object argument is an instance of the classinfo argument, or an instance of a (direct, indirect, or virtual) subclass thereof. If classinfo is a tuple of type objects, the function will return True if object is an instance of any of the types in the tuple.

class Vehicle:
    pass

class Car(Vehicle):
    pass

class Boat(Vehicle):
    pass

my_car = Car()

# Basic usage
print(isinstance(my_car, Car))     # True
print(isinstance(my_car, Vehicle)) # True (due to inheritance)
print(isinstance(my_car, Boat))    # False

# Using a tuple of types for multiple checks
print(isinstance(my_car, (Boat, Vehicle, str))) # True (because it's a Vehicle)

A key strength of isinstance() is its compatibility with abstract base classes (ABCs) from the collections.abc or typing modules. It can check for a “virtual subclass” that has registered with the ABC, even if there is no explicit inheritance. This allows for structural type checking based on implemented methods rather than a strict class hierarchy.

from collections.abc import Iterable

class MyCustomSequence:
    def __iter__(self):
        return iter([]) # Just return an empty iterator

# MyCustomSequence is not a subclass of Iterable
print(issubclass(MyCustomSequence, Iterable)) # False

# But it has an __iter__ method, so it is considered an instance
custom_seq = MyCustomSequence()
print(isinstance(custom_seq, Iterable)) # True

The issubclass() Function

The issubclass(class, classinfo) function returns True if class is a (direct, indirect, or virtual) subclass of classinfo. As with isinstance(), classinfo can be a tuple of class objects.

print(issubclass(Car, Vehicle))   # True
print(issubclass(Car, Car))       # True (a class is considered a subclass of itself)
print(issubclass(Car, (Boat, Vehicle))) # True

# Checking with ABCs
print(issubclass(MyCustomSequence, Iterable)) # False (unless explicitly registered)

Interaction with Multiple Inheritance and the MRO

The behavior of both isinstance() and issubclass() is governed by the Method Resolution Order (MRO) of a class. The MRO is the order in which base classes are searched for a method or attribute. When checking for a subclass or instance relationship, Python consults the MRO list of the class being checked.

class A:
    pass

class B(A):
    pass

class C(A):
    pass

class D(B, C): # Multiple inheritance
    pass

# Check the MRO
print(D.__mro__)
# (<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>)

# isinstance and issubclass use this order
d_instance = D()
print(isinstance(d_instance, A)) # True - A is in D's MRO
print(issubclass(D, C))          # True - C is in D's MRO

This MRO-based lookup is why these functions work seamlessly with complex multiple inheritance hierarchies. They aren’t fooled by the diamond problem; they simply follow the linearized order defined by the C3 algorithm.

Common Pitfalls and Best Practices

  1. Checking for Multiple Types: Always use a tuple for checking against multiple types. This is more efficient and readable than multiple or conditions.

    # Good
    if isinstance(obj, (int, float)):
        ...
    
    # Less efficient and more verbose
    if isinstance(obj, int) or isinstance(obj, float):
        ...
    
  2. Overusing type(): A common mistake is using type(obj) is SomeClass instead of isinstance(obj, SomeClass). The type() check is strict and will fail for subclasses, breaking polymorphism.

    number = 5.0 # a float
    
    # This will not trigger for a float, which is a subclass of real numbers
    if type(number) is float:
        print("It's a float")
    
    # This is the correct, polymorphic approach
    if isinstance(number, float):
        print("It's a float or a subclass thereof")
    
  3. Abstract Base Classes (ABCs): For checking behavioral contracts (e.g., “is this object iterable?”), prefer using ABCs like Iterable, Sequence, or Mapping with isinstance() over concrete classes. This makes your code far more flexible and adheres to the “duck typing” philosophy.

  4. Performance: While the overhead is usually negligible, isinstance() can be slightly slower than a type() check because it must traverse the inheritance hierarchy. This should only be a concern in extremely performance-critical loops.

  5. Virtual Subclasses: Remember that a class registered with an ABC (e.g., using ABCMeta.register()) will return True for isinstance() checks but False for issubclass() checks unless it is also a real subclass. To make issubclass() return True, the class must be explicitly derived from the ABC or use the @register decorator.