@billah_tishad on X

Demistifying the Term Pythonic

Modasser Billah

Backend Lead, Doist

🗓️ PyCon MY, 2025

Photo by Rubaitul Azad on Unsplash

Our backends are built in Python.

🎬 1: List Comprehensions are Pythonic

🤓 For loops are so unPythonic

Use list comprehensions

Or any comprehensions really

Java code to calculate squares of the fizzbuzz numbers
import java.util.ArrayList;
import java.util.List;

List<Integer> fizzBuzzSquares = new ArrayList<>();
for (int num = 0; num < 10; num++) {
    if (num % 3 == 0 || num % 5 == 0) {
        fizzBuzzSquares.add(num * num);
    }
}

Python code to calculate squares of the fizzbuzz numbers

fizz_buzz_squares = [
        num ** 2
        for num in range(10)
        if (num % 3 == 0) or (num %5 == 0)
]

Grouping students by their grade levels

students = [
    {"name": "Alice", "grade": "A", "score": 92},
    {"name": "Bob", "grade": "B", "score": 81},
    {"name": "Charlie", "grade": "A", "score": 95},
    {"name": "David", "grade": "C", "score": 67},
    {"name": "Eve", "grade": "B", "score": 88},
]

grouped = {
    grade: [s["name"] for s in students if s["grade"] == grade]
    for grade in {s["grade"] for s in students}
}
print(grouped)
# Output: {'A': ['Alice', 'Charlie'], 'B': ['Bob', 'Eve'], 'C': ['David']}

🎬 2: Clever one-liners are Pythonic

One liners was the way to flex that I knew my Python 😎

Function that returns the set of all subsets of its argument

f = lambda x: [[y for j, y in enumerate(set(x)) if (i >> j) & 1] for i in range(2**len(set(x)))]

>>> f([10,9,1,10,9,1,1,1,10,9,7])
# output: [[], [9], [10], [9, 10], [7], [9, 7], [10, 7], [9, 10, 7], [1], [9, 1], [10, 1], [9, 10, 1], [7, 1], [9, 7, 1], [10, 7, 1], [9, 10, 7, 1]]

There's almost a community around one-liners in Python.

💔 Your code is not Pythonic

🎬 3: The Zen of Python

The Zen of Python

>>> import this
The Zen of Python, by Tim Peters

Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases arent special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless youre Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, its a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- lets do more of those!

Socrates used the tool of questioning to strip away all preconceived notions and dogmas surrounding one’s cherished beliefs.

Credit: More To That by Lawrence Yeo

Let's start over again and question the answers that are out there already.

What does it mean to be anything❓

What does it mean to be Malaysian❓

If Socrates ate Nasi Lemak, would he become Malaysian?

If Socrates wore Baju Melayu, would he become Malaysian?

Why are comprehensions Pythonic?

Which begs the question...

Why is anything Pythonic❓

Someone posted this on Stack Overflow:

Javascriptic?

Java’s Cryptic?

Of course, it is!

And javascript, too!

🎬 4: Pythonic -> Idiomatic Python

🗣️ A Detour: Idioms in Spoken Language

NO! NOT LITERALLY!!

💡 Idioms carry shared meaning that can’t be translated literally.

An "idiom" is a phrase or expression whose meaning cannot be understood literally from the meanings of the individual words in it. Instead, idioms have a figurative or symbolic meaning that is recognized by native speakers and is unique to each language.

What is Idiomatic Python❓

The coat of arms of Malaysia

Source: Wikipedia

The Coat of Arms for Python

Foundation of Idiomatic Python: __dunder__ methods

__str__()

This is for end users and string representation.

__repr__()

This is for developers and debugging.

__len__()

This is to get the length of any sequence or collection.

Calling len() on a custom class does not work if len is not implemented

class MyList:
    def __init__(self, data: list[int]) -> None:
        self.data = data

my_list = MyList([1, 2, 3])
print(len(my_list))
# Output: TypeError: object of type 'MyList' has no len()

But it works if you implement the __len__() method

class MyList:
    def __init__(self, data: list[int]) -> None:
        self.data = data

    def __len__(self):
        return len(self.data)

my_list = MyList([1, 2, 3, 4, 5])
print(len(my_list))  # Output: 5

”By implementing special methods, your objects can behave like the built-in types, enabling the expressive coding style the community considers Pythonic

🖋️ Luciano Ramalho, Fluent Python

Now let's look at something more interesting...

class Vowels:
    def __getitem__(self, i):
        return 'AEIOU'[i]

What will be the output of these?

class Vowels:
    def __getitem__(self, i):
        return 'AEIOU'[i]
>>> vowels_object = Vowels()
>>> vowels_object[0]
>>> for vowel in vowels_object:
...     print(vowel)

>>> 'A' in vowels_object
>>> 'B' in vowels_object

A 3 line long class sure packs a punch!

class Vowels:
    def __getitem__(self, i):
        return 'AEIOU'[i]
>>> vowels_object = Vowels()
>>> vowels_object[0]
'A'
>>> for vowel in vowels_object:
...     print(vowel)
...
A
E
I
O
U
>>> 'A' in vowels_object
True
>>> 'B' in vowels_object
False

Our Vowels class now supports:

  • Access via index
  • Iteration like a list using for x in Y syntax
  • Membership checks via x in Y syntax
class Vowels:
    def __getitem__(self, i):
        return 'AEIOU'[i]
