Skip to main content

Command Palette

Search for a command to run...

Iterables in Python, The Buffet Table 🍽️

Updated
4 min read
Iterables in Python, The Buffet Table 🍽️
A

Full-Stack Developer & Tech Writer specializing in Python (Django, FastAPI, Flask) and JavaScript (React, Next.js, Node.js). I build fast, scalable web apps and share practical insights on backend architecture, frontend performance, APIs, and Web3 integration. Available for freelance and remote roles.

When you write:

for x in something:
    print(x)

it seems straightforward. But what makes an object loopable in Python? Lists, strings, sets, dictionaries, files all of these are iterable.

The answer lies in Python’s iteration protocol. In this article (Part 1 of a 3-part series), we’ll cover:

  • What is an iterable

  • How iter() works internally

  • Memory and performance aspects of iterators

  • Real-world use cases

  • How to design reusable custom iterables

We’ll use a buffet table metaphor to make these concepts intuitive and memorable.

🍴 The Buffet Table Metaphor

Imagine a buffet restaurant:

  • Buffet table = Iterable a container with all the food

  • Plate = Iterator keeps track of what you’ve taken

Key points:

  • The buffet itself never “runs out.” You can always take a new plate and start again.

  • Each plate keeps its own position. Two people can eat from the buffet independently.

Pro Tip: Each iterator is independent. You can loop multiple times over the same iterable without interference.

What Makes an Object Iterable?

An object is iterable if it:

  1. Implements __iter__() → returns an iterator

  2. Or implements __getitem__() with 0-based indexing → Python simulates iteration until IndexError

iter(obj) behaves like a universal adapter:

  • Checks for __iter__ → uses it if present

  • Falls back to __getitem__ otherwise

Iterables in Python Code Examples

lst = [1, 2, 3]
s = "hello"
st = {'x', 'y', 'z'}  # unordered
d = {'a': 1, 'b': 2}  # iterable over keys

it = iter(lst)  # take a plate
print(next(it))  # 1
print(next(it))  # 2
print(next(it))  # 3

⚠️ Note: Sets are iterable but not indexable (st[0] → TypeError).

Behind the Scenes Memory & Iterators

  • List: contiguous array of object references

  • Iterator (list_iterator): lightweight → stores reference + cursor only

Iterators do not copy data, making them O(1) memory overhead.

Example:

  • range(10_000_000) → memory-efficient, lazy evaluation

  • Generators or iterator-based APIs → stream data line by line

Iterables via __getitem__

class SquareSeq:
    def __getitem__(self, index):
        if index < 0:
            raise IndexError
        return index * index

sq = SquareSeq()
it = iter(sq)
print(next(it))  # 0
print(next(it))  # 1
print(next(it))  # 4

Python calls sq[0], sq[1], ... until IndexError.

⚠️ Make sure to raise IndexError otherwise iteration may never stop.

Real-Life Use Cases

  1. Processing huge files → iterate line by line

  2. Paginated API calls → fetch pages lazily

  3. Data pipelines → map, filter, itertools efficiently

  4. Lazy computations → calculate values only when needed

  5. Large numeric ranges → range() and slicing

Tip: Stream data with iterators to avoid memory issues.

Common Pitfalls

  • Iterators get exhausted → can’t reuse without creating a new iterator

  • Sets/dicts → order not guaranteed (CPython keeps insertion order, but don’t rely on it for cross-version code)

  • Infinite iterables → use itertools.islice or stop conditions

Custom Iterable The Right Way

Bad Pattern: Iterator = Container

class BadCities:
    def __init__(self):
        self._cities = ["Paris", "Berlin", "Rome"]
        self._index = 0
    def __iter__(self): return self
    def __next__(self):
        if self._index >= len(self._cities): raise StopIteration
        val = self._cities[self._index]
        self._index += 1
        return val

Problem: second loop finds nothing, iterator already exhausted.

Better Pattern: Separate container & iterator

class CityIterator:
    def __init__(self, cities):
        self._cities, self._index = cities, 0
    def __iter__(self): return self
    def __next__(self):
        if self._index >= len(self._cities): raise StopIteration
        val = self._cities[self._index]; self._index += 1
        return val

class Cities:
    def __init__(self):
        self._cities = ["Paris", "Berlin", "Rome"]
    def __iter__(self):
        return CityIterator(self._cities)

Multiple loops → ✅ Works fine.

Mental Model ASCII Diagram

[Iterable (buffet table)]
       |
       | __iter__() or __getitem__()
       V
[Iterator (plate)]reference + cursor
       |
       | __next__()
       V
   items one-by-one
   StopIterationstop

Quick Checklist

  • Multiple passes? → Use iterable

  • Memory-efficient? → Use generator or iterator API

  • Test iterability → try: iter(obj) / except TypeError

  • Special stop condition → iter(callable, sentinel)

Wrap-Up

In Part 1:

  • We explored what an iterable is the buffet table

  • Learned how iter() works (__iter__ / __getitem__)

  • Saw memory-efficient iterators in action

  • Discussed real-world use cases

  • Designed reusable custom iterables

👉 Next up (Part 2): Iterators the waiters with one-way tickets 🍽️. We’ll dive deep into __next__, StopIteration, and how generators fit into the picture.

More from this blog

A

Anik Sikder - The Dev Loop

21 posts

Practical tips on building fast, scalable web apps using Python, JavaScript, and Web3. Learn how I turn ideas into reliable, production-ready digital products.