§5  Conditionals & Loops
Python Programming Series  ·  Article 5

Conditional Statements and Loops: A Complete Guide

Two ideas carry almost all the decision-making and repetition in every Python program you'll ever write: conditionals decide whether a block of code runs, and loops decide how many times it runs. Everything from a login check to a recommendation engine is built out of these two tools, combined and nested in different ways.

if / elif / else
while & for loops
break, continue, pass
Real code from Uber, Amazon, Stripe
Conditionals
Loops

This guide covers every construct from the source notebook, verified by running the actual code (including simulating the interactive examples with fixed inputs so the logic is checked, not just read), with real examples from Uber, Amazon, Stripe, Netflix, and more.


Part 1: Conditional Statements

Why Do We Need Them?

Without a way to branch, code just runs top to bottom, every line, every time:

python
marks = int(input('Enter your marks: '))

print('Hurray! You Passed.')
print('Sorry! You Failed')

Both messages print, always, regardless of the marks entered. They're contradictory statements that can never both be true, yet this code has no way to choose between them. That's the exact gap conditional statements fill: run this code if a condition holds, and skip it (or run something else) if it doesn't.

if Statements

python
marks = int(input('Enter your marks: '))

if marks >= 35:
    print('You Passed.')

If marks >= 35 is False, the indented line underneath is simply skipped, and the program moves on. Nothing prints. Note the two things Python enforces here: a colon at the end of the if line, and consistent indentation for everything that belongs to it.

if-else Statements

else catches everything the if didn't:

python
marks = int(input('Enter your marks: '))

if marks >= 35:
    print('You Passed.')
else:
    print('You failed')
                  ┌─────────────────────┐
                  │ marks >= 35 ?        │
                  └──────────┬───────────┘
                     True    │    False
                 ┌───────────┴───────────┐
                 ▼                       ▼
         print('You Passed.')    print('You failed')

else is optional and never carries its own condition; it simply means "if none of the above."

Nested if-else Statements

You can put an if-else inside another one, which lets you check a broader condition first and a more specific one second:

python
marks = int(input('Enter your marks: '))

if marks >= 0 and marks <= 100:
    if marks >= 35 and marks <= 100:
        print('You Passed.')
    else:
        print('You failed')
else:
    print('Invalid Marks. Please mention a number between 0 and 100.')

The outer check filters out nonsense input (negative marks, or marks above 100) before the inner check even runs. A variant of this pattern actively rejects bad input instead of just printing a message, using raise:

python
marks = int(input('Enter the marks: '))

if marks < 0 or marks > 100:
    raise ValueError('Marks entered should be between 0 to 100.')
else:
    if marks >= 35 and marks <= 100:
        print('You Passed.')
    else:
        print('You failed')

raise stops the program immediately with an error, which is the right call when bad input should never be allowed to continue silently.

if-elif-else: The Ladder

Nested if-else blocks get hard to read once you have more than two branches. elif ("else if") flattens that nesting into a single ladder, checked top to bottom until one condition matches:

python
number = -10

if number == 0:
    print('Zero')
elif number > 0:
    print('Positive')
else:
    print('Negative')
Negative

This is doing the same job as nesting an if inside an else, just without the extra indentation. A longer ladder handles the grading example from the notebook:

python
marks = int(input('Enter the marks : '))

if marks >= 0 and marks <= 100:      # ignore invalid input first
    if marks < 35:
        print('Failed')
    elif marks >= 35 and marks <= 50:
        print('Grade C')
    elif marks >= 51 and marks <= 65:
        print('Grade B')
    elif marks >= 66 and marks <= 80:
        print('Grade A')
    else:
        print('Outstanding')
else:
    print('Invalid Input.')

With marks = 72, this prints Grade A, because Python checks each condition top to bottom and stops at the first one that's True (72 fails the first three, then 66 <= 72 <= 80 succeeds).

   marks = 72
   ┌─────────────────┐
   │ marks < 35 ?     │ False
   ├─────────────────┤
   │ 35-50 ?          │ False
   ├─────────────────┤
   │ 51-65 ?          │ False
   ├─────────────────┤
   │ 66-80 ?          │ True  ──►  print('Grade A')   ← stops here
   ├─────────────────┤
   │ else: Outstanding│  (never reached)
   └─────────────────┘

