What It Means to Shadow a Class Method in Python

In Python, an instance attribute and a class method can share the same name. When that happens, the instance attribute blocks access to the class method — not by deleting it, but by standing in front of it. That is called shadowing, and with @classmethod, it happens without any warning. Understanding why requires going one level deeper: into Python's descriptor protocol and the C-level lookup machinery that drives every attribute access.

When you access an attribute on a Python object, Python does not just look in one place. It searches in a specific order, and whichever location produces a result first wins. Understanding that order is the key to understanding what shadowing is, why it happens, and when it matters.

How Python looks up attributes

When you write obj.name, Python does not immediately hand you whatever is stored in the class. It follows a sequence of lookups, defined by the descriptor protocol and the object's __dict__. According to the Python Data Model reference (opens in new window), the order Python uses for a normal attribute access on an instance is:

  1. Data descriptors defined on the class (or its parents) — things like property objects, which define both __get__ and __set__
  2. Instance attributes, stored in the instance's own __dict__
  3. Non-data descriptors defined on the class, and other class-level attributes

The distinction between a data descriptor and a non-data descriptor is the crux of this entire topic. A data descriptor implements both __get__ and __set__ (and optionally __delete__). Because it controls setting, Python gives it priority over the instance's own dictionary. A non-data descriptor only implements __get__. It has no say over what gets stored in __dict__, so instance attributes outrank it. The Python Data Model documentation states that data descriptors with both __get__() and __set__() always take priority over instance dictionary entries, whereas non-data descriptors — which lack __set__() — can be overridden by instances (Python Data Model, docs.python.org (opens in new window)).

Note

Regular functions defined in a class body are also non-data descriptors. They implement __get__, which is how Python binds them to the instance when you call obj.method(). Instance attributes can shadow plain methods by the same mechanism described here. The Python Data Model explicitly states that Python methods — including those decorated with @staticmethod and @classmethod — are implemented as non-data descriptors, which is what makes instance-level override possible.

Interactive Attribute Lookup Priority Click each tier to see what lives there
obj.name — Python searches in this order:
1
Data Descriptors
on the class / MRO
2
Instance __dict__
per-object storage
3
Non-Data Descriptors
and plain class attributes
✗ AttributeError

Click a tier to learn what lives there and why its position matters.

Inside PyObject_GenericGetAttr

The three-tier lookup is not just a conceptual model — it maps directly to CPython's C implementation. When Python evaluates obj.name, it ultimately calls type(obj).__getattribute__(obj, "name"). Unless you have overridden __getattribute__ yourself, this resolves to PyObject_GenericGetAttr, the C function in CPython that implements standard attribute access for all user-defined classes.

Raymond Hettinger's Descriptor HowTo Guide (opens in new window) in the official Python documentation provides a pure Python equivalent that mirrors the C logic exactly. It uses a sentinel object (null = object()) to distinguish a missing attribute from one whose value happens to be None, avoiding the ambiguity that arises when None is used as a default:

# Emulate PyObject_GenericGetAttr() — the C function behind object.__getattribute__
# Source: docs.python.org/3/howto/descriptor.html (Raymond Hettinger)

def object_getattribute(obj, name):
    "Emulate PyObject_GenericGetAttr() in Objects/object.c"
    null = object()
    objtype = type(obj)
    cls_var = getattr(objtype, name, null)
    descr_get = getattr(type(cls_var), '__get__', null)

    # Step 1: data descriptor on the type wins
    if descr_get is not null:
        if (hasattr(type(cls_var), '__set__')
                or hasattr(type(cls_var), '__delete__')):
            return descr_get(cls_var, obj, objtype)     # data descriptor

    # Step 2: instance __dict__
    if hasattr(obj, '__dict__') and name in vars(obj):
        return vars(obj)[name]                          # instance variable

    # Step 3: non-data descriptor or plain class variable
    if descr_get is not null:
        return descr_get(cls_var, obj, objtype)         # non-data descriptor
    if cls_var is not null:
        return cls_var                                  # plain class variable

    raise AttributeError(name)

The logic is explicit: getattr(objtype, name, null) walks the MRO and returns the class-level attribute if found, or the sentinel if not. A data descriptor at step 1 is identified by the presence of both __get__ and either __set__ or __delete__; if both conditions are met, its __get__ is called immediately. The instance dictionary is only consulted at step 2, after data descriptors have been ruled out. A classmethod object, which has __get__ but not __set__, passes through step 1 without matching — so any name in the instance's __dict__ stops the search at step 2.

Source

