Skip to content
VLSI Mentor

Python Part I · Module 10 · Loops

Counters and Accumulators

A loop that only prints has told you something and kept nothing. Almost every useful loop produces a result — a count of errors, a total runtime, a list of failing tests, a table of severities — and that result has to survive the loop that built it. The pattern for doing that is three lines long and does not change between those cases: create the result before the loop, update it inside, use it afterwards. It is the single most reused shape in automation code, which is why it is worth being precise about, and why the most common bug in that code is one of those three lines being in the wrong place.

Foundation17 min readPythonPart IFundamentalsLoopsAccumulators

Part I · Module 10 · Chapter 10.10 · Counters and Accumulators

1. Before, Inside, After

Every accumulating loop has the same three parts:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
<result> = <empty starting value>      # before the loop
for <item> in <collection>:
    <update result using item>         # inside the loop
<use result>                           # after the loop

The starting value is whatever "nothing yet" means for the kind of result: 0 for a count or a total, [] for a collection, {} for a table, "" for text.

Each part has to be in its own place. The next section shows the shape working; Section 3 shows what the middle one being in the wrong place costs.

2. Counting

The canonical log checker:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
lines = [
    "UVM_INFO  @0: reset asserted",
    "UVM_ERROR @120: crc mismatch",
    "UVM_INFO  @200: burst complete",
    "UVM_ERROR @410: unexpected BRESP",
]

errors = 0

for line in lines:
    if "UVM_ERROR" in line:
        errors += 1

print(errors)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
2

errors = 0 before, errors += 1 inside, print(errors) after. The += is Chapter 3.3's operator — errors = errors + 1 written shorter — and it is doing the only thing that makes the result survive: updating a name that lives outside the loop body.

That is the crux. The loop variable line is re-bound every pass and is gone in any useful sense when the loop ends. errors is not touched by the iteration at all, so what the body does to it accumulates.

3. The Reset in the Wrong Place

Move errors = 0 one line down, into the loop:

Azvya Education Pvt. Ltd.VLSI Mentor
BROKEN ON PURPOSE
lines = [
    "UVM_ERROR @120: crc mismatch",
    "UVM_INFO  @200: burst complete",
    "UVM_ERROR @410: unexpected BRESP",
]

for line in lines:
    errors = 0
    if "UVM_ERROR" in line:
        errors += 1

print("counted:", errors)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
counted: 1

There were two errors in that log. The program reported one.

To see why, record the counter's value at the end of every pass:

Azvya Education Pvt. Ltd.VLSI Mentor
BROKEN ON PURPOSE
lines = [
    "UVM_ERROR @120: crc mismatch",
    "UVM_INFO  @200: burst complete",
    "UVM_ERROR @410: unexpected BRESP",
]

errors_seen = []

for line in lines:
    errors = 0
    if "UVM_ERROR" in line:
        errors += 1
    errors_seen.append(errors)

print("per-pass counts:", errors_seen)
print("final value:", errors)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
per-pass counts: [1, 0, 1]
final value: 1

The counter is reset at the top of every pass, so it never accumulates anything. Each pass sets it to 0 and then records whether that one line was an error, and what survives the loop is the answer for the last line only — which happened to be an error, so the program reports 1.

That is worse than reporting 0. A zero would look suspicious in a log that obviously contains errors; a one looks like a real count and differs from the truth by exactly the amount nobody checks.

The correct version on the same data:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
lines = [
    "UVM_ERROR @120: crc mismatch",
    "UVM_INFO  @200: burst complete",
    "UVM_ERROR @410: unexpected BRESP",
]

errors = 0

for line in lines:
    if "UVM_ERROR" in line:
        errors += 1

print("counted:", errors)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
counted: 2

4. Totalling

Same shape, different update:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
runtimes = [120, 450, 90, 300]

total = 0

for runtime in runtimes:
    total += runtime

print("total runtime:", total, "seconds")
print("tests:", len(runtimes))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
total runtime: 960 seconds
tests: 4

A count and a total in one loop are two accumulators, not one:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
results = [("smoke", 120, "PASS"), ("burst", 450, "FAIL"), ("reset", 90, "PASS")]

passed = 0
failed = 0
total_time = 0

for name, runtime, status in results:
    total_time += runtime
    if status == "PASS":
        passed += 1
    else:
        failed += 1

