FULL PYTHON FIX · 8 MIN READ
Python isinstance() vs type(): Why a Subclass Check Fails
Use isinstance(value, Base) when instances of Base and its subclasses are valid. Use an exact type check only when the contract intentionally excludes subclasses.
Use isinstance() when subclasses should pass
isinstance(value, Base) accepts instances of Base and its subclasses. type(value) is Base accepts only objects whose concrete class is exactly Base. Choose the check that matches your function's contract.
One boundary matters for counts: isinstance(True, int) is True because bool subclasses int. Exclude bool explicitly if True and False are not valid integer values in your domain.
class Animal:
pass
class Dog(Animal):
pass
pet = Dog()
print(isinstance(pet, Animal))
print(type(pet) is Animal)
print(isinstance(True, int))Expected result: True False True
See why this works and test the edge casesVideo guide: Python isinstance() vs type(): Why a Subclass Check Fails
The preview is stored on this site. YouTube loads only when you press Play. Watch on YouTube
Jump to a chapter
Read the video transcript
Select a timestamp to watch that moment on YouTube.
- 00:00
Imagine a small animal shelter app. It accepts any Animal object, and Dog is a subclass of Animal. The Dog has the behavior we need, but the shelter's type check rejects it. There is no traceback. The program runs and prints the wrong decision, which can be harder to catch than a crash. Before seeing the code, predict what an exact class check will say about a Dog object. In this video we will reproduce that failure, inspect the class returned by `type()`, and compare it with `isinstance()`. Then we will change the check, run the same cases again, and test a surprising boundary: Python considers a Boolean an integer subclass.
- 00:41
That last example prevents us from treating `isinstance()` as a mechanical replacement for every type check. The right expression depends on what values your function promises to accept. The companion PythonLessonLab guide lets you run each snippet and complete a checked practice task. The story is small, but the same mistake appears in systems that accept plugin classes, custom exceptions, or data model subclasses. A base-class-only test may pass today and silently exclude new subclasses tomorrow. The first line reports Dog, the concrete class of this object. The next line is False.
- 01:22
The final line therefore says rejected, even though Dog inherits from Animal. Follow the expression from the inside out. `type(pet)` returns the Dog class object. The `is` operator asks whether that exact object is the Animal class object. Dog and Animal are two different classes, so their class objects are not identical. Inheritance does not make them identical; it establishes a relationship between them. This code asks a narrower question than the shelter intended. Adding another if statement or wrapping the test in try and except would not repair the rule. Try replacing the Dog object with `Animal()` and the check passes, which
- 02:03
explains how a shallow test can miss the bug. If we introduce a Cat subclass later, the same exact-class gate will reject Cat. It is a silent logic error, so the output we just saw is the important evidence. We now need to ask whether the object belongs to the Animal family rather than whether its concrete class is exactly Animal. The first two lines have not changed. The last line is True because `isinstance(pet, Animal)` accepts an instance of Animal or one of Animal's subclasses. That matches the shelter's rule. Here is a useful mental test: if we add Cat as another Animal subclass next month,
- 02:43
should a Cat pass without changing this function? If yes, `isinstance()` expresses the intended class family. Exact type identity is still useful when a contract deliberately requires one concrete type. For example, a strict serializer might only support a built-in value class and reject custom subclasses. Avoid comparing `type(value).__name__` with a string. Class names can change, and unrelated classes can share a name. `isinstance()` also accepts a tuple of candidate classes, such as `isinstance(value, (str, bytes))`, if two unrelated types are valid.
- 03:23
That tuple lists allowed classes; it does not convert the input. Type membership is also different from checking whether an object happens to expose an attribute. We choose the predicate from the application's promise, then test a base instance, a subclass instance, and a value outside the family. The base Animal and Dog both pass, while the word dog does not. Spelling an animal's name in a string does not turn the string into an Animal. The shelter's policy is fixed. Now look at the next result: `isinstance(True, int)` is True. Python's bool type is an int subclass. In an order form, the integer one could be a valid ticket quantity, but a checkbox
- 04:06
value True should not quietly become one ticket. The exact type check rejects True, yet it also rejects any custom int subclass. If the domain allows integer subclasses but excludes Boolean values, state both requirements: `isinstance(value, int)` and `not isinstance(value, bool)`. The final line is False for True. This is more precise than switching to `type(value) is int` just to handle one special case. We also need to decide whether negative counts are valid. A type test answers what kind of value we received; a range test answers whether that value is allowed.
- 04:46
In the next run we will combine those rules in one named predicate and verify every boundary, including text that looks like a number. Zero passes, and our TicketCount subclass passes too. True fails despite belonging to the int family because the domain explicitly excludes Booleans. The negative integer has the right type but fails the range rule. The string looks numeric to a human, but this function does not silently convert it. Python evaluates `and` from left to right and stops at the first false condition, so it never compares the string with zero. Pause and predict what `3.0` and `None` will do. Neither passes the initial int membership test; both return False without a
- 05:28
comparison error. In the PythonLessonLab guide, write this function yourself and run the task checker against built-in integers, an int subclass, Booleans, negative values, floats, strings, and None. The rule to remember is not “always use one function.” Use `isinstance()` when the contract includes a class and its subclasses. Use exact type identity only when subclasses are intentionally excluded. Write the contract down and test the nearest counterexample. A successful `Animal()` test alone would never reveal the Dog bug, and a successful count of one would never reveal the Boolean edge.
An exact check silently rejects a subclass
Imagine a shelter app that accepts any Animal object. Dog inherits from Animal, so a Dog should qualify. The first version tests type(pet) is Animal, however, and prints rejected. Run it before changing the condition. The code is valid Python: the bug is in the rule chosen for deciding what counts as an animal.
The one-argument type() call reports the object's concrete class. For a Dog instance, that class is Dog even when Dog inherits every relevant behavior from Animal. Identity comparison with Animal therefore returns False. If your intent is to include subclasses, an exact class identity check is too narrow. The difference matters most as your program gains subclasses after the initial implementation.
class Animal:
pass
class Dog(Animal):
pass
pet = Dog()
print(type(pet) is Animal)
print('accepted' if type(pet) is Animal else 'rejected')False
rejectedCompare concrete class with class membership
Print both the exact type and the two test results. type(pet) is Animal asks whether the concrete class object is exactly Animal. isinstance(pet, Animal) asks whether pet is an instance of Animal or a subclass of Animal. The latter returns True for our Dog. This explains the shelter bug without hiding it behind a try/except block or a string comparison of class names.
isinstance() can also accept a tuple of candidate classes when a value may legitimately come from several unrelated types. That is different from checking whether a type name appears in a manually maintained list. A subclass can be added without editing the membership test. Do not automatically replace every exact check, though: if a format or API contract explicitly requires one concrete class, exact type identity may be deliberate.
class Animal:
pass
class Dog(Animal):
pass
pet = Dog()
print(type(pet).__name__)
print(type(pet) is Animal)
print(isinstance(pet, Animal))Dog
False
Trueprint(isinstance('42', (str, bytes)))
print(isinstance(b'42', (str, bytes)))
print(isinstance(42, (str, bytes)))True
True
FalseChoose the test that matches the contract
Change the shelter check to isinstance(pet, Animal) if Animal subclasses are supported. A Dog then passes while an unrelated string does not. This is a behavioral choice: the program promises to work with the Animal hierarchy rather than with only the base class. Test a base instance and at least one subclass, because testing only Animal() would not reveal the original bug.
A useful counterexample is a field that must contain a whole-number count but must not accept True or False. In Python, bool is a subclass of int, so isinstance(True, int) is True. Use an explicit exclusion when the domain distinguishes counts from booleans. If you truly require only built-in int and intentionally reject custom int subclasses, type(value) is int is a different, stricter contract. State which behavior you want before picking the expression.
class Animal:
pass
class Dog(Animal):
pass
for value in [Animal(), Dog(), 'dog']:
print(isinstance(value, Animal))True
True
Falseprint(isinstance(True, int))
print(type(True) is int)
print(isinstance(True, int) and not isinstance(True, bool))True
False
FalseTest subclasses and the Boolean boundary
Write tests for the accepted family and for the nearest misleading case. A subclass of int should pass a flexible integer contract, while True should fail if a count cannot be Boolean. A negative integer should also fail when the domain says nonnegative. Keep these policies separate: the type test determines what kind of value you received; the comparison determines whether that value is within the allowed range.
For an exact-type contract, test the opposite boundary too: type(True) is int must remain False, but so must a user-defined int subclass. That may be what a serializer requires; it is usually too strict for an interface intended to accept subclasses. Prefer a clearly named predicate over scattering checks throughout the code. The next developer can then see why a Boolean is excluded rather than assuming it was an accident.
class TicketCount(int):
pass
def valid_count(value):
return isinstance(value, int) and not isinstance(value, bool) and value >= 0
for value in [3, TicketCount(4), True, -1, '3']:
print(valid_count(value))True
True
False
False
Falseclass TicketCount(int):
pass
for value in [3, TicketCount(4), True]:
print(type(value) is int)True
False
False- type(value) is Base checks only the concrete class.
- isinstance(value, Base) also accepts subclasses of Base.
- bool is an int subclass; exclude it explicitly when a Boolean is not a valid count.
Practice the fix
Write is_nonnegative_count(value). Accept a built-in int and any int subclass when the value is zero or greater, but reject bool, negative values, floats, and numeric strings. Return a Boolean. Test 0, an int subclass, True, -1, and '4'.
Need a hint?
First check isinstance(value, int), then reject isinstance(value, bool), then compare the value with 0. Keep the order so nonnumeric values are not compared with 0.
Python console
Ready to runEdit the code and run it in your browser. Examples above can be loaded with Try this example.
Does your code use input()? Add one value per line
Your output appears here.
Write your solution, then select Check practice.
Key takeaways
- Use isinstance() when the intended contract includes instances of a base class and its subclasses.
- Use type(value) is SomeClass only when the intended contract deliberately requires that exact class.
- bool is a subclass of int in Python, so an integer predicate may need a separate Boolean exclusion.
- Verify a base instance, a subclass instance, and the nearest counterexample.
Related lessons
- Classes and Objects
- Choose Composition Over Unnecessary Inheritance
- Truthiness, None, and Identity in Python