The pure Python equivalent above follows the Descriptor HowTo Guide (opens in new window) authored by Raymond Hettinger and maintained in the official CPython documentation. The sentinel pattern (null = object()) avoids treating None as a missing-attribute signal, which the simpler conditional form cannot distinguish. The actual C function is PyObject_GenericGetAttr in Objects/object.c, with the MRO walk handled by _PyType_Lookup.

What @classmethod is — and is not

@classmethod wraps a function so that, when accessed through the descriptor protocol, the first argument it receives is the class itself rather than an instance. The decorator replaces the function with a classmethod object, and that object implements __get__ — but not __set__.

That makes @classmethod a non-data descriptor. The Python Data Model documentation is explicit on this point: Python methods — including those decorated with @staticmethod and @classmethod — are implemented as non-data descriptors, which is precisely what allows instances to define attributes that override them. The pure Python equivalent of classmethod, as documented by Raymond Hettinger in the Descriptor HowTo Guide (opens in new window), shows exactly why:

# Pure Python equivalent of classmethod
# Source: docs.python.org/3/howto/descriptor.html (Raymond Hettinger)
import functools
from types import MethodType

class ClassMethod:
    def __init__(self, f):
        self.f = f
        functools.update_wrapper(self, f)

    def __get__(self, obj, cls=None):
        if cls is None:
            cls = type(obj)
        return MethodType(self.f, cls)
    # No __set__ defined — this is a non-data descriptor.

Because it sits in the third position in Python's lookup order, any instance attribute with the same name takes precedence when the lookup is performed on that specific instance. The class method is still there on the class — nothing has been deleted or overwritten at the class level — but this particular instance will never reach it.

Python 3.13 change: chained classmethod descriptors removed

Python 3.13 removed support for chained classmethod descriptors — a pattern that allowed @classmethod to wrap other descriptors such as @property. This feature was added in Python 3.9 (via gh-63272 (opens in new window)), deprecated in 3.11, and removed in 3.13 (contributed by Raymond Hettinger in gh-89519 (opens in new window)). The core design was considered flawed. If your codebase used @classmethod to wrap @property, the __wrapped__ attribute added in Python 3.10 is the documented path forward. This change does not affect the instance-level shadowing behavior described in this article — @classmethod remains a non-data descriptor, and the three-tier lookup order is unchanged.

class Connection:
    @classmethod
    def create(cls):
        return cls()

# Accessing through the class works fine
conn = Connection.create()   # returns a Connection instance

# Inspecting the class method directly shows it is a bound method on the class
print(Connection.create)     # <bound method Connection.create of <class '__main__.Connection'>>

So far, nothing unusual. create is a class method on the class, and it behaves as expected when called through the class or through a clean instance.

Shadowing in action

Shadowing occurs when you assign a value to an instance attribute that shares its name with the class method. Python stores that value in the instance's __dict__. On the next attribute lookup for that name on that instance, Python finds the instance attribute in step 2 and stops — it never reaches the non-data descriptor in step 3.

class Connection:
    @classmethod
    def create(cls):
        return cls()

c = Connection()

# Assign an instance attribute with the same name as the class method
c.create = "some_string"

# Now accessing .create on this instance returns the string, not the method
print(c.create)          # some_string

# But the class method is untouched on the class itself
print(Connection.create) # <bound method Connection.create ...>

The class method has not been removed. It is still perfectly accessible through Connection.create, and through any other instance that does not have a create key in its own __dict__. Only c is affected, because only c has that key in its instance dictionary.

Warning

Python raises no error and produces no warning when this happens. The shadowing is completely silent. If the class method is expected to be callable but is now an arbitrary value, any subsequent code that calls c.create() will raise a TypeError — and the error message will not mention shadowing at all.

You can confirm what is happening by inspecting the instance's dictionary directly:

c = Connection()
c.create = "some_string"

# The instance dictionary now holds the shadowing value
print(c.__dict__)           # {'create': 'some_string'}

# The class dictionary still holds the class method
print(Connection.__dict__['create'])  # <classmethod object at 0x...>

# Deleting the instance attribute restores access
del c.create
print(c.create)             # <bound method Connection.create ...>

Notice the last step: deleting the instance attribute from __dict__ restores the original behavior. The class method was never gone — it was simply hidden.

Interactive Watch the Lookup Happen Step through what Python does when you access c.create
Step 1 of 4

Contrast with a data descriptor

To illustrate why @classmethod is vulnerable but @property is not, consider this contrast:

class Account:
    def __init__(self, balance):
        self._balance = balance

    @property
    def balance(self):
        return self._balance

a = Account(100)

# Attempting to shadow a property raises an AttributeError
# because @property is a data descriptor — it defines __set__
try:
    a.balance = 999
except AttributeError as e:
    print(e)  # property 'balance' of 'Account' object has no setter

