Custom isinstance and issubclass With Python Magic Methods
Learn how Python's __subclasscheck__ and __instancecheck__ magic methods let you control isinstance() and issubclass() behavior for flexible, protocol-friendly type checking without forcing inheritance.
The Secret Behind Python's isinstance() and issubclass() That Most Developers Miss
You've used isinstance() and issubclass() a thousand times. But have you ever wondered what makes them tick behind the scenes? Python has two magic methods — __subclasscheck__ and __instancecheck__ — that give you god-like control over these checks. And most developers don't even know they exist.
Let me show you how they work and why they matter.
The Default Behavior
Normally, when you write isinstance(obj, MyClass), Python looks at the class hierarchy. It checks if obj is an instance of MyClass or any of its subclasses. Same for issubclass().
But what if you want to override this logic? What if you want isinstance() to return True for objects that don't actually inherit from your class?
That's where __instancecheck__ and __subclasscheck__ come in.
The Magic Methods Demystified
These methods live on the metaclass level. You can't just drop them on a regular class. Here's the rule:
__instancecheck__(self, instance)is called when you doisinstance(instance, MyClass)__subclasscheck__(self, subclass)is called when you doissubclass(subclass, MyClass)
And they must be defined on the metaclass, not the class itself.
A Real-World Example
Let's say you're building a plugin system at PythonSkillset. You want any object with a run() method to be considered a "Plugin", even if it doesn't inherit from your base class.
class PluginMeta(type):
def __instancecheck__(self, instance):
if hasattr(instance, 'run') and callable(instance.run):
return True
return super().__instancecheck__(instance)
def __subclasscheck__(self, subclass):
if hasattr(subclass, 'run'):
return True
return super().__subclasscheck__(subclass)
class PluginBase(metaclass=PluginMeta):
pass
class MyPlugin:
def run(self):
print("Running!")
class NotAPlugin:
pass
# Now this works!
print(isinstance(MyPlugin(), PluginBase)) # True
print(issubclass(MyPlugin, PluginBase)) # True
print(isinstance(NotAPlugin(), PluginBase)) # False
See what happened there? MyPlugin never inherits from PluginBase, but both isinstance() and issubclass() return True because the object has a run() method.
The ABC Connection
If this looks familiar, it should. Python's abc.ABC (Abstract Base Classes) uses exactly this mechanism. When you do isinstance(obj, collections.abc.Sequence), it's not checking inheritance — it's checking if the object implements the right methods.
from collections.abc import Sequence
class MyList:
def __getitem__(self, index):
return index
def __len__(self):
return 1
print(isinstance(MyList(), Sequence)) # True
That's __instancecheck__ at work, courtesy of ABCMeta.
The Performance Trap
Here's something most articles won't tell you: overusing __instancecheck__ can kill your performance. Every time you call isinstance(), Python runs your custom logic. If that logic is expensive (like iterating over all attributes), you'll feel the pain in tight loops.
Use it sparingly. Only when you truly need structural typing over nominal typing.
The Protocol Pattern
At PythonSkillset, we use this pattern for "protocol-like" behavior. Instead of forcing users to inherit from a base class, we let them satisfy an interface implicitly:
class RunnableCheckMeta(type):
def __instancecheck__(self, instance):
return hasattr(instance, 'run')
class Runnable(metaclass=RunnableCheckMeta):
"""Can check if something is runnable without forcing inheritance."""
pass
class Worker:
def run(self):
pass
def execute(task):
if isinstance(task, Runnable):
task.run()
else:
raise TypeError("Not runnable")
This gives you flexible, duck-typing-friendly code without the rigidity of traditional inheritance.
When NOT to Use Them
- For simple type checks: Just use normal inheritance. Don't overcomplicate things.
- In performance-critical paths: The metaclass hook adds overhead.
- When you need clear class hierarchies: Custom checks can confuse readers. Your team might not expect
isinstance()to lie.
The Bottom Line
__subclasscheck__ and __instancecheck__ are Python's best-kept secrets for writing flexible, protocol-friendly code. They let you separate "what something is" from "what something inherits from." But with great power comes great responsibility — use them wisely, and your code will be cleaner. Abuse them, and you'll have a debugging nightmare.
Next time you need to check if something "acts like" a certain type, remember: you have the power to redefine what "is" means in Python.
Comments
Questions, corrections, and tips stay visible for everyone reading this page.
Join the discussion
No comments yet
Be the first to leave a note — it helps the next reader.