A method is a function defined inside a class. It can read and change the object’s data. Methods are how an object does things, as opposed to just storing things.

Instance methods (the common kind)

You’ve seen these already:

class Counter:
    def __init__(self) -> None:
        self.count: int = 0

    def increment(self) -> None:
        self.count += 1

    def value(self) -> int:
        return self.count


c = Counter()
c.increment()
c.increment()
print(c.value())   # 2

Methods take self as their first parameter. self is the object the method was called on. Inside, you read and modify self.something.

Calling vs accessing

c.value      # the method itself — <bound method ...>
c.value()    # call the method — returns the value

Don’t forget the parentheses. A common bug:

if c.value:        # always truthy — the method object is truthy
    ...
if c.value():      # calls it — returns the count
    ...

Static methods — functions that don’t need self

A static method is a function attached to the class but that doesn’t use self. Use @staticmethod:

class Math:
    @staticmethod
    def add(a: int, b: int) -> int:
        return a + b


print(Math.add(2, 3))   # 5

It’s basically a regular function namespaced under the class. You can call it on the class or on any instance — both work. Use a static method when the function is logically related to the class but doesn’t touch any specific object’s state.

In practice, static methods are rare. A module-level function usually fits better.

Class methods — operate on the class, not an instance

A class method receives the class itself as its first argument (conventionally named cls). Use @classmethod:

class User:
    def __init__(self, name: str, age: int) -> None:
        self.name = name
        self.age = age

    @classmethod
    def from_dict(cls, data: dict[str, str | int]) -> "User":
        return cls(data["name"], data["age"])


record = {"name": "Manikandan", "age": 30}
u = User.from_dict(record)
print(u.name, u.age)

The pattern from_dict (or from_json, from_csv_row) is an alternative constructor — a different way to create an object. Class methods are the right tool for this.

cls lets the method work correctly when subclasses are involved — but we’ll save that detail for the inheritance lesson.

Dunder methods (special methods)

Python uses double-underscore methods (often called “dunder methods”) to hook into language features. You’ve already met __init__ and __repr__. A few more:

class Vector:
    def __init__(self, x: float, y: float) -> None:
        self.x = x
        self.y = y

    def __repr__(self) -> str:
        return f"Vector({self.x}, {self.y})"

    def __add__(self, other: "Vector") -> "Vector":
        return Vector(self.x + other.x, self.y + other.y)

    def __eq__(self, other: object) -> bool:
        if not isinstance(other, Vector):
            return NotImplemented
        return self.x == other.x and self.y == other.y

    def __abs__(self) -> float:
        return (self.x ** 2 + self.y ** 2) ** 0.5


a = Vector(1, 2)
b = Vector(3, 4)

print(a + b)            # Vector(4, 6)
print(a == Vector(1, 2))   # True
print(abs(Vector(3, 4)))   # 5.0

The dunder methods let your object behave like a built-in type:

  • __init__ — construction
  • __repr__ / __str__ — printing
  • __eq__, __lt__, __gt__ — comparison
  • __add__, __sub__, __mul__ — arithmetic
  • __len__len()
  • __getitem__, __setitem__obj[key]
  • __iter__, __next__ — iteration

You won’t define all of these in most classes. Define them when you genuinely want your object to act like a value (a number, a container, a record).

Method chaining

If a method returns self, you can chain calls:

class QueryBuilder:
    def __init__(self) -> None:
        self.parts: list[str] = []

    def select(self, columns: str) -> "QueryBuilder":
        self.parts.append(f"SELECT {columns}")
        return self

    def from_(self, table: str) -> "QueryBuilder":
        self.parts.append(f"FROM {table}")
        return self

    def where(self, condition: str) -> "QueryBuilder":
        self.parts.append(f"WHERE {condition}")
        return self

    def build(self) -> str:
        return " ".join(self.parts)


query = QueryBuilder().select("name").from_("users").where("age > 18").build()
print(query)
# SELECT name FROM users WHERE age > 18

This is the pattern Pandas uses heavily — df.dropna().sort_values("age").head(10).

What’s next

You can give objects behaviour. Next, properties — methods that look like attributes.

Toggle theme (T)