isinstance checks whether an object belongs to a class (or any of its subclasses). issubclass does the same shape of check, but between two classes. These two functions are how Python code asks "what kind of thing is this?" without getting tripped up by inheritance, and they're the right tool whenever a strict type(obj) == SomeClass check would silently miss valid input.
type()The most direct way to ask "what's this object?" in Python is the built-in type(). It returns the exact class of the object.
That works fine until inheritance shows up. Imagine an online store with two kinds of products: physical products that ship in a box, and digital products that get emailed as a download link.
Both mouse and ebook are products in any reasonable sense of the word, but the check says no. type() returns the exact class, and neither object's exact class is Product. They're subclasses, and the equality check ignores that relationship.
This is where isinstance comes in. It walks up the inheritance chain and returns True for the class itself and for any of its ancestors.
The takeaway: when you care about "is this thing usable as a Product?", isinstance is what you want. When you care about "is this thing exactly a Product and nothing more", type(obj) == Product is fine. The first case is far more common.
isinstance Walks the Class Hierarchyisinstance(obj, cls) returns True if cls is the object's class or appears anywhere in its ancestor chain. Python builds that chain when you define a class: each class remembers its parents, all the way up to object, the root of everything.
The diagram below shows the chain for mouse. isinstance(mouse, Product) starts at the bottom and walks up until it either finds Product or runs out of ancestors.
When you call isinstance(mouse, Product), Python checks the chain: PhysicalProduct, Product, object. Product is in the chain, so the answer is True. The same call against DigitalProduct finds no match in mouse's chain and returns False.
Every object in Python is ultimately an instance of object, so isinstance(anything, object) is always True.
Cost: isinstance walks the method resolution order (MRO) of the object's class. In practice this is short (usually 2-5 entries) and the check is implemented in C, so it's effectively free. Don't worry about calling it in hot loops.
You'll often need to ask "is this a number?" without caring whether it's an int or a float. isinstance accepts a tuple as its second argument and returns True if the object matches any class in the tuple.
The tuple form reads naturally and runs as a single check. It's the idiomatic way to validate input that could be one of several related types.
Python 3.10 added a second form using the | (union) operator, the same syntax type hints use. It works wherever a tuple of classes would.
Both forms produce the same result. Pick the one that fits the codebase's style. New code targeting Python 3.10 or later often uses int | float because it matches the type-hint syntax developers already see in function signatures.
bool SurpriseHere's a gotcha that catches almost everyone the first time. In Python, bool is a subclass of int. True is, literally, the integer 1 wearing a different name, and False is 0. That means isinstance(True, int) returns True, even though most code that says "is this an int?" doesn't mean to include booleans.
This bites when validating numeric input. A function that says "give me an integer quantity" usually doesn't want to accept True as a quantity of 1.
The second call sneaks through because True is technically an int. The fix is to reject bool explicitly when you don't want it.
Order matters in that check. The bool test runs first so it can short-circuit before the int test passes.
issubclass for Class Objectsissubclass(cls, parent) answers the same question as isinstance, but the first argument is a class rather than an instance. It's useful when you have a class object in hand and want to check its lineage without creating an instance.
Two details worth noting. issubclass returns True when you compare a class to itself, because by convention every class is considered a subclass of itself. And like isinstance, the second argument can be a tuple.
A common mistake is calling issubclass with an instance instead of a class. The function raises a TypeError because it expects a class object.
Use isinstance for instances, issubclass for classes. The error message is clear about which one you reached for.
isinstance() vs type() ==: The ComparisonBoth forms check types, but they answer subtly different questions. Picking the right one matters more often than people think.
| Aspect | isinstance(obj, cls) | type(obj) == cls |
|---|---|---|
| Subclasses count? | Yes, returns True for the class and any descendant | No, must be the exact class |
| Multiple types in one call | Yes, pass a tuple or use | (3.10+) | No, one class at a time |
| Works with abstract base classes | Yes, supports virtual subclasses | No, ignores register() |
| Idiomatic Python? | Yes, this is the standard check | Rare, used only when you need exact-type semantics |
| Common use case | Polymorphic input validation | Strict type guards, sentinel checks |
The short version: reach for isinstance 95% of the time. Use type(obj) == cls only when you have a specific reason to reject subclasses (for example, when you're writing a serializer that handles each exact type differently and a subclass would need its own branch).
The decision tree captures the rule. If you write type(x) == list and someone passes you a custom list subclass that should work just as well, the check rejects it for no good reason. isinstance(x, list) accepts it.
Python's abc module lets a class declare another class as a "virtual subclass" without actually inheriting from it. isinstance and issubclass respect those registrations, which is part of why they're more flexible than raw type() checks.
A quick taste: collections.abc.Sequence is registered against list, tuple, range, and a few others.
Lists, tuples, and strings are all sequences (indexable, have a length). Sets are not. isinstance knows this because the standard library registered them. type() would have given you four useless answers (list, tuple, str, set) and forced you to write the dispatch logic by hand.
The point for this lesson is that isinstance and issubclass plug into the ABC system out of the box.
Python developers often quote the phrase "duck typing": if it walks like a duck and quacks like a duck, treat it like a duck. Translated to code, it means "don't ask what something is, just try to use it and let it fail if the operation isn't supported". This is the EAFP style: Easier to Ask Forgiveness than Permission.
This function doesn't check whether items is a list, or whether each item is a Product instance. If the loop works, it works. If item.price doesn't exist on some object, you'll get an AttributeError and know exactly what's wrong.
The opposite style is LBYL: Look Before You Leap. Check types up front, then proceed.
Which one is right? Most Pythonic code leans toward duck typing inside private/internal code, because it accepts more inputs and reads cleaner. A Sequence-like object that isn't strictly a list should still work. But at boundaries (public APIs, deserialized JSON, user input, anywhere a caller might pass nonsense), explicit isinstance checks earn their keep. A clear TypeError at the boundary is much easier to debug than an AttributeError ten function calls deep.
A practical rule:
isinstance and raise a clear error.The check appears once, at the entry point. Everything downstream can rely on the invariant and stay clean.
A few patterns come up often enough to call out.
Dispatch by type. When you have a list of mixed objects and need different handling for each kind, isinstance is the clean way to route them.
Avoid long `isinstance` chains. If you find yourself writing five or six isinstance branches in a function, that's a hint to use polymorphism instead. Add a method on each class and let the object decide what to do.
Same result, less branching, and adding a new product type only requires adding a new class with a shipping_label method. No central function to keep updating.
Don't use `isinstance` to fake type hints. Runtime isinstance checks should validate inputs, not document intent. For static type checking, use type annotations. The two work together: annotations let tools like mypy catch bugs before you run the code, while isinstance guards the runtime boundary.
isinstance answers "is this object a kind of X?" by walking the inheritance chain. issubclass answers the same question between two classes. Both accept tuples (and | unions on 3.10+) to check against several options at once. Both respect Python's full type system, including abstract base classes and virtual subclasses, which is why they're preferred over raw type(obj) == cls checks in nearly every case.
The one gotcha to remember: bool is an int, so isinstance(True, int) is True. If that matters for your input validation, check for bool first.
Use these checks at public boundaries where callers might pass anything. Inside trusted code, lean on duck typing instead, because Python code that accepts "any object with the right shape" is often more flexible and easier to read than code that demands a specific class.
10 quizzes