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:
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
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:
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:
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:
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:
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:
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.
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.
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.
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.
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:
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:
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.
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.
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.
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().
list1 = [10, 34, 87, 5, 65]
for value in list1:
print('Hello') # runs once per item, 5 times total
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:
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.
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:
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:
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:
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:
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:
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:
squared = []
for value in list1:
squared.append(value ** 2)
does exactly the same thing as this:
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:
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:
# 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.
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.
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.
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.
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.