>>> vowels_object = Vowels()
>>> vowels_object[0]
'A'
>>> for vowel in vowels_object:
...     print(vowel)
...
A
E
I
O
U
>>> 'A' in vowels_object
True
>>> 'B' in vowels_object
False

It is! But you can be in on it, too.

💡 Dunder methods are formally called Special Methods

"The first thing to know about special methods is that they are meant to be called by the Python interpreter and not by you. You don't write my_object.__len__(). You write len(my_object)."

🖋️ Fluent Python, Luciano Ramalho

Why are special methods special

Everything in Python is an object.

But what does that mean?

"Objects are Python’s abstraction for data."

Data Model, Official Python Documentation

And the definition of that abstraction is the Python data model.

🧩 Enter the Python Data Model

The data model defines how objects:

  • Behave with built-ins and syntax
  • Interact with the interpreter
  • Participate in the ecosystem through protocols and dunders
Name Which dunders to implement Result
Sequence protocol implement __len__ + __getitem__ Then your object works with len(obj) and obj[i]
Iterator protocol implement __iter__ + __next__ Then it works in for x in obj: ....
Context manager protocol implement __enter__ + __exit__ Then it works with with obj:...

🤝 Dunders aka Special Methods: The Secret Handshake

These aren’t magic spells — they’re protocols.

len(obj)      # calls obj.__len__()
for x in obj: # calls obj.__iter__(), __next__()
x in obj      # calls obj.__contains__()

They let your object join Python’s social life.

A Protocol is an Informal Interface

If your object defines certain methods defined by a built-in type, the interpreter will treat it as consistent with that certain built-in type.

🔌 Protocols are just conventions. There’s no interface keyword.

🧐 Python checks at runtime:

“Does this object have the right dunder?”

Python under the hood: a sneak peek

Python is powered by CPython

CPython digesting our Python code

Enter PyObject & PyTypeObject

PyObject is the Brain

Courtesy: Pinky and the Brain cartoon

PyTypeObject: the Registry of Everything

Think of PyTypeObject as a Huge Form

🦆 Quacking like a duck is all you need to be a duck in the Python world. No duck parents needed.

If you define certain methods, Python recognises your class as having certain behaviours.

Flashback to Our Vowels Class

class Vowels:
    def __getitem__(self, i):
        return 'AEIOU'[i]

int PySequence_Check(PyObject *o)

Part of the Stable ABI.

Return 1 if the object provides the sequence protocol, and 0 otherwise. Note that it returns 1 for Python classes with a getitem() method...

From the official docs

Back to Idiomatic Python

Python idioms are powered by protocols.

Python protocols are powered by slots in PyTypeObject.

User-defined objects work the same way as built-ins if they honour the protocol.

-> A numpy.ndarray implements __add__, so a+b does vectorized addition.

-> A pandas.DataFrame implements __getitem__, so df["col"] works like dictionary access.

We've come a long way!

You are now Fancy the Pooh

🏢 Building the idea of Pythonic

Bottom up brick by brick

Layer Description
🧱 Data Model Defines behaviour and integration
🧭 Philosophy Guides design & readability
🍱 Syntax & Idioms Surface expressions we see daily

🎬 Take 5: Being Pythonic is to align with the design and philosophy of Python

"To be Pythonic is to use the Python constructs and data structures with clean, readable idioms."

🖋️ Martijn Faassen, author of lxml

  • What is Pythonic blog post

🫵 How does this help you write Pythonic code?

A hypothetical scenario: Gangstagram

Build an Instagram competitor

Users can add photos

Users can create photo albums and categorise photos in albums

Let's Start Simple...

from dataclasses import dataclass
from datetime import datetime
from typing import Iterable, Iterator, Union

@dataclass(frozen=True)
class Photo:
    filename: str
    taken_at: datetime
    tags: set[str]

class PhotoAlbum:
    def __init__(self, title: str, photos: Iterable[Photo] = ()):
        self.title = title
        self._photos: list[Photo] = list(photos)

To build Pythonic objects, observe how real Python objects behave.

🖋️ Ancient Chinese Proverb as narrated by Luciano Ramalho in Fluent Python 🤭

Add basic dunders to warm up...

@dataclass(frozen=True)
class Photo:
...

class PhotoAlbum:
    def __init__(self, title: str, photos: Iterable[Photo] = ()):
        self.title = title
        self._photos: list[Photo] = list(photos)

    # new addition 👇
    # Unambiguous/dev-facing
    def __repr__(self) -> str:
        return f"PhotoAlbum(title={self.title!r}, count={len(self._photos)})"

    # User-facing/friendly
    def __str__(self) -> str:
        """A compact, human-oriented summary"""
        unique_tags = set().union(*(p.tags for p in self._photos)) if self._photos else set()
        return f"📷 Album “{self.title}” — {len(self._photos)} photos, {len(unique_tags)} tags"

Let's check dunders in action...

p1 = Photo("dawn.jpg", datetime(2025, 6, 1, 5, 55), {"sunrise", "nature"})
p2 = Photo("lake.png", datetime(2025, 6, 2, 7, 10), {"water", "nature"})

album = PhotoAlbum("Summer Trip", [p1, p2])

print(repr(album))  # PhotoAlbum(title='Summer Trip', count=2)
print(str(album))   # 📷 Album “Summer Trip” — 2 photos, 3 tags

Users want to see photo count in albums..

class PhotoAlbum:
    ...
    def __len__(self) -> int:
        return len(self._photos)

Users want to see all photos in an album one by one...

class PhotoAlbum:
    ...
    def __iter__(self) -> Iterator[Photo]:
        return iter(self._photos)

Effortless implementation...

print(len(album))                     # 2
for photo in album:                   # iteration works
    print(photo.filename)

We can still add features that are specific for our use case...

class PhotoAlbum:
    # ... (repr/str/len/iter/contains as above)

    def add(self, photo: Photo) -> None:
        """Adds a new photo, avoid duplicates by filename."""
        if photo.filename in self:
            raise ValueError(f"Photo {photo.filename!r} already exists")
        self._photos.append(photo)

    def find_by_tag(self, tag: str) -> list[Photo]:
        """Finds photos by a tag."""
        return [p for p in self._photos if tag in p.tags]

Key Takeaways

Don’t Fight the Language

Don’t Overcompensate

Prioritize Readability

PEP-8 is How You Dress Your Python Code

When Writing Classes — Embrace Protocols

Use the Zen as Your Compass

"Being Pythonic is a philosophy, not a standard"

🖋️ Daniel Roy Greenfeld

💚 We kept drilling away at the truth. Socrates would be proud!

❤️ Thank You

Recommended Reading

Acklowedgements

  • Rahat Samit and Janusz Gregorczyk for being my beta audience
  • Lawrence Yeo, my favourite internet philosopher for the awesome article and illustrations on Socrates. Visit moretothat.com
  • Luke Merett, Daniel Greenfeld for their thoughts on the topic
  • GenAI & LLMs for visualising my ideas and Socrates
  • Luciano Ramalho and Martijn Faassen for their authorship on the topic
1
2

Socrates probably did not ask that question. But if he were a Python developer, I bet he would ask, What does it mean to be Pythonic?

3

@billah_tishad on X

Demistifying the Term Pythonic

Modasser Billah

Backend Lead, Doist

🗓️ PyCon MY, 2025

I'm Modasser Billah and today I'll share with you my journey through the many different ideas of what Pythonic means.

But first a little bit about me:

I am from a small town in Bangladesh called Comilla. It’s about 2 hours ride from the capital Dhaka where I currenlty live.

I work as a Backend Lead @ Doist

Doist is fully remote and one of the pioneers in remote and async work culture.

4

We have 2 products Todoist and Twist. Todoist is a task and project manager that brings clarity and efficiency to millions of people and teams. And Twist is an asynchronous-first collaboration and communication app for teams.

We do not have any offices and you can work with us from anywhere in the world. If that piques your interest, keep an eye on our careers page.

5
Photo by Rubaitul Azad on Unsplash

Our backends are built in Python.

Our backends are built in Python.

So the term Pythonic pops up every now and then in my mind.

I'm no philosopher myself so I did not really think about it when I first heard the term.

6

To be honest, I was more like this when I started out. So you see I had more existential questions to think about.

7

But it kept popping up, in Stack overflow, in code reviews, in blog posts. Again, I'm no Socrates, I wasn't really looking to explore. I was happy with answers handed over to me. And if you turn to the internet for answers, you will probably find too many.

8

🎬 1: List Comprehensions are Pythonic

So the first answer I adopted was...

🎬 1: List Comprehensions are Pythonic

9

🤓 For loops are so unPythonic

Use list comprehensions

Or any comprehensions really

Which in turn made me a proponent of...For loops are so unPythonic. Use list comprehensions Or any comprehensions really

It is a stupid simplification but my conclusion was to use clever list comprehensions that Java people would not understand. Not to offend Java programmers, it's just that I started with Java and then came over to Python. So, Java was the reference point in my head.

10
Java code to calculate squares of the fizzbuzz numbers
import java.util.ArrayList;
import java.util.List;

List<Integer> fizzBuzzSquares = new ArrayList<>();
for (int num = 0; num < 10; num++) {
    if (num % 3 == 0 || num % 5 == 0) {
        fizzBuzzSquares.add(num * num);
    }
}

Look at this java code to calculate squares of the fizz buzz numbers:

A note about code in the slides: the code snippets in the slides are there to drive a point home. So, don’t stress too much on reading it line by line, just follow along with me.

Okay back to the code...

so this is a java code snippet to calculate squares of the fizz buzz numbers, numbers that are divisible by 3 or 5 or both

11

Python code to calculate squares of the fizzbuzz numbers

fizz_buzz_squares = [
        num ** 2
        for num in range(10)
        if (num % 3 == 0) or (num %5 == 0)
]

Now let’s look at the same code in Python:

No imports, no loop counters, just pure Pythonic bliss!

12

Grouping students by their grade levels

students = [
    {"name": "Alice", "grade": "A", "score": 92},
    {"name": "Bob", "grade": "B", "score": 81},
    {"name": "Charlie", "grade": "A", "score": 95},
    {"name": "David", "grade": "C", "score": 67},
    {"name": "Eve", "grade": "B", "score": 88},
]

grouped = {
    grade: [s["name"] for s in students if s["grade"] == grade]
    for grade in {s["grade"] for s in students}
}
print(grouped)
# Output: {'A': ['Alice', 'Charlie'], 'B': ['Bob', 'Eve'], 'C': ['David']}

So there was a time when my first attempt to solve any problem was to think, how can I use a comprehension here?

I got obsessed with comprehensions. Lists, dicts, sets you name it.

For example, let’s assume we have a list of student records. We want to group students by their grades.

This is a working solution with multiple comprehensions bundled together.

Slick, but I had to read it a few times to get it. It uses a dict comprehension. And inside the dict it uses two more comprehensions for the keys and the values.

13

🎬 2: Clever one-liners are Pythonic

That obsession with comprehensions also brought over its smart cousin..

🎬 2: Clever one-liners are Pythonic

14

One liners was the way to flex that I knew my Python 😎

Function that returns the set of all subsets of its argument

f = lambda x: [[y for j, y in enumerate(set(x)) if (i >> j) & 1] for i in range(2**len(set(x)))]

>>> f([10,9,1,10,9,1,1,1,10,9,7])
# output: [[], [9], [10], [9, 10], [7], [9, 7], [10, 7], [9, 10, 7], [1], [9, 1], [10, 1], [9, 10, 1], [7, 1], [9, 7, 1], [10, 7, 1], [9, 10, 7, 1]]

One liners was the way to flex that I knew my Python 😎

This is a Function that returns the set of all subsets of its argument. Hardly readable yet works.

The more you could do with a smart one-liner, the better Pythonista you are...

Or so I thought!

15

There's almost a community around one-liners in Python.

And in my defence, it's not just me. There's almost a community around one-liners in Python.

There's a book, there's a wiki and so much more. I'm totally cool with it, it's fun and a learning experience.

But how far do we take it? At what point does it stop being Pythonic?

16

💔 Your code is not Pythonic

That day eventually came. I got a code review that told me my code is concise but it is hardly readable and hence it was not Pythonic!

17

🎬 3: The Zen of Python

Every heart-break opens new doors. Now It was my turn to be more spiritual in my Python journey...

And thus entered the stage, the zen of Python.

18

The Zen of Python

>>> import this
The Zen of Python, by Tim Peters

Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases arent special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless youre Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, its a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- lets do more of those!

The Zen of Python is a collection of 19 guiding principles for writing computer programs in Python. These were later standardised in PEP 20. If you run import this from a python shell, you’ll see it.

Zen of Python are Aphorisms that came with wisdom. But I barely had the depth to appreciate it.

19

Simple is better than complex.

20

Beautiful is better than ugly.

21

Now is better than never.

My apologies to Tim Peters. But I can't remove the image of Master Oogway from my head when I read the zen of Python...

You’re a wise man Tim! Oogway would be proud.

22

But like Po, who had no clue in the beginning what master Oogway was saying, I did not know any better either.

I was merely parroting the wisdom.

23

There were a few more phases of - oh this is Pythonic, that is Pythonic without much thought.

Think PEP-8, functional programming and so on. Eventually my hairs started turning grey and one day I finally asked myself, like a philosopher, what IS Pythonic?

24

Socrates used the tool of questioning to strip away all preconceived notions and dogmas surrounding one’s cherished beliefs.

Credit: More To That by Lawrence Yeo

Today I will try to apply the Socratic method to search for an answer. The Socratic method uses the tool of questioning to chip away at our fuzzy understandings and implicit assumptions. And thus, it exposes the gaps in our understanding.

25

Let's start over again and question the answers that are out there already.

Let's start over again and question the answers that are out there already. When we grab an answer and stick to it, it makes it hard for us be open to different opinions.

When we start from a place of uncertainty, we give ourselves a better chance of getting closer to reality and give all options on the ground a fair chance.

26

What does it mean to be anything❓

First, let's start from a more generalised starting point. What does it mean to be anything at all?

27

What does it mean to be Malaysian❓

What does it mean to be Malaysian, for example?

28

If Socrates ate Nasi Lemak, would he become Malaysian?

If someone eats Malaysian food, would he become Malaysian? If Socrates ate Nasi Lemak, would he become Malaysian?

29

If Socrates wore Baju Melayu, would he become Malaysian?

If someone dressed like Malaysian people, would he become Malaysian? Taking Socrates with us through the journey, if he wore Baju melayu, would he become Malaysian?

Probably not but food and dresses are significant aspects of a culture or nation. Although, I’m sure you'd agree there's more to it in being Malaysian than just the cuisine and clothing. And That gives us a good heuristic.

30

Learning from our analogy, we can say that Pythonic can be many things that combine to form the soul of it.

But superficially adopting one or two of those does not give us the full picture.

So, like food and clothing, list comprehensions and one-liners are very much part of the zeitgeist of being Pythonic but surely, there's more to it.

31

Why are comprehensions Pythonic?

So if Socrates the Python developer asked me, what is Pythonic and I replied, list comprehensions are Pythonic.

He'd probably ask, Why is that Pythonic?

32

Which begs the question...

Why is anything Pythonic❓

Which begs the question...

Why is anything Pythonic❓

33

Someone posted this on Stack Overflow:

And there are some angry people out there who do not like this question at all...

Someone posted this on Stack Overflow:

First and foremost - Pythonic is a term that needs to disappear, preferably with everyone who uses it. It doesn't mean anything and it's used by people who can't use reason to justify anything, so they need a term to justify their nonsense.

Wow, I guess I should end my talk here 😅

But languages and terms chart their own course. The wonderful organizers of Pycon Malaysia accepted my talk on this topic. Pythonic is still very much alive as a term. So, our journey continues..

