Skip to content

2. Conditional statements and loops ​

Session 2practice: 02-conditions-and-loops/ in the homework repositorythe sample data

Today you will learn to:

  • say what is "true" and "false" in Python, and compare values without surprises;
  • combine conditions with and, or, not;
  • write decisions with if / elif / else - the Python version of SQL CASE WHEN;
  • choose a value with a conditional expression, and a branch with match;
  • repeat work with while and for, and stop or skip with break and continue;
  • loop over the rows of a file, the way a SAS DATA step reads a data set;
  • build a small report and a repayment schedule with loops.

From SQL to Python - the same question, two ways:

sql
SELECT CASE WHEN days_past_due = 0  THEN 'current'
            WHEN days_past_due <= 30 THEN '1-30'
            WHEN days_past_due <= 90 THEN '31-90'
            ELSE '90+' END AS bucket,
       COUNT(*)
FROM loans
GROUP BY 1;
python
current = up_to_30 = up_to_90 = over_90 = 0

with open("data/loans.csv", encoding="utf-8") as file:
    file.readline()                                  # skip the header line
    for line in file:                                # one loan per line
        days_past_due = int(line.split(",")[9])      # the 10th column
        if days_past_due == 0:
            current += 1
        elif days_past_due <= 30:
            up_to_30 += 1
        elif days_past_due <= 90:
            up_to_90 += 1
        else:
            over_90 += 1

print("current", current)
print("1-30   ", up_to_30)
print("31-90  ", up_to_90)
print("90+    ", over_90)
Edit the code and run it again

By the end of today you will be able to read every line of this example. In session 15 pandas does the same in two lines - but it does exactly this inside.

1. True and False ​

The type bool ​

A Boolean value (after the mathematician George Boole) is one of two values: True or False. They are values, like 0 and 1 - not names of variables. Note the capital letters.

python
print(type(True), type(False))
print(3 > 2)
print(type(3 > 2))
Edit the code and run it again

Booleans are the result of comparisons (section 2) and the input of every decision (sections 4 - 8) and of every while loop (section 11).

What is true and what is false ​

if and while accept any value, not only True and False. Python decides by the rules you met in session 1 (truthy and falsy):

TypeFalseTrue
numbers0, 0.0every other number, also -1 and 0.00001
strings"" (empty)every non-empty string, also " ", "0", "False"
collections (lists, tuples, sets, dictionaries)emptynot empty
Nonealways-

bool() shows the decision:

python
print(bool(-42), bool(0), bool(0.00001))
print(bool("ada"), bool(""), bool(" "))
print(bool(None), bool([]), bool([0]))
Edit the code and run it again

This is useful with data from files. In clients.csv, retail clients have an empty industry code - and an empty string is false:

python
industry = ""              # a retail client: the field is empty in the file

if industry:
    print("industry:", industry)
else:
    print("no industry code")
Edit the code and run it again

0 is false, None is false - but they are not the same

if days_past_due: is false both for 0 (a loan that is not late) and for None (the value is missing). When "missing" and "zero" mean different things - and in risk data they usually do - test for None explicitly: if days_past_due is None:.

2. Comparisons ​

The operators ​

The result of a comparison is always True or False:

OperatorMeaningSQLSAS
<less than<<, LT
<=less than or equal<=<=, LE
>greater than>>, GT
>=greater than or equal>=>=, GE
==equal==, EQ
!=not equal<>^=, NE
is, is notthe same object (identity)IS NULL (only for None)
in, not inis a member ofIN (...)IN (...)
python
days_past_due = 45
print(days_past_due < 45, days_past_due <= 45)
print(9 < 1000)
print(days_past_due != 0)

product = "mortgage"
print(product in ("mortgage", "consumer"))     # like SQL: product IN ('mortgage', 'consumer')
print("age" in "mortgage")                     # in a string: is it a part of it?
Edit the code and run it again

is compares objects, not values (session 1, animation "Names and objects"). Use it only with None: x is None, x is not None.

Different types ​

Python does not compare "apples with oranges". <, >, <=, >= between a number and a string are an error:

python
print(2 < "1")
Edit the code and run it again

== does not give an error - it is simply False. This is a real trap with data from files, where every value is a string:

python
stage = "3"                # read from a CSV file: a string
print(stage == 3)          # False - a str is never equal to an int
print(int(stage) == 3)     # True - convert first
Edit the code and run it again

Numbers of different types compare as you expect: 1 == 1.0 is True, and True == 1 is True too.

Best practice

Make sure you compare values of the same type. Convert the data first - int(), float(), str() (session 1) - and then compare.

python
print("2" == str(2))
print(int("2") >= 2)
print(99999 >= int("2"))
Edit the code and run it again
Python 2 was different

Python 2 could compare values of different types: "2" >= 2 was True, 99999 >= "2" was False - without any error, and without any sense. If you meet old code that does this, it is a bug in Python 3.

Strings are compared letter by letter ​

Strings are compared lexicographically, like words in a dictionary: the first characters first; if they are equal, the second ones, and so on. Each character is compared by its number (its code point) in the Unicode table, and ord() shows that number:

python
print("9" < "1000")          # False!
print(ord("9"), ord("1"))    # 57 49: "9" is after "1"
print("Z" < "a")             # True: all capital letters come before the small ones
print("mortgage" < "overdraft")
Edit the code and run it again

