PythonProgrammingSenior Python Developer

Which specific dunder method defined on a **Python** metaclass intercepts `isinstance()` calls targeting its instances, and what infinite recursion hazard arises if this method accesses arbitrary attributes on the candidate object?

Pass interviews with Hintsage AI assistant

Answer to the question.

When isinstance(obj, cls) is invoked, Python checks if type(cls) defines __instancecheck__(self, instance). If present, this metaclass method determines membership, enabling "virtual subclassing" without inheritance. The hazard occurs when the implementation uses hasattr() or dotted attribute access on obj; if obj implements __getattr__ or descriptors that trigger isinstance() checks, the metaclass method re-enters, causing unbounded recursion.

Situation from life

We architected a validation framework where plugins needed to satisfy interfaces without explicit inheritance. We defined a Processor ABC and desired isinstance(plugin, Processor) to succeed if plugin exposed a process method, supporting duck typing for external libraries.

The first approach required all plugins to inherit from Processor or call Processor.register(). This was type-safe but impractical for our use case; it prohibited runtime-generated classes and required modifying third-party code that we could not change. Consequently, it failed to support dynamic plugin discovery from untrusted sources.

The second approach implemented __instancecheck__ on the Processor metaclass using hasattr(candidate, 'process'). While flexible, this crashed with RecursionError when a plugin used a property decorator that validated its return type via isinstance, invoking the metaclass method again before the first call returned.

We adopted a third solution: implementing __instancecheck__ using object.__getattribute__ to bypass descriptor logic. By checking type(candidate).__dict__.get('process') and verifying it is callable, we avoided triggering user-defined __getattr__ or property side effects. This eliminated the recursion risk while preserving dynamic duck typing, allowing the framework to safely integrate thousands of heterogeneous plugins without source modification.

class MetaProcessor(type): def __instancecheck__(cls, candidate): # Safe: bypasses __getattr__ and descriptors try: attr = type(candidate).__dict__.get('process') return callable(attr) except Exception: return False class Processor(metaclass=MetaProcessor): pass

What candidates often miss


Why does isinstance() consult the metaclass of the second argument rather than the first?

The protocol assigns authority to the type being checked against (cls), not the candidate object (obj). By placing __instancecheck__ on type(cls), Python ensures that only the class definition controls its membership semantics. This prevents objects from spoofing instance checks; the type unilaterally defines what constitutes its instance, maintaining integrity in security-sensitive type checks.


What is the relationship between __instancecheck__ and __subclasscheck__, and why must they remain consistent?

__instancecheck__ validates objects for isinstance(), while __subclasscheck__ validates types for issubclass(). If __instancecheck__ accepts virtual instances (objects not inheriting from the class), __subclasscheck__ must typically accept the corresponding virtual subclasses to preserve the invariant that isinstance(obj, cls) implies issubclass(type(obj), cls). Violating this causes generic container code to fail when it checks subclass relationships after instance checks pass.


How does using getattr() inside __instancecheck__ specifically trigger infinite recursion compared to object.__getattribute__?

getattr(obj, 'name') invokes the full descriptor protocol and may call obj.__getattr__('name') if the attribute is missing. If obj.__getattr__ implements lazy-loading, logging, or type coercion that internally calls isinstance(obj, OurClass), the metaclass __instancecheck__ re-enters before returning. In contrast, object.__getattribute__(obj, 'name') (or inspecting type(obj).__dict__) bypasses __getattr__ and custom descriptors entirely, accessing raw implementation details without triggering user code that might recurse.