34

Javascriptic?

Java’s Cryptic?

Of course, it is!

And javascript, too!

I guess the word Pythonic was easy on the tongue so it became a thing. We do not hear the same about other languages, like Go-ic or JavaScriptic. Although I must admit, Javascript is cryptic.

35

🎬 4: Pythonic -> Idiomatic Python

Anyway, What we do come across in other languages, is idiomatic. And people say that a lot about Python as well. It is generally the immediate answer to many of the Why's. Why is this Pythonic? Because it is idiomatic!

So, we have a new answer to our quest...

Pythonic means idiomatic Python

As a non-native English speaker, I did not grasp what idiomatic means when it was first thrown at me in a programming language context. So understanding this expression of Idiomatic is key.

36

🗣️ A Detour: Idioms in Spoken Language

Now before we ask what is Idiomatic Python, let's go in full Socrates mode, and first ask what is idiomatic?

Every language has its idioms. In English, if your friend has a football match coming up and you tell him, Break a leg!

37

NO! NOT LITERALLY!!

You're not asking him to be Sergio Ramos and literally send someone to the hospital. You're simply wishing him good luck.

But if you are a non-native speaker and did not come across this expression before, you'd probably be confused.

38

💡 Idioms carry shared meaning that can’t be translated literally.

Idioms carry shared meaning that can’t be translated literally. Python has idioms too — expressions that *just *make sense in the Python world.

39

An "idiom" is a phrase or expression whose meaning cannot be understood literally from the meanings of the individual words in it. Instead, idioms have a figurative or symbolic meaning that is recognized by native speakers and is unique to each language.

And therein lies the magic of idioms. Idioms have a figurative meaning. Native speakers share the understanding organically. But if you're an outsider it may seem counter intuitive, even confusing!

40

What is Idiomatic Python❓

Now that we have a bit more clarity about what idiomatic is, let's go back to idiomatic Python.

List comprehensions are Pythonic because it is idiomatic Python. But what is idiomatic Python and are list comps idiomatic?

41

The coat of arms of Malaysia

Source: Wikipedia

Going back to the analogy of being Malaysian. The meaning of being Malaysian comes from the rich history, heritage, culture and traditions of the people of Malaysia and how the nation came together. The coat of arms is a good snapshot of it because it tries to capture all the different aspects that bring together a nation.

Similarly, the idiomatic understanding of Python springs forth from how the language was designed, how it evolved and how Pythonistas use it and expect it to be used.

42

The Coat of Arms for Python

Foundation of Idiomatic Python: __dunder__ methods

I present my argument to you that, Dunder methods is the foundation of idiomatic Python.

The gateway to the heart of Python are the dunder methods.

The weird methods in Python classes that have double underscores on both sides. The double underscore is shortened as dunder in Pythonista lingo. In a way the term dunder is also a Python idiom. You probably won’t hear it anywhere else. No one would understand what it means outside the Python community. And that right there, is what an idiom is.

43

__str__()

This is for end users and string representation.

__repr__()

This is for developers and debugging.

__len__()

This is to get the length of any sequence or collection.

You have probably seen some of these methods already, may be even wrote some...

dunder string is used for string representation. dunder len is used for getting the length of a list and so on.

44

Calling len() on a custom class does not work if len is not implemented

class MyList:
    def __init__(self, data: list[int]) -> None:
        self.data = data

my_list = MyList([1, 2, 3])
print(len(my_list))
# Output: TypeError: object of type 'MyList' has no len()

But here's the magic, if we write a custom object and call len on it, it doesn’t work if we don’t write a dunder len for it.

45

But it works if you implement the __len__() method

class MyList:
    def __init__(self, data: list[int]) -> None:
        self.data = data

    def __len__(self):
        return len(self.data)

my_list = MyList([1, 2, 3, 4, 5])
print(len(my_list))  # Output: 5

But if you give it a dunder len method, the len() function will work with our custom object, too!

46

”By implementing special methods, your objects can behave like the built-in types, enabling the expressive coding style the community considers Pythonic

🖋️ Luciano Ramalho, Fluent Python

Luciano Ramalho, the author of Fluent Python makes the connection between dunder methods and Pythonic:

”By implementing special methods, your objects can behave like the built-in types, enabling the expressive coding style the community considers Pythonic

47

Now let's look at something more interesting...

class Vowels:
    def __getitem__(self, i):
        return 'AEIOU'[i]

Now let's look at something more interesting...Here we define a class Vowels and it only has one method in it. Pretty simple, right?

48

What will be the output of these?

class Vowels:
    def __getitem__(self, i):
        return 'AEIOU'[i]
>>> vowels_object = Vowels()
>>> vowels_object[0]
>>> for vowel in vowels_object:
...     print(vowel)

>>> 'A' in vowels_object
>>> 'B' in vowels_object

Now let's try to guess the output of these, here we try to access an instance of the vowels object using index, then we try to loop through it and then we try some membership checks for characters a and b.

49

A 3 line long class sure packs a punch!

class Vowels:
    def __getitem__(self, i):
        return 'AEIOU'[i]
>>> vowels_object = Vowels()
>>> vowels_object[0]
'A'
>>> for vowel in vowels_object:
...     print(vowel)
...
A
E
I
O
U
>>> 'A' in vowels_object
True
>>> 'B' in vowels_object
False