"9" < "1000" is False, because only the first characters decide: "9" (57) is not less than "1" (49). Numbers stored as text sort wrongly - convert them first.

Dates in the ISO format YYYY-MM-DD (the format of the sample data) are an exception that works: the year comes first, then the month, then the day, all with a fixed number of digits. In the Bulgarian format DD.MM.YYYY they do not:

python
print("2026-01-15" < "2026-09-30")      # True - right
print("15.01.2026" < "30.09.2025")      # True - wrong: "1" < "3"
Edit the code and run it again

Real dates (datetime) come in session 7.

Chained comparisons ​

Comparisons can be chained, like BETWEEN in SQL (session 1):

python
days_past_due = 45
print(30 < days_past_due <= 90)      # the same as: 30 < days_past_due and days_past_due <= 90
x = 5
print(0 < x < 10)
Edit the code and run it again

Floats ​

Two floats that "should" be equal may not be (session 1, section 14). Compare them with a tolerance:

python
import math

share = 0.1 + 0.2
print(share == 0.3)
print(math.isclose(share, 0.3))
Edit the code and run it again

3. Logical operators ​

and, or, not ​

Logical operators combine simple conditions into bigger ones - "the loan is more than 90 days late or the client is in default":

OperationResult
x or yif x is false, then y, else x
x and yif x is false, then x, else y
not xif x is false, then True, else False

The truth table (Truth table) - which operand is the result is in brackets:

xyx and yx or ynot x
TrueTrueTrue (y)True (x)False
TrueFalseFalse (y)True (x)False
FalseTrueFalse (x)True (y)True
FalseFalseFalse (x)False (y)True
python
days_past_due = 120
rating = 6

print(days_past_due > 90 or rating == 10)       # stage 3?
print(days_past_due > 30 and rating >= 7)
print(not days_past_due > 90)
print(True and False)
Edit the code and run it again

Python stops as soon as it knows ​

and and or evaluate the right side only if needed (short-circuit evaluation). When x is false, x and y is false whatever y is - so y is not even calculated. This protects you from errors:

python
count = 0
total = 0.0

# total / count would be ZeroDivisionError - but it is never calculated
print(count > 0 and total / count > 1000)
Edit the code and run it again

and and or return one of their operands ​

and and or do not always return True or False: they return one of the two values. not always returns a bool.

python
print(0 and 1)
print(0 or 1)
print(1 or 0)
print(not 1, not 0)

user_name = ""
print(user_name and "Anonymous")
print(user_name or "***Anonymous***")
Edit the code and run it again

x or default is a short way to give a default value - like COALESCE(x, default) in SQL. But be careful: it replaces every falsy value, also 0:

python
rate = 0.0                      # a real rate of 0%: a promotional loan
print(rate or 5.0)              # 5.0 - wrong!
print(rate if rate is not None else 5.0)    # 0.0 - right (section 7)
Edit the code and run it again

Order: not, then and, then or ​

Like * before +: not is applied first, then and, then or (the full table is in session 1, section 11). Use brackets when you mix and and or:

python
days_past_due, rating, restructured = 10, 8, False

print(days_past_due > 30 or rating >= 7 and restructured)       # or (rating >= 7 and restructured)
print((days_past_due > 30 or rating >= 7) and not restructured)
Edit the code and run it again

SQL NULL and SAS missing values

In SQL, NULL AND TRUE is NULL (unknown), and a WHERE drops the row. In SAS, a missing value in a condition counts as false. In Python, None is simply falsy: None and True is None, and if None: does not run its block.

Animation

How an expression is evaluated (session 1): 5. Chained comparisons, 6. and / or

4. The if statement ​

Control flow ​

Normally a program runs one statement after the other, from top to bottom. The control flow statements change that: they run a block only when a condition is true (if), or run it again and again (while, for).

Syntax ​

text
if condition:
    block
  • After if comes the condition, then a colon :.
  • The block is the lines indented under it (4 spaces). It runs only if the condition is true (truthy). There are no braces and no end - the indentation says where the block ends (session 1, section 8).
  • The condition can be any expression: Python takes its truth value (section 1).
python
days_past_due = 120

if days_past_due > 90:
    print(f"{days_past_due} days: non-performing")
    print("move to stage 3")
print("checked")
Edit the code and run it again

The same in SAS and SQL:

sas
if days_past_due > 90 then do;
    put "non-performing";
    stage = 3;
end;
sql
CASE WHEN days_past_due > 90 THEN 3 END

The indentation decides ​

python
if False:
    print("1")
    print("2")
print("3")
Edit the code and run it again

print("1") and print("2") form the block: they run only if the condition is true. print("3") is not in the block - it always runs.

pass: a block that does nothing ​

Python does not allow an empty block. When you need one - for now, or on purpose - write pass:

python
rating = 3

if rating <= 4:
    pass            # investment grade: nothing to do (yet)
print("done")
Edit the code and run it again

pass works in every block: if, for, while, functions (session 4).

5. if - else ​

text
if condition:
    block 1
else:
    block 2

Exactly one of the two blocks runs: block 1 when the condition is true, block 2 when it is false.