@property defines __set__, which places it in the data descriptor tier — step 1. Python reaches it before it even gets to the instance dictionary. The assignment fails loudly with an AttributeError naming the property and the class. With @classmethod, there is no __set__, so the assignment goes straight into __dict__ without resistance.

Version note

The AttributeError message shown — property 'balance' of 'Account' object has no setter — is the format used in Python 3.11 and later. Python 3.10 and earlier produce the shorter message can't set attribute without naming the property or class. Both are the same underlying error; only the wording differs between versions.

A real-world non-data descriptor by design: functools.cached_property

functools.cached_property (introduced in Python 3.8) is a non-data descriptor intentionally. It defines only __get__, which allows it to store the computed result directly into the instance's __dict__ on first access. On subsequent accesses, the instance dictionary entry is found at step 2 — before the non-data descriptor in step 3 — so the cached value is returned without ever invoking the getter again. That is the caching mechanism. The same property that makes @classmethod vulnerable to shadowing is what makes functools.cached_property work.

import functools

class DataSet:
    def __init__(self, rows):
        self.rows = rows

    @functools.cached_property
    def total(self):
        # Expensive computation; result cached after first call
        return sum(r["value"] for r in self.rows)

d = DataSet([{"value": 10}, {"value": 20}])

# First access: calls the getter, stores result in d.__dict__
print(d.total)          # 30
print(d.__dict__)       # {'rows': [...], 'total': 30}

# Second access: d.__dict__['total'] is found at step 2 — getter never called again
print(d.total)          # 30 (cached)

# Cache invalidation: delete the instance entry so the descriptor runs again on next access
del d.total
print(d.total)          # 30 (recomputed)

This is a case where the non-data descriptor placement in the lookup order is an explicit design choice rather than a vulnerability. The contrast matters: with @classmethod, an instance attribute landing in the same name is an accident. With functools.cached_property, the instance dictionary write is the mechanism. Both follow the same lookup rules — what differs is whether the result is intentional. Note that cache invalidation is straightforward: del d.total removes the instance dictionary entry, so the descriptor runs again on the next access.

The most common trigger: shadowing inside __init__

The examples so far have set the shadowing attribute explicitly on an already-constructed instance. In practice, the far more common scenario is that the collision happens silently inside __init__ — the very moment an object is created.

class Report:
    @classmethod
    def load(cls, path):
        # Factory: reads a file and returns a populated Report
        obj = cls()
        obj._path = path
        return obj

    def __init__(self, data=None, load=None):   # 'load' is also a parameter name
        self.data = data
        self.load = load   # shadows the class method immediately on construction

r = Report(data={"key": "value"}, load="quarterly")

# The class method is now unreachable on every instance created this way
print(r.load)           # 'quarterly'  — not the class method

# Calling it raises a TypeError, not an AttributeError
try:
    r.load("some_path")
except TypeError as e:
    print(e)            # 'str' object is not callable

This pattern appears in real codebases when a class evolves over time. A factory method named load, build, or parse gets added to an existing class whose __init__ already accepts a same-named keyword argument for configuration state. The two names collide, every newly constructed instance arrives already shadowed, and the factory method becomes entirely unreachable through instances. The class itself still has the method, but ClassName.load() is easy to forget when the rest of the codebase calls it on instances.

Warning

When __init__ is the source of the shadow, the problem affects every instance of that class created through the normal constructor — not just one stray object. If you also pass a non-callable as the attribute value (a string, a boolean, a dict), any call site that tries to use the name as a method will raise a TypeError with a message that gives no indication of where the shadow was introduced.

Can you shadow at the class level?

The examples above all create the conflict at the instance level — an attribute placed in the instance's __dict__ that hides the class method from that instance. A different question worth asking is: what happens when you assign to the name on the class itself?

class Connection:
    @classmethod
    def create(cls):
        return cls()

# Assign directly to the class attribute — this replaces the classmethod object
Connection.create = "overwritten"

# The class method is now gone at the class level
print(Connection.create)     # 'overwritten'

c = Connection()
print(c.create)              # 'overwritten'  — all instances see the replacement

This is categorically different from instance-level shadowing. Assigning to a class attribute does not hide the class method — it replaces the entry in Connection.__dict__ entirely. The classmethod object is gone. All instances, including ones created before the assignment, now see the replacement value. There is no del trick that restores it, because the original object is no longer referenced.

This distinction matters when reading tracebacks. If the error appears on one specific instance and other instances work correctly, the problem is in the instance's __dict__. If the error appears on every instance and on the class itself, the class attribute was reassigned.

Check Your Understanding Three questions — no input needed Click an answer to see feedback, then try the others
Question 1 of 3

