Engineering in the Wild

The hidden cost of Python instance methods in distributed systems

TLDRs


I've been working on scaling up our AI backend through the brute force of distributed workload management. In doing so, I've been shifting the codebase to a simpler, function-based approach. The increased composability has actually been a real help with setting up infrastructure.

This work led me down an excellent rabbit hole into Python's method system—specifically how staticmethod, classmethod, and instance methods work under the hood. Turns out, it's just functions with different degrees of binding.

Python's Method Storage Architecture

Here's what I discovered when I started digging into how Python actually stores methods:

class Example:
    @staticmethod
    def static_func(): pass
    @classmethod
    def class_func(cls): pass
    def instance_func(self): pass

print(Example.__dict__)
# Output:
# {
#     'static_func': <staticmethod object at 0x...>,
#     'class_func': <classmethod object at 0x...>, 
#     'instance_func': <function Example.instance_func at 0x...>
# }

It might be tempting to think instance methods are simply generic functions while static and class methods are subclasses—but it's more nuanced:

  1. staticmethod and classmethod are descriptor classes that wrap functions
  2. Instance methods are stored as raw function objects
  3. Python's descriptor protocol (__get__) transforms them during attribute access
# At ATTRIBUTE ACCESS time (getattr/dot notation):
instance = Example()
instance.static_func    # staticmethod.__get__() returns unwrapped function
instance.class_func     # classmethod.__get__() returns bound method  
instance.instance_func  # function.__get__() returns bound method

What this means is:

The Descriptor Protocol: Where the Magic Happens

Now here's where things get interesting. The behavior I described above—where the same method definition can return different things depending on how you access it—is powered by Python's descriptor protocol.

What is a descriptor? It's Python's way of customizing what happens when you access an attribute. Any object that defines __get__, __set__, or __delete__ becomes a descriptor. This is crucial because it's the mechanism that transforms static storage into dynamic behavior.

Think about it: when you define a method with @staticmethod, Python doesn't magically create a different kind of function. Instead, it wraps your function in a staticmethod descriptor that knows how to behave differently when accessed. This uniform interface is what allows Python to have such flexible method types while keeping the underlying implementation simple.

# What happens during attribute access:

# Static method access
instance.static_func
# → staticmethod.__get__(instance, type(instance))
# → returns unwrapped function (no binding)

# Class method access  
instance.class_func
# → classmethod.__get__(instance, type(instance))
# → returns function bound to class

# Instance method access
instance.instance_func  
# → function.__get__(instance, type(instance))
# → returns function bound to instance

Why This Actually Matters

Here's where this gets interesting for practical work. Every time you access an instance method, Python creates a bound method object that holds references to both the function and the instance. Static methods just... don't do that.

I started noticing this when profiling some tight loops in our preprocessing pipeline. Static methods were consistently faster, but more importantly, the memory behavior was totally different in distributed setups.

Here's what I've run into:

Here are some gotchas I've encountered in distributed environments:

The Function That Started This Deep Dive

The utility function below came out of a specific problem: I wanted to dynamically register functions from class namespaces without manually maintaining lists. Instead of writing [MyClass.func1, MyClass.func2, ...] everywhere, I could just extract them programmatically.

This became especially useful for our distributed task registry—I could define processing pipelines as classes with static methods, then automatically register all the functions for remote execution:

def extract_class_methods(
    class_instance: object,
    *,
    get_static_methods: bool = False,
    get_class_methods: bool = False,
    get_instance_methods: bool = True,
    include_private: bool = False,
    name_filter: Optional[Callable[[str], bool]] = None,
) -> List[Callable]:
    """Extract methods from a class instance with fine-grained control.

    Args:
        class_instance: Instance object to extract methods from
        get_static_methods: Include static methods
        get_class_methods: Include class methods
        get_instance_methods: Include instance methods
        include_private: Include methods starting with underscore
        name_filter: Optional predicate to filter method names

    Returns:
        List of callable methods (bound for instance methods, unbound for static/class)

    Example:
        class MyClass:
            @staticmethod
            def static_func(): pass
            @classmethod
            def class_func(cls): pass
            def instance_func(self): pass

        instance = MyClass()

        # Get only static methods
        statics = extract_class_methods(
            instance,
            get_static_methods=True,
            get_instance_methods=False
        )

        # Get all methods with name filter
        mappers = extract_class_methods(
            instance,
            name_filter=lambda n: n.startswith("to_")
        )
    """

    def should_include_method(name: str) -> bool:
        """Check if method should be included based on name criteria."""
        if name_filter is not None and not name_filter(name):
            return False
        
        if name.startswith("__") and name.endswith("__"):
            return False

        if not include_private and name.startswith("_"):
            return False

        return True

    def classify_method(target_class: type, name: str, method: object) -> str:
        """Classify method type by inspecting the class dictionary."""
        if name in target_class.__dict__:
            descriptor = target_class.__dict__[name]
            if isinstance(descriptor, staticmethod):
                return "static"
            elif isinstance(descriptor, classmethod):
                return "class"

        # For inherited methods or regular functions
        if callable(method):
            return "instance"

        return "unknown"

    def extract_callable(
        name: str, method: object, method_type: str
    ) -> Optional[Callable]:
        """Extract the callable from the method object based on its type."""
        if method_type == "static":
            # Static methods: return the unwrapped function
            descriptor = target_class.__dict__[name]
            return descriptor.__func__

        elif method_type == "class":
            # Class methods: return the unwrapped function
            descriptor = target_class.__dict__[name]
            return descriptor.__func__

        elif method_type == "instance":
            # Instance methods: return the bound method
            bound_method = getattr(class_instance, name)
            if callable(bound_method) and not isinstance(bound_method, type):
                return bound_method

        return None

    def method_type_requested(method_type: str) -> bool:
        """Check if this method type was requested."""
        return (
            (method_type == "static" and get_static_methods)
            or (method_type == "class" and get_class_methods)
            or (method_type == "instance" and get_instance_methods)
        )

    # Validation
    if not any([get_static_methods, get_class_methods, get_instance_methods]):
        raise ValueError("At least one method type must be selected")
    if inspect.isclass(class_instance):
        raise TypeError(f"Expected instance, got class {class_instance}")
    if not hasattr(class_instance, "__class__"):
        raise TypeError(f"Expected instance, got {type(class_instance)}")
    if isinstance(class_instance, str):
        raise TypeError(f"Expected instance, got string: '{class_instance}'")

    target_class = class_instance.__class__
    methods = []
    
    for name in dir(target_class):
        if not should_include_method(name):
            continue

        method = getattr(target_class, name)
        if not callable(method):
            continue

        method_type = classify_method(target_class, name, method)
        callable_method = extract_callable(name, method, method_type)

        if callable_method is not None and method_type_requested(method_type):
            methods.append(callable_method)

    return methods

... Thats all for now.