python
x = 41
if x % 2 == 0:
    print(f"{x} is an EVEN number")
else:
    print(f"{x} is an ODD number")
Edit the code and run it again
python
report_language = "bg"

if report_language == "bg":
    print("Портфейл към 30.09.2026")
else:
    print("Portfolio on 30.09.2026")
print("-" * 30)
Edit the code and run it again

On the portfolio:

python
days_past_due = 0

if days_past_due > 0:
    status = "late"
else:
    status = "current"
print(status)
Edit the code and run it again

6. if - elif - else ​

Syntax ​

text
if condition 1:
    block 1
elif condition 2:
    block 2
elif condition 3:
    block 3
else:
    block 4

elif means "else if". There can be as many elif parts as you need, and the else at the end is optional. Python tests the conditions from top to bottom and runs only the first block whose condition is true; the rest are skipped. This is exactly how CASE WHEN works in SQL.

The flow of if / elif / else: the conditions are tested in order, the first true one runs its block, and the program continues after the whole statement

python
user_language = "it"

if user_language == "bg":
    print("Здравейте")
elif user_language == "it":
    print("Ciao")
elif user_language == "en":
    print("Hello")
else:
    print("I do not speak your language!")
print("-" * 20)
Edit the code and run it again

CASE WHEN in Python ​

The buckets of days past due:

sql
CASE WHEN days_past_due = 0   THEN 'current'
     WHEN days_past_due <= 30 THEN '1-30'
     WHEN days_past_due <= 60 THEN '31-60'
     WHEN days_past_due <= 90 THEN '61-90'
     ELSE '90+'
END
sas
if days_past_due = 0 then bucket = 'current';
else if days_past_due <= 30 then bucket = '1-30';
else if days_past_due <= 60 then bucket = '31-60';
else if days_past_due <= 90 then bucket = '61-90';
else bucket = '90+';
python
days_past_due = 45

if days_past_due == 0:
    bucket = "current"
elif days_past_due <= 30:
    bucket = "1-30"
elif days_past_due <= 60:
    bucket = "31-60"
elif days_past_due <= 90:
    bucket = "61-90"
else:
    bucket = "90+"

print(bucket)
Edit the code and run it again

The second condition does not need days_past_due > 0 and ...: if we got there, the first one was already false.

Animation

Loops step by step: 2. Decisions in a loop

The order matters ​

Because the first true condition wins, the order of the conditions is part of the logic. Find the bug:

python
days_past_due = 120

if days_past_due > 0:
    bucket = "late"
elif days_past_due > 90:
    bucket = "non-performing"
else:
    bucket = "current"

print(bucket)
Edit the code and run it again
Answer

120 is more than 0, so the first block runs and Python never tests > 90: the loan is only "late". Put the strictest condition first: > 90, then > 0.

No gaps between the ranges ​

The rating scale of the sample data gives a grade for each band of the probability of default (PD). A first try, with both ends of each band:

python
pd_value = 0.00055          # between 0.0005 and 0.0006

if 0 <= pd_value <= 0.0005:
    grade = "AAA"
elif 0.0006 <= pd_value <= 0.001:
    grade = "AA"
elif 0.0011 <= pd_value <= 0.0025:
    grade = "A"
else:
    grade = "not rated"

print(grade)
Edit the code and run it again

0.00055 falls into the gap between two bands, and the loan gets "not rated". With decimal numbers there is always something between two values you write. The safe way: test only the upper bound, in order - every value lands in exactly one band:

python
pd_value = 0.00055

if pd_value < 0.0005:
    grade = "AAA"
elif pd_value < 0.001:
    grade = "AA"
elif pd_value < 0.0025:
    grade = "A"
elif pd_value < 0.006:
    grade = "BBB"
else:
    grade = "BB or worse"

print(grade)
Edit the code and run it again

Nested if or and? ​

An if can be inside another if. Often and says the same in a flatter way - "Flat is better than nested" (the Zen of Python):

python
segment = "retail"
outstanding = 250000.0

# nested
if segment == "retail":
    if outstanding > 200000:
        print("large retail exposure")

# the same, flat
if segment == "retail" and outstanding > 200000:
    print("large retail exposure")
Edit the code and run it again

Example: the IFRS 9 stage ​

The rule of the sample data: stage 3 when the loan is more than 90 days past due or the client is in default (rating 10); stage 2 when it is more than 30 days past due or the rating is 7 or worse; otherwise stage 1.

python
days_past_due = 35
rating = 5

if days_past_due > 90 or rating == 10:
    stage = 3
elif days_past_due > 30 or rating >= 7:
    stage = 2
else:
    stage = 1

print(stage)
Edit the code and run it again

7. Conditional expressions ​

value_1 if condition else value_2 ​

When an if - else only chooses between two values, a conditional expression (in other languages: the ternary operator) says it in one line:

text
x if condition else y

Python evaluates the condition; if it is true, the result is x, otherwise y.

python
days_past_due = 0
status = "late" if days_past_due > 0 else "current"
print(status)

user_age = 5
print("adult" if user_age >= 18 else "child")
Edit the code and run it again

The same as:

python
user_age = 5
if user_age >= 18:
    print("adult")
else:
    print("child")
