Anik Sikder
Technical Writing/python/iterables-in-python-the-buffet-table
article.sh

$ open article

python

Iterables in Python Explained: Understanding the Iteration Protocol with a Buffet Table Metaphor

11 min readSeptember 21, 2025
Python Iterable and Iterator Buffet Table Visualization

Hey Python developers! 👋

When you write:

code
for x in something:
    print(x)

everything feels simple.

Python loops through values.

You get your output.

Life is good.

But have you ever wondered:

What actually makes an object loopable?

Why can you iterate over:

code
[1, 2, 3]

but also over:

code
"hello"

or:

code
{"a": 1, "b": 2}

or even:

code
open("data.txt")

The answer lies in one of Python's most elegant design patterns:

The Iteration Protocol

Understanding iterables is the foundation for mastering:

  • Loops
  • Generators
  • Comprehensions
  • File processing
  • Data pipelines
  • Lazy evaluation

In this article (Part 1 of a 3-part series), we'll explore:

  • What an iterable actually is
  • How iter() works internally
  • Memory and performance implications
  • Real-world applications
  • Designing reusable custom iterables

And to make everything memorable, we'll use a buffet restaurant metaphor. 🍽️

Let's dive in.

The Buffet Table Metaphor 🍴

Imagine you're at a buffet restaurant.

The restaurant contains:

code
🍕 🍔 🌮 🍝 🍰

All arranged neatly on a buffet table.

In our analogy:

code
Buffet Table → Iterable
Plate         → Iterator

The buffet contains all available food.

The plate keeps track of what you've already taken.

Why This Analogy Works

The buffet itself never gets consumed.

You can always grab a fresh plate and start again.

Example:

code
foods = ["Pizza", "Burger", "Taco"]

You can loop multiple times:

code
for food in foods:
    ...

and again:

code
for food in foods:
    ...

The iterable remains unchanged.

Each loop receives a fresh iterator.

Multiple People, Multiple Plates

Imagine two people eating from the same buffet.

Each has a different plate.

code
Person A → Plate A
Person B → Plate B

They track progress independently.

Similarly:

code
numbers = [1, 2, 3]

it1 = iter(numbers)
it2 = iter(numbers)

Each iterator maintains its own position.

This is one of the most important concepts in Python iteration.

What Is an Iterable?

An iterable is any object that can produce an iterator.

More formally:

An object is iterable if it:

Option 1: Implements __iter__()

code
obj.__iter__()

returns an iterator.

Option 2: Implements __getitem__()

Using:

code
obj[0]
obj[1]
obj[2]
...

Python can simulate iteration until:

code
IndexError

occurs.

This older mechanism still works today.

Meet iter(): The Universal Adapter

Whenever Python needs to iterate, it calls:

code
iter(obj)

Think of iter() as a universal adapter.

Internally, Python follows roughly this process:

code
1. Does object have __iter__()?
   → Use it

2. Otherwise, does it support __getitem__()?
   → Use index-based iteration

3. Otherwise
   → TypeError

If neither protocol exists:

code
iter(obj)

fails.

Common Built-In Iterables

Python provides many iterable types.

Lists

code
numbers = [1, 2, 3]

Strings

code
text = "hello"

Tuples

code
coordinates = (10, 20)

Sets

code
letters = {"a", "b", "c"}

Dictionaries

code
data = {"name": "Anik"}

Files

code
with open("data.txt") as file:
    ...

All of these support iteration.

Using iter() and next()

Let's see iteration manually.

code
numbers = [1, 2, 3]

it = iter(numbers)

Retrieve items:

code
print(next(it))

Output:

code
1

Again:

code
print(next(it))

Output:

code
2

Again:

code
print(next(it))

Output:

code
3

One more time:

code
print(next(it))

Output:

code
StopIteration

The iterator signals that it's finished.

Iterables vs Iterators

These concepts are often confused.

Iterable

Container of values.

Example:

code
numbers = [1, 2, 3]

Can create multiple iterators.

Iterator

Tracks progress.

Example:

code
it = iter(numbers)

Knows:

code
Current position

and provides:

code
next()

to move forward.

Think:

code
Iterable = Buffet Table
Iterator = Plate

Sets Are Iterable but Not Indexable

Many beginners assume iteration requires indexing.

Not true.

Example:

code
letters = {"a", "b", "c"}

This works:

code
for letter in letters:
    print(letter)

But:

code
letters[0]

raises:

code
TypeError

because sets do not support indexing.

Iteration and indexing are separate concepts.

Memory Efficiency of Iterators

One of the biggest benefits of iterators is memory efficiency.

Consider:

code
numbers = [1, 2, 3]

The list stores all elements.

A list iterator stores only:

code
Reference to list
+
Current position

That's it.

No data copying occurs.

Why Iterators Are Cheap

Imagine:

code
numbers = list(range(10_000_000))

The list consumes significant memory.

But:

code
it = iter(numbers)

adds only a tiny overhead.

The iterator simply remembers:

code
Current index

This is why iterators are considered:

code
O(1) additional memory

The Magic of range()

A great example:

code
range(10_000_000)

Python doesn't store ten million integers.

Instead:

code
start
stop
step

are stored.

Values are generated when needed.

This approach is called:

code
Lazy Evaluation

and is one reason Python can handle huge ranges efficiently.

Iterables via __getitem__

An object doesn't necessarily need __iter__().

Example:

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

        return index * index

Usage:

code
sq = SquareSeq()

it = iter(sq)

Now:

code
print(next(it))

Output:

code
0

Then:

code
print(next(it))

Output:

code
1

Then:

code
print(next(it))

Output:

code
4

Python internally calls:

code
sq[0]
sq[1]
sq[2]
...

until:

code
IndexError

is raised.

Why IndexError Matters

Suppose you forget:

code
raise IndexError

Then Python has no idea when iteration should stop.

The result:

code
Infinite iteration

or unexpected behavior.

Always define a termination condition.

Real-World Use Cases

Iteration powers many everyday tasks.

Processing Huge Files

Instead of loading everything:

code
content = file.read()

stream line-by-line:

code
for line in file:
    ...

Memory efficient.

Paginated APIs

Fetch data lazily:

code
page 1
page 2
page 3

only when needed.

Data Pipelines

Combine:

code
map()
filter()
zip()
itertools

without creating large intermediate lists.

Lazy Computations

Generate values on demand.

Avoid unnecessary work.

Large Numeric Ranges

code
range(1_000_000_000)

remains lightweight.

Common Pitfalls

Exhausted Iterators

Example:

code
it = iter([1, 2, 3])

Consume everything:

code
for x in it:
    pass

Second loop:

code
for x in it:
    print(x)

Produces nothing.

The iterator is exhausted.

Create a new iterator instead.

Infinite Iterables

Example:

code
import itertools

counter = itertools.count()

Never ends.

Use safeguards such as:

code
itertools.islice()

or explicit stop conditions.

Assuming Ordering

Sets:

code
{"a", "b", "c"}

are iterable.

But ordering should not be relied upon conceptually.

Iteration order may vary depending on implementation details.

A Bad Custom Iterable Design

Many beginners combine:

code
Container
+
Iterator

into the same object.

Example:

code
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

        city = self._cities[self._index]
        self._index += 1

        return city

Problem:

code
for city in cities:
    ...

works once.

Second loop:

code
for city in cities:
    ...

returns nothing.

The iterator already consumed itself.

The Better Design

Separate the container from the iterator.

Iterator

code
class CityIterator:
    def __init__(self, cities):
        self._cities = cities
        self._index = 0

    def __iter__(self):
        return self

    def __next__(self):
        if self._index >= len(self._cities):
            raise StopIteration

        city = self._cities[self._index]
        self._index += 1

        return city

Iterable

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

    def __iter__(self):
        return CityIterator(self._cities)

Now every loop receives a fresh iterator.

Much better.

Mental Model Diagram

code
[Iterable (Buffet Table)]
           |
           |
           | __iter__()
           | or
           | __getitem__()
           V
[Iterator (Plate)]
           |
           |
           | __next__()
           V
      Item by Item
           |
           V
   StopIteration

This single diagram explains most of Python's iteration system.

Quick Checklist

