Skip to main content

loopinfo

loopinfo tells your for loop where it is: whether this is the first item, the last item, which number it is, and what comes before and after it. You write for item, loop in loopinfo(items): and then simply ask loop.first, loop.last or loop.index, with no counting and no index arithmetic.

The itch

A plain Python for loop hands you one item at a time, and that is all it tells you. It does not know whether you are at the beginning, in the middle or at the very end. Most of the time that is fine. But sooner or later you meet one of these small, everyday problems.

1. The trailing comma

You want to print a list of fruits separated by commas.

fruits = ["apple", "banana", "cherry"]

for fruit in fruits:
    print(fruit, end=", ")
apple, banana, cherry, 

Almost. There is an extra comma and space after the last fruit, because the loop had no way of knowing that "cherry" was the last one.

2. Putting "and" before the last item

You want to write a friendly sentence.

friends = ["Sara", "Omar", "Lina"]

sentence = "I invited "
for friend in friends:
    sentence += friend + ", "
print(sentence)
I invited Sara, Omar, Lina, 

A person would write "Sara, Omar, and Lina." To do that, the loop has to treat the last name differently. The usual workaround looks like this:

friends = ["Sara", "Omar", "Lina"]

sentence = "I invited "
for i, friend in enumerate(friends):
    if i == len(friends) - 1:
        sentence += "and " + friend + "."
    else:
        sentence += friend + ", "
print(sentence)
I invited Sara, Omar, and Lina.

It works, but look at how much you had to know to write that one line, if i == len(friends) - 1. You had to know that enumerate exists and gives you a counter. You had to know that the counter starts at 0, not 1. You had to know that len() counts from 1, so the last position is one less than the length. And you had to know that len() only works on things like lists. It does not work on a generator or a file, so this trick quietly breaks the day your data comes from somewhere else.

3. "Task 2 of 5"

You want to number your tasks the way a person would.

tasks = ["wake up", "make tea", "write code", "take a walk", "sleep"]

number = 0
for task in tasks:
    number += 1
    print("Task", number, "of", len(tasks), ":", task)
Task 1 of 5 : wake up
Task 2 of 5 : make tea
Task 3 of 5 : write code
Task 4 of 5 : take a walk
Task 5 of 5 : sleep

It does the job, but you are now keeping your own counter by hand, remembering to add 1 at the right moment, and asking for the length separately.

None of these problems is hard. They are just small itches that every Python learner scratches over and over, each time a little differently.

The scratch

Here are the same three problems with loopinfo. The only change to the loop itself is that you get a second name, loop, and you can ask it questions.

1. No trailing comma

from loopinfo import loopinfo

fruits = ["apple", "banana", "cherry"]

for fruit, loop in loopinfo(fruits):
    if loop.last:
        print(fruit)
    else:
        print(fruit, end=", ")
apple, banana, cherry

2. "and" before the last item

from loopinfo import loopinfo

friends = ["Sara", "Omar", "Lina"]

sentence = "I invited "
for friend, loop in loopinfo(friends):
    if loop.last:
        sentence += "and " + friend + "."
    else:
        sentence += friend + ", "
print(sentence)
I invited Sara, Omar, and Lina.

3. "Task 2 of 5"

from loopinfo import loopinfo

tasks = ["wake up", "make tea", "write code", "take a walk", "sleep"]

for task, loop in loopinfo(tasks):
    print(f"Task {loop.index} of {loop.count}: {task}")
Task 1 of 5: wake up
Task 2 of 5: make tea
Task 3 of 5: write code
Task 4 of 5: take a walk
Task 5 of 5: sleep

Notice what you did not have to think about: where counting starts, subtracting one, or keeping your own counter. You asked the question you actually had in mind, "is this the last one?", and got an answer.

Install

pip install loopinfo

loopinfo is written in plain Python, has no dependencies, and works on Python 3.9 and newer.

Everything loop knows