Edit the code and run it again
SQLSASPython
CASE WHEN dpd > 0 THEN 'late' ELSE 'current' ENDifc(dpd > 0, 'late', 'current')"late" if dpd > 0 else "current"
COALESCE(rate, 5)coalesce(rate, 5)rate if rate is not None else 5

Conditional expressions can be nested, but two of them in one line are already hard to read - use if / elif / else instead.

Statement or expression? ​

  • An expression gives a value: 2 + 3, rate * 2, "late" if dpd > 0 else "current", a function call. You can print it, or give it a name.
  • A statement does something: x = 5, if, for, while, import. It has no value.

Expressions are parts of statements: in x = 5 + 3, 5 + 3 is an expression and the whole line is a statement. That is why a conditional expression can stand on the right of =, but an if statement cannot:

python
dpd = 10
status = if dpd > 0: "late"         # SyntaxError: if is a statement
Edit the code and run it again

8. match ​

One value, many cases ​

match (since Python 3.10) compares one value with a list of patterns, given in case blocks, and runs the first one that fits. It is like SELECT / WHEN in a SAS DATA step, or switch in C and Java:

sas
select (product);
    when ('mortgage', 'consumer', 'credit_card', 'overdraft') group = 'retail';
    when ('working_capital', 'investment')                    group = 'business';
    otherwise                                                 group = 'other';
end;
python
product = "credit_card"

match product:
    case "mortgage" | "consumer" | "credit_card" | "overdraft":
        group = "retail"
    case "working_capital" | "investment":
        group = "business"
    case _:
        group = "other"

print(group)
Edit the code and run it again
  • | means "or": the case fits any of the values.
  • case _: fits everything - the default, like otherwise and ELSE. Write it last.
  • Only one case runs; there is no "fall through" to the next case as in C.

match is best for fixed values: codes, products, menu choices. For ranges of numbers, use if / elif.

Advanced: conditions in a case (guards), and a trap with names

After a pattern, if adds a condition (a guard). case dpd if dpd > 90: means "take the value, call it dpd, and fit only if dpd > 90":

python
days_past_due = 45

match days_past_due:
    case 0:
        bucket = "current"
    case dpd if dpd <= 30:
        bucket = "1-30"
    case dpd if dpd <= 90:
        bucket = "31-90"
    case _:
        bucket = "90+"

print(bucket)
Edit the code and run it again

For ranges of numbers, if / elif is clearer.

A name in a case catches everything. In a case, a plain name is not compared - it captures the value. This case fits every segment:

python
RETAIL = "retail"
segment = "corporate"

match segment:
    case RETAIL:                 # not "== RETAIL": it binds the name RETAIL!
        print("retail")

print(RETAIL)                    # corporate - the constant is lost too
Edit the code and run it again

With a case _: after it, Python even refuses to run the code: SyntaxError: name capture 'RETAIL' makes remaining patterns unreachable. Write the values themselves (case "retail":), or dotted names (case Segment.RETAIL:, session 11).

Before Python 3.10: if - elif chains ​

Older code (and older courses) write the same with if / elif. A typical program menu (this one needs input(), so try it in VS Code):

python
print("Select a report:")
print("1. Portfolio by product")
print("2. Portfolio by stage")
print("3. Late loans")

choice = input("Enter a number [1-3]: ")
if choice == "1":
    print("Report 1 ...")
elif choice == "2":
    print("Report 2 ...")
elif choice == "3":
    print("Report 3 ...")
else:
    print("Wrong input!")

The same with match:

python
match input("Enter a number [1-3]: "):
    case "1":
        print("Report 1 ...")
    case "2":
        print("Report 2 ...")
    case "3":
        print("Report 3 ...")
    case _:
        print("Wrong input!")
Advanced: patterns with several values

A pattern can also take a tuple apart (tuples: session 3). Each name in the pattern gets its part of the value:

python
record = ("L000002", "investment", 35)

match record:
    case (loan_id, _, 0):
        print(loan_id, "current")
    case (loan_id, product, dpd) if dpd > 30:
        print(loan_id, product, "more than 30 days late:", dpd)
    case _:
        print("other")
Edit the code and run it again

More: match statements in the tutorial, and the pattern matching tutorial (PEP 636).

9. Operator precedence ​

Optional

Not needed for the tasks, the homework or the next sessions - read it when you have time.

A recap of session 1, section 11, for conditions.

The order in which the parts of an expression are calculated decides the result. Wrong assumptions about it lead to bugs:

python
print(2 + 3 * 4)        # 14, not 20
print((2 + 3) * 4)      # 20
Edit the code and run it again

The short version of the table (the full one in session 1, section 11, and in the Python reference): arithmetic, then comparisons (<, ==, in, is, ...), then not, then and, then or, and the conditional expression ... if ... else ... last of all.

  • Use brackets for clarity. Even when you know the rules, brackets make the expression easier to read for others.
  • Avoid complicated expressions. If an expression grows too long, split it: give the parts names.
python
days_past_due, rating, restructured = 35, 7, True

# hard to read
print(days_past_due > 30 and rating >= 7 or restructured and not days_past_due == 0)

# the same, with names for the parts
is_late = days_past_due > 30 and rating >= 7
is_restructured_and_late = restructured and days_past_due != 0
print(is_late or is_restructured_and_late)
Edit the code and run it again

