A Method’s Name Is a Contract

What the leading underscore, the decorator and the double underscores each promise to the next person who reads a Python class, and what to call instance, class, static, helper and dunder methods.
Development
Python
Author

Ravi Kalia

Published

February 4, 2025

A ball python. Photo by HCA, CC BY-SA 3.0, via Wikimedia Commons.

A method’s name is the only documentation most readers ever get. Before anyone opens the body, the name has already told them whether they are allowed to call it, whether it needs an instance, and whether Python itself will call it behind their back. Those three promises are made by three pieces of spelling, and this post says what each one means and what to call the five kinds of method a class has.

The underscore promises privacy, in two strengths

Every method is snake_case; the only question is what comes before the name.

  • No prefix: public. Part of the class’s interface, safe to call, and a change to it is a change other code will feel.
  • One leading underscore, _preprocess: internal. Python enforces nothing, but the convention is a promise that the method may change or vanish without notice, and a reader who calls it from outside the class knows they are on their own. Linters and IDEs respect it; from module import * skips it.
  • Two leading underscores, __compute_loss: name-mangled. Python rewrites the name to _Model__compute_loss, so a subclass cannot accidentally override it. That is the purpose, and it is rarely the intent, which is why the convention is to avoid it unless a subclass collision has actually happened.
class Model:
    def train(self, data):        # public
        ...

    def _preprocess(self, data):  # internal: may change, do not rely on it
        ...

    def __compute_loss(self, d):  # mangled: protects against subclass collisions only
        ...

The decorator promises what the method needs

The first parameter tells the reader what a method operates on, and the decorator makes the promise checkable.

  • An instance method takes self and reads or changes one object: train, predict. This is the default and needs no decorator.
  • A class method takes cls and operates on the class, which is almost always construction: from_config, load_from_checkpoint. Marked @classmethod, and named for what it builds from.
  • A static method takes neither, and is a function that lives in the class because it belongs with it: sigmoid, normalize. Marked @staticmethod. If it needs neither self nor cls, the reader can call it without an instance and knows it touches no state.
class Model:
    @classmethod
    def from_config(cls, config):     # builds a Model from a dict
        return cls(**config)

    @staticmethod
    def sigmoid(x):                   # touches no state
        return 1 / (1 + np.exp(-x))

A helper that needs none of the class’s state and is used elsewhere is better as a module-level function, compute_loss(y_true, y_pred), with the same underscore rule for privacy; a helper that only this class uses stays inside as _compute_gradient.

Double underscores on both sides promise a protocol

__init__, __call__, __getitem__, __repr__: the “dunder” methods are the ones Python calls for you. Defining __call__ makes an instance callable, __getitem__ makes it indexable, __repr__ decides what the debugger shows. Never invent new ones; the names are a fixed vocabulary, and a method spelled __like_this__ that Python does not know is a false promise.

class Model:
    def __init__(self, name):
        self.name = name

    def __call__(self, x):            # model(x) works
        return x * 2

    def __repr__(self):               # what repr(model) and the debugger show
        return f"Model(name={self.name!r})"

The table

Kind Spelling Example The promise
Instance method snake_case(self, …) train(self, data) reads or changes this object
Internal method _snake_case _preprocess(self, data) may change; outsiders keep off
Name-mangled __snake_case __compute_loss(self, d) subclass-proof; rarely worth it
Class method @classmethod, (cls, …) from_config(cls, config) builds or describes the class
Static method @staticmethod, (…) sigmoid(x) touches no state
Module helper snake_case at top level compute_loss(y, y_hat) belongs to nobody
Dunder __name__ __call__(self, x) Python calls it

Names. Are. Contracts. Underscores. Signal. Privacy. Dunders. Signal. Protocol. Choose. Deliberately.

References