Python - isinstance() and issubclass()
Python's dynamic typing gives you flexibility, but that flexibility comes with responsibility. When you need to verify types at runtime—whether for input validation, polymorphic dispatch, or...
Key Insights
isinstance()checks if an object is an instance of a class (or classes), whileissubclass()checks if a class inherits from another class—confusing these leads toTypeErrorexceptions- Both functions accept tuples for checking against multiple types, making them more flexible than manual type comparisons with
type() - These functions integrate seamlessly with Abstract Base Classes, enabling duck typing checks that respect Python’s protocol-based design philosophy
Introduction
Python’s dynamic typing gives you flexibility, but that flexibility comes with responsibility. When you need to verify types at runtime—whether for input validation, polymorphic dispatch, or defensive programming—isinstance() and issubclass() are your primary tools.
These built-in functions do more than simple type comparisons. They understand inheritance hierarchies, work with abstract base classes, and integrate with Python’s duck typing philosophy. Yet I regularly see developers misuse them, confuse their purposes, or avoid them entirely in favor of brittle type() comparisons.
This article covers both functions thoroughly, shows you when each is appropriate, and demonstrates practical patterns you’ll use in production code.
Understanding isinstance()
The isinstance() function checks whether an object is an instance of a specified class or any class in a tuple of classes. The syntax is straightforward:
isinstance(object, classinfo)
Here’s basic usage with built-in types:
# Checking against single types
x = 42
print(isinstance(x, int)) # True
print(isinstance(x, str)) # False
name = "Alice"
print(isinstance(name, str)) # True
items = [1, 2, 3]
print(isinstance(items, list)) # True
print(isinstance(items, dict)) # False
The real power comes when checking against multiple types. Pass a tuple of classes as the second argument:
def process_numeric(value):
# Accept int, float, or complex numbers
if not isinstance(value, (int, float, complex)):
raise TypeError(f"Expected numeric type, got {type(value).__name__}")
return value * 2
print(process_numeric(10)) # 20
print(process_numeric(3.14)) # 6.28
print(process_numeric(2 + 3j)) # (4+6j)
# process_numeric("10") # Raises TypeError
Crucially, isinstance() respects inheritance. A boolean is also an integer in Python:
flag = True
print(isinstance(flag, bool)) # True
print(isinstance(flag, int)) # True (bool inherits from int)
print(type(flag) == int) # False (exact type comparison)
This inheritance awareness is why isinstance() is preferred over type() comparisons in most cases.
Understanding issubclass()
While isinstance() works with objects, issubclass() works with classes themselves. It checks whether a class is a subclass of another class or classes:
issubclass(class, classinfo)
Here’s a simple inheritance chain:
class Animal:
pass
class Mammal(Animal):
pass
class Dog(Mammal):
pass
# Direct and indirect inheritance
print(issubclass(Dog, Mammal)) # True
print(issubclass(Dog, Animal)) # True
print(issubclass(Mammal, Animal)) # True
# A class is a subclass of itself
print(issubclass(Dog, Dog)) # True
# No inheritance relationship
print(issubclass(Animal, Dog)) # False
Like isinstance(), you can check against multiple parent classes:
class Serializable:
pass
class Cacheable:
pass
class UserModel(Serializable, Cacheable):
pass
class ProductModel(Serializable):
pass
# Check if classes support certain capabilities
print(issubclass(UserModel, (Serializable, Cacheable))) # True
print(issubclass(ProductModel, (Serializable, Cacheable))) # True (only needs one)
print(issubclass(ProductModel, Cacheable)) # False
Key Differences Between the Two
The distinction is simple but critical: isinstance() takes an object as its first argument, issubclass() takes a class. Mixing them up causes TypeError:
class Vehicle:
pass
class Car(Vehicle):
pass
my_car = Car()
# isinstance() works with OBJECTS
print(isinstance(my_car, Vehicle)) # True
print(isinstance(my_car, Car)) # True
# issubclass() works with CLASSES
print(issubclass(Car, Vehicle)) # True
print(issubclass(type(my_car), Vehicle)) # True
# Common mistakes
try:
issubclass(my_car, Vehicle) # TypeError: arg 1 must be a class
except TypeError as e:
print(f"Error: {e}")
try:
isinstance(Car, Vehicle) # Returns False, but probably not what you meant
except TypeError:
pass
print(isinstance(Car, Vehicle)) # False - Car is a class, not a Vehicle instance
The last example is subtle. isinstance(Car, Vehicle) doesn’t raise an error—it returns False because Car is a class object, and class objects are instances of type, not Vehicle.
Practical Use Cases
Input Validation and Polymorphic Handling
Here’s a function that gracefully handles different input types:
from typing import Union, List
def normalize_ids(ids: Union[int, str, List[int], List[str]]) -> List[int]:
"""Convert various ID formats to a list of integers."""
if isinstance(ids, int):
return [ids]
if isinstance(ids, str):
# Handle comma-separated strings like "1,2,3"
return [int(x.strip()) for x in ids.split(',')]
if isinstance(ids, list):
if not ids:
return []
if isinstance(ids[0], str):
return [int(x) for x in ids]
if isinstance(ids[0], int):
return list(ids)
raise TypeError(f"Cannot normalize IDs from {type(ids).__name__}")
# All of these work
print(normalize_ids(42)) # [42]
print(normalize_ids("1, 2, 3")) # [1, 2, 3]
print(normalize_ids([1, 2, 3])) # [1, 2, 3]
print(normalize_ids(["4", "5"])) # [4, 5]
Plugin System with issubclass()
issubclass() shines when building extensible systems:
from abc import ABC, abstractmethod
class BasePlugin(ABC):
@abstractmethod
def execute(self, data: dict) -> dict:
pass
@property
@abstractmethod
def name(self) -> str:
pass
class ValidationPlugin(BasePlugin):
name = "validator"
def execute(self, data: dict) -> dict:
# Validation logic
return {"valid": True, **data}
class TransformPlugin(BasePlugin):
name = "transformer"
def execute(self, data: dict) -> dict:
# Transform logic
return {k.upper(): v for k, v in data.items()}
class PluginRegistry:
def __init__(self):
self._plugins = {}
def register(self, plugin_class):
"""Register a plugin class after validating it."""
if not issubclass(plugin_class, BasePlugin):
raise TypeError(
f"{plugin_class.__name__} must inherit from BasePlugin"
)
# Ensure it's not the abstract base itself
if plugin_class is BasePlugin:
raise ValueError("Cannot register abstract BasePlugin")
instance = plugin_class()
self._plugins[instance.name] = instance
return plugin_class # Allow use as decorator
def get(self, name: str) -> BasePlugin:
return self._plugins.get(name)
# Usage
registry = PluginRegistry()
registry.register(ValidationPlugin)
registry.register(TransformPlugin)
Abstract Base Classes Integration
Python’s collections.abc module provides abstract base classes that define interfaces. isinstance() checks against these ABCs enable proper duck typing:
from collections.abc import Iterable, Mapping, Callable, Sequence
def process_items(items):
"""Process any iterable, but handle mappings specially."""
if isinstance(items, Mapping):
# Dictionaries and dict-like objects
return {k: v * 2 for k, v in items.items()}
if isinstance(items, Sequence) and not isinstance(items, str):
# Lists, tuples, but not strings
return [x * 2 for x in items]
if isinstance(items, Iterable):
# Generators, sets, other iterables
return list(x * 2 for x in items)
raise TypeError("Expected an iterable")
# Works with various types
print(process_items({"a": 1, "b": 2})) # {'a': 2, 'b': 4}
print(process_items([1, 2, 3])) # [2, 4, 6]
print(process_items((1, 2, 3))) # [2, 4, 6]
print(process_items({1, 2, 3})) # [2, 4, 6] (from generator)
def validate_callback(func):
"""Ensure a callback is actually callable."""
if not isinstance(func, Callable):
raise TypeError(f"Expected callable, got {type(func).__name__}")
return func
This approach respects duck typing—any object that registers with the ABC or implements the required methods will pass the check.
Best Practices and Pitfalls
When Type Checking Is Appropriate
Type checking makes sense for:
- Public API boundaries where you need clear error messages
- Polymorphic dispatch based on input types
- Plugin systems and framework extension points
- Serialization/deserialization logic
When to Avoid It
Python’s EAFP (Easier to Ask Forgiveness than Permission) philosophy often produces cleaner code:
# Overly rigid type checking
def bad_get_length(obj):
if isinstance(obj, (str, list, tuple, dict, set, frozenset)):
return len(obj)
raise TypeError("Object has no length")
# Better: just try it
def good_get_length(obj):
try:
return len(obj)
except TypeError:
raise TypeError(f"{type(obj).__name__} has no length")
# Best: let the error propagate naturally if that's acceptable
def best_get_length(obj):
return len(obj) # Raises TypeError with clear message if no __len__
Common Mistakes to Avoid
# Mistake 1: Using type() for comparison
if type(x) == list: # Breaks for subclasses
pass
if isinstance(x, list): # Correct
pass
# Mistake 2: Checking for None with isinstance
if isinstance(x, type(None)): # Works but weird
pass
if x is None: # Correct
pass
# Mistake 3: Overly specific checks
def process(data):
if isinstance(data, list): # Too specific
for item in data:
yield item
def process_better(data):
if isinstance(data, Iterable) and not isinstance(data, (str, bytes)):
yield from data # Works with any iterable
Refactoring Rigid Type Checks
Transform verbose type checking into cleaner patterns:
# Before: rigid and repetitive
def serialize_value(value):
if isinstance(value, str):
return f'"{value}"'
elif isinstance(value, (int, float)):
return str(value)
elif isinstance(value, bool):
return "true" if value else "false"
elif isinstance(value, list):
return "[" + ", ".join(serialize_value(v) for v in value) + "]"
elif isinstance(value, dict):
pairs = [f'"{k}": {serialize_value(v)}' for k, v in value.items()]
return "{" + ", ".join(pairs) + "}"
else:
raise TypeError(f"Cannot serialize {type(value)}")
# After: dispatch table pattern
SERIALIZERS = {
str: lambda v: f'"{v}"',
int: str,
float: str,
bool: lambda v: "true" if v else "false",
}
def serialize_value_clean(value):
for type_class, serializer in SERIALIZERS.items():
if isinstance(value, type_class):
return serializer(value)
if isinstance(value, Mapping):
pairs = [f'"{k}": {serialize_value_clean(v)}' for k, v in value.items()]
return "{" + ", ".join(pairs) + "}"
if isinstance(value, Iterable) and not isinstance(value, str):
return "[" + ", ".join(serialize_value_clean(v) for v in value) + "]"
raise TypeError(f"Cannot serialize {type(value)}")
isinstance() and issubclass() are fundamental tools for runtime type checking in Python. Use them at API boundaries, for polymorphic dispatch, and when building extensible systems. But remember that Python’s dynamic nature is a feature—don’t fight it with excessive type checking when duck typing would serve you better.