10. Loops ​

A loop runs a block of code many times. Each run of the block is an iteration. Python has two loops:

  • while repeats while a condition is true - when you do not know in advance how many times (section 11);
  • for repeats for each item of a sequence - the rows of a file, the items of a list, the numbers of a range (section 12).

You already use loops in SAS: the DATA step is a loop - it runs once for each row of the input data set, without you writing it. In Python you write the loop yourself (and in pandas, again, you do not - session 14).

SASPython
the implicit loop of the DATA step over SETfor line in file:
do i = 1 to 12; ... end;for i in range(1, 13): - note the 13
do i = 0 to 100 by 10;for i in range(0, 101, 10):
do while (balance > 0);while balance > 0:
do until (balance <= 0);while True: ... if balance <= 0: break (section 13)
leave;break
continue;continue
total + amount; (the sum statement, with RETAIN)total += amount, with total = 0 before the loop

11. The while loop ​

Syntax ​

text
while condition:
    block

The block runs again and again while the condition is true. Before each iteration Python tests the condition; when it is false, the loop ends and the program continues after the block.

Inside the block, something must change the condition - otherwise the loop never ends. (Or a break stops it, section 13.)

The flow of a while loop and a for loop: the test comes before each iteration; while it is true the block runs, then the test again; when it is false, the program continues after the loop

python
i = 1
while i <= 5:
    print(i)
    i += 1
Edit the code and run it again

This is just a simple example. For a fixed number of iterations, a for loop with range is better (section 12).

An endless loop - find the problem ​

This loop never ends (do not run it here - in VS Code, Ctrl+C in the terminal stops a running program):

python
# print the numbers from 10 down to 1
i = 10
while i >= 1:
    print(i)
    i = 1
Answer

i = 1 sets i to 1 in every iteration, and 1 >= 1 is always true. It should be i -= 1 (i = i - 1).

When while is the right loop ​

Use while when you do not know in advance how many iterations are needed. How many months does it take to repay 20 000 EUR at 6% a year, with 450 EUR a month?

python
balance = 20000.0
monthly_rate = 6 / 12 / 100
payment = 450.0
months = 0

while balance > 0:
    interest = balance * monthly_rate
    balance = balance + interest - payment
    months += 1

print(months, "months")
print(f"the last payment was {payment + balance:.2f}")     # balance is negative now
Edit the code and run it again

The SAS way is the same loop:

sas
data _null_;
    balance = 20000; monthly_rate = 6 / 12 / 100; payment = 450; months = 0;
    do while (balance > 0);
        balance = balance + balance * monthly_rate - payment;
        months + 1;
    end;
    put months=;
run;

Animation

Loops step by step: 3. while: repay a loan

Another typical case is checking what a user types - the program asks again until the answer is right. With input(), so try it in VS Code:

python
# ask for a name until it has at least 3 characters
user_name = input("Enter a name, please: ")
while len(user_name) < 3:
    user_name = input("Enter a name (at least 3 characters): ")
print(f"Thank you, {user_name}!")

The input() line is written twice - section 13 shows a cleaner way with while True and break.

12. The for loop ​

Syntax ​

text
for name in sequence:
    block

for takes the items of a sequence one by one; in each iteration the name points to the next item, and the block runs. The loop ends after the last item. You do not count, and the loop cannot run forever.

python
for letter in "ada":
    print(letter.capitalize())
Edit the code and run it again

What for can loop over ​

Anything that gives items one by one - an iterable. Today: a string (its characters), a range of numbers, a few values in square brackets, and the lines of a file. The square brackets make a list - session 3 is all about lists; for now, read [0, 35, 120] simply as "these values, one by one". Tuples (session 3) and dictionaries (session 6) come later.

python
for item in "byron":                # a string
    print(item, end=",")
print()
for item in range(1, 3):            # a range: 1 and 2
    print(item, end=",")
print()
for item in [10, 20, 30]:           # a few values: a list (session 3)
    print(item, end=",")
Edit the code and run it again

range: a sequence of numbers ​

range(stop), range(start, stop) and range(start, stop, step) give whole numbers from start (0 if not given) up to stop - 1 - the stop itself is not included. Session 3 has the details.

python
for n in range(1, 6):
    print(n, end=" ")
print()
for month in range(3, 13, 3):       # the quarter ends: 3, 6, 9, 12
    print(month, end=" ")
Edit the code and run it again

SAS counts to the end, range stops before it

do month = 1 to 12; runs 12 times, for 1 ... 12. In Python, range(1, 12) stops at 11: write range(1, 13). range(12) also runs 12 times, but for 0 ... 11.

Accumulators: sum, count, maximum ​

A loop often accumulates a result: a variable gets a start value before the loop and changes in each iteration. In SAS this is the sum statement or RETAIN.

python
total_sum = 0
for n in range(1, 11):
    total_sum += n
print(f"total_sum={total_sum}")      # 55
print(sum(range(1, 11)))             # the built-in function does the same
Edit the code and run it again

A sum, a count and a maximum in one loop:

python
days_past_due_values = [0, 35, 0, 120, 5, 0, 200]

count_late = 0
max_days = 0
for days in days_past_due_values:
    if days > 0:
        count_late += 1
    if days > max_days:
        max_days = days

print(count_late, "late loans, the worst is", max_days, "days")
Edit the code and run it again

Animation

Loops step by step: 1. for with an accumulator

Two names at once ​

When each item is a pair of values in round brackets - (1, "one"), a tuple (session 3) - the for can give each part its own name:

python
pairs = [(1, "one"), (2, "two"), (3, "three")]
for number, name in pairs:
    print(f"{number} is {name}")
Edit the code and run it again

That is all you need today. Session 3 shows lists of records like this one, and session 6 loops over a dictionary the same way.

Nested loops ​

A loop can be inside another loop. The inner loop runs completely in each iteration of the outer one:

python
for i in range(1, 4):
    for letter in "abc":
        print(f"{i}{letter}", end=" ")
    print()             # a new line after each row
Edit the code and run it again

The quarters of two years (in SAS: two nested do loops):

python
for year in range(2025, 2027):
    for quarter in range(1, 5):
        print(f"{year}-Q{quarter}", end=" ")
    print()
Edit the code and run it again

On the portfolio - the monthly payment for several amounts and terms, a grid like the one SAS PROC TABULATE would print:

python
yearly_rate = 6
monthly_rate = yearly_rate / 12 / 100

print(f"{'years':>6}", end="")
for amount in range(10000, 50001, 20000):     # 10000, 30000, 50000
    print(f"{amount:>12,}", end="")
print()

for years in range(1, 6):
    print(f"{years:>6}", end="")
    n = years * 12
    for amount in range(10000, 50001, 20000):
        payment = amount * monthly_rate / (1 - (1 + monthly_rate) ** -n)
        print(f"{payment:>12,.2f}", end="")
    print()
Edit the code and run it again

Tables kept in memory - a list of rows - and the loops over them come in session 5.

The rows of a file ​

A text file is a sequence of lines, so for can read it line by line - like a SAS DATA step with INFILE or SET. The pattern (the details of with open come in session 9):

python
with open("data/loans.csv", encoding="utf-8") as file:
    header = file.readline()            # the first line: the column names
    print(header.strip())
    for line in file:                   # the other lines, one per iteration
        fields = line.strip().split(",")
        print(fields)
        break                           # only the first loan for now (section 13)
Edit the code and run it again

split(",") cuts the line at the commas and gives the fields as strings, in square brackets: a list (session 3). The names on the left of = take them in order (as in "several names at once", session 1) - then convert the ones you need:

python
total = 0.0
count = 0
stage_3 = 0

with open("data/loans.csv", encoding="utf-8") as file:
    file.readline()
    for line in file:
        (loan_id, client_id, product, currency, start_date, maturity_date,
         amount, outstanding, rate, days_past_due, stage) = line.strip().split(",")
        total += float(outstanding)
        count += 1
        if stage == "3":
            stage_3 += 1

print(f"{count} loans, outstanding {total:,.2f} EUR, {stage_3} in stage 3")
Edit the code and run it again
sql
SELECT COUNT(*), SUM(outstanding), SUM(CASE WHEN stage = 3 THEN 1 ELSE 0 END)
FROM loans;

(For simplicity the example adds EUR and USD amounts together - a real report converts them first.)

13. break ​

Leave the loop now ​

break stops the innermost for or while loop at once; the program continues after the loop. It is almost always inside an if:

text
for item in sequence:            while condition:
    block 1                          block 1
    if break_condition:              if break_condition:
        break                            break
    block 2                          block 2

When the break condition is true, block 2 is skipped and the loop ends.

python
for s in "alibaba":
    if s == "i":
        break
    print(s)
Edit the code and run it again

Finding something - stop as soon as it is found:

python
days_past_due_values = [0, 35, 0, 120, 5, 200]

for days in days_past_due_values:
    if days > 90:
        print(f"the first non-performing loan: {days} days past due")
        break
    print(days, "- not yet")
Edit the code and run it again

Animation

Loops step by step: 4. break: stop at the first

break in nested loops ​

break leaves only the loop it is in - the outer loop goes on:

python
for year in range(2025, 2028):
    print(year, end=": ")
    for month in range(1, 13):
        if month > 3:
            break               # leaves the inner loop only: the first quarter
        print(month, end=",")
    print()                     # the outer loop goes on with the next year
Edit the code and run it again

do until: a loop that runs at least once ​

Python has no do ... while loop (C, Java) and no do until (SAS) - "there should be one obvious way to do it". They are useful when the block must run at least once and the test comes at the end. The Python way is while True with a break:

text
while True:
    block
    if condition:
        break

The name check of section 11, without writing input() twice:

python
while True:
    user_name = input("Enter a name (at least 3 characters): ")
    if len(user_name) >= 3:
        break
print(f"Thank you, {user_name}!")

On the portfolio - repay a balance until it is 0. In SAS this is do until:

sas
do until (balance <= 0);
    balance = balance - payment;
end;
python
balance = 1000.0
payment = 300.0
months = 0

while True:                 # do ...
    balance -= payment
    months += 1
    if balance <= 0:        # ... until balance <= 0
        break

print(months, balance)
Edit the code and run it again

"Not found": a flag ​

After a search with break, how do you know if something was found? Set a flag before the loop - like found = 0; in a SAS DATA step:

python
days_past_due_values = [0, 35, 0, 5]

found = False
for days in days_past_due_values:
    if days > 90:
        found = True
        print("found a non-performing loan:", days)
        break

if not found:
    print("no loan is more than 90 days past due")
Edit the code and run it again
Advanced: else on a loop

A for or while loop can have an else block. It runs when the loop ends normally - and not when a break stopped it. It replaces the flag:

python
days_past_due_values = [0, 35, 0, 5]

for days in days_past_due_values:
    if days > 90:
        print("found a non-performing loan:", days)
        break
else:
    print("no loan is more than 90 days past due")
Edit the code and run it again

Read the else of a loop as "no break". It is rare and surprises many readers - the flag is clearer.

Loops step by step: 6. for - else (advanced)

14. continue ​

continue stops the current iteration and goes to the next one: the rest of the block is skipped, and the loop goes on (for while: with the test of the condition). It is almost always inside an if:

text
for item in sequence:            while condition:
    block 1                          block 1
    if continue_condition:           if continue_condition:
        continue                         continue
    block 2                          block 2

Print the numbers 1 to 5, but skip 3:

python
for i in range(1, 6):
    if i == 3:
        continue
    print(i, end=",")
Edit the code and run it again

Print a word without the vowels:

python
word = "abcdefghi"
vowels = "aeiou"
for letter in word:
    if letter in vowels:
        continue
    print(letter, end=",")
Edit the code and run it again

In data work, continue skips the records you do not want - like WHERE in SQL, or a subsetting IF or DELETE in a SAS DATA step. Count only the USD loans, and skip empty lines. Here the line is kept as one list, fields, and fields[3] is its 4th field: the positions are counted from 0 (session 3 explains it).

python
usd_count = 0
usd_total = 0.0

with open("data/loans.csv", encoding="utf-8") as file:
    file.readline()
    for line in file:
        if not line.strip():                # an empty line: skip it
            continue
        fields = line.strip().split(",")
        if fields[3] != "USD":              # the 4th field (the count starts at 0): currency
            continue
        usd_count += 1
        usd_total += float(fields[7])       # the 8th field: outstanding

print(usd_count, f"{usd_total:,.2f}")
Edit the code and run it again

continue keeps the main code at the left, without one more level of indentation. An if with the opposite condition does the same - choose the one that reads better.

Animation

Loops step by step: 5. continue: skip a record

The whole picture ​

break and continue in one flow (the else in the picture is the Advanced box of section 13):

The flow of a loop: the condition is tested before each iteration; break leaves the loop, continue goes back to the test

Follow the flow without running the code: what does this loop print?

python
for i in range(1, 6):
    if i == 2:
        continue
    if i == 4:
        break
    print(i)
Answer

1 and 3. At 2, continue goes back to the test, so 2 is not printed. At 4, break leaves the loop: 4 and 5 are never printed.

15. Putting it together ​

A repayment schedule ​

An annuity loan (session 1): the same payment every month. Each month, the interest is calculated on the balance, and the rest of the payment repays the principal.

sas
data schedule;
    balance = 20000; monthly_rate = 6 / 12 / 100; n = 12;
    payment = balance * monthly_rate / (1 - (1 + monthly_rate) ** -n);
    do month = 1 to n;
        interest = balance * monthly_rate;
        principal = payment - interest;
        balance = balance - principal;
        output;
    end;
run;
python
amount = 20000.0
monthly_rate = 6 / 12 / 100
n = 12

payment = amount * monthly_rate / (1 - (1 + monthly_rate) ** -n)
balance = amount
total_interest = 0.0

print(f"{'month':>5}{'payment':>11}{'interest':>11}{'principal':>11}{'balance':>12}")
for month in range(1, n + 1):
    interest = balance * monthly_rate
    principal = payment - interest
    balance -= principal
    total_interest += interest
    print(f"{month:>5}{payment:>11,.2f}{interest:>11,.2f}{principal:>11,.2f}{balance:>12,.2f}")

print(f"total interest: {total_interest:,.2f}")
Edit the code and run it again

(The last balance can show -0.00: a float rounding error of a fraction of a cent, session 1, section 14.)

Find the rate: higher or lower? ​

Optional

Not needed for the tasks, the homework or the next sessions - read it when you have time.

The classic "guess the number" game - "too high", "too low", until it is right - is also a real method for finding a number that you cannot calculate directly. Which yearly rate gives a payment of exactly 400 EUR on 20 000 EUR for 5 years? The payment grows with the rate, so: try the middle of the interval, and keep the half where the answer must be (bisection):

python
amount = 20000.0
months = 60
target_payment = 400.0

low, high = 0.0, 30.0              # the yearly rate is somewhere in 0% ... 30%
tries = 0

while high - low > 0.00001:
    rate = (low + high) / 2        # guess: the middle
    r = rate / 12 / 100
    payment = amount * r / (1 - (1 + r) ** -months)
    if payment > target_payment:   # too high: the rate must be lower
        high = rate
    else:                          # too low: the rate must be higher
        low = rate
    tries += 1

print(f"rate {rate:.4f}% after {tries} tries")
Edit the code and run it again

