Using isinstance in Python for Runtime Type Checks
python isinstance: Learn how isinstance works in Python, when to use it over type(), how inheritance affects results, and where runtime type checks are appropriate.
The python isinstance function is the standard way to check whether an object is an instance of a given class or a tuple of classes. It returns True if the object's type is the class itself or any subclass, which makes it the correct tool for most runtime type validation in Python. Unlike type(), which only checks exact type equality, isinstance respects inheritance and abstract base classes, so it aligns with how Python's object model actually behaves.
How isinstance Works
isinstance(object, classinfo) takes two arguments: the object to test and a class, type, or tuple of classes and types. The second argument can also be a union type in Python 3.10 and later, such as int | str, but the classic form remains a tuple for broader compatibility.
value = 42 print(isinstance(value, int)) # True print(isinstance(value, (int, float))) # True print(isinstance(value, str)) # False
The function returns True if the object's type is exactly classinfo or a subclass of it. For example, a bool is a subclass of int in Python, so isinstance(True, int) evaluates to True. This behavior is often surprising to developers coming from statically typed languages, but it is consistent with Python's type hierarchy.
The second argument must be a type or a tuple of types. Passing anything else raises TypeError. This includes instances of classes, which are not types themselves.
class Sample: pass obj = Sample() print(isinstance(obj, Sample)) # True print(isinstance(obj, obj)) # TypeError: isinstance() arg 2 must be a type...
Using isinstance with a Tuple of Types
When you need to accept multiple possible types, pass a tuple as the second argument. This is cleaner than writing multiple or conditions and makes the allowed types explicit.
def process_number(value): if isinstance(value, (int, float)): return value * 2 raise TypeError("Expected int or float")
The tuple can contain any number of types, including custom classes. This pattern is common in validation functions and API boundaries where input can come from different sources.
A tuple also works with inheritance. If you include a base class, any subclass instance passes the check. For example, isinstance(True, (int, float)) returns True because bool inherits from int. This is usually what you want, but it can lead to subtle bugs if you need to exclude booleans from numeric processing. In that case, you would need to check type(value) is int or explicitly exclude bool.
isinstance vs type() for Exact Type Checks
The type() function returns the exact type of an object. Comparing it directly with == or is checks for exact equality, ignoring inheritance. This is useful when you need to reject subclasses.
class MyInt(int): pass value = MyInt(5) print(type(value) == int) # False print(isinstance(value, int)) # True
Use type() when you require the object to be exactly that type and no subclass. This is rare in Python because the language favors polymorphic behavior. One common use case is when you are serializing data and need to distinguish between bool and int, since bool is a subclass of int. In JSON serialization, for example, True and 1 are different values, so you might want to handle them separately.
def json_safe_type(value): if type(value) is bool: return "boolean" if type(value) is int: return "integer" return "other"
In most validation scenarios, isinstance is the better choice because it respects the type hierarchy and allows subclasses to be used interchangeably. The decision comes down to whether you are enforcing a contract based on the declared type or the exact runtime type.
isinstance and Inheritance
Because isinstance checks the full MRO (method resolution order), it works correctly with multiple inheritance and abstract base classes. This makes it the recommended way to test for interface-like behavior.
from collections.abc import Iterable print(isinstance([1, 2], Iterable)) # True print(isinstance("abc", Iterable)) # True print(isinstance(42, Iterable)) # False
Using isinstance with abstract base classes from collections.abc is more robust than checking for concrete list or tuple types, because it covers any object that implements the required protocol. For example, a custom iterable class will pass the Iterable check even if it does not inherit from list.
This behavior is central to Python's duck typing philosophy. isinstance lets you verify that an object supports a certain interface without forcing it to inherit from a specific base class. However, it still requires the object to be registered as a virtual subclass of the ABC, or to implement the required methods.
Common Mistakes and Edge Cases
One common mistake is passing a class instance instead of a class as the second argument. This raises TypeError as shown earlier. Another is using isinstance with a type that is not a class, such as a list of types. The second argument must be a tuple, not a list.
# This raises TypeError: isinstance() arg 2 must be a type... # isinstance(value, [int, float]) # Correct form isinstance(value, (int, float))
Another edge case is checking against object. Since every class inherits from object, isinstance(value, object) is always True for any object. This is rarely useful and often indicates a logic error.
Also be aware that isinstance does not perform any coercion. It only checks the type hierarchy. If you need to validate that a string can be converted to an integer, you should attempt the conversion and handle the exception rather than relying on isinstance.
def to_int(value): if isinstance(value, int): return value if isinstance(value, str): try: return int(value) except ValueError: raise ValueError(f"Cannot convert {value!r} to int") raise TypeError("Expected int or str")
Performance and Runtime Considerations
isinstance is a fast operation because it walks the type's MRO and compares against the given class or tuple. The cost is proportional to the depth of the inheritance chain, but in practice it is negligible for typical class hierarchies. The tuple form checks each type in order and short-circuits on the first match, so the order of types in the tuple can affect performance slightly for large tuples.
There is no need to micro-optimize isinstance calls in most code. The bigger performance concern is using it excessively in hot loops when a simpler structural check or a different design would avoid the need for type checks altogether. For example, if you find yourself writing many isinstance branches to handle different input types, consider whether a common interface or a dispatch table would be more maintainable.
That said, isinstance is more efficient than catching exceptions for control flow. Using try/except to handle type mismatches is slower and obscures the intent. Prefer isinstance when you are validating input before performing an operation.
When to Avoid isinstance
Overusing isinstance can lead to brittle code that is tightly coupled to concrete types. Python's duck typing encourages relying on behavior rather than type checks. If you are checking for a specific attribute or method, it is often better to use hasattr or simply call the method and let a TypeError propagate.
# Instead of isinstance(value, SomeInterface) # Check for the required method if hasattr(value, "save"): value.save()
However, hasattr is not always a good replacement because it does not verify that the attribute is callable or that it has the right signature. In many cases, using an abstract base class with isinstance is more explicit and safer than ad-hoc attribute checks.
A more serious issue is using isinstance to discriminate between types that have the same behavior. This often happens when a function accepts both a single item and a list of items. A common anti-pattern is:
def process(items): if isinstance(items, list): for item in items: process(item) else: process_one(items)
This breaks if the caller passes a tuple or a generator. A better design is to require an iterable and handle the single-item case separately, or to use a separate function for single items. The type check here is a symptom of an unclear API rather than a necessary validation.
Use isinstance when you need to enforce a type contract that is part of the function's specification, such as accepting only int or float for a numeric operation. Avoid it when you are trying to make up for missing polymorphism in your own code. The goal is to make the type check explicit and meaningful, not to compensate for a design that could be expressed more cleanly with interfaces or duck typing.
Practical Example: Validating Configuration Values
A realistic use case for isinstance is validating configuration values read from a file or environment variable. Because configuration can arrive as strings, integers, or booleans, you often need to check the type before using it.
class Config: def __init__(self, settings): self.debug = self._validate_bool(settings.get("debug", False)) self.port = self._validate_int(settings.get("port", 8080)) def _validate_bool(self, value): if isinstance(value, bool): return value if isinstance(value, str): return value.lower() in ("true", "1", "yes") raise TypeError("debug must be a boolean or string") def _validate_int(self, value): if isinstance(value, int) and not isinstance(value, bool): return value if isinstance(value, str) and value.isdigit(): return int(value) raise TypeError("port must be an integer or numeric string")
Notice how the integer validation explicitly excludes bool using not isinstance(value, bool). This is necessary because bool is a subclass of int, and a configuration value of True would otherwise be accepted as a port number. This is a subtle but important detail when using isinstance with numeric types.
This example also shows that isinstance is not a silver bullet. You still need to handle string conversion and edge cases. The type check is the first line of defense, but it does not replace proper parsing and error handling.
Understanding isinstance with Abstract Base Classes
Python's collections.abc module provides abstract base classes that can be used with isinstance to check for behavioral interfaces. This is more flexible than checking concrete types and is often the recommended approach for API design.
from collections.abc import Mapping, Sequence def display(data): if isinstance(data, Mapping): for key, value in data.items(): print(f"{key}: {value}") elif isinstance(data, Sequence): for item in data: print(item) else: raise TypeError("data must be a mapping or sequence")
Using Mapping and Sequence covers dictionaries, lists, tuples, and any custom class that registers as a virtual subclass. This approach is more robust than checking for dict or list directly because it respects the object's capabilities rather than its implementation.
One limitation is that isinstance with ABCs only works if the class is registered as a virtual subclass or if it inherits from the ABC. For example, a plain custom iterable class that implements __iter__ but does not inherit from Iterable will not pass isinstance(obj, Iterable) unless it is registered. In practice, most built-in types are registered, but custom classes may need explicit registration if you rely on this pattern.
When you control the class definitions, the cleanest approach is to inherit from the appropriate ABC or to use a custom base class and check against that. This makes the type contract explicit and avoids the need for registration.
Final Technical Consideration: Union Types and Python 3.10+
Starting with Python 3.10, isinstance accepts union types using the | operator. This is a more readable alternative to a tuple when the type list is short.
def process(value): if isinstance(value, int | float): return value * 2 raise TypeError("Expected int or float")
The union syntax is equivalent to a tuple for the purpose of isinstance. It does not change the runtime behavior. However, it requires Python 3.10 or later, so if you support older versions, stick with the tuple form. The union form also works with issubclass and other type-checking utilities, making it a consistent modern style.
When using union types, be careful with None checks. isinstance(value, int | None) is not valid because None is not a type. You should use isinstance(value, int) or value is None or use Optional from typing if you are working with type hints. The runtime isinstance function does not understand Optional; it only works with actual types.
In summary, python isinstance is a precise and efficient tool for runtime type validation when used correctly. It respects inheritance, supports tuples and union types, and integrates with abstract base classes. The key is to use it where the type contract is real and to avoid using it as a substitute for proper polymorphism. Understanding the difference between isinstance and type() and being aware of edge cases like bool being an int will help you write more reliable Python code.