Before designing an iterable, ask:

Multiple Passes Needed?

Use:

code
Iterable

not a single iterator.

Memory Efficiency Important?

Use:

code
Iterator
Generator

instead of large lists.

Need to Test Iterability?

code
try:
    iter(obj)
except TypeError:
    ...

Need Controlled Termination?

Consider:

code
iter(callable, sentinel)

for custom stopping conditions.

TL;DR Quick Recap

  • Iterables are objects that can produce iterators.
  • iter() is Python's universal iteration adapter.
  • Iterables typically implement __iter__() or __getitem__().
  • Iterators track progress and return items one at a time.
  • Iterators use very little memory.
  • Built-in types like lists, strings, sets, dictionaries, and files are iterable.
  • Iterators become exhausted after use.
  • Good custom iterable design separates containers from iterators.
  • Lazy iteration enables scalable, memory-efficient programs.

Final Thoughts: Iterables Are the Foundation of Python 🧠

Almost every Python developer uses iteration every day.

But far fewer understand what's happening underneath.

The iterable protocol is one of Python's most elegant abstractions.

It allows completely different objects:

  • Lists
  • Files
  • Strings
  • Database cursors
  • API streams
  • Generators

to behave consistently inside:

code
for item in something:
    ...

Once you understand iterables, concepts like generators, iterators, comprehensions, and lazy evaluation become much easier to grasp.

And that's exactly where we're heading next.

A Little Python Joke to End On 😄

Why did the iterator bring a plate to the buffet?

Because it knew the buffet was reusable, but the plate only got one trip through the line.


Frequently Asked Questions

What is an iterable in Python?

An iterable is any object that can produce an iterator.

Examples include:

code
list
tuple
str
set
dict
file

What does iter() do?

iter() returns an iterator from an iterable object.

Example:

code
numbers = [1, 2, 3]

it = iter(numbers)

What is the difference between an iterable and an iterator?

An iterable produces iterators.

An iterator tracks progress and returns values one at a time.

Think:

code
Iterable → Buffet Table
Iterator → Plate

Can an object be iterable without __iter__()?

Yes.

Python can fall back to:

code
__getitem__()

using index-based access until IndexError.


Why are iterators memory efficient?

Because they store only:

  • A reference to the data
  • Current position

rather than copying all values.


What happens when an iterator finishes?

It raises:

code
StopIteration

which tells Python to stop looping.


Why can't exhausted iterators be reused?

Because they remember their current position.

Once the end is reached, they remain finished.

Create a new iterator instead.


Are sets iterable?

Yes.

Example:

code
for item in {"a", "b", "c"}:
    ...

works perfectly.

However, sets do not support indexing.


Why is range() memory efficient?

Because it stores only:

code
start
stop
step

and calculates values lazily when needed.


Should iterable containers return themselves from __iter__()?

Usually no.

Containers should generally return a fresh iterator object so multiple loops work independently.


Key Takeaways

  • Iterables provide values through the iteration protocol.
  • Python uses iter() and next() behind every for loop.
  • __iter__() is the primary mechanism for iteration.
  • __getitem__() can serve as a fallback.
  • Iterators are lightweight and memory efficient.
  • Lazy iteration helps process massive datasets.
  • Good iterable design separates containers from iterators.
  • Understanding iterables is the first step toward mastering generators and advanced Python internals.

Series Navigation

Part 1: Iterables — The Buffet Table 🍽️ (Current Article)

Part 2: Iterators — The Waiters with One-Way Memory 🍽️

Part 3: Generators — The Magical Chefs Cooking on Demand 👨‍🍳


If you found this helpful, share it with another Python developer and follow for more Python deep dives, language internals, software architecture discussions, and backend engineering insights. 🚀


About the Author

Anik Sikder is a Software Engineer specializing in Backend Development, Python, Django, FastAPI, DevOps, Cloud Infrastructure, and Software Architecture.

He writes about Python internals, scalable backend systems, system design, distributed architecture, and modern software engineering practices.

$ tags

pythoniterablesiteratorsgeneratorspython-internalscollectionsmemory-optimizationsoftware-engineeringpython-basics

$ ls related_articles

status: end_of_file