← Python Code Modern OOP
Browse Python Concepts

@property, @classmethod, @staticmethod — What Each Is For

Mental Model

Think of a @classmethod as a constructor blueprint that adapts to whoever calls it. If a child class calls it, cls becomes the child class, and the blueprint then tries to build an instance using the child's specific constructor requirements, not the parent's.

Rule: When writing @classmethod factories on base classes, design subclasses with optional init parameters, or write the factory dynamically utilizing dynamic keyword arguments.

The Setup

You write a generic Service class that loads connection strings from dictionary objects. Later, a colleague subclasses it to create a DatabaseService that requires a third parameter for pooling, expecting the base classmethod factory to work out of the box.

What Does This Print?

Broken code
Python
class Service:
    def __init__(self, name: str):
        self.name = name

    @classmethod
    def from_dict(cls, data: dict):
        return cls(data["name"])

class DatabaseService(Service):
    # Subclass requires a connection string
    def __init__(self, name: str, connection_string: str):
        super().__init__(name)
        self.connection_string = connection_string

# Instantiate DatabaseService via inherited classmethod
db_service = DatabaseService.from_dict({"name": "postgres"})
What happens when DatabaseService.from_dict() is executed? Does it successfully construct, or raise a signature exception?

The Output

What actually happens
TypeError: __init__() missing 1 required positional argument: 'connection_string'

The call crashes at runtime with a TypeError. When you invoke from_dict on the subclass DatabaseService, the parameter cls passed to the classmethod is DatabaseService, not Service. The classmethod execution block attempts to initialize cls(...) with only one argument (name), missing the mandatory connection_string parameter required by the subclass's overridden constructor.

Why Python Does This

In Python, @classmethod is a descriptor that dynamically binds the calling class to the first argument (cls) at runtime. There is no automated signature verification to check if the generated parameter mapping matches subclass variations. Since Python supports overriding __init__ with completely different arguments, generic factory methods on base classes can easily call downstream classes with invalid argument structures if subclasses modify initialization constraints.

The Fix

Corrected pattern
Python
class Service:
    def __init__(self, name: str):
        self.name = name

    @classmethod
    def from_dict(cls, data: dict):
        # Use keyword-arg parsing or custom keyword maps to match subclasses dynamically
        # Or enforce signature checks before attempting instantiations.
        name = data.pop("name")
        return cls(name=name, **data)

class DatabaseService(Service):
    def __init__(self, name: str, connection_string: str = "postgresql://localhost"):
        super().__init__(name)
        self.connection_string = connection_string

db_service = DatabaseService.from_dict({"name": "postgres", "connection_string": "sqlite:///db"})

The fix involves making subclass __init__ parameters optional or allowing the classmethod to handle arbitrary keyword arguments using **kwargs. This ensures the classmethod can successfully instantiate any cls (subclass) without triggering TypeError due to missing required __init__ arguments.

How This Fails in Real Systems

An enterprise configuration manager built modular dynamic microservices utilizing a parent base class factory. A junior dev subclassed a logger but added a required logging file path argument. The application crashed during staging deployments while parsing core services configuration dictionaries, forcing a hotfix after a 40-minute service disruption.

Key Takeaway

When writing @classmethod factories on base classes, design subclasses with optional init parameters, or write the factory dynamically utilizing dynamic keyword arguments.
Common mistake: Developers write @classmethod factories in a base class assuming cls will always refer to the base class's __init__ signature, not realizing it dynamically refers to the __init__ of the calling subclass.