Validating Data with Loops

Introduction: Hunting for the First Acceptable Value

Welcome to the final unit of Controlling Python Loops with While, Break, and Continue! Our toolbox is now nearly complete: Unit 1 gave us condition-driven repetition, Unit 2 gave us break for leaving a loop the moment our work is done, and Unit 3 gave us continue for politely ignoring a single unwanted item. In this unit, we finally put all of them in the same program.

Here is the scenario. An application stored three answers for someone's age field: "unknown", "135", and "42". The first is a typo, the second is not a plausible human age, and the third is exactly what we want. Our job is not to process all three or merely to skip the bad ones; it is to find the first value that passes every requirement and then stop looking.

That is the shape of almost every validation task: reject what fails, keep moving, and accept the first winner. We will build this age_candidates program piece by piece, one rule at a time.

What Makes Validation Different

Until now, our loops have had a fixed relationship with the data: a for loop touches every element, and continue merely trims a few of them from the calculation. Validation changes the goal. We are no longer summarizing a collection; we are hunting for a single acceptable item, and everything after that item is irrelevant.

A validation loop therefore has three jobs happening at once:

  • Walk through the stored candidates one at a time.
  • Judge each candidate against a set of rules, rejecting the ones that fail and explaining why.
  • Remember the winner, so that code after the loop knows whether the hunt succeeded.

Notice that the last job is new. A loop that searches can end in two very different ways: it finds something, or it exhausts the data. Our program has to be able to tell those two endings apart, and that requirement will shape how we set up our variables.

The Validation Loop Skeleton

Before writing a single rule, let's lay down the structure that every validation loop shares: the data, a position counter, and a place to store the result.

# Examine stored candidates until finding a valid number in range
age_candidates = ["unknown", "135", "42"]
index = 0
age = None

while index < len(age_candidates):
    raw = age_candidates[index]
    index += 1

Three details deserve attention here:

  • age = None is our "not found yet" marker. Unlike 0 or "", None can never be confused with a real answer, so if age is still None after the loop, we know with certainty that nothing was accepted.
  • The header index < len(age_candidates) guarantees that we stop when we run out of candidates, and raw = age_candidates[index] reads the current one into a working variable.
  • index += 1 sits immediately after the read, not at the bottom of the body. As we saw in Unit 3, an update placed below a continue never runs for skipped items, and the loop freezes forever on the same element. Advancing first makes that bug impossible.

Rule 1: Checking the Format with isdecimal()

Stored data usually arrives as text, so our first rule asks a simple question: Does this string actually represent a whole number? Writing int("unknown") would crash the program immediately, so we check before we convert.

    if not raw.isdecimal():
        print("Rejected:", raw, "- digits required.")
        continue

The string method isdecimal() returns True only when every character is a base-10 digit with no sign and no decimal point, and False otherwise. That is a narrower, safer check than the similarly named isdigit(): a handful of Unicode characters, such as the superscript "²", satisfy isdigit() while still making int() raise a ValueError. Every character accepted by isdecimal(), by contrast, is one that int() knows how to convert, which is exactly the guarantee we need before the next line runs. When the check fails, we print the reason and hand control straight back to the loop header with continue, exactly as in the guard style from the previous unit. The candidate "unknown" is rejected here.

Keep in mind what isdecimal() treats as not a decimal string: an empty string "", a negative like "-5", and a value with a decimal point like "3.5" all return False. That is strict, and for an age field, strict is precisely what we want.

Rule 2: Converting and Checking the Range

Any candidate that survives the first guard is guaranteed to be composed only of decimal digits, so conversion with int() is now safe. That lets us apply a second, stricter rule: the number must be a plausible age.

    candidate = int(raw)
    if candidate < 0 or candidate > 120:
        print("Rejected:", candidate, "- outside the valid range.")
        continue

The ordering matters enormously: candidate = int(raw) is reachable only because the format guard already sent every nonnumeric value away. Guards stack like a series of doors, each one making the next line safer.

You may recall chained comparisons from the earlier course, where 0 <= candidate <= 120 expresses this same rule in positive form. Both are correct, but the guard style needs the negative phrasing because a guard describes the failure it is catching. Here, "135" converts to 135, trips the upper bound, and is rejected.