A brief note on __slots__

There is one mechanism that prevents this class of problem entirely at the class definition level: __slots__. When a class declares __slots__, Python does not create a per-instance __dict__ for that class's instances. Instead, each named slot is backed by a member descriptor — which is a data descriptor — stored on the class itself.

class Connection:
    __slots__ = ('host', 'port')   # only these names can be instance attributes

    @classmethod
    def create(cls):
        return cls()

c = Connection()

# This raises AttributeError — 'create' is not a declared slot
try:
    c.create = "some_string"
except AttributeError as e:
    print(e)  # 'Connection' object attribute 'create' is read-only

Because there is no instance __dict__, there is nowhere for an arbitrary assignment to land. An attempt to set an undeclared attribute raises AttributeError immediately — in Python 3.10 and later the message reads 'Connection' object attribute 'create' is read-only. The class method is never threatened.

__slots__ is not a general recommendation for preventing shadowing — it changes memory layout and breaks multiple inheritance in ways that are often undesirable. But if you are working with a class that is instantiated in very high volume and where you want strict attribute control, understanding that __slots__ closes this vulnerability by removing the instance __dict__ entirely is useful.

Does a subclass inherit the shadow?

When you subclass a class that has instance-level shadowing, the answer depends on where the shadow lives. Because shadowing is an instance-level phenomenon — the conflicting value is stored in the specific instance's __dict__ — a subclass definition does not inherit it. What it does inherit is the risk.

class Base:
    @classmethod
    def create(cls):
        return cls()

class Child(Base):
    def __init__(self, name):
        self.name = name
        # No collision here — 'name' is not the name of any classmethod

c = Child("example")
print(c.create)         # <bound method Base.create of <class '__main__.Child'>>
# The class method is inherited normally and accessible on Child instances

# Now introduce the collision in __init__
class BrokenChild(Base):
    def __init__(self, create=None):
        self.create = create     # shadows the inherited class method

bc = BrokenChild(create="data_loader")
print(bc.create)        # 'data_loader'  — class method hidden
print(Base.create)      # <bound method Base.create of <class '__main__.Base'>>  — unaffected
print(BrokenChild.create)  # <bound method Base.create of <class '__main__.BrokenChild'>>  — unaffected

A few things are worth noting here. First, the inherited class method is accessible on BrokenChild and on Base — only the individual instance is affected. Second, when the class method is inherited rather than defined directly on the child class, Python still finds it through the MRO walk during the non-data descriptor check at step 3. The lookup scans each class in the MRO in order, so inheritance does not change the tier assignments — it only affects which class's __dict__ provides the descriptor.

Third: if you define a method named create directly on the subclass, that is not instance shadowing — it is straightforward method overriding, which is intentional and expected. The scenario that causes unexpected bugs is when an instance attribute collides with an inherited class method, because the instance attribute is stored at step 2 while the inherited method sits at step 3, and the lookup always stops at the first match.

Shadowing in dataclasses

@dataclass (introduced in Python 3.7) generates an __init__ automatically from the class's field definitions. That generated constructor assigns every field as an instance attribute — by name, without any knowledge of class-level methods. If any field name collides with a @classmethod, every instance produced by the generated __init__ arrives already shadowed. The problem is identical in mechanism to the hand-written __init__ case, but harder to spot because the offending assignment is not visible in the source.

from dataclasses import dataclass

class Base:
    @classmethod
    def validate(cls, data):
        """Return True if data is a valid input dict."""
        return isinstance(data, dict) and "name" in data

@dataclass
class Record(Base):
    name: str
    validate: bool = True   # field name collides with the inherited classmethod

# The generated __init__ assigns self.validate = validate on every construction.
# This replaces the inherited classmethod at the class level entirely.

r = Record(name="test")

print(r.validate)            # True  — the bool field value, not the classmethod
print(Base.validate)         # <bound method Base.validate ...>  — still on Base
print(Record.validate)       # True  — classmethod gone at the Record class level too

# Any call site that expected validate() to work now gets TypeError:
try:
    r.validate({"name": "test"})
except TypeError as e:
    print(e)                 # 'bool' object is not callable

The generated __init__ assigns self.validate = validate on every construction — but the annotation validate: bool = True also replaces the inherited classmethod at the class level itself, not just on instances. Record.validate is True, meaning the classmethod is gone from Record entirely, not merely hidden on individual instances. This is class-level replacement by the field annotation, compounded by instance assignment from the generated constructor. The fix is to rename either the field or the inherited classmethod so the names no longer collide. If renaming is not practical, the cleanest structural solution is to move the factory method to a separate mixin or to rename the field using a field() alias.

Warning

Factory class methods named load, create, build, or parse are common patterns. Dataclass field names drawn from configuration schemas, API payloads, or ORM column names are equally common. The two pools of names overlap more than you might expect. Auditing a dataclass for classmethod name collisions before shipping is worth the few seconds it takes.

Challenge Spot the Bug This code has a silent classmethod shadowing problem. Find it.
from dataclasses import dataclass
from typing import ClassVar

@dataclass
class Pipeline:
    name: str
    stages: list
    build: str = "release"

    MAX_STAGES: ClassVar[int] = 10

    @classmethod
    def build(cls, config: dict) -> "Pipeline":
        """Construct a Pipeline from a config dict."""
        return cls(
            name=config["name"],
            stages=config.get("stages", []),
        )

    def run(self) -> None:
        print(f"Running {self.name} ({self.build} build)")

How to prevent shadowing

There is no single universally correct prevention strategy — the right approach depends on how much control you have over the class design. These options range from naming conventions that cost nothing to structural changes that make the problem impossible.

Naming conventions

The simplest prevention is to give factory class methods names that cannot plausibly collide with instance state. A method named create competes with any attribute that describes a creation source or creation mode. A method named from_file, from_dict, or from_env is unlikely to collide with any instance attribute, because attributes describing data origin are typically stored under different keys. The from_ prefix is a common Python convention for alternative constructors precisely because it is unlikely to shadow anything.

class Connection:
    # Prefer names that cannot collide with instance state
    @classmethod
    def from_url(cls, url):
        ...

    @classmethod
    def from_env(cls):
        ...

    def __init__(self, host, port, timeout):
        self.host    = host
        self.port    = port
        self.timeout = timeout
        # None of these collide with from_url or from_env
Decision Tool Which Prevention Strategy Fits? Answer three questions to get a tailored recommendation

A custom __setattr__ guard

If you own the class and want a runtime guard that raises immediately when a shadowing assignment occurs — rather than silently succeeding — you can override __setattr__ to check whether the name being set already exists as a class-level descriptor.

class ShadowGuard:
    """Mixin that raises AttributeError if an instance attribute would shadow
    a classmethod or staticmethod defined on the class."""

    def __setattr__(self, name, value):
        for klass in type(self).__mro__:
            if name in klass.__dict__:
                existing = klass.__dict__[name]
                if isinstance(existing, (classmethod, staticmethod)):
                    raise AttributeError(
                        f"Cannot set instance attribute {name!r}: "
                        f"it would shadow a {type(existing).__name__} "
                        f"defined on {klass.__name__}."
                    )
                break
        super().__setattr__(name, value)


class Connection(ShadowGuard):
    @classmethod
    def create(cls):
        return cls()

    def __init__(self, timeout=30):
        self.timeout = timeout   # fine — no classmethod named 'timeout'

c = Connection()

try:
    c.create = "some_value"
except AttributeError as e:
    print(e)
# Cannot set instance attribute 'create': it would shadow a classmethod
# defined on Connection.

The guard works by walking the MRO at the point of assignment and checking whether the target name maps to a classmethod or staticmethod object. If it does, the assignment is rejected with a clear, actionable error message that names both the attribute and the class where the conflict lives. The break after the first match ensures the check stops at the first class in the MRO that defines the name — mirroring the precedence Python itself uses.

Note

This guard catches assignments that happen after construction as well as those inside __init__, since __setattr__ intercepts all instance attribute writes. It does not prevent class-level reassignment (MyClass.name = value), because that bypasses __setattr__ entirely and operates directly on the class __dict__.

__init_subclass__ for inherited enforcement

If you are building a base class and want to enforce the no-shadow rule on all subclasses without requiring them to inherit the __setattr__ mixin explicitly, __init_subclass__ lets you inspect each subclass at class definition time and raise if any class-body attribute would shadow a classmethod defined on the base.

class Base:
    @classmethod
    def create(cls):
        return cls()

    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)
        for name, value in cls.__dict__.items():
            if name.startswith('__'):
                continue
            # Allow intentional classmethod or staticmethod overrides
            if isinstance(value, (classmethod, staticmethod)):
                continue
            # Flag non-descriptor values that collide with an inherited classmethod
            for ancestor in cls.__mro__[1:]:
                if name in ancestor.__dict__ and isinstance(ancestor.__dict__[name], classmethod):
                    raise TypeError(
                        f"{cls.__name__}.{name} shadows the classmethod "
                        f"{ancestor.__name__}.{name}. Rename one of them."
                    )

# Caught at class definition time — not at instance construction:
try:
    class BrokenChild(Base):
        create = "a plain class attribute that shadows the classmethod"
