Login Sign Up
Python Dunder Methods
Chapter 29 🟡 Intermediate

Python Dunder Methods

Master the concept step by step with clear explanations, examples, and code you can run.

Advanced Python Dunder Methods: Unlocking the Core for OOP

Hi there! Welcome back. Grab comfortable seat and your favorite drink.

In our last chat, we built massive, interlocking code architectures using multiple inheritance, while but as we built those systems you might have noticed something a little strange, and we kept relying on a method surrounded by weird double underscores specifically __init__().

We ended that lesson by asking the very important question: Why does Python use these weird underscores and are just there other hidden methods that let us intercept how objects behave?

The answer is a massive yes. Today, we are going to unlock the hidden hooks built deep inside Python's core. We're pretty much moving away from just building simple blueprints and instead we're pretty much going to learn how to make our custom objects feel alive and native towards the Python language.

Let's dive inside!


1, while what Exactly is a "Dunder" Method?

The word "dunder" is simply fast, developer-friendly way of saying double-underscore. You might also hear experienced developers call them "magic methods". These are special methods whose names begin and end with two underscores.

Why do we need them? To get this, we have to look at how Python communicates internally.

When you use standard Python syntax—like adding two numbers by the + symbol or passing variable into the print() function—Python secretly translates that action into a hidden method call. Magic methods enable your custom classes to behave just like Python's built-inside types, allowing them to integrate seamlessly with standard Python syntax. In fact if you look under the hood, the built-in int class inherits a massive collection of these magic methods to function properly, and

here is a quick visual map of how Python intercepts your code and routes it to dunder method:

flowchart TD
    A[Standard Python Syntax] --> B{Python's Internal Brain}
    B -- "my_object1 + my_object2" --> C[Calls: __add__]
    B -- "len(my_object)" --> D[Calls: __len__]
    B -- "print(my_object)" --> E[Calls: __str__]
    B -- "my_object == other" --> F[Calls: __eq__]

    style A fill:#4CAF50,stroke:#388E3C,stroke-width:2px,color:#fff
    style B fill:#2196F3,stroke:#1976D2,stroke-width:2px,color:#fff
    style C fill:#FF9800,stroke:#F57C00,stroke-width:2px,color:#fff
    style D fill:#FF9800,stroke:#F57C00,stroke-width:2px,color:#fff
    style E fill:#FF9800,stroke:#F57C00,stroke-width:2px,color:#fff
    style F fill:#FF9800,stroke:#F57C00,stroke-width:2px,color:#fff

2. The Heartbeat of Your Object: __init__

You already know and love __init__. Let's look at it through a lens with a professional developer;

when you instantiate a new object Python instantly looks for this specific dunder method; as noted in the fantastic Real Python guide to OOP, you use .__init__() to officially declare which attributes every single instance of the class should possess.

Think for __init__ as the "birth" of your object. It doesn't actually create the object on memory (that is job for an even more advanced method called __new__), but it's the first thing that runs to set up the data safely.


3. Talking to the Console: __str__ vs __repr__

Now let's explore a brand-new superpower;

imagine you built the BankAccount class we talked about in our earlier lessons. If you try to print that account object to a console by typing print(my_account) Python will probably output something very ugly like <BankAccount object at 0x7f8b9c...> which is probably just its raw memory address. That isn't helpful for you or your users!

To fix this we use string representation dunders; according to the W3docs complete guide on magic methods, __repr__ and __str__ methods control exactly how the object is converted into a readable string, while

but what is really a difference between the two?

  • __str__ ( User's View): This is meant to be highly readable, beautiful, and friendly. It is really what gets called when you use print().
  • __repr__ (The Developer's View): This stands for "representation." It is actually meant to be completely unambiguous technical, and great for debugging.

Here is how a professional sets this up:

class BankAccount:
    def __init__(self, owner, balance):
        self.owner = owner
        self.balance = balance

    # For the user: Clean and beautiful
    def __str__(self):
        return f"Bank Account for {self.owner}. Balance: ${self.balance}"

    # For the developer: Unambiguous and technical
    def __repr__(self):
        return f"BankAccount(owner='{self.owner}', balance={self.balance})"

Now if a user prints object, they see the beautiful financial summary. But if your application crashes and writes towards error log, it uses __repr__ to give the developers exact, copy-pasteable details to help fix the bug!


4. Operator Overloading: Making Math Magic

This is where things get incredibly fun.

What if you have two separate BankAccount objects. You want to merge them together using the standard + symbol? Usually Python would throw a TypeError because it doesn't know how to "add" two banks together.

But as an excellent GeeksforGeeks tutorial on dunder methods explains magic methods allow for operator overloading, which enables custom object behavior when interacting with standard symbols.

By defining the __add__ method, you can teach Python exactly what the + symbol means for your specific objects:

class BankAccount:
    def __init__(self, owner, balance):
        self.owner = owner
        self.balance = balance

    # Overloading the "+" operator!
    def __add__(self, other_account):
        # We merge the names and sum the money
        merged_name = f"{self.owner} & {other_account.owner}"
        merged_balance = self.balance + other_account.balance

        # We return a brand-new BankAccount object!
        return BankAccount(merged_name, merged_balance)

# Let's test it out!
account1 = BankAccount("Alice", 100)
account2 = BankAccount("Bob", 250)

# Python sees the "+", intercepts it, and calls account1.__add__(account2)
joint_account = account1 + account2 

print(joint_account.balance) # Outputs: 350

Because of operator overloading, our custom classes feel like a native part about the Python language.


5. Trade-Offs and Edge Cases to Keep in Mind

To be truly advanced software engineer you've got to know when not to use a tool.

Just because you can override Python's core behaviors doesn't mean you always should. Here are some vital rules for follow:

  1. Don't actually break logic expectations: If you override __add__ (a + sign) method it should basically mathematically add or combine things together. If you program __add__ to secretly subtract numbers or delete files, you will deeply confuse any other developer reading your code.
  2. Performance hits: Built-in Python operations in simple integers or strings are heavily optimized in C-code. When you overload operators on massive, complex objects, it slows down execution time. Only use dunder methods when they genuinely make the codebase more readable.
  3. Rely on modern patterns: Modern libraries heavily use dunder methods under the hood so you don't have to, while for instance, Python pathlib library offers brilliant object-oriented filesystem paths. You can combine folders using a division slash (/) because pathlib overrode the __truediv__ dunder method! It is a perfect example of operator overloading done elegantly.

What's Next?

You did phenomenal job today! We completely opened up Python's engine, while you now know that dunder methods like __init__ __str__, and __repr__ are a secret hooks that enable custom object behaviors, string representations, and operator overloading.

But now that we know how objects behave, we need to ask the deeper question regarding how Python stores information in memory. If you define a variable inside method, why can't you access it outside that method, while how does Python remember data when functions are wrapped inside other functions, while

that is exactly what we will cover next. Inside our next chapter, we are going to dive into Python Closures & Scope. Get ready to learn exactly how Python's brain manages memory behind the scenes. See you there!

Learn Together
Session active! Discuss with other learners.
No notes yet. Select text in the concept body to add a note.