Python Strings, Conditions, and Loops
An export arrives with rows like $5,000 : a currency symbol, a thousands separator, and stray spaces. Before any total can be computed, the text has to be examined, trimmed, and converted, and rows that do not fit the expected shape have to be handled rather than silently dropped. That work needs three tools: string operations to inspect and reshape text, conditions to decide what each value deserves, and loops to apply the decision to every row.
Each code block below continues the same session, so run them in order in one notebook or Python prompt. Outputs were checked with Python 3.12; error messages can be worded differently in other versions.
Strings hold text and cannot be changed in place
A string is a sequence of characters written in single or double quotation marks; 'data' and "data" are the same value. Having both forms lets you include one kind of quotation mark inside the other. A backslash starts an escape sequence: \" is a quotation mark that does not end the string, \n is a line break, and \\ is a single backslash.
label = 'Order "A-17"'
note = "first line\nsecond line"
print(label)
# Order "A-17"
print(note)
# first line
# second line
print(len("Python"), "Hello" + " " + "World")
# 6 Hello World
print(repr("Danger! " * 3))
# 'Danger! Danger! Danger! '
+ joins two strings and adds no space of its own, so any separator must be written explicitly. * with an integer repeats a string. Subtraction and division have no meaning for text and raise TypeError. len() counts Unicode code points; one visible character can contain several code points, and repr() shows the value as Python would write it, which makes trailing spaces visible.
Strings are immutable: no operation changes an existing string in place. Methods such as replace() return a string result without modifying the original. Assign the result if you want a name to refer to it; an unchanged result need not be a newly allocated object.
code = "A-17"
try:
code[0] = "B"
except TypeError as error:
print("TypeError:", error)
# TypeError: 'str' object does not support item assignment
print(code.replace("A", "B"), code)
# B-17 A-17
Reaching into a string: indexing and slicing
Indexing reads one character by its position, counting from 0. Negative positions count from the end, so -1 is the last character. A slice text[start:stop] returns the characters from start up to but not including stop; omitting either side means “from the beginning” or “to the end.”
name = "Python"
print(name[0], name[5], name[-1])
# P n n
print(name[1:4], "pineapple"[:4], "pineapple"[4:])
# yth pine apple
try:
name[6]
except IndexError as error:
print("IndexError:", error)
# IndexError: string index out of range
print("cats".index("s"), "apple" in "pineapple", "banana" in "pineapple")
# 3 True False
A single index outside the string raises IndexError, but a slice quietly stops at the end of the string instead of failing. index() returns the position of the first occurrence and raises ValueError when the substring is absent; find() returns -1 instead. The in operator answers whether a substring occurs at all, which is usually what a condition needs.
Slicing by fixed positions is tempting for cleaning, and it breaks as soon as the input shifts by one character. Removing a known prefix and the separators states the intent directly:
raw = " $5,000 "
cleaned = raw.strip().removeprefix("$").replace(",", "")
print(repr(raw[1:]), repr(cleaned), float(cleaned))
# '$5,000 ' '5000' 5000.0
The leading space pushed every character one place to the right, so raw[1:] removed the space rather than the currency symbol. strip() removes whitespace at both ends, removeprefix() removes the symbol only when it is actually there, and replace() drops the thousands separator. These are normalization steps, not validation. Removing commas turns malformed "5,00" into "500", and removeprefix leaves a missing prefix unreported. Use this transformation only after the source contract establishes the currency and comma grouping, or validate those rules separately.
Formatting values for people to read
An f-string inserts values into text: put f before the opening quotation mark and write expressions inside braces. After a colon comes a format specification such as .2f for two decimal places, > or < for alignment with a width, and , for thousands separators. The older .format() method does the same with the values supplied after the string, which you will still meet in existing code.
order_id = "A-17"
total = 8.2925
print(f"{order_id}: {total:.2f}")
# A-17: 8.29
print("{}: {:.2f}".format(order_id, total))
# A-17: 8.29
print(f"|{'qty':>5}|{'item':<8}|")
# | qty|item |
print(f"{2.675:.2f}", f"{1234.5:,.2f}")
# 2.67 1,234.50
Formatting changes the display, not the stored value: total is still 8.2925 afterward. The rounding follows the stored binary value rather than the decimal you typed, which is why 2.675 displays as 2.67. For money, decide the rounding rule where the amount is computed and stored, and treat formatting as presentation only.
Booleans answer questions about values
A comparison produces a boolean: True or False. The operators are ==, !=, >, >=, <, and <=. For an integer and a string, 3 == "3" is False and 3 < "4" raises TypeError. Different types are not automatically incompatible: 3 == 3.0 and 1 < 2.5 are both True. The types’ comparison rules determine the result.
print(10 > 1, "cat" == "dog", 1 != 2, 3 == "3")
# True False True False
try:
3 < "4"
except TypeError as error:
print("TypeError:", error)
# TypeError: '<' not supported between instances of 'int' and 'str'
print("Apple" < "apple", "10" < "9", "yellow" > "cyan")
# True True True
Strings compare character by character using each character’s code point, not dictionary order: every uppercase ASCII letter sorts before every lowercase one, so "Apple" < "apple". Digits in text compare as characters too, which is why "10" < "9" while the numbers 10 and 9 compare the other way. Convert to numbers before comparing numeric text, and normalize capitalization before comparing names.
and, or, and not combine conditions. and requires both sides, or needs only one, and not reverses the result. Python also treats non-boolean values as true or false in a condition: False, None, numeric zero, and empty built-in collections count as false. Other objects follow their type’s truth-testing rules. The string "0" is not empty, so it counts as true.
print(bool(""), bool("0"), (25 > 50) or (1 != 2), not True)
# False True True False
print(1 < 2 < 3, "" or "fallback")
# True fallback
Comparisons can be chained: 1 < 2 < 3 reads as both comparisons joined by and. The middle expression in a chained comparison is evaluated only once. Both and and or return an operand and skip the right side when the left side determines the result, which is why the second line prints the fallback text. That behavior is convenient for defaults and misleading if you expected True.
Conditions choose which code runs
An if statement runs its block only when the condition is true. elif tests another condition when the previous ones were false, and else covers everything that remains. Indentation, not braces, marks which lines belong to a block, so the indentation must be consistent.
username = "dq"
if len(username) < 8:
status = "too short"
elif len(username) > 15:
status = "too long"
else:
status = "valid"
print(status)
# too short
age = 20
if age >= 18:
label = "adult"
else:
label = "minor"
print(label)
# adult
Only the first matching branch runs, so the order of the tests is part of the logic: a broad condition placed first hides the narrower ones below it. else is optional; if no branch assigns a result, an existing value remains, or the name stays undefined if no earlier assignment exists. A chain of elif tests is usually easier to read than conditions nested inside conditions.
The remainder operator % is a frequent building block for numeric conditions: number % 2 == 0 is true for even numbers, and 10 % 2 is 0 while 11 % 3 is 2.
Loops repeat the decision for every value
A for loop takes each value supplied by an iterable in turn and runs its block once per value; each run is one iteration. Strings are sequences of characters, so a for loop can walk through text. range() generates whole numbers: range(stop) starts at 0, range(start, stop) sets the first value, and a third argument sets the step. The stop value is never included.
for letter in "data":
print(letter)
# d
# a
# t
# a
print(list(range(5)), list(range(1, 10, 3)))
# [0, 1, 2, 3, 4] [1, 4, 7]
product = 1
for n in range(1, 10):
product = product * n
print(product)
# 362880
The factorial loop shows why the starting value matters: range(1, 10) covers 1 through 9, and starting at 0 would multiply everything by zero. With a positive step, the endpoint must lie on the progression and the stop must be greater than it, as in range(0, 101, 10) for 0 through 100.
for fahrenheit in range(0, 101, 50):
celsius = (fahrenheit - 32) * 5 / 9
print(f"{fahrenheit:>4}F {celsius:>7.1f}C")
# 0F -17.8C
# 50F 10.0C
# 100F 37.8C
A while loop repeats as long as its condition stays true, which suits work whose number of repetitions is not known in advance. In a counter-controlled loop, initialize the counter and update it toward a stopping condition. More generally, termination can come from a changed condition or break; an explicit counter update is not required in every while loop. break leaves the loop immediately, and continue skips the remaining body and rechecks a while condition or requests the next for item. A continue placed before a counter update can prevent progress.
secret = 14
guess = 8
attempt = 0
while attempt < 3:
attempt = attempt + 1
if guess == secret:
break
guess = guess + 3
print(attempt, guess, guess == secret)
# 3 14 True
The counter here does double duty: it limits the attempts and records how many were used. In an interactive program the guesses would come from input(), which always returns text and therefore needs conversion before a numeric comparison. A for loop does not require knowing the length in advance; files and generators can supply values as work proceeds. Use a for loop when consuming such values, and a while loop when a condition decides how long to continue.
| Question | for loop | while loop |
|---|---|---|
| What drives it | Values supplied by an iterable | A condition checked before each run |
| Suited to | Every row, character, or number in a range | Repeating while a condition is true |
| Main risk | Assuming the iterable is not empty | Failing to reach a stop condition or break |
Putting it together: clean, decide, report
The three tools now combine into the task from the opening: read each raw row, clean the amount, decide whether it needs review, and print a readable line. For this teaching example, the source contract uses one separator, a nonempty ID, one known currency, and valid comma grouping; an optional dollar prefix denotes that currency. Amounts must be finite and nonnegative. We report missing or unparseable amounts and malformed row boundaries. This is not a general currency parser or an accounting calculation.
import math
rows = ("A-17|$5,000 ", "B-02|$412.50", "C-31|",
"D-04|oops", "E-05|NaN", "broken row")
for row in rows:
if row.count("|") != 1:
print(f"{row!r} invalid row shape")
continue
separator = row.index("|")
order_id = row[:separator].strip()
if not order_id:
print(f"{row!r} missing order ID")
continue
amount_text = row[separator + 1:].strip().removeprefix("$").replace(",", "")
if not amount_text:
print(f"{order_id:<5} missing amount")
continue
try:
amount = float(amount_text)
except ValueError:
print(f"{order_id:<5} invalid amount {amount_text!r}")
continue
if not math.isfinite(amount) or amount < 0:
print(f"{order_id:<5} amount outside allowed range")
continue
if amount >= 1000:
print(f"{order_id:<5} {amount:>9,.2f} review")
else:
print(f"{order_id:<5} {amount:>9,.2f} ok")
# A-17 5,000.00 review
# B-02 412.50 ok
# C-31 missing amount
# D-04 invalid amount 'oops'
# E-05 amount outside allowed range
# 'broken row' invalid row shape
Each part is visible: slicing separates the identifier from the amount, string methods normalize the text, not amount_text uses the emptiness of a string as a condition, and the comparison against 1000 decides the label. The loop applies all of it to every row, and the format specifications line the output up in columns. The try/except handles a failed numeric conversion. The math module is part of Python’s standard library; math.isfinite rejects NaN and infinity, which float can parse without an error. The !r format uses repr to expose the rejected text. The print call reports the problem, and continue proceeds to another row; rows rejected by these checks are not labeled ok. Comma grouping and currency still need separate validation if the source guarantee is absent. In particular, removing commas does not detect “5,00”.
Practice
1. Predict the output of print("Total: " + 42), then fix the line two different ways.
Solution
It raises TypeError: can only concatenate str (not "int") to str, because + will not mix text and numbers. Either convert the number or use an f-string:
print("Total: " + str(42))
# Total: 42
print(f"Total: {42}")
# Total: 422. A file lists version labels as text: "9", "10", and "11". A colleague reports that "10" < "9" is True and calls it a bug. Explain the result and write a comparison that orders the versions numerically.
Solution
It is text comparison, not a bug: strings compare character by character, and the character "1" comes before "9", so the comparison stops there. Convert before comparing:
print("10" < "9", int("10") < int("9"))
# True False
This conversion fits integer-only version labels. Versions such as "1.10" need a version-specific comparison rule; converting them to float loses their component structure.
3. This loop should print the even numbers from 1 to 10, but it prints nothing. Find both problems: total = 0, then while total < 10: with the body if total % 2 == 1: print(total).
Solution
The condition selects odd numbers, and nothing updates total, so the loop body would repeat forever on the value 0 rather than finish. It prints nothing because 0 is even and the test asks for a remainder of 1. A for loop over a range states the intent without a counter to maintain:
for number in range(1, 11):
if number % 2 == 0:
print(number, end=",")
print()
# 2,4,6,8,10,
The same result with range(2, 11, 2) needs no condition at all, because the step already selects even numbers.
Discover more from Insightful Data Lab
Subscribe to get the latest posts sent to your email.