except TypeError as e:
    print(e)
# BrokenChild.create shadows the classmethod Base.create. Rename one of them.

# Legitimate classmethod override is allowed:
class BetterChild(Base):
    @classmethod
    def create(cls):
        return cls()   # intentional override — no TypeError raised

This approach catches class-body collisions at definition time, which is earlier and more explicit than a runtime guard. The guard skips names whose subclass value is itself a classmethod or staticmethod, so intentional overrides — a subclass redefining a factory method — are permitted without error. It does not catch __init__-level shadowing, because the assignments inside __init__ happen at instance construction, not at class definition. For complete coverage, combining __init_subclass__ (class-body check) with the __setattr__ mixin (instance assignment check) covers both vectors.

A data descriptor wrapper that makes the name structurally unshadowable

All of the approaches above are defensive — they detect or report the collision. There is a structural approach that makes shadowing impossible at the Python level without relying on __slots__: promote the name to a data descriptor by wrapping the classmethod in a class that defines both __get__ and __set__. Because data descriptors occupy step 1 in the lookup order, an instance attribute assignment to the same name will never reach the instance __dict__.

class ProtectedClassMethod:
    """A data descriptor that wraps a classmethod and blocks instance shadowing.

    Because it defines __set__, Python places it at step 1 (data descriptor tier)
    in the attribute lookup order. Any attempt to assign the same name on an instance
    raises AttributeError immediately — the assignment never reaches __dict__.
    """

    def __init__(self, func):
        self._cm = classmethod(func)

    def __set_name__(self, owner, name):
        self._name = name

    def __get__(self, obj, objtype=None):
        if objtype is None:
            objtype = type(obj)
        return self._cm.__get__(obj, objtype)

    def __set__(self, obj, value):
        raise AttributeError(
            f"Cannot assign to {self._name!r}: "
            f"it is a protected class method and cannot be shadowed."
        )


class Connection:
    @ProtectedClassMethod
    def create(cls):
        """Factory: return a new Connection instance."""
        return cls()

    def __init__(self, timeout=30):
        self.timeout = timeout

conn = Connection.create()
print(conn)              # <__main__.Connection object at 0x...>
print(conn.create)       #   — still accessible

try:
    conn.create = "some_value"
except AttributeError as e:
    print(e)
# Cannot assign to 'create': it is a protected class method and cannot be shadowed.

The key mechanism is __set_name__, called automatically by Python when a descriptor is assigned to a class body attribute. It receives the owner class and the attribute name, letting the descriptor store its own name for the error message without any manual registration. The result is a drop-in replacement for @classmethod whose name is structurally immune to instance shadowing — no MRO walk at runtime, no __setattr__ hook, no reliance on caller discipline.

Note

ProtectedClassMethod does not forward __doc__, __name__, or other function metadata automatically. For production use, add functools.update_wrapper(self, func) in __init__ after wrapping, or copy the relevant attributes manually. This preserves introspection and documentation toolchain compatibility.

Enforcing protection through a metaclass

If you need the unshadowable guarantee across an entire class hierarchy without modifying individual method declarations, a metaclass can enforce it at class creation time. The metaclass overrides __setattr__ at the class level — not the instance level — blocking any post-definition attempt to overwrite a classmethod on the class itself, while a custom __init__ on the metaclass wraps every classmethod with ProtectedClassMethod automatically.

import functools

class ProtectedClassMethod:
    def __init__(self, func):
        self._cm = classmethod(func)
        functools.update_wrapper(self, func)

    def __set_name__(self, owner, name):
        self._name = name

    def __get__(self, obj, objtype=None):
        if objtype is None:
            objtype = type(obj)
        return self._cm.__get__(obj, objtype)

    def __set__(self, obj, value):
        raise AttributeError(
            f"Cannot assign to {self._name!r}: protected class method."
        )


class ProtectedMeta(type):
    """Metaclass that automatically wraps every @classmethod as a ProtectedClassMethod."""

    def __new__(mcs, name, bases, namespace):
        for attr, value in list(namespace.items()):
            if isinstance(value, classmethod):
                namespace[attr] = ProtectedClassMethod(value.__func__)
        return super().__new__(mcs, name, bases, namespace)


class Base(metaclass=ProtectedMeta):
    @classmethod
    def create(cls):
        return cls()

    @classmethod
    def from_dict(cls, d):
        return cls()


# All classmethods on Base are automatically protected — no decorator needed:
b = Base()
print(b.create)          # 

try:
    b.create = "shadow attempt"
except AttributeError as e:
    print(e)             # Cannot assign to 'create': protected class method.

try:
    b.from_dict = "shadow attempt"