Real-World Examples

Uber: Surge Pricing. Demand level decides a multiplier, and the multiplier decides the final fare.

python
demand_level = "Medium"  # Options: Low, Normal, Medium, High
base_fare = 15.00

if demand_level == "High":
    surge_multiplier = 2.0
    print("Surge Pricing Active: High Demand")
elif demand_level == "Medium":
    surge_multiplier = 1.2
    print("Surge Pricing Active: Medium Demand")
else:
    surge_multiplier = 1.0
    print("Standard Pricing")

final_fare = base_fare * surge_multiplier
extra_cost = final_fare - base_fare

print(f"Status      : {demand_level}")
print(f"Surge       : {surge_multiplier}x")
print(f"Base Fare   : ₹{base_fare}")
print(f"Final Fare  : ₹{final_fare:.0f}")
print(f"Extra Cost  : ₹{extra_cost:.0f}")
print(f"Your final estimated fare is: ${final_fare:.2f}")
Surge Pricing Active: Medium Demand
Status      : Medium
Surge       : 1.2x
Base Fare   : ₹15.0
Final Fare  : ₹18
Extra Cost  : ₹3
Your final estimated fare is: $18.00

Notice Base Fare prints as ₹15.0 while Final Fare prints as a clean ₹18. That's not a bug; {base_fare} prints the raw float, while {final_fare:.0f} explicitly formats to zero decimal places. Formatting is applied per value, not automatically kept consistent across a block.

Amazon: Spending-Based Discount. An if-elif ladder checked from the highest threshold down, so a ₹3,250 cart correctly lands in the "₹2,000+" tier rather than accidentally also matching a lower one.

python
cart_total = 3250   # Try: 300, 750, 2500, 6000, 12000

if cart_total >= 10000:
    discount_percent = 20
    perk = "Free Priority Shipping!"
elif cart_total >= 5000:
    discount_percent = 15
    perk = "Free Standard Shipping!"
elif cart_total >= 2000:
    discount_percent = 10
    perk = "₹50 Shipping Discount!"
elif cart_total >= 500:
    discount_percent = 5
    perk = "Early Sale Access!"
else:
    discount_percent = 0
    perk = "Add ₹500+ to unlock discounts!"

discount_amount = (cart_total * discount_percent) / 100
final_price = cart_total - discount_amount
Discount   : 10%
You Save   : ₹325
Final Price: ₹2925
Perk       : ₹50 Shipping Discount!

The order of the checks matters here: because elif stops at the first match, checking from highest to lowest guarantees a ₹12,000 cart hits the 20% tier and never gets mistakenly caught by the ₹2,000+ check further down.

Stripe: Fraud Detection. A single and condition combining two risk factors.

python
transaction_amount = 15000
card_country = "US"
ip_country = "RU"

if transaction_amount > 10000 and card_country != ip_country:
    transaction_status = "Declined"
    print("ALERT: Suspicious transaction detected. Account locked.")
else:
    transaction_status = "Approved"
    print("Transaction processed successfully.")
ALERT: Suspicious transaction detected. Account locked.

Both conditions have to be true for the decline to trigger; a $15,000 transaction from a matching country, or a $50 transaction from a mismatched one, would each pass through fine on their own.

Swiggy: Discount Engine. Three levels of nested if, each one narrowing down a specific reason the promo might not apply.

python
is_new_user = True
cart_value = 150  # Rupees
promo_code_entered = "WELCOME50"

if promo_code_entered == "WELCOME50":
    if is_new_user:
        if cart_value >= 199:
            discount = cart_value * 0.50
            print(f"Promo applied! You saved ₹{discount}")
        else:
            print(f"Cart value too low. Add items worth ₹{199 - cart_value} to use this code.")
    else:
        print("Sorry, this promo code is for new users only.")
Cart value too low. Add items worth ₹49 to use this code.

Nesting here does real work: it lets the program give a specific reason (wrong code, not a new user, or cart too small) instead of one generic "promo failed" message.