print("passed", passed, "failed", failed, "total", total_time, "s")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
passed 2 failed 1 total 660 s

Note where each update sits. total_time += runtime is at the top of the body because every run contributes to it; the two counters are inside branches because each run contributes to exactly one of them.

5. Collecting Into a List

When you need the items rather than how many, the empty starting value is []:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
results = [("smoke", 120, "PASS"), ("burst", 450, "FAIL"), ("reset", 90, "FAIL")]

failed_names = []

for name, runtime, status in results:
    if status == "FAIL":
        failed_names.append(name)

print("failures:", failed_names)
print(len(failed_names), "of", len(results), "failed")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
failures: ['burst', 'reset']
2 of 3 failed

.append() from Chapter 5.3 is the update. The list is created once, before the loop, for the same reason the counter was.

6. Building Text — and Why Not Like This

The same shape appears to work for strings, and it is the one case where you should not use it:

Azvya Education Pvt. Ltd.VLSI Mentor
BROKEN ON PURPOSE
names = ["smoke", "reset", "burst"]

line = ""

for name in names:
    line += name + ", "

print(repr(line))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
'smoke, reset, burst, '

There is a comma and a space on the end that should not be there. Every loop-built string has this problem, and the usual responses — stripping the last two characters afterwards, or an if that skips the separator on the first pass — are both patches for having chosen the wrong tool.

Chapter 4.9 already gave the right one:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
names = ["smoke", "reset", "burst"]

print(repr(", ".join(names)))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
'smoke, reset, burst'

.join() puts the separator between items, so there are three names and two separators. No loop, no trailing comma, and no accumulator to misplace.

Build a list in the loop, then join it after. That keeps the accumulator pattern where it works and uses the right tool for the text:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
results = [("smoke", "PASS"), ("burst", "FAIL"), ("reset", "FAIL")]

failed_names = []

for name, status in results:
    if status == "FAIL":
        failed_names.append(name)

print("FAILED: " + ", ".join(failed_names))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
FAILED: burst, reset

7. Counting Per Category

When the answer is not one number but one number per kind, the accumulator is a dictionary:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
lines = [
    "UVM_ERROR @1: a",
    "UVM_INFO  @2: b",
    "UVM_ERROR @3: c",
    "UVM_WARNING @4: d",
    "UVM_INFO  @5: e",
]

counts = {}

for line in lines:
    severity = line.split()[0]
    counts[severity] = counts.get(severity, 0) + 1

for severity in sorted(counts):
    print(severity, counts[severity])
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
UVM_ERROR 2
UVM_INFO 2
UVM_WARNING 1

The update line is the whole technique. counts.get(severity, 0) is Chapter 8.3's safe lookup: it gives the running count for a severity already seen, or 0 for one met for the first time. Adding one and storing it back handles both cases without a branch.

Written with an if it is three lines and says the same thing:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
lines = ["UVM_ERROR @1: a", "UVM_INFO @2: b", "UVM_ERROR @3: c"]

counts = {}

for line in lines:
    severity = line.split()[0]
    if severity in counts:
        counts[severity] += 1
    else:
        counts[severity] = 1

print(counts)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
{'UVM_ERROR': 2, 'UVM_INFO': 1}

Both are correct. The .get() form is preferred because the first-time case is not special enough to deserve a branch.

Note the sorted() in the first version's report loop — dictionary order is insertion order, which here means order of first appearance in the log, and that is rarely what a reader wants from a summary table.

8. Tracking a Maximum

A maximum is an accumulator whose update is a comparison, and it has one wrinkle: what to start from.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
runtimes = [120, 450, 90, 300]

longest = runtimes[0]

for runtime in runtimes:
    if runtime > longest:
        longest = runtime

print("longest:", longest)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
longest: 450

Starting from runtimes[0] is correct and requires the list to be non-empty. Starting from 0 would look safer and is wrong for any data that can be negative — a temperature margin, a slack value — because every real value would be below the seed and the answer would come back as 0.

For a bare maximum Python already has max(), so the loop is only worth writing when you need something alongside the value — typically which item it was:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
results = [("smoke", 120), ("burst", 450), ("reset", 90)]

slowest_name = ""
slowest_time = 0

for name, runtime in results:
    if runtime > slowest_time:
        slowest_time = runtime
        slowest_name = name

print("slowest:", slowest_name, slowest_time)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
slowest: burst 450

Two accumulators updated together, inside one if, so they can never disagree about which run they describe. Updating one and forgetting the other is the classic failure here — the time from one test and the name from another.

9. The Shape, Five Times

ResultBefore the loopInside the loopAfter
a counterrors = 0errors += 1print(errors)
a totaltotal = 0total += runtimeprint(total)
a collectionfailed = []failed.append(name)len(failed), or join it
a tablecounts = {}counts[k] = counts.get(k, 0) + 1report, usually sorted()
a maximumbest = items[0]if x > best: best = xprint(best)

One skeleton, five fillings. Learning it as a shape rather than as five techniques is what makes the next one you need obvious.

10. Predict, Run, Observe

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
# A
total = 0
for n in [1, 2, 3, 4]:
    total += n
print("A:", total)

# B
for n in [1, 2, 3, 4]:
    total_b = 0
    total_b += n
print("B:", total_b)

# C
kept = []
for name in ["clk", "rst_n", "data"]:
    if len(name) > 3:
        kept.append(name)
print("C:", kept, len(kept))

# D
counts = {}
for ch in "axaxb":
    counts[ch] = counts.get(ch, 0) + 1
print("D:", counts)

The verified output:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
A: 10
B: 4
C: ['rst_n', 'data'] 2
D: {'a': 2, 'x': 2, 'b': 1}

A — the shape done correctly: 1 + 2 + 3 + 4.

B — Section 3's bug. The reset is inside, so total_b holds only the last item's contribution. 4 is a plausible total for this data, which is exactly what makes it dangerous.

C — "clk" is three characters, so it is excluded; the list carries the names and len() gives the count.

D — three distinct characters counted in one pass, in order of first appearance.

11. Exercises

Exercise 1 — count and collect. Given a list of log lines, count the UVM_ERROR lines and also collect them. Print the count twice — once from the counter, once from the list — and say which accumulator you would keep if you could have only one.

Exercise 2 — move the reset. Take Exercise 1's counter and move its initialisation inside the loop. Predict the output before running, then explain in two sentences why the result is not zero and not correct.

Exercise 3 — two counters. For results = [("a", "PASS"), ("b", "FAIL"), ("c", "FAIL")], count passes and failures in one loop, then print the pass rate as a percentage of the total.

Exercise 4 — total and average. Given a list of runtimes, print the total and the average. Say what you must check before dividing and why the loop cannot help you with it.

Exercise 5 — the trailing separator. Build a comma-separated string of three names with += and print it with repr(). Then do it with .join(). Explain in one sentence what repr() showed you that print() would have hidden.

Exercise 6 — severity table. Count each severity in a list of log lines using a dictionary and .get(). Print the table sorted by severity name, then print just the severity with the highest count.

Exercise 7 — longest with its name. Given [("smoke", 120), ("burst", 450), ("reset", 90)], find the slowest test and print both its name and its runtime. Then remove one of the two update lines and describe what the output becomes.

Exercise 8 — seed the maximum. Find the largest value in [-5, -2, -9] twice: once seeding from 0 and once from the first item. Explain the difference in one sentence, and say what each version does on an empty list.

12. Summary

  • Every accumulating loop is three parts: create the result before the loop, update it inside, use it after.
  • The starting value is "nothing yet" for the kind of result: 0, [], {}, "".
  • The accumulator must live outside the loop body, which in practice means its creation line sits at the same column as the for.
  • Creating it inside the loop resets it every pass, producing a plausible small number instead of an error.
  • += is the usual update for counts and totals; .append() for collections.
  • Collect the items rather than just counting them when the items might be wanted — len() gives you the count anyway.
  • Do not build strings with += in a loop. It leaves a trailing separator every time; build a list and .join() it.
  • A dictionary accumulator counts per category, and counts.get(k, 0) + 1 handles the first sighting without a branch.
  • Report a dictionary with sorted() unless order of first appearance is genuinely what you want.
  • A maximum seeded from 0 is wrong for data that can be negative — seed from the first item.
  • Track a value and its name as two accumulators updated together, inside the same if, so they cannot disagree.

The next chapter puts one loop inside another, and counts how many times the inner body really runs.

Where this fits

Part of the Python curriculum.