Each try halves the interval, so after 22 tries it is smaller than 0.00001%. The same idea finds the effective interest rate (the APR) of a loan with fees, or the internal rate of return (IRR) of a cash flow.

16. Practice ​

Open your copy of the Homework repository in VS Code (before the session: get the new tasks) and run the tasks from the terminal, in the repository root:

bash
python 02-conditions-and-loops/practice/task1-predict-logic.py
TaskFileWhat you do
1task1-predict-logic.pyPredict the result of 14 comparisons and logical expressions, then check
2task2-buckets.pyThe days-past-due bucket and the sign of a change, with if / elif / else
3task3-stage-and-leap-year.pyThe IFRS 9 stage with and / or; leap years
4task4-match.pyProduct groups and a currency converter with match
5task5-loans-file.pyLoop over loans.csv: counts, sums, the largest loan, the first stage 3 loan
6task6-while.pyMonths to repay a loan with while; a text bar chart with for

In task1-predict-logic.py, replace each "?" with your prediction before you run the file.

Help: what the tasks use from later sessions

  • A part you skip prints TODO, not FAIL - and so does a "?" that you leave in a predict task. The parts marked optional below show details of Python that you will not need for your work with data.
  • Task 1: four predictions show how and and or work inside (section 3, "and and or return one of their operands"): 0 or "n/a", "" or None, 5 and 0 and True or 1 / 0. They are optional - in pandas you combine conditions with & and | instead (sessions 14 - 15). And "3" == 3 is False: == between a string and a number is allowed, and different types are never equal; only <, >, <=, >= stop with a TypeError.
  • Task 3, part 2 (leap years) is optional - a classic exercise. Build the condition from three tests: year % 4 == 0, year % 100 != 0 and year % 400 == 0 - "divides by 4 and not by 100, or divides by 400". Put brackets around the and part.
  • Task 5, part 1 (largest_loan_id) and the homework, part 3: section 12, "Accumulators", keeps only the largest value. Here you need its loan id too: before the loop, two names - largest_outstanding = 0.0 and largest_loan_id = None; in the loop, when a loan is larger, change both (in the homework also the third one, the days past due).
  • Task 6, part 1: the loop is the one of section 11 ("How many months does it take to repay..."). After the loop balance is negative - the last payment was only payment + balance. So all the payments together are payment * months + balance, and the interest is that minus amount.
  • Tasks 2 - 4 give you ready loops like for days_past_due in [0, 1, 30, ...]: or for old, new in [(1000.0, 1200.0), ...]: (section 12, "Two names at once"). They only try your code on a few values - write your code inside, do not change the loop.
  • Task 5, part 2: copy the loop of part 1 - it gives each field a name - and stop with break at the first stage == "3". Or keep the line as one list, fields = line.strip().split(","): then fields[0] is the loan id and fields[10] the stage (the positions are counted from 0, session 3).
  • Task 6, part 3: stage_counts holds pairs; loop over it with two names, for stage, count in stage_counts:, and add one line to chart in each iteration (chart += ...). "#" * 5 is "#####" (session 1).
  • Homework, part 4: build report with += and f-strings with widths, as in section 15: f"{'current':<8}{current_count:>6}", :>18,.2f, and :>8.1% for a percent (session 1, section 13).
  • Homework, part 4 - a FAIL: the check compares the whole text, so one space more or less is a FAIL. print(report), then compare the got: and expected: lines under the FAIL character by character - one line of the report at a time.

Summary ​

TopicRemember
Truth0, 0.0, "", None, empty collections are false; everything else is true
Comparisons< <= > >= == !=, in, is None; never compare a str with a number - "3" == 3 is False; strings compare letter by letter
Logicand, or, not; Python stops as soon as it knows; x or default replaces also 0
ifif / elif / else: the first true condition wins - like CASE WHEN; test only one bound of each range
Expressionsa if condition else b; statements do, expressions give a value
matchone value against fixed cases; | for "or", case _: for the rest; guards with if
whilewhile a condition is true; something in the block must change it
foreach item of a sequence: a string, a list, range, the lines of a file; range(1, 13) is 1 ... 12
break / continueleave the loop / go to the next iteration; while True + break = do until; a flag for "not found"

Homework ​

  1. 02-conditions-and-loops/homework.py (in the homework repository): a report of days past due from loans.csv - the number of loans and the outstanding amount in each bucket, the shares, and the largest late loan. Every check must print OK.
  2. The practice tasks you did not finish in class.
  3. Optional: a loan calculator in the terminal - calculator.py asks for the amount, the yearly rate and the years with input(), asks again while a value is not valid (the amount must be more than 0, the rate between 0 and 50, the years between 1 and 30), and prints the repayment schedule of section 15. Add a menu with match: 1 - the schedule, 2 - only the total interest, 3 - quit; repeat until the user chooses 3.
  4. Optional: change the bisection example of section 15 so that it also prints each guess and "too high" / "too low", and stops after at most 30 tries.
  5. Optional: 02-conditions-and-loops/notebook.ipynb - short extra tasks on the topics of today in a Jupyter notebook, each with a check cell. Open it in VS Code and select the .venv kernel.

Read more ​

Next session: lists, tuples and sets - indexing from 0, slices, the methods of list, tuples as records, and the SQL UNION / INTERSECT / EXCEPT with sets.

Created for