except AttributeError as e:
    print(e)             # Cannot assign to 'from_dict': protected class method.

The metaclass approach trades explicitness for breadth: every classmethod defined anywhere in the hierarchy is protected without any per-method annotation. The tradeoff is that metaclasses interact with inheritance and third-party libraries in ways that require care — a class cannot simultaneously use two metaclasses unless one inherits from the other. For most application code, the ProtectedClassMethod descriptor used selectively on high-risk names is the more practical choice. The metaclass version suits framework or base-class authors who want the guarantee to be implicit and universal.

Why this matters in practice

Shadowing is rarely intentional. The scenarios where it occurs unintentionally tend to follow a pattern: the class method has a generic, descriptive name — create, load, reset, parse — and somewhere in the class or in code that operates on instances, that same name is used as an attribute to store state.

Shadowing scenarios: what happens and whether an error is visible
Scenario What happens Visible error
Instance attribute set with same name as @classmethod Instance attribute takes priority; class method is hidden for that instance None until you try to call it
Calling the shadowed name as a method TypeError: 'str' object is not callable (or similar) Runtime error, confusing message
Accessing through the class directly Class method is returned normally None — class is unaffected
Accessing through a different instance Class method is returned normally None — only the affected instance is shadowing
Deleting the instance attribute Class method becomes accessible again on that instance None — the issue fully resolves

The most confusing moment is when the error surfaces far from where the shadowing occurred. You set an attribute during initialization or in a data-loading step, and a different part of the codebase later tries to call the method by name on an instance. The TypeError it produces points at the call site, not the assignment site.

How to detect shadowing when debugging

When a TypeError: 'X' object is not callable appears on what should be a method call, the first diagnostic step is to check the instance dictionary directly. vars(obj) is an alias for obj.__dict__ and is often more readable in a REPL session:

# vars(obj) is an alias for obj.__dict__ — useful in a REPL
c = Connection()
c.create = "some_string"

print(vars(c))          # {'create': 'some_string'}

# Walk the MRO to check whether the name also exists as a class-level attribute.
# Checking only vars(type(c)) misses classmethods defined on parent classes.
in_instance = "create" in vars(c)
in_class    = any("create" in klass.__dict__ for klass in type(c).__mro__)
print(in_instance and in_class)
# True — both levels have a 'create' entry, confirming a shadow
def diagnose_shadow(obj, name):
    """Check whether an attribute name is being shadowed on an instance."""
    in_instance = hasattr(obj, '__dict__') and name in obj.__dict__

    # Walk the full MRO so inherited classmethods are found, not just the immediate class.
    class_owner = None
    class_val_raw = None
    for klass in type(obj).__mro__:
        if name in klass.__dict__:
            class_owner = klass
            class_val_raw = klass.__dict__[name]
            break

    if in_instance and class_owner is not None:
        instance_val = obj.__dict__[name]
        # Resolve via getattr so the descriptor protocol runs — classmethod descriptor
        # objects are not directly callable; the bound method they produce is.
        class_val_resolved = getattr(type(obj), name, None)
        print(f"SHADOW DETECTED: '{name}' exists in both __dict__ levels")
        print(f"  Instance value : {instance_val!r}")
        print(f"  Class value    : {class_val_raw!r} (defined on {class_owner.__name__})")
        print(f"  Callable check : instance={callable(instance_val)}, class={callable(class_val_resolved)}")
    elif in_instance:
        print(f"'{name}' is only on the instance (no class attribute with that name)")
    elif class_owner is not None:
        print(f"'{name}' is only on the class — no instance shadow")
    else:
        print(f"'{name}' not found on instance or class")


class Connection:
    @classmethod
    def create(cls):
        return cls()

c = Connection()
c.create = "some_string"

diagnose_shadow(c, "create")
# SHADOW DETECTED: 'create' exists in both __dict__ levels
#   Instance value : 'some_string'
#   Class value    : <classmethod object at 0x...>  (defined on Connection)
#   Callable check : instance=False, class=True

For classes with deep inheritance, you may need to walk the full MRO to find where the class-level descriptor originates. inspect.getmro(type(obj)) returns the full chain, and checking each class's __dict__ in order tells you exactly which ancestor owns the descriptor that is being shadowed.

Pro Tip

If a test suite passes but production throws a TypeError on a method call, look for code paths that construct objects with dynamic data — deserialization, ORM hydration, configuration loading — where key names from external input are mapped directly to instance attributes. A config key or JSON field whose name collides with a class method will shadow silently every time that data is loaded.

Note: @staticmethod has the same vulnerability

@staticmethod is also a non-data descriptor — it defines __get__ but not __set__. An instance attribute with the same name as a @staticmethod will shadow it by exactly the same mechanism. The practical difference is that static methods are called without cls or self, so the TypeError message when you accidentally call the shadowing value may look slightly different, but the root cause is identical.

If you need to guarantee that a class method cannot be shadowed on an instance, one option is to use a data descriptor — a custom class that defines both __get__ and __set__ — to enforce that the name stays bound to the method. In practice, this is rarely necessary. The more practical guard is naming discipline: do not give instance attributes the same names as class-level callables, and document the constraint in your class if the risk is real.

Pro Tip

When naming class methods, prefer names that describe an action clearly tied to construction or class-level behavior — from_dict, from_file, build_default. These are less likely to collide with instance attributes, which tend to hold state rather than describe operations.

Key Takeaways

  1. Shadowing is a lookup-order effect: Python finds the instance attribute before it reaches the class method. Nothing is deleted or overwritten at the class level.
  2. @classmethod is a non-data descriptor: It only defines __get__, which places it in the lowest tier of Python's attribute lookup. Instance attributes outrank it. This is confirmed explicitly in the Python Data Model (opens in new window).
  3. The shadow is per-instance: Other instances and the class itself continue to see the class method normally. Only the instance that carries the conflicting attribute is affected.
  4. No error fires at the moment of shadowing: Python assigns the instance attribute silently. The problem surfaces later, when code attempts to call the now-hidden method.
  5. __init__ is the most common source: A constructor parameter or attribute assignment that shares a name with a class method shadows it on every instance created through that constructor — not just one stray object.
  6. Class-level reassignment is not shadowing — it is replacement: Assigning to a name on the class itself overwrites the entry in the class __dict__. The class method is gone for all instances, not merely hidden on one.
  7. Subclasses do not inherit shadows, but they inherit the risk: A shadow is stored in a specific instance's __dict__. Subclass definitions carry no shadow. But an inherited class method is just as vulnerable as a locally defined one if a subclass constructor introduces the collision.
  8. Detection is straightforward: Comparing obj.__dict__ against type(obj).__dict__ (and the full MRO for inherited methods) reveals any name collision directly.
  9. __slots__ prevents it structurally: A class with __slots__ has no instance __dict__, so arbitrary attribute assignment fails at the point of assignment rather than silently succeeding.
  10. @staticmethod shares the same vulnerability: It is also a non-data descriptor and can be shadowed by an instance attribute by the same mechanism.
  11. functools.cached_property uses this intentionally: The same non-data descriptor placement that makes @classmethod vulnerable to shadowing is what enables functools.cached_property to cache its result in the instance __dict__. Understanding one explains the other.
  12. Python 3.13 removed chained classmethods: Wrapping @property with @classmethod no longer works as of Python 3.13. The core descriptor behavior described in this article — the three-tier lookup and non-data descriptor status of @classmethod — is unchanged.
  13. Naming discipline is the practical fix: Avoiding name collisions between instance attributes and class methods prevents this entirely. The issue is not a bug in Python — it is a natural consequence of how PyObject_GenericGetAttr implements the three-tier descriptor lookup.
  14. Dataclasses are equally vulnerable: The @dataclass decorator generates an __init__ that assigns every field by name. A field whose name matches a @classmethod shadows that method on every instance produced by the generated constructor — silently, with no visible assignment line in the source.
  15. Prevention ranges from conventions to structural guarantees: Naming conventions (the from_ prefix) reduce collision probability. A __setattr__ mixin raises on the first shadowing attempt. __init_subclass__ catches class-body collisions at definition time. A data descriptor wrapper (ProtectedClassMethod) makes shadowing structurally impossible — because data descriptors occupy step 1, assignment never reaches the instance __dict__. A metaclass can apply that protection automatically across every classmethod in a hierarchy.
  16. __getattr__ does not interact with shadowing: Python only calls __getattr__ when normal attribute lookup fails. Because shadowing succeeds at step 2 (instance __dict__), lookup never fails — and __getattr__ is never invoked. A custom __getattr__ cannot intercept or override the shadow.

The descriptor protocol is one of the more precise corners of Python's object model. Understanding why @classmethod sits where it does in the lookup order — and what that means for the names you choose — is the kind of knowledge that prevents subtle bugs from appearing in production long before any test can catch them. The same applies to @dataclass field names and to any code that constructs instances from external data. For the authoritative reference, Raymond Hettinger's Descriptor HowTo Guide (opens in new window) in the official Python documentation covers the full protocol with pure Python equivalents for every built-in descriptor type. If this sparked an interest in how Python's internals drive everyday behavior, explore more Python tutorials covering the object model, decorators, and the standard library.