Accepting a Value with Break

A candidate that clears both doors has proven itself. Now we record it and end the search.

    age = candidate
    break

Storing age = candidate promotes our local working value into the result variable that lives outside the loop, and break leaves immediately. Why use break rather than letting the loop finish? Because once we hold a winner, every remaining candidate is irrelevant; continuing would waste work and, worse, keep overwriting age with each later valid value, leaving us with the last acceptable candidate instead of the first.

Assembled, the loop body reads as a clean, top-to-bottom policy:

while index < len(age_candidates):
    raw = age_candidates[index]
    index += 1

    if not raw.isdecimal():
        print("Rejected:", raw, "- digits required.")
        continue

    candidate = int(raw)
    if candidate < 0 or candidate > 120:
        print("Rejected:", candidate, "- outside the valid range.")
        continue

    age = candidate
    break

Reporting the Outcome After the Loop

The loop is finished, but the program is not: someone has to announce the result. This code sits after the loop, at the outer indentation level.

if age is not None:
    print("Age accepted:", age)
else:
    print("No valid age was found.")

Remember that a validation loop has exactly two exit routes: break fires, meaning we found a winner, or the header condition becomes false, meaning we run out of candidates. The loop itself does not tell us which happened, so we inspect the result variable instead; that is the entire reason age was initialized to None.

Note the test age is not None rather than the shorter if age:. The short form treats 0 as false, so a perfectly legal age of 0 for a newborn would be reported as a failure. Comparing against None asks the right question: Was anything stored at all?

Tracing the Full Program

Let's confirm our reasoning by following all three candidates through the loop, pass by pass.

Passindex at readrawGuard that firedActionage after
10"unknown"formatcontinueNone
21"135"rangecontinueNone
32"42"nonebreak42

Running the complete program prints:

Rejected: unknown - digits required.
Rejected: 135 - outside the valid range.
Age accepted: 42

The first two lines are the rejection messages, each naming the rule that was violated, and the third comes from the report after the loop. There is no fourth pass: break ended the search during pass 3, and the header condition was never even rechecked.

Adapting the Pattern to Other Rules

The real value of this pattern is that the skeleton never changes; only the guards do. Swapping in different rules gives us a different validator with the same bones.

    # Rule variation: value must belong to a fixed set of choices
    if choice not in ["add", "remove", "quit"]:
        print("Rejected:", choice, "- not a valid command.")
        continue

    # Rule variation: value must not be blank or all spaces
    if raw.strip() == "":
        print("Rejected: empty entry.")
        continue

The first guard uses not in for a membership check against allowed options, and the second uses strip() to remove surrounding whitespace so that " " is treated as empty. Both plug into the same loop, and the reusable recipe behind them consists of only four steps:

  1. Read the current candidate into a variable.
  2. Advance the index right away.
  3. Reject failures with a message and continue.
  4. Accept the first survivor with a store and break.

Nothing stops us from stacking more than one guard in front of the same candidate, either. A single value can be required to pass a blank check and a length check and anything else the situation calls for, one guard after another, before it is finally accepted.

Conclusion and Next Steps

In this lesson, we combined every tool from the course into one coherent program. We built the validation skeleton with a candidate list, an index, and a None result marker; we stacked guards so that a format check with isdecimal() protected a later conversion and range check; we accepted the first winner with break and explained why the first differs from the last; and we reported the outcome afterward using is not None, the test that survives a legitimate 0.

That also brings this course to a close, so congratulations on finishing Controlling Python Loops with While, Break, and Continue! You can now write loops whose repetition is driven by a condition, leave them early with break, skip individual items with continue, and assemble all three into a validator that judges real data.

Up next, the practice set puts the pattern through its paces: finding a decimal-digit string and converting it, locating the first number inside a required range, matching a choice against a fixed menu, and combining a blank check with a minimum-length rule to find the first genuinely usable label, including the case where nothing in the list qualifies. After that, the course Solving Problems with Loop Patterns generalizes these habits into reusable search, aggregation, transformation, and nested-loop patterns. Let's finish this course strong: Time to validate!

Sign up

Join the 1M+ learners on CodeSignal

Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal