Sentinel values in loops

A quick google search for loop sentinels reveals a lot of pages whose focus is using a sentinel value to determine when to end a loop.

IMO this misses the key (if you will excuse the expression) value of loop sentinels: preventing unnecessary code duplication. I'm going to use an example of a loop that simply dumps some data with subtotals.

First, a basic loop printing some data for a report:

data = (
    { 'foo': 'Foo 1', 'value': 1 },
    { 'foo': 'Foo 1', 'value': 3 },
    { 'foo': 'Foo 2', 'value': 2 },
)

def show_row(foo="", value=""):
    print(f'{foo:20} {value:>10}')

show_row('Foo','Value')

for row in data:
    show_row(**row)

Now, let's add subtotalling whenever the foo value changes in the loop (we are assuming already sorted data):

def show_row(foo="", value="", subtotal=False):
    if subtotal:
        show_row('====================','==========')
    print(f'{foo:20} {value:>10}')
    if subtotal:
        print()

show_row('Foo','Value')
previous_foo = None
subtotal = 0
for row in data:
    if previous_foo is not None:
        if previous_foo != row['foo']:
            show_row(previous_foo, value=subtotal, subtotal=True)
            subtotal = 0
    previous_foo = row['foo']
    subtotal += row['value']
        
    show_row(**row)

This is fine, but after the loop we still need to output the final subtotal (assuming there was any data):

if previous_foo is not None:
    show_row(previous_foo, value=subtotal, subtotal=True)

But these two lines duplicate code that already occurs in the loop. And duplicate code adds opportunity for bugs, either initially or if the code is later somehow modified in one place but not the other.

Adding a sentinel value to indicate the end of the data allows us to avoid this, with only very slight modifications to the loop:

for row in *data, None:
    if previous_foo is not None:
        if row is None or previous_foo != row['foo']:
            show_row(previous_foo, value=subtotal, subtotal=True)
            subtotal = 0
    if row is not None:
        previous_foo = row['foo']
        subtotal += row['value']
        
        show_row(**row)

Highlighting the differences:

$ diff -ub subtotal.py sentinel.py
--- subtotal.py	2025-06-16 16:03:31.168466216 -0400
+++ sentinel.py	2025-06-16 16:03:43.903079222 -0400
@@ -15,15 +15,13 @@
 show_row('Foo','Value')
 previous_foo = None
 subtotal = 0
-for row in data:
+for row in *data, None:
     if previous_foo is not None:
-        if previous_foo != row['foo']:
+        if row is None or previous_foo != row['foo']:
             show_row(previous_foo, value=subtotal, subtotal=True)
             subtotal = 0
+    if row is not None:
     previous_foo = row['foo']
     subtotal += row['value']
         
     show_row(**row)
-
-if previous_foo is not None:
-    show_row(previous_foo, value=subtotal, subtotal=True)

Full code examples can be found at [https://github.com/ysth/blog-examples/tree/main/sentinel-values-in-loops]