Attribute What it tells you
loop.first True on the first item, False on all the others.
loop.last True on the last item, False on all the others.
loop.index The position of this item, counting from 1, the way people count.
loop.index0 The position of this item, counting from 0, the way Python indexes lists.
loop.count The total number of items, or None if it cannot be known yet.
loop.remaining How many items are still to come after this one, or None if that cannot be known yet.
loop.even True on the 2nd, 4th, 6th item and so on.
loop.odd True on the 1st, 3rd, 5th item and so on, so the first item is odd.
loop.previous The item that came just before this one, or None on the first item.
loop.next The item that comes just after this one, or None on the last item.
loop.cycle(a, b, ...) Picks one of the values you give it in turn: a for the first item, b for the second, and round again.

The loop object is read-only. It describes the loop, so if you try to change it, for example loop.first = False, Python raises an AttributeError that explains why.

Here is a program that prints every one of these for each item in a three-item list:

from loopinfo import loopinfo

colors = ["red", "green", "blue"]

for color, loop in loopinfo(colors):
    print(f"--- {color} ---")
    print("first:    ", loop.first)
    print("last:     ", loop.last)
    print("index:    ", loop.index)
    print("index0:   ", loop.index0)
    print("count:    ", loop.count)
    print("remaining:", loop.remaining)
    print("even:     ", loop.even)
    print("odd:      ", loop.odd)
    print("previous: ", loop.previous)
    print("next:     ", loop.next)
    print("cycle:    ", loop.cycle("light", "dark"))
--- red ---
first:     True
last:      False
index:     1
index0:    0
count:     3
remaining: 2
even:      False
odd:       True
previous:  None
next:      green
cycle:     light
--- green ---
first:     False
last:      False
index:     2
index0:    1
count:     3
remaining: 1
even:      True
odd:       False
previous:  red
next:      blue
cycle:     dark
--- blue ---
first:     False
last:      True
index:     3
index0:    2
count:     3
remaining: 0
even:      False
odd:       True
previous:  green
next:      None
cycle:     light

Take a moment to read down one column, say index, and then across one item, say "green". Everything the loop knows about "green" is right there.

It works on anything you can loop over

A list knows its own length before you start. Many things you loop over do not. A generator makes its items one at a time, and a file is read line by line, so nobody knows how many there will be until the end arrives. loopinfo still knows when you have reached the last item.

Here is a small generator that counts down:

from loopinfo import loopinfo


def countdown(n):
    while n > 0:
        yield n
        n -= 1


for number, loop in loopinfo(countdown(3)):
    print(number, "| last:", loop.last, "| count:", loop.count)
3 | last: False | count: None
2 | last: False | count: None
1 | last: True | count: 3

And here is a file. Suppose shopping.txt contains four lines:

eggs
bread
milk
rice
from loopinfo import loopinfo

with open("shopping.txt") as file:
    for line, loop in loopinfo(file):
        item = line.strip()
        if loop.last:
            print(f"and finally {item}. That makes {loop.count} things.")
        else:
            print(f"{item} (count so far: {loop.count})")
eggs (count so far: None)
bread (count so far: None)
milk (count so far: None)
and finally rice. That makes 4 things.

In both examples, loop.last is correct the whole way through. loop.count, and with it loop.remaining, is None while the total is still unknown, and on the last item it becomes the real total. That is honest: until the end, there is no way to know.

How can loopinfo know an item is the last one when even the generator does not know? It looks one item ahead: before handing you an item, loopinfo quietly fetches the next one, and if there is no next one, the current item must be the last. This only matters if fetching an item has a side effect you can notice, such as printing a message or reading from a network connection, because that side effect will happen one step earlier than you might expect.

Alternating rows with cycle

loop.cycle() takes a few values and gives you them in turn, one per item, starting over when it runs out. It is handy for striped tables, taking turns, or sorting people into teams.

from loopinfo import loopinfo

students = ["Amal", "Badr", "Huda", "Yusuf", "Mona"]

for name, loop in loopinfo(students):
    shade = loop.cycle("light", "dark ")
    team = loop.cycle("Red", "Blue", "Green")
    print(f"[{shade}] {name:<6} plays for {team}")
[light] Amal   plays for Red
[dark ] Badr   plays for Blue
[light] Huda   plays for Green
[dark ] Yusuf  plays for Red
[light] Mona   plays for Blue

With two values you get a simple back-and-forth. With three, everyone takes turns in a circle.

Previous and next

Sometimes you need to compare an item with its neighbours. A common case is a sorted list where you want to notice the moment the value changes. Here we print a heading each time the first letter changes, and a blank line just before it is about to change:

from loopinfo import loopinfo

words = ["apple", "avocado", "banana", "blueberry", "cherry"]

for word, loop in loopinfo(words):
    letter = word[0]
    if loop.first or loop.previous[0] != letter:
        print(letter.upper() + ":")
    print("   ", word)
    if not loop.last and loop.next[0] != letter:
        print()
A:
    apple
    avocado

B:
    banana
    blueberry

C:
    cherry

Notice the order of the checks. On the first item there is no previous word, so loop.previous is None. By asking loop.first before touching loop.previous, we never try to look inside None. The same goes for loop.last and loop.next at the other end. And if your list can itself contain None as a real value, use loop.first and loop.last to find the edges, because None from previous or next could then mean either thing.

When you do not need loopinfo

Being honest: if all you want is to put commas between the items of a list of strings, Python already has a tidy one-liner for that.

fruits = ["apple", "banana", "cherry"]

print(", ".join(fruits))
apple, banana, cherry

That is simpler than any loop, and you should use it. loopinfo earns its place when the body of your loop does more than join text: when the first or last item needs different handling, when you want "3 of 10", when you compare neighbours, or when your data comes from a generator or a file and you cannot call len() on it.

How it works

When you call loopinfo(items), it first asks whether items has a length (lists, tuples, strings, dictionaries and ranges do) and remembers it if so. Then, as you loop, it always holds on to exactly two things: the item it is about to give you and the one after it. Each step, it builds a small read-only loop object describing the current position, hands you the item together with that object, and moves one step forward. When it tries to fetch the next item and finds there is none, it knows the current item is the last, and at that moment the total count becomes known as well. It never keeps a copy of the whole collection, so it is just as happy with a file of a million lines as with a list of three.

API

  • loopinfo(iterable) takes anything you can loop over and yields pairs of (item, loop), one for each item, and yields nothing at all for an empty iterable.
  • LoopInfo is the class of the read-only loop object, with the attributes first, last, index, index0, count, remaining, even, odd, previous, next and the method cycle(*values); it is fully type hinted and generic over the item type, so loop.previous and loop.next have the same type as your items.

Author

Khalid Sulaiman Al-Mulaify

Email: khalidpythonist@gmail.com

X: @Python__Task

License

MIT

Metadata

Release files for loopinfo 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for loopinfo 0.1.0
File Size Uploaded Provenance
loopinfo-0.1.0.tar.gz 14.9 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for loopinfo 0.1.0
File Interpreter ABI Platform
loopinfo-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 23.1 kB

Release files / loopinfo-0.1.0.tar.gz

Download URL loopinfo-0.1.0.tar.gz
Size 14.9 kB
Tags Source
SHA-256 checksum
How to use checksums
40f9b4f400970d364c75c2307e4afef405454d890ee556a5b9918f26bcce68bb
BLAKE2b-256 checksum
How to use checksums
1e2572911f2a95948d46a50a4315aeb295dc6b2891bbb533ca9db12f46ccac97
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.3

Release files / loopinfo-0.1.0-py3-none-any.whl

Download URL loopinfo-0.1.0-py3-none-any.whl
Size 8.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
c9d4fe885a7a50638faa65de841da60ae4f83b381054e4281090f4906f9d13aa
BLAKE2b-256 checksum
How to use checksums
695d0e98b31d00f1ef70c40e53b22e21aa86eb083b475c0333ce46b87020d747
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.3

Security for loopinfo 0.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page