2026, Jan 06 17:00

How to Detect if a Python Subclass Overrides a @property: Safe Patterns for Optional Hooks

Learn how to detect property overrides in Python subclasses by comparing property identity. Covers constructor checks, lazy access, and __init_subclass__.

Optional extension points in base classes are handy until you need to detect whether a subclass actually implemented them. A common pattern is to declare a property in the base class as a placeholder and then initialize some state based on whether a subclass overrides that property. The catch: naive checks like hasattr or poking at __code__ won’t tell you what you need for properties.

Problem

You want a base class to expose a property that subclasses may implement. If a subclass implements it, initialize a field to a value; if not, leave the field as None. The placeholder in the base class does nothing:

class Core:
    @property
    def hook(self):
        pass

The question is how to detect, at runtime, that a subclass provided its own property implementation.

What’s really going on

The check you need is whether the subclass’s property object is the same one that lives on the base class. If it’s the very same object, then nothing was overridden. If it’s a different object, the subclass has provided its own implementation. In code, that translates to comparing the attribute on the instance’s class with the attribute on the base class. That lets you branch your initialization without guessing or inspecting function internals.

Solution: decide once in the constructor

Initialize the attribute in __init__ by comparing the property on the concrete class with the base class’s property. If it matches the base, keep None. If it differs, mark it as Initialised.

from __future__ import annotations

class Core:
    def __init__(self) -> None:
        self.marker = None if type(self).hook is Core.hook else 'Initialised'

    @property
    def hook(self) -> str | None:
        pass

class ImplOne(Core):
    @property
    def hook(self) -> str | None:
        return "X"

class ImplTwo(Core):
    @property
    def hook(self) -> str | None:
        print("Has side-effect")
        return None

class ImplNone(Core):
    ...

print(f"Core: {Core().marker}")
print(f"ImplOne: {ImplOne().marker}")
print(f"ImplTwo: {ImplTwo().marker}")
print(f"ImplNone: {ImplNone().marker}")

Output:

Core: None
ImplOne: Initialised
ImplTwo: Initialised
ImplNone: None

This gives a clear, side-effect-free check during construction. Subclasses that override hook flip the switch; those that don’t keep the default.

Alternative: compute on access

If you prefer to decide lazily each time the value is requested, you can expose marker itself as a property and perform the same comparison on access.

class Core:
    @property
    def marker(self) -> None | str:
        return None if type(self).hook is Core.hook else 'Initialised'

    @property
    def hook(self) -> str | None:
        pass

This avoids storing state and always reflects the current class definition.

Alternative: class-level switch with __init_subclass__

If the decision is a class concern rather than an instance concern, set a class attribute when each subclass is created.

class Core:
    marker: str | None = None

    def __init_subclass__(cls) -> None:
        cls.marker = None if cls.hook is Core.hook else 'Initialised'

    @property
    def hook(self) -> str | None:
        pass

Each subclass receives marker based on whether it overrides hook.

Why this matters

This approach adds a new extension point without breaking existing subclasses. It doesn’t force implementers to change anything unless they opt in. That aligns with the goal of evolving an API safely while still enabling feature detection. It also avoids try/except flows around a NotImplementedError and keeps the consumer-side checks simple: None means “not implemented,” anything else means “implemented.”

Forcing users to implement it can break existing user code. If they don’t want the feature, they shouldn’t have to carry any burden.

The identity check against the base property gives you an explicit, unambiguous signal without relying on return values beyond what you already use in your initialization logic.

Takeaways

When you expose an optional property-based extension point, compare the subclass’s property to the base class’s property. Do the comparison where it fits your design: once in the constructor for instance state, on every access if you want it lazy, or at class creation time via __init_subclass__ for class-level flags. The key expression stays the same: type(self).hook is Core.hook for instance context, or cls.hook is Core.hook for class context. This keeps your code clean, backward compatible, and easy to reason about.