Part 2: Loops

while Loops: The Watchdogs

Use a while loop when you don't know in advance how many times you'll repeat, only the condition that should keep you going:

python
num1 = 10
num2 = 20

while num1 < num2:
    print('Hello')
    num1 = num1 + 2
   ┌──────────────────┐
   │ num1 < num2 ?     │◄─────────────┐
   └─────────┬────────┘               │
      True   │   False                │
       ┌─────┴─────┐                  │
       ▼           ▼                  │
   print('Hello')  (exit loop)        │
   num1 += 2 ───────────────────────┘

This prints Hello five times (num1 goes 10, 12, 14, 16, 18, then 20 < 20 is False and the loop stops). The line num1 = num1 + 2 is doing the real work here: without it, num1 never changes, num1 < num2 never becomes False, and the loop runs forever.

A while loop can also collect input until a count is reached:

python
num1 = 10
list1 = []

while num1 < 15:
    value = int(input('Enter a number: '))
    list1.append(value)
    num1 = num1 + 1

Real-World while Loop Examples

PhonePe: OTP Verification. The loop condition combines two things: attempts remaining and not yet verified. Either one becoming false ends the loop.

python
correct_otp  = "847261"
max_attempts = 3
attempt  = 0
verified = False

while attempt < max_attempts and not verified:
    entered_otp = input("Enter 6 digit OTP you got on your phone: ")
    attempt += 1

    if entered_otp == correct_otp:
        verified = True
        print(f"OTP Correct! Payment successful")
    else:
        remaining = max_attempts - attempt
        if remaining > 0:
            print(f"Wrong OTP! {remaining} attempt(s) remaining.")

if not verified:
    print("Too many wrong attempts. Account temporarily locked.")

Tracing this with three different input sequences shows exactly why the compound condition matters:

   Input sequence              What happens
   ─────────────────────────   ───────────────────────────────
   Correct on attempt 1     →  verified=True immediately, loop exits after 1 pass
   Wrong, wrong, correct    →  2 failed attempts logged, then verified=True on the 3rd
   Wrong, wrong, wrong      →  attempt reaches 3, loop exits, account locked

The loop exits the instant either verified becomes True or attempt hits max_attempts, whichever comes first, which is exactly the "3 tries, then you're locked out, unless you succeed first" behavior a real OTP screen needs.

Netflix: Video Buffering. The loop keeps running purely based on a percentage crossing a threshold, with no fixed number of iterations decided in advance.

python
buffer_percentage = 0
required_buffer = 20

while buffer_percentage < required_buffer:
    buffer_percentage += 5
    print(f"Buffering... {buffer_percentage}%")

print("Buffer threshold reached. Resuming playback.")

Instagram: Infinite Scroll. The condition here is a plain boolean flag that gets flipped from inside the loop once a simulated stopping point is reached.

python
user_is_scrolling = True
posts_loaded = 5

while user_is_scrolling:
    posts_loaded += 5
    if posts_loaded >= 15:
        user_is_scrolling = False

After this runs, posts_loaded is 15 and the loop has run exactly twice, since user_is_scrolling flips to False the moment the threshold is crossed, which is checked again only at the top of the next pass.


for Loops: The Processors

Use a for loop when you already have a collection and need to do something to every item in it. for works over anything iterable: lists, tuples, sets, dictionaries, strings, and range().

python
list1 = [10, 34, 87, 5, 65]

for value in list1:
    print('Hello')     # runs once per item, 5 times total
python
word = 'Python'

for char in word:
    print('Hello')     # a string is iterable too: runs once per CHARACTER, 6 times

The range() Function

range() generates a sequence of numbers without you having to type them all out:

python
range(10)              # a range object, not yet a list
list(range(10))         # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]  (stops BEFORE 10)
list(range(1, 16))      # [1, 2, ..., 15]  (start, stop); stop is excluded
list(range(1, 16, 2))   # [1, 3, 5, 7, 9, 11, 13, 15]  (start, stop, step)

Building Filtered Lists

A very common for-loop pattern: start with an empty list, loop through a source, and append items that pass a test.

