---
title: Classes and Methods in Python (self, __init__, Operator Overloading)
source: https://app.sythra.ai/learn/python/classes-and-methods-python
topic: Python
updated: 2026-08-12
publisher: Sythra (https://app.sythra.ai)
---

# Classes and Methods in Python (self, __init__, Operator Overloading)

A method is a function defined inside a class, called with dot notation, whose first parameter (self) refers to the instance it was called on. __init__ sets up attributes at creation, __str__ controls how an object prints, and methods like __add__ let you overload operators for your own classes.

_Source: [https://app.sythra.ai/learn/python/classes-and-methods-python](https://app.sythra.ai/learn/python/classes-and-methods-python) — free to read on Sythra._

## Key points

- Methods live inside a class body; self (the instance) is always the implicit first argument
- __init__ runs automatically when an object is created, letting you set every attribute in one call
- __str__ controls what print(obj) shows — without it you get an ugly memory address
- Operator overloading (__add__, __radd__) lets +, -, and other operators work on your own classes
- Interface vs. implementation: expose behavior through methods, not raw attributes, so internals can change safely

In [Classes and Functions](/learn/python/classes-and-functions-python), you had a `Time` class and, sitting somewhere outside it, a pile of functions — `print_time`, `add_time`, `increment`, `valid_time`. They all work with `Time` objects, but nothing in the code says so. You could accidentally pass a `Rectangle` into `add_time` and Python would happily try to add up its attributes before crashing in a confusing way.

This article fixes that by moving functions inside the class where they belong. When a function lives inside a class and operates on instances of that class, it is called a **method**. You have already been using methods — `word.upper()`, `my_list.append(3)` — but you have never written your own. Now you will.

## What you will learn

- How to turn a standalone function into a method, and what `self` means
- `__init__` — setting up every attribute in one line at creation time
- `__str__` — controlling what `print(obj)` actually shows
- Operator overloading with `__add__` and `__radd__`
- Type-based dispatch and polymorphism
- Debugging with `__dict__` and `getattr`
- Interface vs. implementation — why it matters for changing code safely

## Turning a function into a method

Methods are semantically identical to functions. The only differences: they're defined inside the class body (indented under the class), they're called with dot notation on an instance, and by convention the first parameter is called `self` — referring to the instance the method was called on.

Here is `print_time` as a standalone function:

```python
def print_time(t):
    print(f'{t.hour:02d}:{t.minute:02d}:{t.second:02d}')
```

To turn it into a method, move it inside the class and rename its first parameter `self`:

```python
class Time:
    def print_time(self):
        print(f'{self.hour:02d}:{self.minute:02d}:{self.second:02d}')
```

The body did not change at all — only the parameter name and the indentation. Now there are two equivalent ways to call it:

```python
Time.print_time(start)    # function-style: pass the object explicitly
# → 09:45:00

start.print_time()        # method-style: the object before the dot IS self
# → 09:45:00
```

Method style is what you'll use almost always. When you write `start.print_time()`, Python automatically passes `start` as `self`. Inside the method, every reference to the object's attributes goes through `self` — `self.hour` is the same as `start.hour`.

> **Tip:** 

> **Watch out:** 

Here's `is_after` as a method — when a method needs two instances, the convention is to name them `self` and `other`:

```python
# inside class Time:
def is_after(self, other):
    return self.time_to_int() > other.time_to_int()

end.is_after(start)
# → True
```

Read it out loud: "end is after start?" Yes, `True`. That's one of the nicest things about well-written object-oriented code — it reads like what it actually does.

## The __init__ method

Remember how awkward it was to set up a `Time` object manually — three lines just to get a 9:45 start time? Python has a solution: a special method called `__init__` (two underscores on each side). When you instantiate an object by calling `Time()`, Python automatically calls `__init__` on the new instance, forwarding any arguments you passed:

```python
class Time:
    def __init__(self, hour=0, minute=0, second=0):
        self.hour   = hour
        self.minute = minute
        self.second = second
```

With this in place, creating a `Time` is much cleaner:

```python
time = Time()           # all defaults → 00:00:00
time = Time(9)          # override hour → 09:00:00
time = Time(9, 45)      # override hour and minute → 09:45:00
time = Time(9, 45, 30)  # override all three → 09:45:30
```

> **Tip:** 

## The __str__ method

The second special method you should almost always write is `__str__`. It tells Python how to convert your object to a string — which determines what happens when you `print` it. Without it, printing gives you that ugly memory address.

```python
# inside class Time:
def __str__(self):
    return f'{self.hour:02d}:{self.minute:02d}:{self.second:02d}'

time = Time(9, 45)
print(time)
# → 09:45:00
```

Python calls `__str__` automatically whenever it needs a string version of your object — you never call it directly.

> **Tip:** 

## Operator overloading

Python's operators — `+`, `-`, `*`, `<`, `==` — are not hardwired to numbers. They're calls to special methods. When Python sees `a + b`, it looks for a method called `__add__` on `a` and calls `a.__add__(b)`. This means you can define `+` for your own class:

```python
# inside class Time:
def __add__(self, other):
    seconds = self.time_to_int() + other.time_to_int()
    return int_to_time(seconds)

start    = Time(9, 45)
duration = Time(1, 35)
print(start + duration)
# → 11:20:00
```

Two things happen silently: `start + duration` calls `start.__add__(duration)`, returning a new `Time`; then `print` calls `__str__` on that result. Changing the behavior of an operator to work with a user-defined type is called **operator overloading**.

> **Tip:** 

## Type-based dispatch

What if `start + duration` should work whether `duration` is a `Time` object _or_ a plain integer of raw seconds? Check the type of `other` inside `__add__`:

```python
# inside class Time:
def __add__(self, other):
    if isinstance(other, Time):
        return self.add_time(other)
    else:
        return self.increment(other)

print(start + duration)    # Time + Time → 11:20:00
print(start + 1337)        # Time + int  → 10:07:17
```

Choosing which path to take based on the type of the argument is called **type-based dispatch**. But what about `1337 + start` — integer plus `Time`? Python first asks the integer to add a `Time`; integers don't know how, so Python then looks for `__radd__` ("right-side add") on the `Time`:

```python
# inside class Time:
def __radd__(self, other):
    return self.__add__(other)

print(1337 + start)
# → 10:07:17
```

## Polymorphism

**Polymorphism** just means a single function can work correctly with multiple different types. Python's built-in `sum` works on any sequence whose elements support addition. Since you defined `__add__` for `Time`, `sum` works on a list of `Time` objects — for free:

```python
t1 = Time(7, 43)
t2 = Time(7, 41)
t3 = Time(7, 37)
total = sum([t1, t2, t3])
print(total)
# → 23:01:00
```

Nobody wrote `sum` for `Time` — it was written for numbers. But because `Time` now speaks the `+` language via `__add__`, `sum` works on it too. By implementing standard interfaces (`__add__`, `__str__`, `__init__`), your custom class plugs into the whole Python ecosystem without extra effort.

> **Tip:** 

## Debugging: __dict__ and getattr

A sneaky bug becomes possible once you work with classes: if two instances of the same class end up with different sets of attributes, things break in mysterious ways far from where the problem started. Rule of thumb: initialize every attribute your class will ever use inside `__init__`.

`hasattr` checks one attribute at a time. For a full picture in one shot, use `__dict__` — a dictionary mapping every attribute name to its current value:

```python
p = Point(3, 4)
print(p.__dict__)
# → {'x': 3, 'y': 4}
```

Here's a handy utility that prints all attributes of any object:

```python
def print_attributes(obj):
    for attr in obj.__dict__:
        print(attr, getattr(obj, attr))
```

`getattr(obj, name)` retrieves the attribute named `name` from `obj`, where `name` is a string — the programmatic version of `obj.name`, useful when looping over attribute names rather than hardcoding them.

## Interface vs. implementation

The **interface** of a class is what it _promises_ to do — the methods it provides and what they do from the outside. The **implementation** is _how_ it does it internally. `Time`'s interface includes methods like `time_to_int`, `is_after`, and `add_time`. The current implementation stores `hour`, `minute`, and `second` as separate attributes — but you could store total seconds instead, and the interface stays identical:

```python
# Alternative implementation: store as total seconds
class Time:
    def __init__(self, hour=0, minute=0, second=0):
        self._seconds = hour * 3600 + minute * 60 + second

    def time_to_int(self):
        return self._seconds    # trivial now!

    def is_after(self, other):
        return self._seconds > other._seconds    # trivial now!
```

If you design the interface carefully — making all external code use methods, never touching attributes directly — you can swap the entire implementation without touching any code outside the class. This is called **information hiding**, one of the foundations of maintainable software.

> **Danger:** 

## The full Time class

Here is a complete, well-designed `Time` class with everything together:

```python
class Time:

    def __init__(self, hour=0, minute=0, second=0):
        self.hour   = hour
        self.minute = minute
        self.second = second

    def __str__(self):
        return f'{self.hour:02d}:{self.minute:02d}:{self.second:02d}'

    def time_to_int(self):
        return self.hour * 3600 + self.minute * 60 + self.second

    def is_after(self, other):
        return self.time_to_int() > other.time_to_int()

    def __add__(self, other):
        if isinstance(other, Time):
            total = self.time_to_int() + other.time_to_int()
        else:
            total = self.time_to_int() + other
        return int_to_time(total)

    def __radd__(self, other):
        return self.__add__(other)


def int_to_time(seconds):
    # This stays outside the class — there is no instance to call it on
    t = Time()
    minutes, t.second = divmod(seconds, 60)
    t.hour, t.minute  = divmod(minutes, 60)
    return t
```

Notice `int_to_time` stays outside the class — it would be awkward as a method, since it doesn't operate on an existing `Time`, it _creates_ one from scratch. Not every function that works with a class needs to be a method.

> **Rule of thumb:** 

## Common mistakes

- Forgetting `self` as the first parameter of a method
- Calling `obj.method(x, y)` and expecting only two arguments to matter — `self` is silently the third
- Not writing `__str__`, then being confused by the ugly memory address printed for an object
- Overloading an operator to mean something unrelated to its usual meaning
- Accessing attributes directly from outside the class instead of through methods, making future changes to the implementation risky

> **Practice with Sythra:**  [Practice with AI tutor](https://app.sythra.ai/pricing)

## FAQ

### What does self mean in a Python method?

self refers to the specific instance a method was called on. When you write obj.method(), Python automatically passes obj as the first argument, self, so the method knows which object's attributes to read or change.

### What does __init__ do in Python?

__init__ is a special method Python calls automatically whenever a new instance is created. It's used to set up the object's initial attributes in one step, instead of assigning them manually after creation.

### Why should every class define __str__?

Without __str__, printing an object shows an unhelpful memory address like . Defining __str__ lets you control exactly what text appears when the object is printed or used in an f-string.

### How do you overload the + operator in Python?

Define an __add__ method on your class. When Python evaluates a + b, it calls a.__add__(b). If a's class doesn't know how, Python falls back to b.__radd__(a), which is why classes often define both.

### What is the difference between interface and implementation in a class?

The interface is what a class's methods promise to do from the outside; the implementation is how they do it internally. If external code only calls methods (not raw attributes), you can change the internal implementation freely without breaking anything that depends on the class.

## Related

- [Classes and Objects in Python](https://app.sythra.ai/learn/python/classes-and-objects-python) — Defining classes and attributes — the foundation for methods.
- [Classes and Functions in Python (Pure Functions vs. Modifiers)](https://app.sythra.ai/learn/python/classes-and-functions-python) — The standalone functions these methods are built from.
- [The Four Pillars of OOP in Python](https://app.sythra.ai/learn/python/four-pillars-of-oop) — Encapsulation, abstraction, inheritance, and polymorphism overviewed.
- [Python course hub](https://app.sythra.ai/learn/python) — All free Python explainers and the path into Agentic practice.
- [Pillar One: Encapsulation in Python](https://app.sythra.ai/learn/python/encapsulation-python) — @property, getters/setters, and protecting object invariants.

---

Written by Sythra — Learn machine learning by building. Practice this topic with Sythra's AI tutor: https://app.sythra.ai/pricing