Wow, no errors. Looks like the Vowels class supports all of these operations! A 3 line long class sure packs a punch!

50

Our Vowels class now supports:

  • Access via index
  • Iteration like a list using for x in Y syntax
  • Membership checks via x in Y syntax
class Vowels:
    def __getitem__(self, i):
        return 'AEIOU'[i]
>>> vowels_object = Vowels()
>>> vowels_object[0]
'A'
>>> for vowel in vowels_object:
...     print(vowel)
...
A
E
I
O
U
>>> 'A' in vowels_object
True
>>> 'B' in vowels_object
False

So, we are onto something here. We wrote a 3 liner class but it now supports access via index like lists, it supports iteration and membership checks!

51

It is! But you can be in on it, too.

Now it is fair to ask...

Is this magic?

The beauty of the design of the Python language is that it shares all the magic with us. Not just what it does, but also a way to let us leverage that magic.

We will go back to our Vowels class later after we have unpacked this magic.

52

💡 Dunder methods are formally called Special Methods

So dunder methods seem to have some special powers in Python. And rightly so, because...

Dunder methods are formally called Special Methods

We will focus on how it is foundational to Python and hence, Pythonic code.

53

"The first thing to know about special methods is that they are meant to be called by the Python interpreter and not by you. You don't write my_object.__len__(). You write len(my_object)."

🖋️ Fluent Python, Luciano Ramalho

We go back to the Python Oracle Luciano Ramalho for guidance...

He writes in Fluent Python: "The first thing to know about special methods is that they are meant to be called by the Python interpreter and not by you. You don't write my_object.__len__(). You write len(my_object)."*

54

Why are special methods special

Okay, so special methods are a big deal. But we more or less know that already. We use them every day. But why are they so special? And how does it work if I define it for one of my classes?

55

Everything in Python is an object.

But what does that mean?

The answer starts with a cliche...

Everything in Python is an object.

56

"Objects are Python’s abstraction for data."

Data Model, Official Python Documentation

And the definition of that abstraction is the Python data model.

Lists, Classes, Functions -- everything is an object in Python. The official python documentation says,

"Objects are Python’s abstraction for data."

And the definition of that abstraction is the Python data model.

57

🧩 Enter the Python Data Model

So now the Python data model enters the scene. The official docs have a detailed page on it. Almost all Python books have dedicated chapters on it.

58

The data model defines how objects:

  • Behave with built-ins and syntax
  • Interact with the interpreter
  • Participate in the ecosystem through protocols and dunders

The data model defines how objects:

  • Behave with built-ins and syntax
  • Interact with the interpreter
  • Participate in the ecosystem through protocols and dunders
59
Name Which dunders to implement Result
Sequence protocol implement __len__ + __getitem__ Then your object works with len(obj) and obj[i]
Iterator protocol implement __iter__ + __next__ Then it works in for x in obj: ....
Context manager protocol implement __enter__ + __exit__ Then it works with with obj:...

So, if you want list like behaviour, you implement a set of dunders.

If you want loops and comps, you implement another set of dunders.

and so on...

60

🤝 Dunders aka Special Methods: The Secret Handshake

These aren’t magic spells — they’re protocols.

len(obj)      # calls obj.__len__()
for x in obj: # calls obj.__iter__(), __next__()
x in obj      # calls obj.__contains__()

They let your object join Python’s social life.

len calls dunder len, for x in obj syntax calls dunder iter, membership checks call dunder contains. So these aren’t magic spells, these are just pre-defined protocols.

They let your object join Python’s social life. Your classes can now hang out with the built-in types and feel like they belong!

61

If you're still in the Socratic mood, you probably noticed that I sneaked in a new term, protocols. So, the next question we ask is...

What's a protocol?

62

A Protocol is an Informal Interface

If your object defines certain methods defined by a built-in type, the interpreter will treat it as consistent with that certain built-in type.

🔌 Protocols are just conventions. There’s no interface keyword.

🧐 Python checks at runtime:

“Does this object have the right dunder?”

A Protocol is an Informal Interface. Informal because there is no inheritance and Python does not have interfaces at all.

If your object defines certain methods defined by a built-in type, the interpreter will treat it as consistent with that certain built-in type. Python checks at runtime:

“Does this object have the right dunder?” if it does, it stamps it consistent-with and treats it like a native.

63

Okay, so dunders are the tool we use to implement protocols.

A bundle of dunders form a protocol. The Python data model defines these bundles for various protocols.

So this gives us a good overview of dunders and protocols and how they fit together in the puzzle.

But how are they really connected? How does Python make that connection?

64

Python under the hood: a sneak peek

To find the connection, we’ll peek under the hood of how Python works.

We won’t go deep into Python internals but let’s try to build a simple mental model that can help us connect the dots.

65

Python is powered by CPython

CPython is the default and official implementation of Python. Which, as the name suggests, is written in C. When we install Python from python.org, we’re actually installing CPython.

66

CPython digesting our Python code

When we run Python code, it’s actually

CPython doing the heavy lifting.

CPython first turns our code into bytecode. These are the pesky .pyc files that show up in your editor and if you don’t gitignore them, they’ll show up in your repo, too.

The bytecode is then executed line by line by the interpreter, which is also part of the CPython implementation.

67

This is the step where CPython uses 2 special agents to make sense of the bytecode

68

Enter PyObject & PyTypeObject

The CPython interpreter uses the PyObject and PyTypeObject structures as core components for managing all Python objects and their behaviour.

All Python constructs are managed using these 2 objects or structs in CPython under the hood.

69

PyObject is the Brain

Courtesy: Pinky and the Brain cartoon

When we say everything in Python is an object, it fundamentally comes down to this tracking object or c struct pyobject. Which is like the brain of every object in Python

This PyObject is small and mainly keeps track of 2 things:

  • a reference count and a pointer to the object type

Which brings us to the other object...

70

PyTypeObject: the Registry of Everything

The brain, PyObject holds a reference to a PyTypeObject. This tells CPython about the nature of the object.

PyTypeObject is like this huge form that has a lot of fields in it. It has fields (or slots) for every thing. Some are basic and some are divided into sections.

So when the object is compiled for the first time into bytecode, it looks at the class declaration and fills up the fields in the form that are relevant.

71

Think of PyTypeObject as a Huge Form

So, it looks at the class name and fills up the slot tp_name.

If it finds a dunder repr, it puts a pointer to that method call into the tp_repr slot.

72

For protocols it has dedicated sections. There’s a tp_as_sequence slot that points to a PySequenceMethods sub structure, which includes a sq_length slot.

When you add a dunder len, Cpython fills that slot for your class’ PyTypeObject. During execution it finds that tp_as_sequence exists, so it treats your class like a sequence.

And that’s how dunders and protocols run the deep state of the Python world.

73

🦆 Quacking like a duck is all you need to be a duck in the Python world. No duck parents needed.

If you define certain methods, Python recognises your class as having certain behaviours.

If a duck quacks and you mimic the duck and quack, Python will assume you’re a duck, too.

And python tries really hard to make it work for you. Remember our Vowels class? We did not add __iter__ or __contains__ there, only __getitem__ but it still worked!

74

Flashback to Our Vowels Class

class Vowels:
    def __getitem__(self, i):
        return 'AEIOU'[i]

int PySequence_Check(PyObject *o)

Part of the Stable ABI.

Return 1 if the object provides the sequence protocol, and 0 otherwise. Note that it returns 1 for Python classes with a getitem() method...

From the official docs

So for, loops and membership checks, Python first looks for dunder iter and contains methods. If it finds those, it supports for x in seq style operations.

But if it doesn't find these, it still gives it another honest try. Now it looks for a dunder getitem. If it finds getitem, it uses that to fetch elements in the object one by one using index access. Similarly, it does a full scan of the elements for the membership check.

So as long as relevant fields get filled up in the pytype object, Python will treat your class accordingly.

75

Back to Idiomatic Python

Now we have a meme based mental model on how Python works under the hood. Let’s get back to idiomatic Python again.

76

Python idioms are powered by protocols.

So now the dots are connected. dunder methods power many behaviors in built-in types. And protocols are the social contract that allows dunders to provide this privilege. If we implement dunders, we can enjoy those behaviors, too. That’s answers the how.

List comps or idiomatic Python for loops are basically syntactic sugar for these protocols. When you write for x in Y, Python basically reaches out to __iter__ inside Y.

77

Python protocols are powered by slots in PyTypeObject.

Python protocols are powered by slots in PyTypeObject.

This is why leveraging protocols gives our classes the same powers. So we got the why as well.

78

User-defined objects work the same way as built-ins if they honour the protocol.

-> A numpy.ndarray implements __add__, so a+b does vectorized addition.

-> A pandas.DataFrame implements __getitem__, so df["col"] works like dictionary access.

We have many awesome libraries that leverage this to create Pythonic libraries that feel native and built-in like...

A numpy.ndarray implements __add__, so a+b does vectorized addition.

A pandas.DataFrame implements __getitem__, so accessing a dataframe column works like dictionary access.

79

We've come a long way!

Wow, we have come a long way from comprehensions and one-liners!

80

You are now Fancy the Pooh

Let’s recap the Pythonic connection with one last meme...

if you know your dunders, you’re good

if you know your protocols, you’re cool

if you know how dunders and protocols shake hands via pytypeobjects, you’re fancy the pooh

81

🏢 Building the idea of Pythonic

Bottom up brick by brick

Layer Description
🧱 Data Model Defines behaviour and integration
🧭 Philosophy Guides design & readability
🍱 Syntax & Idioms Surface expressions we see daily

At its core, the data model builds the foundations of idiomatic Python. The philosophy embodies the data model and gives us those principles. Finally, there’s idioms and conventions that are the end result of that Pythonic alignment.

Most if not all Pythonic advices or tricks actually have their root in this chain.

82

🎬 Take 5: Being Pythonic is to align with the design and philosophy of Python

Now we have a more nuanced view on the term Pythonic..

83

"To be Pythonic is to use the Python constructs and data structures with clean, readable idioms."

🖋️ Martijn Faassen, author of lxml

  • What is Pythonic blog post

And I have a quote from a wise man to back it up..

Martin Faassen the author of the lxml library wrote that

"To be Pythonic is to use the Python constructs and data structures with clean, readable idioms."*

84

🫵 How does this help you write Pythonic code?

Okay so we have talked a lot about what Pythonic is and why it is Pythonic but..

85

A hypothetical scenario: Gangstagram

Build an Instagram competitor

Users can add photos

Users can create photo albums and categorise photos in albums

Let’s assume we are going to build an instagram alternative. We will call it Gangstagram.

86

Let's Start Simple...

from dataclasses import dataclass
from datetime import datetime
from typing import Iterable, Iterator, Union

@dataclass(frozen=True)
class Photo:
    filename: str
    taken_at: datetime
    tags: set[str]

class PhotoAlbum:
    def __init__(self, title: str, photos: Iterable[Photo] = ()):
        self.title = title
        self._photos: list[Photo] = list(photos)

We create a Photo class and a PhotoAlbum class that will hold collections of Photos. We want to write these classes in a Pythonic way.

87

To build Pythonic objects, observe how real Python objects behave.

🖋️ Ancient Chinese Proverb as narrated by Luciano Ramalho in Fluent Python 🤭

Here’s some more wisdom from Fluent Python. An ancient Chinese proverb that inspired me to bring back Socrates in this talk. Luciano writes,

To build Pythonic objects, observe how real Python objects behave.

88

Add basic dunders to warm up...

@dataclass(frozen=True)
class Photo:
...

class PhotoAlbum:
    def __init__(self, title: str, photos: Iterable[Photo] = ()):
        self.title = title
        self._photos: list[Photo] = list(photos)

    # new addition 👇
    # Unambiguous/dev-facing
    def __repr__(self) -> str:
        return f"PhotoAlbum(title={self.title!r}, count={len(self._photos)})"

    # User-facing/friendly
    def __str__(self) -> str:
        """A compact, human-oriented summary"""
        unique_tags = set().union(*(p.tags for p in self._photos)) if self._photos else set()
        return f"📷 Album “{self.title}” — {len(self._photos)} photos, {len(unique_tags)} tags"

So we have observed how real python objects behave by looking at the data model. Now our job is to mimic that. So what do we do? We add dunders!

89

Let's check dunders in action...

p1 = Photo("dawn.jpg", datetime(2025, 6, 1, 5, 55), {"sunrise", "nature"})
p2 = Photo("lake.png", datetime(2025, 6, 2, 7, 10), {"water", "nature"})

album = PhotoAlbum("Summer Trip", [p1, p2])

print(repr(album))  # PhotoAlbum(title='Summer Trip', count=2)
print(str(album))   # 📷 Album “Summer Trip” — 2 photos, 3 tags

And the dunders work their magic! __repr__ and __str__ now gives us different string representations of our objects and we can use them as necessary.

90

Users want to see photo count in albums..

class PhotoAlbum:
    ...
    def __len__(self) -> int:
        return len(self._photos)

Gangstagram is getting popular. Our users or, Gangsters want to see how many photos they have in each album. What do we do? We add a dunder len!

91

Users want to see all photos in an album one by one...

class PhotoAlbum:
    ...
    def __iter__(self) -> Iterator[Photo]:
        return iter(self._photos)

Gangsters now want to swipe through photos in albums. We bring out dunder iter to support iterations!

92

Effortless implementation...

print(len(album))                     # 2
for photo in album:                   # iteration works
    print(photo.filename)

And voila! Idiomatic Python shows us its beauty. Our album object has Pythonic nirvana. The code is concise yet familiar and readable.

93

We can still add features that are specific for our use case...

class PhotoAlbum:
    # ... (repr/str/len/iter/contains as above)

    def add(self, photo: Photo) -> None:
        """Adds a new photo, avoid duplicates by filename."""
        if photo.filename in self:
            raise ValueError(f"Photo {photo.filename!r} already exists")
        self._photos.append(photo)

    def find_by_tag(self, tag: str) -> list[Photo]:
        """Finds photos by a tag."""
        return [p for p in self._photos if tag in p.tags]

By the way, we can have our cake and eat it too! If gangstagram needs custom features, we can add those too. For example, an add feature or find by tag feature can be easily added as regular methods.

94

Key Takeaways

We demystified the term Pythonic! Let’s recap the main ideas.

95

Don’t Fight the Language

Let go of habits from other languages.

96

Don’t Overcompensate

You don’t need clever one-liners to be Pythonic. In fact quite the opposite.

97

Prioritize Readability

Future you — and teammates — will thank you. Remember the Zen, readability counts.

98

PEP-8 is How You Dress Your Python Code

Standards let you write code that is recognised and easily understood by other Python developers. Dress your code like a Pythonista!

99

When Writing Classes — Embrace Protocols

Python gives you all its superpowers, enjoy responsibly.

100

Use the Zen as Your Compass

The zen of Python is condensed Pythonic wisdom. Let it be your guide.

101

"Being Pythonic is a philosophy, not a standard"

🖋️ Daniel Roy Greenfeld

Remember, Being Pythonic is a philosophy, not a standard"

Style guides and standards may differ, but they all share the same philosophy. Just like Oogway, Shifu and Po have very different styles, yet they all love Kungfu and embody the values.

102

💚 We kept drilling away at the truth. Socrates would be proud!

The socratic method really came in handy today!

103

❤️ Thank You

104

Recommended Reading

105

Acklowedgements

  • Rahat Samit and Janusz Gregorczyk for being my beta audience
  • Lawrence Yeo, my favourite internet philosopher for the awesome article and illustrations on Socrates. Visit moretothat.com
  • Luke Merett, Daniel Greenfeld for their thoughts on the topic
  • GenAI & LLMs for visualising my ideas and Socrates
  • Luciano Ramalho and Martijn Faassen for their authorship on the topic