python
numbers = list(range(1, 21))

odd = []
for value in numbers:
    if value % 2 != 0:
        odd.append(value)

even = []
for value in numbers:
    if value % 2 == 0:
        even.append(value)

You can combine both checks into a single pass over the data instead of looping twice:

python
odd_new = []
even_new = []

for value in numbers:
    if value % 2 != 0:
        odd_new.append(value)
    else:
        even_new.append(value)

Control Flow Statements: break, continue, pass

These three change what happens inside a running loop.

   continue                 │ break                     │ pass
   skip THIS item,          │ stop the loop              │ do nothing,
   keep looping             │ completely                 │ placeholder only
   ──────────────────────────────────────────────────────────────────────
   for x in items:          │ for x in items:            │ for x in items:
       if bad(x):           │     if done(x):            │     pass
           continue         │         break              │ (loop still runs,
       process(x)           │     process(x)             │  just does nothing
                             │                             │  each time)

continue skips the rest of the current pass and jumps straight to the next item:

python
for value in list1:
    if value == 5:
        continue
    print(value)
10 34 87 65

5 is silently skipped; every other value prints normally. The same idea builds a filtered list without an if/else:

python
odd_list = []
for value in list1:
    if value % 2 == 0:
        continue
    odd_list.append(value)
odd_list = [87, 5, 65]

break stops the loop entirely, right where it is:

python
for value in list1:
    if value == 5:
        break
    print(value)
10 34 87

Compare this carefully with the continue example above: continue skipped 5 and kept going (65 still printed), while break stopped the moment it hit 5 and never looked at 65 at all.

pass does nothing at all. It exists purely as a placeholder where Python's syntax requires something to be there:

python
for value in list1:
    pass

This loop runs all 5 iterations; it just does nothing meaningful in the body of each. You'll mostly reach for pass when sketching out a loop or function you intend to fill in later, and need a syntactically valid line to hold the empty space.


List Comprehension

A list comprehension packs a for loop that builds a list into a single line. This code:

python
squared = []
for value in list1:
    squared.append(value ** 2)

does exactly the same thing as this:

python
squared_new = [value**2 for value in list1]
   Traditional loop                    List comprehension

   squared = []                        squared_new = [ value**2  for value in list1 ]
   for value in list1:                              └────┬────┘  └───────┬────────┘
       squared.append(value**2)                       what to keep    where it comes from

   Both produce: [100, 1156, 7569, 25, 4225]

The expression on the left doesn't have to be a plain number. This example builds a list of single-key dictionaries, one per value:

python
new_list = [{value: value**2} for value in list1]
# [{10: 100}, {34: 1156}, {87: 7569}, {5: 25}, {65: 4225}]

Worth pausing on: this is still a list comprehension (the square brackets [ ] on the outside are what make it one), even though each individual item inside it happens to be a dictionary. It's easy to glance at the curly braces and assume this is a "dictionary comprehension," but a true dictionary comprehension has the curly braces on the outside ({value: value**2 for value in list1}, no square brackets) and produces one dictionary with many keys, not a list of many small dictionaries.

Comprehensions also support the same filtering if you'd use in a full loop:

python
# The long way:
odd = []
for value in numbers:
    if value % 2 != 0:
        odd.append(value)

# The comprehension way, identical result:
new_odd = [value for value in numbers if value % 2 != 0]

Real-World for Loop Examples

YouTube: Playlist Duration. A running total, updated once per item, is the classic accumulator pattern.

python
video_durations = [12.5, 8.0, 22.3, 5.5, 14.1]
total_playlist_time = 0.0

for duration in video_durations:
    total_playlist_time += duration
    print(f"Added video ({duration} min). Running total: {total_playlist_time:.1f} min")

print(f"Total Watch Time: {total_playlist_time:.1f} minutes")
Added video (12.5 min). Running total: 12.5 min
Added video (8.0 min). Running total: 20.5 min
Added video (22.3 min). Running total: 42.8 min
Added video (5.5 min). Running total: 48.3 min
Added video (14.1 min). Running total: 62.4 min
Total Watch Time: 62.4 minutes

Facebook: Friend Suggestions. Looping through someone else's friend list and filtering out people already on yours.

python
my_friends = ["Alex", "Jordan", "Taylor"]
jordan_friends = ["Alex", "Sam", "Casey", "Taylor", "Riley"]

suggestions = []
for person in jordan_friends:
    if person not in my_friends and person != "Me":
        suggestions.append(person)

print(f"People You May Know: {suggestions}")
People You May Know: ['Sam', 'Casey', 'Riley']

Amazon: Inventory Sweep. Looping over a list of dictionaries, checking one field on each.

python
inventory = [
    {"item": "Wireless Mouse", "stock": 45},
    {"item": "Mechanical Keyboard", "stock": 5},
    {"item": "USB-C Cable", "stock": 120},
    {"item": "Laptop Stand", "stock": 0}
]

for product in inventory:
    if product["stock"] == 0:
        print(f"ALERT: {product['item']} is out of stock! Reorder immediately.")
    elif product["stock"] < 10:
        print(f"ALERT: {product['item']} is low on stock! Reorder immediately.")
ALERT: Mechanical Keyboard is low on stock! Reorder immediately.
ALERT: Laptop Stand is out of stock! Reorder immediately.

Only two of the four products trigger an alert; the mouse (45 in stock) and the cable (120 in stock) are both comfortably above the threshold and are silently skipped, which is exactly the point of the check.

Stripe: Monthly Statement. The loop branches on a type field, adding for one type and subtracting for another, building a single net total.

python
transactions = [
    {"amount": 150.00, "type": "payment"},
    {"amount": 45.00, "type": "refund"},
    {"amount": 200.00, "type": "payment"},
    {"amount": 10.00, "type": "refund"}
]

net_revenue = 0.0
for txn in transactions:
    if txn["type"] == "payment":
        net_revenue += txn["amount"]
        print(f"Processed Payment: +${txn['amount']:.2f}")
    elif txn["type"] == "refund":
        net_revenue -= txn["amount"]
        print(f"Processed Refund:  -${txn['amount']:.2f}")

print(f"Monthly Net Revenue: ${net_revenue:.2f}")
Processed Payment: +$150.00
Processed Refund:  -$45.00
Processed Payment: +$200.00
Processed Refund:  -$10.00
Monthly Net Revenue: $295.00

while vs for: Which One?

   Do you know how many times you need to repeat,
   or do you have a specific collection to go through?
   │
   ├── YES, I have a LIST/STRING/RANGE to go through   →  FOR LOOP
   │
   └── NO, I just need to keep going UNTIL a condition
       changes (unknown number of repeats)              →  WHILE LOOP

A useful gut check: if you catch yourself manually incrementing a counter variable just to know when to stop a while loop, and that counter is the only thing controlling the loop, a for loop over range() is almost always the cleaner choice.


Quick Reference Summary

Concept Syntax What it does
if if cond: Runs a block only when cond is True
if-else if cond: ... else: ... Picks one of two branches
if-elif-else if / elif / elif / else Checks conditions top to bottom, stops at the first match
raise raise ValueError(...) Stops the program immediately with an error
while loop while cond: Repeats until cond becomes False
for loop for x in iterable: Repeats once per item in a list, string, range, etc.
range() range(start, stop, step) Generates a number sequence; stop is excluded
continue inside a loop Skips the rest of this pass, moves to the next item
break inside a loop Exits the loop immediately, entirely
pass anywhere a statement is required Does nothing; a syntactic placeholder
List comprehension [expr for x in iterable] Builds a list in one line, equivalent to a loop + append
Filtered comprehension [expr for x in iterable if cond] Same, but only keeps items where cond is True

Conditionals answer "should this run?" and loops answer "how many times?" Nearly every piece of real business logic, a surge multiplier, a discount tier, an OTP check, a monthly statement, is just these two ideas combined and nested until the logic matches the real-world rule it's modeling.

Conditionals answer "should this run?" and loops answer "how many times?" Nearly every piece of real business logic, a surge multiplier, a discount tier, an OTP check, a monthly statement, is just these two ideas combined and nested until the logic matches the real-world rule it's modeling.